# Copyright © 2023-2026 Cory Petkovsek, Roope Palmroos, and Contributors. # Asset Dock for Terrain3D @tool extends PanelContainer signal confirmation_closed signal confirmation_confirmed signal confirmation_canceled const ES_DOCK_TILE_SIZE: String = "terrain3d/dock/tile_size" const ES_DOCK_PINNED: String = "terrain3d/dock/always_on_top" const ES_DOCK_TAB: String = "terrain3d/dock/tab" var texture_list: ListContainer var mesh_list: ListContainer var current_list: ListContainer var _updating_list: bool var pinned_btn: Button var size_slider: HSlider var box: BoxContainer var buttons: BoxContainer var textures_btn: Button var meshes_btn: Button var asset_container: ScrollContainer var confirm_dialog: ConfirmationDialog var _confirmed: bool = false var search_box: TextEdit var search_button: Button #DEPRECATED 4.5 #class EdDock extends EditorDock: #func _update_layout(layout: int) -> void: #layout 1 vertical, 2 horizontal, 4 window #print("Terrain3DAssetDock: _update_layout called with: ", layout) var _dock: MarginContainer #DEPRECATED 4.5 - Use EdDock var _initialized: bool = false var plugin: EditorPlugin var window: Window func _notification(what: int) -> void: if what == NOTIFICATION_ENTER_TREE: await get_tree().process_frame update_layout() func initialize(p_plugin: EditorPlugin) -> void: if p_plugin: plugin = p_plugin _dock = ClassDB.instantiate("EditorDock") #DEPRECATED 4.5 - EdDock.new() _dock.title = "Terrain3D" _dock.dock_icon = preload("../icons/terrain3d.svg") _dock.default_slot = 8 #DEPRECATED 4.5 - EditorDock.DockSlot.DOCK_SLOT_BOTTOM _dock.closable = false _dock.available_layouts = 0x7 #DEPRECATED 4.5 - EditorDock.DOCK_LAYOUT_ALL _dock.add_child(self) plugin.add_dock(_dock) _dock.open() _dock.make_visible() pinned_btn = $Box/Buttons/Pinned size_slider = $Box/Buttons/SizeSlider size_slider.owner = null box = $Box buttons = $Box/Buttons textures_btn = $Box/Buttons/TexturesBtn meshes_btn = $Box/Buttons/MeshesBtn asset_container = $Box/ScrollContainer search_box = $Box/Buttons/SearchBox search_box.owner = null # Scale left column width to editor scale var editor_scale: float = EditorInterface.get_editor_scale() search_box.custom_minimum_size = Vector2(100. * editor_scale, 30. * editor_scale) search_button = $Box/Buttons/SearchBox/SearchButton texture_list = ListContainer.new() texture_list.name = "TextureList" texture_list.plugin = plugin texture_list.type = Terrain3DAssets.TYPE_TEXTURE asset_container.add_child(texture_list, true) mesh_list = ListContainer.new() mesh_list.name = "MeshList" mesh_list.plugin = plugin mesh_list.type = Terrain3DAssets.TYPE_MESH mesh_list.visible = false asset_container.add_child(mesh_list, true) current_list = texture_list load_editor_settings() # Connect signals resized.connect(update_layout) textures_btn.pressed.connect(_on_textures_pressed) meshes_btn.pressed.connect(_on_meshes_pressed) pinned_btn.toggled.connect(_on_pin_changed) pinned_btn.owner = null size_slider.value_changed.connect(_on_slider_changed) plugin.ui.toolbar.tool_changed.connect(_on_tool_changed) meshes_btn.add_theme_font_size_override( "font_size", int(16. * EditorInterface.get_editor_scale()) ) textures_btn.add_theme_font_size_override( "font_size", int(16. * EditorInterface.get_editor_scale()) ) search_box.text_changed.connect(_on_search_text_changed) search_button.pressed.connect(_on_search_button_pressed) confirm_dialog = ConfirmationDialog.new() add_child(confirm_dialog, true) confirm_dialog.hide() confirm_dialog.confirmed.connect( func(): _confirmed = true confirmation_closed.emit() confirmation_confirmed.emit() ) confirm_dialog.canceled.connect( func(): _confirmed = false confirmation_closed.emit() confirmation_canceled.emit() ) # Setup styles set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel")) # Avoid saving icon resources in tscn when editing w/ a tool script if EditorInterface.get_edited_scene_root() != self: pinned_btn.icon = get_theme_icon("Pin", "EditorIcons") pinned_btn.text = "" search_button.icon = get_theme_icon("Search", "EditorIcons") _initialized = true update_dock() update_layout() func _gui_input(p_event: InputEvent) -> void: if p_event is InputEventMouseButton: if search_box.has_focus(): if plugin.debug: print("Terrain3DAssetDock: _on_box_gui_input: search_box releasing focus") search_box.release_focus() ## Dock placement func remove_dock(p_force: bool = false) -> void: plugin.remove_dock(_dock) func update_dock() -> void: if not _initialized: return update_assets() _dock.make_visible() func update_layout() -> void: if not _initialized: return if plugin.debug > 1: print("Terrain3DAssetDock: update_layout") ## Detect if we have a new window from `Make floating` and grab it if not window: if get_parent().get_parent().get_parent() is Window: window = get_parent().get_parent().get_parent() _on_pin_changed(pinned_btn.button_pressed) plugin.godot_editor_window.mouse_entered.connect(_on_godot_window_entered) return # Displaying will call this function again # Check if window was just freed. Freed objects have a different hash than null elif hash(window) != hash(null): window = null plugin.godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered) return # Will call this function again upon display ## Vertical layout: buttons on top if size.x < 700: box.vertical = true buttons.vertical = false search_box.reparent(box) box.move_child(search_box, 1) size_slider.reparent(box) box.move_child(size_slider, 2) pinned_btn.reparent(buttons) else: # Wide layout: buttons on left box.vertical = false buttons.vertical = true search_box.reparent(buttons) buttons.move_child(search_box, 0) size_slider.reparent(buttons) buttons.move_child(size_slider, 3) pinned_btn.reparent(box) pinned_btn.visible = is_instance_valid(window) save_editor_settings() # When a floating asset dock has focus and the mouse returns, grab focus so cursor decal works func _on_godot_window_entered() -> void: if plugin.debug > 1: print("Terrain3DAssetDock: _on_godot_window_entered") if is_instance_valid(window) and window.has_focus(): plugin.godot_editor_window.grab_focus() func set_selected_by_asset_id(p_id: int) -> void: search_box.text = "" _on_search_text_changed() current_list.set_selected_id(p_id) func _on_search_text_changed() -> void: if plugin.debug: print("Terrain3DAssetDock: _on_search_text_changed: ", search_box.text) search_box.text = search_box.text.strip_escapes() var len: int = search_box.text.length() if len > 0: search_box.set_caret_column(len) search_button.icon = get_theme_icon("Close", "EditorIcons") else: search_button.icon = get_theme_icon("Search", "EditorIcons") mesh_list.search_text = search_box.text texture_list.search_text = search_box.text current_list.update_asset_list() current_list.set_selected_id(0) func _on_search_button_pressed() -> void: if plugin.debug: print("Terrain3DAssetDock: _on_search_button_pressed") if search_box.text.length() > 0: search_box.text = "" _on_search_text_changed() else: if plugin.debug: print("Terrain3DAssetDock: _on_search_button_pressed: Search box grabbing focus") search_box.grab_focus() ## Dock Button handlers func _on_pin_changed(toggled: bool) -> void: if window: window.always_on_top = pinned_btn.button_pressed save_editor_settings() func _on_slider_changed(value: float) -> void: # Set both lists so they match if texture_list: texture_list.set_entry_width(value) if mesh_list: mesh_list.set_entry_width(value) save_editor_settings() # Hack to trigger ScrollContainer::_reposition_children() to update size of scroll bar handle asset_container.layout_direction = Control.LAYOUT_DIRECTION_LTR asset_container.layout_direction = Control.LAYOUT_DIRECTION_INHERITED func _on_textures_pressed() -> void: if plugin.debug: print("Terrain3DAssetDock: _on_textures_pressed") if _updating_list or current_list == texture_list: return _updating_list = true current_list = texture_list texture_list.visible = true mesh_list.visible = false textures_btn.set_pressed_no_signal(true) meshes_btn.set_pressed_no_signal(false) texture_list.update_asset_list() if plugin.is_terrain_valid(): EditorInterface.edit_node(plugin.terrain) save_editor_settings() _updating_list = false func _on_meshes_pressed() -> void: if plugin.debug: print("Terrain3DAssetDock: _on_meshes_pressed") if _updating_list or current_list == mesh_list: return _updating_list = true current_list = mesh_list mesh_list.visible = true texture_list.visible = false meshes_btn.set_pressed_no_signal(true) textures_btn.set_pressed_no_signal(false) mesh_list.update_asset_list() if plugin.is_terrain_valid(): EditorInterface.edit_node(plugin.terrain) save_editor_settings() _updating_list = false func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: if plugin.debug: print("Terrain3DAssetDock: _on_tool_changed: ", p_tool, ", ", p_operation) if p_tool == Terrain3DEditor.INSTANCER: _on_meshes_pressed() elif p_tool in [Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS]: _on_textures_pressed() ## Update Dock Contents func update_assets() -> void: if plugin.debug: print( "Terrain3DAssetDock: update_assets: ", plugin.terrain.assets if plugin.terrain else "" ) if not _initialized: return # Verify signals to individual lists if plugin.is_terrain_valid() and plugin.terrain.assets: if not plugin.terrain.assets.textures_changed.is_connected(texture_list.update_asset_list): plugin.terrain.assets.textures_changed.connect(texture_list.update_asset_list) if not plugin.terrain.assets.meshes_changed.is_connected(mesh_list.update_asset_list): plugin.terrain.assets.meshes_changed.connect(mesh_list.update_asset_list) current_list.update_asset_list() ## Manage Editor Settings func load_editor_settings() -> void: # Remove old editor settings const ES_DOCK: String = "terrain3d/dock/" for setting in ["slot", "floating", "window_position", "window_size"]: plugin.erase_setting(ES_DOCK + setting) pinned_btn.button_pressed = plugin.get_setting(ES_DOCK_PINNED, true) size_slider.value = plugin.get_setting(ES_DOCK_TILE_SIZE, 90) _on_slider_changed(size_slider.value) # TODO Don't save tab until thumbnail generation more reliable #if plugin.get_setting(ES_DOCK_TAB, 0) == 1: # _on_meshes_pressed() func save_editor_settings() -> void: if not _initialized: return plugin.set_setting(ES_DOCK_TILE_SIZE, size_slider.value) plugin.set_setting(ES_DOCK_PINNED, pinned_btn.button_pressed) # TODO Don't save tab until thumbnail generation more reliable # plugin.set_setting(ES_DOCK_TAB, 0 if current_list == texture_list else 1) ############################################################## ## class ListContainer ############################################################## class ListContainer: extends Container var plugin: EditorPlugin var type := Terrain3DAssets.TYPE_TEXTURE var entries: Array[ListEntry] var selected_id: int = 0 var height: float = 0. var width: float = 90. var focus_style: StyleBox var _clearing_resource: bool = false var search_text: String = "" func _ready() -> void: set_v_size_flags(SIZE_EXPAND_FILL) set_h_size_flags(SIZE_EXPAND_FILL) add_theme_color_override("font_color", Color.WHITE) add_theme_color_override("font_shadow_color", Color.BLACK) add_theme_constant_override("shadow_offset_x", 1) add_theme_constant_override("shadow_offset_y", 1) func clear() -> void: for e in entries: e.get_parent().remove_child(e) e.queue_free() entries.clear() func update_asset_list() -> void: if plugin.debug: print("Terrain3DListContainer ", name, ": update_asset_list") clear() # Grab terrain var t: Terrain3D = plugin.get_terrain() if not (t and t.assets): return if type == Terrain3DAssets.TYPE_TEXTURE: var texture_count: int = t.assets.get_texture_count() for i in texture_count: var texture: Terrain3DTextureAsset = t.assets.get_texture(i) add_item(texture) if texture_count < Terrain3DAssets.MAX_TEXTURES: add_item() else: if plugin.terrain: plugin.terrain.assets.create_mesh_thumbnails() var mesh_count: int = t.assets.get_mesh_count() for i in mesh_count: var mesh: Terrain3DMeshAsset = t.assets.get_mesh_asset(i) add_item(mesh) if mesh_count < Terrain3DAssets.MAX_MESHES: add_item() set_selected_id(selected_id) func add_item(p_resource: Resource = null) -> void: var entry: ListEntry = ListEntry.new() entry.focus_style = focus_style entry.set_edited_resource(p_resource) if not entry.get_resource_name().containsn(search_text) and not search_text == "": return var res_id: int = p_resource.id if p_resource else entries.size() entry.hovered.connect(_on_resource_hovered.bind(res_id)) entry.clicked.connect(clicked_id.bind(entries.size())) entry.inspected.connect(_on_resource_inspected) entry.changed.connect(_on_resource_changed.bind(res_id)) entry.type = type add_child(entry, true) entries.push_back(entry) if p_resource: if not p_resource.id_changed.is_connected(set_selected_after_swap): p_resource.id_changed.connect(set_selected_after_swap) func _on_resource_hovered(p_id: int): if type == Terrain3DAssets.TYPE_MESH: if plugin.terrain: plugin.terrain.assets.create_mesh_thumbnails(p_id) func set_selected_after_swap( p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int ) -> void: EditorInterface.mark_scene_as_unsaved() set_selected_id(clamp(p_new_id, 0, entries.size() - 2)) func clicked_id(p_id: int) -> void: # Select Tool if clicking an asset plugin.select_terrain() if ( type == Terrain3DAssets.TYPE_TEXTURE and not ( plugin.editor.get_tool() in [Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS] ) ): var paint_btn: Button = plugin.ui.toolbar.get_node_or_null("PaintTexture") if paint_btn: paint_btn.set_pressed(true) plugin.ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE) elif ( type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER ): var instancer_btn: Button = plugin.ui.toolbar.get_node_or_null("InstanceMeshes") if instancer_btn: instancer_btn.set_pressed(true) plugin.ui._on_tool_changed(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD) set_selected_id(p_id) func set_selected_id(p_id: int) -> void: # "Add new" is the final entry only when search box is blank var max_id: int = max(0, entries.size() - (1 if search_text else 2)) if plugin.debug: print( "Terrain3DListContainer ", name, ": set_selected_id: ", selected_id, " to ", clamp(p_id, 0, max_id) ) selected_id = clamp(p_id, 0, max_id) for i in entries.size(): var entry: ListEntry = entries[i] entry.set_selected(i == selected_id) plugin.ui._on_setting_changed() func get_selected_asset_id() -> int: # "Add new" is the final entry only when search box is blank var max_id: int = max(0, entries.size() - (1 if search_text else 2)) var id: int = clamp(selected_id, 0, max_id) if plugin.debug: print( "Terrain3DListContainer ", name, ": get_selected_asset_id: selected_id: ", selected_id, ", clamped: ", id, ", entries: ", entries.size() ) if id >= entries.size(): return 0 var res: Resource = entries[id].resource if not res: return 0 if type == Terrain3DAssets.AssetType.TYPE_MESH: return (res as Terrain3DMeshAsset).id else: return (res as Terrain3DTextureAsset).id func _on_resource_inspected(p_resource: Resource) -> void: await get_tree().process_frame EditorInterface.edit_resource(p_resource) func _on_resource_changed(p_resource: Resource, p_id: int) -> void: if not p_resource and _clearing_resource: return if not p_resource: if plugin.debug: print( "Terrain3DListContainer ", name, ": _on_resource_changed: removing asset ID: ", p_id ) _clearing_resource = true var asset_dock: Control = get_parent().get_parent().get_parent() if type == Terrain3DAssets.TYPE_TEXTURE: asset_dock.confirm_dialog.dialog_text = "Are you sure you want to clear this texture?" else: asset_dock.confirm_dialog.dialog_text = "Are you sure you want to clear this mesh and delete all instances?" asset_dock.confirm_dialog.popup_centered() await asset_dock.confirmation_closed if not asset_dock._confirmed: update_asset_list() _clearing_resource = false return if not plugin.is_terrain_valid(): plugin.select_terrain() await get_tree().process_frame if plugin.is_terrain_valid(): if type == Terrain3DAssets.TYPE_TEXTURE: plugin.terrain.assets.set_texture(p_id, p_resource) else: plugin.terrain.assets.set_mesh_asset(p_id, p_resource) # If removing an entry, clear inspector if not p_resource: EditorInterface.inspect_object(null) _clearing_resource = false func set_entry_width(value: float) -> void: var min_width: float = 90.0 * max(1.0, EditorInterface.get_editor_scale()) width = clamp(value, min_width, 512.0) redraw() func get_entry_width() -> float: return width func redraw() -> void: height = 0 var id: int = 0 var separation: float = 2. var columns: int = 3 columns = clamp(size.x / width, 1, 100) var tile_size: Vector2 = Vector2(width, width) - Vector2(separation, separation) var name_font_size := int( clamp(tile_size.x / 12., 12., 16.) * EditorInterface.get_editor_scale() ) for c in get_children(): if is_instance_valid(c): c.size = tile_size c.position = ( Vector2(id % columns, id / columns) * width + Vector2(separation / columns, separation / columns) ) height = max(height, c.position.y + width) id += 1 c.name_label.add_theme_font_size_override("font_size", name_font_size) # Needed to enable ScrollContainer scroll bar func _get_minimum_size() -> Vector2: return Vector2(0, height) func _notification(p_what) -> void: if p_what == NOTIFICATION_SORT_CHILDREN: redraw() ############################################################## ## class ListEntry ############################################################## class ListEntry: extends MarginContainer signal hovered signal clicked signal changed(resource: Resource) signal inspected(resource: Resource) var resource: Resource var type := Terrain3DAssets.TYPE_TEXTURE var _thumbnail: Texture2D var drop_data: bool = false var is_hovered: bool = false var is_selected: bool = false var name_label: Label var button_row: FlowContainer var button_enabled: TextureButton var button_edit: TextureButton var spacer: Control var button_clear: TextureButton @onready var focus_style: StyleBox = get_theme_stylebox("focus", "Button").duplicate() @onready var background: StyleBox = get_theme_stylebox("pressed", "Button") @onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons") @onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons") @onready var enabled_icon: Texture2D = get_theme_icon("GuiVisibilityVisible", "EditorIcons") @onready var disabled_icon: Texture2D = get_theme_icon("GuiVisibilityHidden", "EditorIcons") @onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons") func _ready() -> void: name = "ListEntry" custom_minimum_size = Vector2i(86., 86.) mouse_filter = Control.MOUSE_FILTER_PASS add_theme_constant_override("margin_top", 5) add_theme_constant_override("margin_left", 5) add_theme_constant_override("margin_right", 5) setup_buttons() setup_label() focus_style.set_border_width_all(2) focus_style.set_border_color(Color(1, 1, 1, .67)) func setup_buttons() -> void: destroy_buttons() button_row = FlowContainer.new() button_enabled = TextureButton.new() button_edit = TextureButton.new() spacer = Control.new() button_clear = TextureButton.new() var icon_size: Vector2 = Vector2(12, 12) button_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL button_row.alignment = FlowContainer.ALIGNMENT_CENTER button_row.mouse_filter = Control.MOUSE_FILTER_PASS add_child(button_row, true) if type == Terrain3DAssets.TYPE_MESH: button_enabled.set_texture_normal(enabled_icon) button_enabled.set_texture_pressed(disabled_icon) button_enabled.set_custom_minimum_size(icon_size) button_enabled.set_h_size_flags(Control.SIZE_SHRINK_END) button_enabled.set_visible(resource != null) button_enabled.tooltip_text = "Enable Instances" button_enabled.toggle_mode = true button_enabled.mouse_filter = Control.MOUSE_FILTER_PASS button_enabled.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button_enabled.pressed.connect(_on_enable) button_row.add_child(button_enabled, true) button_edit.set_texture_normal(edit_icon) button_edit.set_custom_minimum_size(icon_size) button_edit.set_h_size_flags(Control.SIZE_SHRINK_END) button_edit.set_visible(resource != null) button_edit.tooltip_text = "Edit Asset" button_edit.mouse_filter = Control.MOUSE_FILTER_PASS button_edit.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button_edit.pressed.connect(_on_edit) button_row.add_child(button_edit, true) spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL spacer.mouse_filter = Control.MOUSE_FILTER_PASS button_row.add_child(spacer, true) button_clear.set_texture_normal(clear_icon) button_clear.set_custom_minimum_size(icon_size) button_clear.set_h_size_flags(Control.SIZE_SHRINK_END) button_clear.set_visible(resource != null) button_clear.tooltip_text = "Clear Asset" button_clear.mouse_filter = Control.MOUSE_FILTER_PASS button_clear.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button_clear.pressed.connect(_on_clear) button_row.add_child(button_clear, true) func destroy_buttons() -> void: if button_row: button_row.free() button_row = null if button_enabled: button_enabled.free() button_enabled = null if button_edit: button_edit.free() button_edit = null if spacer: spacer.free() spacer = null if button_clear: button_clear.free() button_clear = null func get_resource_name() -> StringName: if resource: if resource is Terrain3DMeshAsset: return (resource as Terrain3DMeshAsset).get_name() elif resource is Terrain3DTextureAsset: return (resource as Terrain3DTextureAsset).get_name() return "" func setup_label() -> void: name_label = Label.new() name_label.name = "MeshLabel" name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL name_label.add_theme_font_size_override( "font_size", int(14. * EditorInterface.get_editor_scale()) ) name_label.add_theme_color_override("font_color", Color.WHITE) name_label.add_theme_color_override("font_shadow_color", Color.BLACK) name_label.add_theme_constant_override("shadow_offset_x", 1) name_label.add_theme_constant_override("shadow_offset_y", 1) name_label.visible = false name_label.autowrap_mode = TextServer.AUTOWRAP_OFF name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS add_child(name_label, true) func _notification(p_what) -> void: match p_what: NOTIFICATION_PREDELETE: destroy_buttons() NOTIFICATION_DRAW: # Hide spacer if icons are crowding small textures spacer.visible = size.x > 94. or type == Terrain3DAssets.TYPE_TEXTURE var rect: Rect2 = Rect2(Vector2.ZERO, get_size()) if !resource: draw_style_box(background, rect) draw_texture(add_icon, (get_size() / 2) - (add_icon.get_size() / 2)) else: if type == Terrain3DAssets.TYPE_TEXTURE: _thumbnail = resource.get_albedo_texture() else: _thumbnail = resource.get_thumbnail() if _thumbnail: draw_texture_rect(_thumbnail, rect, false) texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS else: draw_rect(rect, Color(.15, .15, .15, 1.)) if type == Terrain3DAssets.TYPE_TEXTURE: self_modulate = resource.get_albedo_color() else: button_enabled.set_pressed_no_signal(!resource.is_enabled()) if drop_data: draw_style_box(focus_style, rect) if is_hovered: draw_rect(rect, Color(1, 1, 1, 0.2)) if is_selected: draw_style_box(focus_style, rect) NOTIFICATION_MOUSE_ENTER: if not resource: name_label.visible = false else: name_label.visible = true is_hovered = true name_label.text = get_resource_name() tooltip_text = get_resource_name() emit_signal("hovered") queue_redraw() NOTIFICATION_MOUSE_EXIT: name_label.visible = false is_hovered = false drop_data = false queue_redraw() func _gui_input(p_event: InputEvent) -> void: if p_event is InputEventMouseButton: if p_event.is_pressed(): match p_event.get_button_index(): MOUSE_BUTTON_LEFT: # If `Add new` is clicked if !resource: if type == Terrain3DAssets.TYPE_TEXTURE: set_edited_resource(Terrain3DTextureAsset.new(), false) else: set_edited_resource(Terrain3DMeshAsset.new(), false) _on_edit() else: emit_signal("clicked") MOUSE_BUTTON_RIGHT: if resource: _on_edit() MOUSE_BUTTON_MIDDLE: if resource: _on_clear() func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool: drop_data = false if typeof(p_data) == TYPE_DICTIONARY: if p_data.files.size() == 1: queue_redraw() drop_data = true return drop_data func _drop_data(p_at_position: Vector2, p_data: Variant) -> void: if typeof(p_data) == TYPE_DICTIONARY: var res: Resource = load(p_data.files[0]) if res is Texture2D and type == Terrain3DAssets.TYPE_TEXTURE: var ta := Terrain3DTextureAsset.new() if resource is Terrain3DTextureAsset: ta.id = resource.id ta.set_albedo_texture(res) set_edited_resource(ta, false) resource = ta elif res is Terrain3DTextureAsset and type == Terrain3DAssets.TYPE_TEXTURE: if resource is Terrain3DTextureAsset: res.id = resource.id set_edited_resource(res, false) elif res is PackedScene and type == Terrain3DAssets.TYPE_MESH: if not resource: resource = Terrain3DMeshAsset.new() set_edited_resource(resource, false) resource.set_scene_file(res) elif res is Terrain3DMeshAsset and type == Terrain3DAssets.TYPE_MESH: if resource is Terrain3DMeshAsset: res.id = resource.id set_edited_resource(res, false) emit_signal("clicked") emit_signal("inspected", resource) func set_edited_resource(p_res: Resource, p_no_signal: bool = true) -> void: resource = p_res if resource: if not resource.setting_changed.is_connected(_on_resource_changed): resource.setting_changed.connect(_on_resource_changed) if resource is Terrain3DTextureAsset: if not resource.file_changed.is_connected(_on_resource_changed): resource.file_changed.connect(_on_resource_changed) elif resource is Terrain3DMeshAsset: if not resource.instancer_setting_changed.is_connected(_on_resource_changed): resource.instancer_setting_changed.connect(_on_resource_changed) if button_clear: button_clear.set_visible(resource != null) queue_redraw() if not p_no_signal: emit_signal("changed", resource) func _on_resource_changed(_value: int = 0) -> void: queue_redraw() emit_signal("changed", resource) func set_selected(value: bool) -> void: if not is_inside_tree(): #push_error("not in tree") return is_selected = value if is_selected: # Handle scrolling to show the selected item await get_tree().process_frame if is_inside_tree(): get_parent().get_parent().get_v_scroll_bar().ratio = ( position.y / get_parent().size.y ) queue_redraw() func _on_clear() -> void: if resource: name_label.hide() set_edited_resource(null, false) func _on_edit() -> void: emit_signal("clicked") emit_signal("inspected", resource) func _on_enable() -> void: if resource is Terrain3DMeshAsset: resource.set_enabled(!resource.is_enabled()) func _format_number(num: int) -> String: var is_negative: bool = num < 0 var str_num: String = str(abs(num)) var result: String = "" var length: int = str_num.length() for i in length: result = str_num[length - 1 - i] + result if i < length - 1 and (i + 1) % 3 == 0: result = "," + result return "-" + result if is_negative else result