Files
romants/addons/scene-library/scripts/scene_library.gd
2024-10-05 13:12:42 +02:00

1470 lines
46 KiB
GDScript

# Copyright (c) 2023-2024 Mansur Isaev and contributors - MIT License
# See `LICENSE.md` included in the source distribution for details.
extends MarginContainer
signal library_changed
signal library_unsaved
signal library_saved
signal collection_changed
signal open_asset_request(path: String)
signal show_in_file_system_request(path: String)
signal show_in_file_manager_request(path: String)
signal asset_display_mode_changed(display_mode: DisplayMode)
enum CollectionTabMenu {
NEW,
RENAME,
DELETE,
}
enum LibraryMenu {
NEW,
OPEN,
SAVE,
SAVE_AS,
}
enum DisplayMode{
THUMBNAILS,
LIST,
}
enum SortMode {
NAME,
NAME_REVERSE,
}
enum AssetContextMenu {
OPEN_ASSET,
COPY_PATH,
COPY_UID,
DELETE_ASSET,
SHOW_IN_FILE_SYSTEM,
SHOW_IN_FILE_MANAGER,
REFRESH,
MAX,
}
const NULL_LIBRARY: Array[Dictionary] = []
const NULL_COLLECTION: Dictionary = {}
const THUMB_GRID_SIZE: int = 192
const THUMB_LIST_SIZE: int = 48
var _main_vbox: VBoxContainer = null
var _collec_hbox: HBoxContainer = null
var _collec_tab_bar: TabBar = null
var _collec_tab_add: Button = null
var _all_tabs_list: MenuButton = null
var _collec_option: MenuButton = null
var _main_container: PanelContainer = null
var _content_vbox: VBoxContainer = null
var _top_hbox: HBoxContainer = null
var _asset_filter_line: LineEdit = null
var _asset_sort_mode_btn: Button = null
var _mode_thumb_btn: Button = null
var _mode_list_btn: Button = null
var _item_list: ItemList = null
var _open_dialog: ConfirmationDialog = null
var _save_dialog: ConfirmationDialog = null
var _save_timer: Timer = null
var _thumb_grid_icon_size: int = 64
var _thumb_list_icon_size: int = 16
# INFO: May be required for debugging.
var _cache_enabled: bool = true
var _cache_path: String = "res://.godot/thumb_cache"
# Create thumbnail scene:
var _viewport: SubViewport = null
var _camera_2d: Camera2D = null
var _camera_3d: Camera3D = null
var _light_3d: DirectionalLight3D = null
var _asset_display_mode: DisplayMode = DisplayMode.THUMBNAILS
var _sort_mode: SortMode = SortMode.NAME
var _thumbnails: Dictionary = {}
var _mutex: Mutex = null
var _thread: Thread = null
var _thread_queue: Array[Dictionary] = []
var _thread_sem: Semaphore = null
var _thread_work: bool = true
var _saved: bool = true
# INFO: Use key-value pairs to store collections.
var _curr_lib: Array[Dictionary] = NULL_LIBRARY # Array[Dictionary[StringName, ImageTexture]]
var _curr_lib_path: String = ""
var _curr_collec: Dictionary = NULL_COLLECTION
func _update_position_new_collection_btn() -> void:
var tab_bar_total_width := float(_collec_tab_bar.get_theme_constant(&"h_separation"))
for i: int in _collec_tab_bar.get_tab_count():
tab_bar_total_width += _collec_tab_bar.get_tab_rect(i).size.x
_collec_tab_bar.size = Vector2(minf(_collec_tab_bar.size.x, tab_bar_total_width), 0.0)
_collec_tab_add.position.x = _collec_tab_bar.size.x
_all_tabs_list.set_visible(_collec_tab_bar.get_offset_buttons_visible())
static func _def_setting(name: String, value: Variant) -> Variant:
if not ProjectSettings.has_setting(name):
ProjectSettings.set_setting(name, value)
ProjectSettings.set_initial_value(name, value)
return ProjectSettings.get_setting_with_override(name)
@warning_ignore("narrowing_conversion", "unsafe_method_access")
func _enter_tree() -> void:
_cache_enabled = _def_setting("addons/scene_library/cache/enabled", true)
_cache_path = _def_setting("addons/scene_library/cache/path", "res://.godot/thumb_cache")
_thumb_grid_icon_size = _def_setting("addons/scene_library/thumbnail/grid_size", 64)
_thumb_list_icon_size = _def_setting("addons/scene_library/thumbnail/list_size", 16)
self.add_theme_constant_override(&"margin_left", -get_theme_stylebox(&"BottomPanel", &"EditorStyles").get_margin(SIDE_LEFT))
self.add_theme_constant_override(&"margin_right", -get_theme_stylebox(&"BottomPanel", &"EditorStyles").get_margin(SIDE_RIGHT))
self.add_theme_constant_override(&"margin_top", -get_theme_stylebox(&"BottomPanel", &"EditorStyles").get_margin(SIDE_TOP))
self.set_custom_minimum_size(Vector2(0.0, 180.0))
# INFO: Required to create a tab pseudo-container background.
var tabbar_background := Panel.new()
tabbar_background.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"tabbar_background", &"TabContainer"))
self.add_child(tabbar_background)
_main_vbox = VBoxContainer.new()
_main_vbox.add_theme_constant_override(&"separation", 0)
self.add_child(_main_vbox)
_collec_hbox = HBoxContainer.new()
_collec_hbox.add_theme_constant_override(&"separation", 0)
# INFO: Required to calculate the position of the "new" button.
_collec_hbox.sort_children.connect(_update_position_new_collection_btn)
_main_vbox.add_child(_collec_hbox)
_collec_tab_bar = TabBar.new()
_collec_tab_bar.set_auto_translate(false)
_collec_tab_bar.set_drag_to_rearrange_enabled(true)
_collec_tab_bar.set_h_size_flags(Control.SIZE_EXPAND_FILL)
_collec_tab_bar.set_max_tab_width(256) # TODO: Make this parameter receive global editor settings.
_collec_tab_bar.set_theme_type_variation(&"TabContainer")
_collec_tab_bar.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"DebuggerPanel", &"EditorStyles"))
_collec_tab_bar.set_select_with_rmb(true)
_collec_tab_bar.add_tab("[null]")
_collec_tab_bar.set_tab_disabled(0, true)
_collec_tab_bar.set_tab_close_display_policy(TabBar.CLOSE_BUTTON_SHOW_NEVER)
_collec_tab_bar.tab_selected.connect(_on_collection_tab_changed)
_collec_tab_bar.tab_close_pressed.connect(_on_collection_tab_close_pressed)
_collec_tab_bar.tab_rmb_clicked.connect(_on_collection_tab_rmb_clicked)
_collec_tab_bar.active_tab_rearranged.connect(_on_collection_tab_rearranged)
_collec_hbox.add_child(_collec_tab_bar)
_collec_tab_add = Button.new()
_collec_tab_add.set_flat(true)
_collec_tab_add.set_disabled(true)
_collec_tab_add.set_tooltip_text("Add a new Collection.")
_collec_tab_add.set_button_icon(get_theme_icon(&"Add", &"EditorIcons"))
_collec_tab_add.add_theme_color_override(&"icon_normal_color", Color(0.6, 0.6, 0.6, 0.8))
_collec_tab_add.set_h_size_flags(Control.SIZE_SHRINK_END)
_collec_tab_add.pressed.connect(show_create_collection_dialog)
_collec_hbox.add_child(_collec_tab_add)
_all_tabs_list = MenuButton.new()
_all_tabs_list.hide()
_all_tabs_list.set_tooltip_text("List all tabs.")
_all_tabs_list.set_button_icon(get_theme_icon(&"GuiOptionArrow", &"EditorIcons"))
_all_tabs_list.add_theme_color_override(&"icon_normal_color", Color(0.6, 0.6, 0.6, 0.8))
_collec_hbox.add_child(_all_tabs_list)
var popup: PopupMenu = _all_tabs_list.get_popup()
popup.index_pressed.connect(_collec_tab_bar.set_current_tab)
_collec_option = MenuButton.new()
_collec_option.set_flat(true)
_collec_option.set_button_icon(get_theme_icon(&"GuiTabMenuHl", &"EditorIcons"))
_collec_option.add_theme_color_override(&"icon_normal_color", Color(0.6, 0.6, 0.6, 0.8))
_collec_hbox.add_child(_collec_option)
popup = _collec_option.get_popup()
popup.add_item("New Library", LibraryMenu.NEW)
popup.add_item("Open Library", LibraryMenu.OPEN)
popup.add_separator()
popup.add_item("Save Library", LibraryMenu.SAVE)
popup.add_item("Save Library As...", LibraryMenu.SAVE_AS)
popup.id_pressed.connect(_on_collection_option_id_pressed)
_main_container = PanelContainer.new()
_main_container.set_mouse_filter(Control.MOUSE_FILTER_IGNORE)
_main_container.set_v_size_flags(Control.SIZE_EXPAND_FILL)
_main_container.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"DebuggerPanel", &"EditorStyles"))
_main_vbox.add_child(_main_container)
_content_vbox = VBoxContainer.new()
_main_container.add_child(_content_vbox)
_top_hbox = HBoxContainer.new()
_content_vbox.add_child(_top_hbox)
_asset_filter_line = LineEdit.new()
_asset_filter_line.set_placeholder("Filter assets")
_asset_filter_line.set_clear_button_enabled(true)
_asset_filter_line.set_right_icon(get_theme_icon(&"Search", &"EditorIcons"))
_asset_filter_line.set_editable(false) # The value will be changed when the collection is changed.
_asset_filter_line.set_h_size_flags(Control.SIZE_EXPAND_FILL)
_asset_filter_line.text_changed.connect(_on_filter_assets_text_changed)
_top_hbox.add_child(_asset_filter_line)
_asset_sort_mode_btn = Button.new()
_asset_sort_mode_btn.set_disabled(true)
_asset_sort_mode_btn.set_tooltip_text("Toggle alphabetical sorting of assets")
_asset_sort_mode_btn.set_flat(true)
_asset_sort_mode_btn.set_toggle_mode(true)
_asset_sort_mode_btn.set_button_icon(get_theme_icon(&"Sort", &"EditorIcons"))
_asset_sort_mode_btn.toggled.connect(_sort_assets_button_toggled)
_top_hbox.add_child(_asset_sort_mode_btn)
_top_hbox.add_child(VSeparator.new())
var button_group := ButtonGroup.new()
_mode_thumb_btn = Button.new()
_mode_thumb_btn.set_flat(true)
_mode_thumb_btn.set_disabled(true)
_mode_thumb_btn.set_tooltip_text("View items as a grid of thumbnails.")
_mode_thumb_btn.set_toggle_mode(true)
_mode_thumb_btn.set_button_icon(get_theme_icon(&"FileThumbnail", &"EditorIcons"))
_mode_thumb_btn.set_button_group(button_group)
_mode_thumb_btn.pressed.connect(set_asset_display_mode.bind(DisplayMode.THUMBNAILS))
_top_hbox.add_child(_mode_thumb_btn)
_mode_list_btn = Button.new()
_mode_list_btn.set_flat(true)
_mode_list_btn.set_disabled(true)
_mode_list_btn.set_tooltip_text("View items as a list.")
_mode_list_btn.set_toggle_mode(true)
_mode_list_btn.set_button_icon(get_theme_icon(&"FileList", &"EditorIcons"))
_mode_list_btn.set_button_group(button_group)
_mode_list_btn.pressed.connect(set_asset_display_mode.bind(DisplayMode.LIST))
_top_hbox.add_child(_mode_list_btn)
_item_list = AssetItemList.new()
_item_list.set_focus_mode(Control.FOCUS_CLICK)
_item_list.set_max_columns(0)
_item_list.set_mouse_filter(Control.MOUSE_FILTER_PASS)
_item_list.set_same_column_width(true)
_item_list.set_select_mode(ItemList.SELECT_MULTI)
_item_list.set_texture_filter(CanvasItem.TEXTURE_FILTER_LINEAR)
_item_list.set_v_size_flags(Control.SIZE_EXPAND_FILL)
_item_list.gui_input.connect(_on_item_list_gui_input)
_item_list.item_clicked.connect(_on_item_list_item_clicked)
_item_list.item_activated.connect(_on_item_list_item_activated)
_content_vbox.add_child(_item_list)
_asset_display_mode = _def_setting("addons/scene_library/thumbnail/mode", DisplayMode.THUMBNAILS)
_update_asset_display_mode(_asset_display_mode)
_open_dialog = _create_file_dialog(true)
_open_dialog.set_title("Open Asset Library")
_open_dialog.connect(&"file_selected", load_library)
self.add_child(_open_dialog)
_save_dialog = _create_file_dialog(false)
_save_dialog.set_title("Save Asset Library As...")
_save_dialog.connect(&"file_selected", save_library)
self.add_child(_save_dialog)
_save_timer = Timer.new()
_save_timer.set_one_shot(true)
_save_timer.set_wait_time(10.0) # Save unsaved data every 10 seconds.
_save_timer.timeout.connect(_on_save_timer_timeout)
library_unsaved.connect(_save_timer.start)
self.add_child(_save_timer)
var world_2d := World2D.new()
var world_3d := World3D.new()
# TODO: Add a feature to change Environment.
world_3d.set_environment(get_viewport().get_world_3d().get_environment())
_viewport = SubViewport.new()
_viewport.set_world_2d(world_2d)
_viewport.set_world_3d(world_3d)
_viewport.set_update_mode(SubViewport.UPDATE_DISABLED) # We'll update the frame manually.
_viewport.set_debug_draw(Viewport.DEBUG_DRAW_DISABLE_LOD) # This is necessary to avoid visual glitches.
_viewport.set_process_mode(Node.PROCESS_MODE_DISABLED) # Needs to disable animations.
_viewport.set_size(Vector2i(THUMB_GRID_SIZE, THUMB_GRID_SIZE))
_viewport.set_disable_input(true)
_viewport.set_transparent_background(true)
_viewport.set_physics_object_picking(false)
_viewport.set_default_canvas_item_texture_filter(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter"))
_viewport.set_default_canvas_item_texture_repeat(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_repeat"))
_viewport.set_fsr_sharpness(ProjectSettings.get_setting("rendering/scaling_3d/fsr_sharpness"))
_viewport.set_msaa_2d(ProjectSettings.get_setting("rendering/anti_aliasing/quality/msaa_2d"))
_viewport.set_msaa_3d(ProjectSettings.get_setting("rendering/anti_aliasing/quality/msaa_3d"))
_viewport.set_positional_shadow_atlas_16_bits(ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_16_bits"))
_viewport.set_positional_shadow_atlas_quadrant_subdiv(0, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_0_subdiv"))
_viewport.set_positional_shadow_atlas_quadrant_subdiv(1, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_1_subdiv"))
_viewport.set_positional_shadow_atlas_quadrant_subdiv(2, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_2_subdiv"))
_viewport.set_positional_shadow_atlas_quadrant_subdiv(3, ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_3_subdiv"))
_viewport.set_positional_shadow_atlas_size(ProjectSettings.get_setting("rendering/lights_and_shadows/positional_shadow/atlas_size"))
_viewport.set_scaling_3d_mode(ProjectSettings.get_setting("rendering/scaling_3d/mode"))
_viewport.set_scaling_3d_scale(ProjectSettings.get_setting("rendering/scaling_3d/scale"))
_viewport.set_screen_space_aa(ProjectSettings.get_setting("rendering/anti_aliasing/quality/screen_space_aa"))
_viewport.set_texture_mipmap_bias(ProjectSettings.get_setting("rendering/textures/default_filters/texture_mipmap_bias"))
self.add_child(_viewport)
_camera_2d = Camera2D.new()
_camera_2d.set_enabled(false)
_viewport.add_child(_camera_2d)
# TODO: Add a feature to set lighting.
_light_3d = DirectionalLight3D.new()
_light_3d.set_shadow_mode(DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS)
_light_3d.set_bake_mode(Light3D.BAKE_STATIC)
_light_3d.set_shadow(true)
_light_3d.basis *= Basis(Vector3.UP, deg_to_rad(45.0))
_light_3d.basis *= Basis(Vector3.LEFT, deg_to_rad(65.0))
_viewport.add_child(_light_3d)
_camera_3d = Camera3D.new()
_camera_3d.set_current(false)
_camera_3d.set_fov(22.5)
_viewport.add_child(_camera_3d)
# Multithreading starts here.
_mutex = Mutex.new()
_thread_sem = Semaphore.new()
_thread = Thread.new()
_thread.start(_thread_process)
library_changed.connect(update_tabs)
collection_changed.connect(update_item_list)
asset_display_mode_changed.connect(_update_asset_display_mode)
_curr_lib_path = _def_setting("addons/scene_library/library/current_library_path", "res://addons/scene-library/scene_library.cfg")
load_library(_curr_lib_path)
collection_changed.connect(_collec_tab_bar.size_flags_changed.emit)
func _exit_tree() -> void:
_mutex.lock()
_thread_work = false
_mutex.unlock()
_thread_sem.post()
if _thread.is_started():
_thread.wait_to_finish()
@warning_ignore("unsafe_method_access")
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
if not _item_list.get_rect().has_point(at_position):
return false
if not data is Dictionary or data.get("type") != "files":
return false
if _curr_lib.is_read_only() or _curr_collec.is_read_only():
return false
var files: PackedStringArray = data["files"]
var rec_ext: PackedStringArray = ResourceLoader.get_recognized_extensions_for_type("PackedScene")
for file: String in files:
var extension: String = file.get_extension().to_lower()
if not rec_ext.has(extension):
return false
if has_asset_path(file) or not is_valid_scene_file(file):
return false
return true
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if data is Dictionary:
var files: PackedStringArray = data["files"]
for path: String in files:
create_asset(path)
func mark_saved() -> void:
library_saved.emit()
_saved = true
func mark_unsaved() -> void:
library_unsaved.emit()
_saved = false
func is_saved() -> bool:
return _saved
func set_current_library(library: Array[Dictionary]) -> void:
if is_same(_curr_lib, library):
return
_curr_lib = library
library_changed.emit()
# Switch to the first tab.
_collec_tab_bar.set_current_tab(0)
func get_current_library() -> Array[Dictionary]:
return _curr_lib
func set_current_library_path(path: String) -> void:
if is_same(_curr_lib_path, path):
return
ProjectSettings.set_setting("addons/scene_library/library/current_library_path", path)
_curr_lib_path = path
func get_current_library_path() -> String:
return _curr_lib_path
func has_collection(collection_name: String) -> bool:
for collection: Dictionary in get_current_library():
if collection.name == collection_name:
return true
return false
func create_collection(collection_name: String) -> void:
assert(not has_collection(collection_name), "Collection with this name already exists.")
var assets: Array[Dictionary] = []
var new_collection: Dictionary = {
&"name": collection_name,
&"assets": assets,
}
_curr_lib.push_back(new_collection)
library_changed.emit()
mark_unsaved()
# Switch to the last tab.
_collec_tab_bar.set_current_tab(_collec_tab_bar.get_tab_count() - 1)
func remove_collection(index: int) -> void:
_curr_lib.remove_at(index)
library_changed.emit()
mark_unsaved()
# Swith to the prev tab.
_collec_tab_bar.set_current_tab(_collec_tab_bar.get_current_tab())
func show_remove_collection_dialog(index: int) -> void:
var assets: Array[Dictionary] = _curr_lib[index].assets
if assets.is_empty():
return remove_collection(index)
var window := ConfirmationDialog.new()
window.set_size(Vector2i.ZERO)
window.set_flag(Window.FLAG_RESIZE_DISABLED, true)
window.focus_exited.connect(window.queue_free)
window.confirmed.connect(remove_collection.bind(index))
window.set_ok_button_text("Remove")
var label := Label.new()
label.set_text("Are you sure you want to delete this collection? (Cannot be undone.)")
window.add_child(label)
self.add_child(window)
window.popup_centered(Vector2i(300, 0))
func _queue_has_id(id: int) -> bool:
_mutex.lock()
for item: Dictionary in _thread_queue:
if item.id == id:
_mutex.unlock()
return true
_mutex.unlock()
return false
func _queue_update_thumbnail(id: int) -> void:
if not _thumbnails.has(id) or _queue_has_id(id):
return
_mutex.lock()
var queue_item: Dictionary = {&"id": id, &"thumb": _thumbnails[id]}
_thread_queue.push_back(queue_item)
_mutex.unlock()
_thread_sem.post()
func _get_or_create_thumbnail(id: int, path: String) -> ImageTexture:
var thumb: ImageTexture = _thumbnails.get(id, null)
if is_instance_valid(thumb):
return thumb
var cache_path: String = _get_thumb_cache_path(path)
if _cache_enabled and FileAccess.file_exists(cache_path):
thumb = ImageTexture.create_from_image(Image.load_from_file(cache_path))
_thumbnails[id] = thumb
else:
thumb = ImageTexture.create_from_image(Image.load_from_file(ProjectSettings.globalize_path("res://addons/scene-library/icons/thumb_placeholder.svg")))
_thumbnails[id] = thumb
_queue_update_thumbnail(id)
return thumb
func _create_asset(id: int, uid: String, path: String) -> Dictionary:
var asset: Dictionary = {
&"id": id,
&"uid": uid,
&"path": path,
&"thumb": _get_or_create_thumbnail(id, path),
}
return asset
static func is_valid_scene_file(path: String) -> bool:
return ResourceLoader.exists(path, "PackedScene") and ResourceLoader.get_recognized_extensions_for_type("PackedScene").has(path.get_extension().to_lower())
static func get_or_create_valid_uid(path: String) -> int:
var id: int = ResourceLoader.get_resource_uid(path)
if id == ResourceUID.INVALID_ID:
id = ResourceUID.create_id()
ResourceUID.add_id(id, path)
return id
func create_asset(path: String) -> void:
assert(is_valid_scene_file(path), "PackedScene file was not found or has an invalid extension.")
var id: int = get_or_create_valid_uid(path)
var new_asset: Dictionary = _create_asset(id, ResourceUID.id_to_text(id), path)
var assets: Array[Dictionary] = _curr_collec.assets
assets.push_back(new_asset)
collection_changed.emit()
mark_unsaved()
func remove_asset(id: int) -> bool:
var assets: Array[Dictionary] = _curr_collec.assets
for i: int in assets.size():
if assets[i].id != id:
continue
assets.remove_at(i)
collection_changed.emit()
mark_unsaved()
return true
return false
func set_current_collection(collection: Dictionary) -> void:
if is_same(_curr_collec, collection):
return
_curr_collec = collection
_item_list.deselect_all()
collection_changed.emit()
func get_current_collection() -> Dictionary:
return _curr_collec
func has_asset_path(path: String) -> bool:
for asset: Dictionary in _curr_collec.assets:
if asset.path == path:
return true
return false
func update_tabs() -> void:
var is_valid: bool = not _curr_lib.is_read_only() and not _curr_lib.is_empty()
_asset_filter_line.set_editable(is_valid)
_collec_tab_add.set_disabled(_curr_lib.is_read_only())
_asset_sort_mode_btn.set_disabled(not is_valid)
_mode_thumb_btn.set_disabled(not is_valid)
_mode_list_btn.set_disabled(not is_valid)
if _curr_lib.size():
_collec_tab_bar.set_tab_count(_curr_lib.size())
_collec_tab_bar.set_tab_close_display_policy(TabBar.CLOSE_BUTTON_SHOW_ACTIVE_ONLY)
var popup: PopupMenu = _all_tabs_list.get_popup()
popup.set_item_count(_curr_lib.size())
for i: int in _curr_lib.size():
_collec_tab_bar.set_tab_title(i, _curr_lib[i].name)
_collec_tab_bar.set_tab_disabled(i, false)
_collec_tab_bar.set_tab_metadata(i, _curr_lib[i])
popup.set_item_text(i, _curr_lib[i].name)
else:
_collec_tab_bar.set_tab_count(1)
_collec_tab_bar.set_tab_close_display_policy(TabBar.CLOSE_BUTTON_SHOW_NEVER)
_collec_tab_bar.set_tab_title(0, "[null]")
_collec_tab_bar.set_tab_disabled(0, true)
_collec_tab_bar.set_tab_metadata(0, NULL_COLLECTION)
# INFO: Required to recalculate position of the "new collection" button.
_collec_tab_bar.size_flags_changed.emit()
@warning_ignore("unsafe_call_argument")
func update_item_list() -> void:
var assets: Array[Dictionary] = _curr_collec.assets
_item_list.set_item_count(assets.size())
var is_list_mode: bool = _asset_display_mode == DisplayMode.LIST
var filter: String = _asset_filter_line.get_text()
var index: int = 0
for asset: Dictionary in assets:
var path: String = asset.path
if not filter.is_subsequence_ofn(path.get_file()):
continue
_item_list.set_item_text(index, path.get_file().get_basename())
_item_list.set_item_icon(index, asset.thumb)
# NOTE: This tooltip will be hidden because used the custom tooltip.
_item_list.set_item_tooltip(index, path)
_item_list.set_item_metadata(index, asset)
index += 1
_item_list.set_item_count(index)
func set_asset_display_mode(display_mode: DisplayMode) -> void:
if is_same(_asset_display_mode, display_mode):
return
ProjectSettings.set_setting("addons/scene_library/thumbnail/mode", display_mode)
_asset_display_mode = display_mode
asset_display_mode_changed.emit(display_mode)
func get_asset_display_mode() -> DisplayMode:
return _asset_display_mode
static func sort_asset_ascending(a: Dictionary, b: Dictionary) -> bool:
@warning_ignore("unsafe_method_access")
return a.path.get_file() < b.path.get_file()
static func sort_asset_descending(a: Dictionary, b: Dictionary) -> bool:
@warning_ignore("unsafe_method_access")
return a.path.get_file() > b.path.get_file()
static func sort_assets(assets: Array[Dictionary], sort_mode: SortMode) -> void:
if sort_mode == SortMode.NAME:
assets.sort_custom(sort_asset_ascending)
else:
assets.sort_custom(sort_asset_descending)
func set_sort_mode(sort_mode: SortMode) -> void:
if is_same(_sort_mode, sort_mode):
return
_sort_mode = sort_mode
sort_assets(_curr_collec.assets, sort_mode)
collection_changed.emit()
func get_sort_mode() -> SortMode:
return _sort_mode
func show_create_collection_dialog() -> AcceptDialog:
var window := AcceptDialog.new()
window.set_size(Vector2i.ZERO)
window.set_title("Create New Collection")
window.add_cancel_button("Cancel")
window.set_flag(Window.FLAG_RESIZE_DISABLED, true)
window.focus_exited.connect(window.queue_free)
self.add_child(window)
var ok_button: Button = window.get_ok_button()
ok_button.set_text("Create")
ok_button.set_disabled(true)
var vbox := VBoxContainer.new()
window.add_child(vbox)
var label := Label.new()
label.set_text("New Collection Name:")
vbox.add_child(label)
var line_edit := LineEdit.new()
window.register_text_enter(line_edit)
line_edit.set_text("new_collection")
line_edit.select_all()
# INFO: Disables the ability to create a collection and set a tooltip.
line_edit.text_changed.connect(func(c_name: String) -> void:
if c_name.is_empty():
line_edit.set_tooltip_text("Collection name is empty.")
elif has_collection(c_name):
line_edit.set_tooltip_text("Collection with this name already exists.")
else:
line_edit.set_tooltip_text("")
ok_button.set_disabled(c_name.is_empty() or has_collection(c_name))
line_edit.set_right_icon(get_theme_icon(&"StatusError", &"EditorIcons") if ok_button.is_disabled() else null)
)
line_edit.text_changed.emit(line_edit.get_text()) # Required for status updates.
vbox.add_child(line_edit)
window.confirmed.connect(func() -> void:
var new_collec_name: String = line_edit.get_text()
create_collection(new_collec_name)
)
window.popup_centered(Vector2i(300, 0))
line_edit.grab_focus()
return window
func _serialize_asset(asset: Dictionary) -> Dictionary:
return {"uid": asset.uid, "path": asset.path}
func _serialize_assets(assets: Array[Dictionary]) -> Array[Dictionary]:
var serialized: Array[Dictionary] = []
serialized.resize(assets.size())
for i: int in assets.size():
serialized[i] = _serialize_asset(assets[i])
return serialized
func _serialize_collection(collection: Dictionary) -> Dictionary:
return {
"name": collection.name,
"assets": _serialize_assets(collection.assets),
}
func _serialize_library(library: Array[Dictionary]) -> Array[Dictionary]:
var serialized: Array[Dictionary] = []
serialized.resize(library.size())
for i: int in library.size():
serialized[i] = _serialize_collection(library[i])
return serialized
func _cfg_save_library(library: Array[Dictionary], path: String) -> void:
var serialized: Array[Dictionary] = _serialize_library(library)
var config := ConfigFile.new()
config.set_value("", "library", serialized)
var error := config.save(path)
assert(error == OK, error_string(error))
func _json_save_library(library: Array[Dictionary], path: String) -> void:
var serialized: Array[Dictionary] = _serialize_library(library)
var file := FileAccess.open(path, FileAccess.WRITE)
assert(FileAccess.get_open_error() == OK, error_string(FileAccess.get_open_error()))
file.store_string(JSON.stringify(serialized, "\t"))
file.close()
func save_library(path: String) -> void:
var extension: String = path.get_extension()
assert(extension == "cfg" or extension == "json", "Invalid extension.")
if extension == "cfg":
_cfg_save_library(_curr_lib, path)
elif extension == "json":
_json_save_library(_curr_lib, path)
else:
return
mark_saved()
func _deserialize_asset(asset: Dictionary) -> Dictionary:
var uid: String = asset.get("uid", "")
var path: String = asset.get("path", "")
var id: int = ResourceUID.text_to_id(uid)
# TODO: Add error handling.
if id != ResourceUID.INVALID_ID and ResourceUID.has_id(id): # If the UID is valid.
path = ResourceUID.get_id_path(id)
# If the UID is wrong, try to load the asset by the path.
# It also checks whether the file extension is valid.
elif is_valid_scene_file(path):
id = ResourceLoader.get_resource_uid(path)
uid = ResourceUID.id_to_text(id)
if not ResourceUID.has_id(id):
ResourceUID.add_id(id, path)
# Invalid assset.
else:
return {}
return _create_asset(id, uid, path)
func _deserialize_assets(assets: Array) -> Array[Dictionary]:
var deserialized: Array[Dictionary] = []
for asset: Dictionary in assets:
asset = _deserialize_asset(asset)
if asset.is_empty():
continue
deserialized.push_back(asset)
return deserialized
func _deserialize_collection(collection: Dictionary) -> Dictionary:
var deserialized: Dictionary = {
&"name": collection[&"name"],
&"assets": _deserialize_assets(collection["assets"])
}
return deserialized
func _deserialize_library(library: Array) -> Array[Dictionary]:
var deserialized: Array[Dictionary] = []
deserialized.resize(library.size())
for i: int in library.size():
deserialized[i] = _deserialize_collection(library[i])
return deserialized
func _load_cfg(path: String) -> Array[Dictionary]:
var config := ConfigFile.new()
var error := config.load(path)
assert(error == OK, error_string(error))
var data: Variant = config.get_value("", "library")
if data is Array:
return _deserialize_library(data)
return NULL_LIBRARY
func _load_json(path: String) -> Array[Dictionary]:
var json := JSON.new()
var error := json.parse(FileAccess.get_file_as_string(path))
assert(error == OK, error_string(error))
var data: Variant = json.get_data()
if data is Array:
return _deserialize_library(data)
return NULL_LIBRARY
func load_library(path: String) -> void:
var library: Array[Dictionary] = []
if FileAccess.file_exists(path):
var extension: String = path.get_extension()
assert(extension == "cfg" or extension == "json", "Invalid extension.")
if extension == "cfg":
library = _load_cfg(path)
elif extension == "json":
library = _load_json(path)
# Check for “null” value.
if library.is_read_only():
return
set_current_library(library)
set_current_library_path(path)
@warning_ignore("unsafe_method_access")
func _calculate_node_rect(node: Node) -> Rect2:
var rect := Rect2()
if node is Node2D and node.is_visible():
# HACK: This works only in editor.
rect = node.get_global_transform() * node.call(&"_edit_get_rect")
for i: int in node.get_child_count():
rect = rect.merge(_calculate_node_rect(node.get_child(i)))
return rect
@warning_ignore("unsafe_method_access")
func _calculate_node_aabb(node: Node) -> AABB:
var aabb := AABB()
if node is Node3D and not node.is_visible():
return aabb
# NOTE: If the node is not MeshInstance3D, the AABB is not calculated correctly.
# The camera may have incorrect distances to objects in the scene.
elif node is MeshInstance3D:
aabb = node.get_global_transform() * node.get_aabb()
for i: int in node.get_child_count():
aabb = aabb.merge(_calculate_node_aabb(node.get_child(i)))
return aabb
func _focus_camera_on_node_2d(node: Node) -> void:
var rect: Rect2 = _calculate_node_rect(node)
_camera_2d.set_position(rect.get_center())
var zoom_ratio: float = THUMB_GRID_SIZE / maxf(rect.size.x, rect.size.y)
_camera_2d.set_zoom(Vector2(zoom_ratio, zoom_ratio))
func _focus_camera_on_node_3d(node: Node) -> void:
var transform := Transform3D.IDENTITY
# TODO: Add a feature to configure the rotation of the camera.
transform.basis *= Basis(Vector3.UP, deg_to_rad(40.0))
transform.basis *= Basis(Vector3.LEFT, deg_to_rad(22.5))
var aabb: AABB = _calculate_node_aabb(node)
var distance: float = aabb.get_longest_axis_size() / tan(deg_to_rad(_camera_3d.get_fov()) * 0.5)
transform.origin = transform * (Vector3.BACK * distance) + aabb.get_center()
_camera_3d.set_global_transform(transform.orthonormalized())
func _get_thumb_cache_dir() -> String:
return ProjectSettings.globalize_path(_cache_path)
func _get_thumb_cache_path(path: String) -> String:
return _get_thumb_cache_dir().path_join(path.md5_text()) + ".png"
func _save_thumb_to_disk(id: int, image: Image) -> void:
if not DirAccess.dir_exists_absolute(_get_thumb_cache_dir()):
var error := DirAccess.make_dir_absolute(_get_thumb_cache_dir())
assert(error == OK, error_string(error))
var error := image.save_png(_get_thumb_cache_path(ResourceUID.get_id_path(id)))
assert(error == OK, error_string(error))
func _create_thumb(item: Dictionary, callback: Callable) -> void:
var path: String = ResourceUID.get_id_path(item.id)
if not is_valid_scene_file(path):
return callback.call()
var packed_scene := ResourceLoader.load(path, "PackedScene") as PackedScene
# INFO: Could be null if, for example, the dependencies are broken.
if not is_instance_valid(packed_scene) or not packed_scene.can_instantiate():
return callback.call()
var instance: Node = packed_scene.instantiate()
_viewport.call_deferred(&"add_child", instance)
await instance.ready
if instance is Node2D:
_camera_3d.set_current(false)
_camera_2d.set_enabled(true)
_focus_camera_on_node_2d(instance)
else:
_camera_2d.set_enabled(false)
_camera_3d.set_current(true)
_focus_camera_on_node_3d(instance)
await RenderingServer.frame_pre_draw
_viewport.set_update_mode(SubViewport.UPDATE_ONCE)
await RenderingServer.frame_post_draw
var image: Image = _viewport.get_texture().get_image()
image.resize(THUMB_GRID_SIZE, THUMB_GRID_SIZE, Image.INTERPOLATE_LANCZOS)
var thumb: ImageTexture = item.thumb
thumb.update(image)
if _cache_enabled:
_save_thumb_to_disk(item.id, image)
instance.call_deferred(&"free")
await instance.tree_exited
callback.call()
func _thread_process() -> void:
var semaphore := Semaphore.new()
while _thread_work:
if _thread_queue.is_empty():
_thread_sem.wait()
else:
_mutex.lock()
var item: Dictionary = _thread_queue.pop_front()
_mutex.unlock()
# This ensures that this method will be executed in the main thread.
call_deferred_thread_group(&"_create_thumb", item, semaphore.post)
semaphore.wait()
func handle_scene_saved(path: String) -> void:
# INFO: When we save a scene, we try to update the asset thumbnail.
# The "_queue_update_thumbnail" method will not create new thumbnails if they have not been previously created.
_queue_update_thumbnail(ResourceLoader.get_resource_uid(path))
func handle_file_moved(old_file: String, new_file: String) -> void:
if not _thumbnails.has(ResourceLoader.get_resource_uid(new_file)):
return
for collection: Dictionary in _curr_lib:
for asset: Dictionary in collection.assets:
if asset.path == old_file:
asset.path = new_file
break
collection_changed.emit()
func handle_file_removed(file: String) -> void:
# TODO: Need to add Dictionary for asset path.
# Because we can't use UID for deleted files.
# And we have to go through all collections and assets.
var removed: int = 0
for collection: Dictionary in _curr_lib:
var assets: Array[Dictionary] = collection.assets
for i: int in assets.size():
if assets[i].path != file:
continue
assets.remove_at(i)
removed += 1
break
if removed:
collection_changed.emit()
func _on_collection_tab_changed(tab: int) -> void:
set_current_collection(_collec_tab_bar.get_tab_metadata(tab))
func _on_collection_tab_close_pressed(tab: int) -> void:
show_remove_collection_dialog(tab)
func _on_collection_tab_rmb_clicked(tab: int) -> void:
var collection: Dictionary = _collec_tab_bar.get_tab_metadata(tab)
var popup := PopupMenu.new()
popup.id_pressed.connect(func(option: CollectionTabMenu) -> void:
match option:
CollectionTabMenu.NEW:
show_create_collection_dialog()
CollectionTabMenu.RENAME:
var old_name: String = collection.name
var rename_collec_window := AcceptDialog.new()
rename_collec_window.set_size(Vector2i.ZERO)
rename_collec_window.set_title("Rename Collection")
rename_collec_window.add_cancel_button("Cancel")
rename_collec_window.set_flag(Window.FLAG_RESIZE_DISABLED, true)
rename_collec_window.focus_exited.connect(rename_collec_window.queue_free)
var ok_button: Button = rename_collec_window.get_ok_button()
ok_button.set_text("OK")
ok_button.set_disabled(true)
var vbox := VBoxContainer.new()
rename_collec_window.add_child(vbox)
var label := Label.new()
label.set_text("Change Collection Name:")
vbox.add_child(label)
var line_edit := LineEdit.new()
line_edit.set_select_all_on_focus(true)
line_edit.set_text(old_name)
rename_collec_window.register_text_enter(line_edit)
# INFO: Disables the ability to create a collection and set a tooltip.
line_edit.text_changed.connect(func(new_name: String) -> void:
var is_valid := false
if new_name.is_empty():
line_edit.set_tooltip_text("Collection name is empty.")
elif has_collection(new_name):
line_edit.set_tooltip_text("Collection with this name already exists.")
else:
line_edit.set_tooltip_text("")
is_valid = true
ok_button.set_disabled(not is_valid)
line_edit.set_right_icon(null if is_valid else get_theme_icon(&"StatusError", &"EditorIcons"))
)
line_edit.text_changed.emit(line_edit.get_text()) # Required for update status.
vbox.add_child(line_edit)
rename_collec_window.confirmed.connect(func() -> void:
collection.name = line_edit.get_text()
_collec_tab_bar.set_tab_title(tab, line_edit.get_text())
mark_unsaved()
)
self.add_child(rename_collec_window)
rename_collec_window.popup_centered(Vector2i(300, 0))
line_edit.grab_focus()
CollectionTabMenu.DELETE:
show_remove_collection_dialog(tab)
)
popup.focus_exited.connect(popup.queue_free)
self.add_child(popup)
if collection.is_read_only(): # If "null" collection.
# BUG: You can't see it because the tab is disabled.
popup.add_item("New Collection", CollectionTabMenu.NEW)
else:
popup.add_item("New Collection", CollectionTabMenu.NEW)
popup.add_separator()
popup.add_item("Rename Collection", CollectionTabMenu.RENAME)
popup.add_item("Delete Collection", CollectionTabMenu.DELETE)
popup.popup(Rect2i(get_screen_position() + get_local_mouse_position(), Vector2i.ZERO))
func _on_collection_tab_rearranged(_to_idx: int) -> void:
for i: int in _collec_tab_bar.get_tab_count():
_curr_lib[i] = _collec_tab_bar.get_tab_metadata(i)
func _create_file_dialog(open: bool) -> ConfirmationDialog:
var dialog: ConfirmationDialog = null
if Engine.is_editor_hint(): # Works only in the editor.
var editor_file_dialog: EditorFileDialog = ClassDB.instantiate(&"EditorFileDialog")
editor_file_dialog.set_access(EditorFileDialog.ACCESS_FILESYSTEM)
editor_file_dialog.set_file_mode(EditorFileDialog.FILE_MODE_OPEN_FILE if open else EditorFileDialog.FILE_MODE_SAVE_FILE)
editor_file_dialog.add_filter("*.cfg", "Config File")
editor_file_dialog.add_filter("*.json", "JSON File")
dialog = editor_file_dialog
else:
var file_dialog := FileDialog.new()
file_dialog.set_access(FileDialog.ACCESS_FILESYSTEM)
file_dialog.set_file_mode(FileDialog.FILE_MODE_OPEN_FILE if open else FileDialog.FILE_MODE_SAVE_FILE)
file_dialog.add_filter("*.cfg", "Config File")
file_dialog.add_filter("*.json", "JSON File")
dialog = file_dialog
dialog.set_exclusive(true)
return dialog
func _popup_file_dialog(window: Window) -> void:
window.popup_centered_clamped(Vector2(1050, 700) * DisplayServer.screen_get_scale(), 0.8)
func _on_collection_option_id_pressed(option: LibraryMenu) -> void:
match option:
# TODO: Add a feature to check if the current library is saved.
LibraryMenu.NEW:
var new_library: Array[Dictionary] = []
set_current_library(new_library)
_curr_lib_path = ""
LibraryMenu.OPEN:
_popup_file_dialog(_open_dialog)
LibraryMenu.SAVE when not _curr_lib_path.is_empty():
save_library(_curr_lib_path)
LibraryMenu.SAVE, LibraryMenu.SAVE_AS:
_popup_file_dialog(_save_dialog)
func _on_filter_assets_text_changed(_filter: String) -> void:
update_item_list()
func _sort_assets_button_toggled(reverse: bool) -> void:
set_sort_mode(SortMode.NAME_REVERSE if reverse else SortMode.NAME)
func _update_thumb_icon_size(display_mode: DisplayMode) -> void:
if display_mode == DisplayMode.THUMBNAILS:
_item_list.set_fixed_column_width(_thumb_grid_icon_size * 1.5)
_item_list.set_fixed_icon_size(Vector2i(_thumb_grid_icon_size, _thumb_grid_icon_size))
else:
_item_list.set_fixed_column_width(0)
_item_list.set_fixed_icon_size(Vector2i(_thumb_list_icon_size, _thumb_list_icon_size))
func _update_asset_display_mode(display_mode: DisplayMode) -> void:
if display_mode == DisplayMode.THUMBNAILS:
_item_list.set_max_columns(0)
_item_list.set_icon_mode(ItemList.ICON_MODE_TOP)
_item_list.set_max_text_lines(2)
_mode_thumb_btn.set_pressed_no_signal(true)
else:
_item_list.set_max_columns(0)
_item_list.set_icon_mode(ItemList.ICON_MODE_LEFT)
_item_list.set_max_text_lines(1)
_mode_list_btn.set_pressed_no_signal(true)
for i: int in _item_list.get_item_count():
var asset: Dictionary = _item_list.get_item_metadata(i)
_item_list.set_item_icon(i, asset.thumb)
_update_thumb_icon_size(display_mode)
func _set_thumb_grid_icon_size(icon_size: int) -> void:
icon_size = clampi(icon_size, THUMB_LIST_SIZE, THUMB_GRID_SIZE)
if _thumb_grid_icon_size == icon_size:
return
ProjectSettings.set_setting("addons/scene_library/thumbnail/grid_size", icon_size)
_thumb_grid_icon_size = icon_size
_update_thumb_icon_size(_asset_display_mode)
func _set_thumb_list_icon_size(icon_size: int) -> void:
const ICON_MIN_SIZE = 16
icon_size = clampi(icon_size, ICON_MIN_SIZE, THUMB_LIST_SIZE)
if _thumb_list_icon_size == icon_size:
return
ProjectSettings.set_setting("addons/scene_library/thumbnail/list_size", icon_size)
_thumb_list_icon_size = icon_size
_update_thumb_icon_size(_asset_display_mode)
func _on_item_list_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.is_pressed() and event.is_command_or_control_pressed():
const ICON_GRID_STEP = 8
const ICON_LIST_STEP = 4
match event.get_button_index():
MOUSE_BUTTON_WHEEL_UP:
if _asset_display_mode == DisplayMode.THUMBNAILS:
_set_thumb_grid_icon_size(_thumb_grid_icon_size + ICON_GRID_STEP)
else:
_set_thumb_list_icon_size(_thumb_list_icon_size + ICON_LIST_STEP)
MOUSE_BUTTON_WHEEL_DOWN:
if _asset_display_mode == DisplayMode.THUMBNAILS:
_set_thumb_grid_icon_size(_thumb_grid_icon_size - ICON_GRID_STEP)
else:
_set_thumb_list_icon_size(_thumb_list_icon_size - ICON_LIST_STEP)
_:
return
accept_event()
func _on_item_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
if mouse_button_index != MOUSE_BUTTON_RIGHT:
return
_item_list.select(index, false)
var selected_assets: PackedInt32Array = _item_list.get_selected_items()
var popup := PopupMenu.new()
popup.connect(&"focus_exited", popup.queue_free)
popup.connect(&"id_pressed", func(option: AssetContextMenu) -> void:
var asset: Dictionary = _item_list.get_item_metadata(selected_assets[0])
match option:
AssetContextMenu.OPEN_ASSET:
open_asset_request.emit(asset.path)
AssetContextMenu.COPY_PATH:
DisplayServer.clipboard_set(asset.path)
AssetContextMenu.COPY_UID:
DisplayServer.clipboard_set(asset.uid)
AssetContextMenu.DELETE_ASSET:
var assets: Array[Dictionary] = _curr_collec.assets
if selected_assets.size() == 1:
assets.remove_at(selected_assets[0])
else:
selected_assets.reverse()
for i: int in selected_assets:
assets.remove_at(i)
collection_changed.emit()
mark_unsaved()
AssetContextMenu.SHOW_IN_FILE_SYSTEM:
show_in_file_system_request.emit(asset.path)
AssetContextMenu.SHOW_IN_FILE_MANAGER:
show_in_file_manager_request.emit(asset.path)
AssetContextMenu.REFRESH:
for i: int in selected_assets:
asset = _item_list.get_item_metadata(i)
_queue_update_thumbnail(asset.id)
)
self.add_child(popup)
if selected_assets.size() == 1: # If only one asset is selected.
popup.add_item("Open", AssetContextMenu.OPEN_ASSET)
popup.set_item_icon(-1, get_theme_icon(&"Load", &"EditorIcons"))
popup.add_separator()
popup.add_item("Copy Path", AssetContextMenu.COPY_PATH)
popup.set_item_icon(-1, get_theme_icon(&"ActionCopy", &"EditorIcons"))
popup.add_item("Copy UID", AssetContextMenu.COPY_UID)
popup.set_item_icon(-1, get_theme_icon(&"Instance", &"EditorIcons"))
popup.add_item("Delete", AssetContextMenu.DELETE_ASSET)
popup.set_item_icon(-1, get_theme_icon(&"Remove", &"EditorIcons"))
popup.add_separator()
popup.add_item("Show in FileSystem", AssetContextMenu.SHOW_IN_FILE_SYSTEM)
popup.set_item_icon(-1, get_theme_icon(&"Filesystem", &"EditorIcons"))
popup.add_item("Show in File Manager", AssetContextMenu.SHOW_IN_FILE_MANAGER)
popup.set_item_icon(-1, get_theme_icon(&"Folder", &"EditorIcons"))
popup.add_separator()
popup.add_item("Refresh", AssetContextMenu.REFRESH)
popup.set_item_icon(-1, get_theme_icon(&"Reload", &"EditorIcons"))
else: # If many assets are selected.
popup.add_item("Delete", AssetContextMenu.DELETE_ASSET)
popup.set_item_icon(popup.get_item_index(AssetContextMenu.DELETE_ASSET), get_theme_icon(&"Remove", &"EditorIcons"))
popup.add_item("Refresh", AssetContextMenu.REFRESH)
popup.set_item_icon(-1, get_theme_icon(&"Reload", &"EditorIcons"))
popup.popup(Rect2i(_item_list.get_screen_position() + at_position, Vector2i.ZERO))
func _on_item_list_item_activated(index: int) -> void:
var asset: Dictionary = _item_list.get_item_metadata(index)
open_asset_request.emit(asset.path)
func _on_save_timer_timeout() -> void:
if _curr_lib_path.is_empty():
return
save_library(_curr_lib_path)
class AssetItemList extends ItemList:
func _gui_input(event: InputEvent) -> void:
if event.is_action_pressed(&"ui_text_select_all"):
for i: int in get_item_count():
select(i, false)
accept_event()
func _create_drag_preview(files: PackedStringArray) -> Control:
const MAX_ROWS = 6
var vbox := VBoxContainer.new()
var num_rows := mini(files.size(), MAX_ROWS)
for i: int in num_rows:
var hbox := HBoxContainer.new()
vbox.add_child(hbox)
var icon := TextureRect.new()
icon.set_texture(get_theme_icon(&"File", &"EditorIcons"))
icon.set_stretch_mode(TextureRect.STRETCH_KEEP_CENTERED)
icon.set_size(Vector2(16.0, 16.0))
hbox.add_child(icon)
var label := Label.new()
label.set_text(files[i].get_file().get_basename())
hbox.add_child(label)
if files.size() > num_rows:
var label := Label.new()
label.set_text("%d more files" % int(files.size() - num_rows))
vbox.add_child(label)
return vbox
func _get_drag_data(at_position: Vector2) -> Variant:
var item: int = get_item_at_position(at_position)
if item < 0:
return null
var files := PackedStringArray()
for i: int in get_selected_items():
var asset: Dictionary = get_item_metadata(i)
files.push_back(asset.path)
set_drag_preview(_create_drag_preview(files))
return {"type": "files", "files": files}
func _make_custom_tooltip(_for_text: String) -> Object:
var item: int = get_item_at_position(get_local_mouse_position())
if item < 0:
return null
var asset: Dictionary = get_item_metadata(item)
if asset.is_empty():
return null
var vbox := VBoxContainer.new()
var thumb_rect := TextureRect.new()
thumb_rect.set_expand_mode(TextureRect.EXPAND_IGNORE_SIZE)
thumb_rect.set_h_size_flags(Control.SIZE_SHRINK_CENTER)
thumb_rect.set_v_size_flags(Control.SIZE_SHRINK_CENTER)
thumb_rect.set_custom_minimum_size(Vector2(THUMB_GRID_SIZE, THUMB_GRID_SIZE))
thumb_rect.set_texture(asset.thumb)
vbox.add_child(thumb_rect)
var label := Label.new()
label.set_text(asset.path)
vbox.add_child(label)
return vbox