From 3dabc1ac71973ae05fb8913299e71fff46133c11 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 10 Aug 2024 21:43:03 +0300 Subject: [PATCH 1/9] Implement floating Windows Credits to Variable for starting the initial work --- .../dockable_container/dockable_container.gd | 64 ++++++++++++++++++ addons/dockable_container/floating_window.gd | 65 +++++++++++++++++++ addons/dockable_container/layout.gd | 18 +++++ .../dockable_container/samples/TestScene.tscn | 4 ++ 4 files changed, 151 insertions(+) create mode 100644 addons/dockable_container/floating_window.gd diff --git a/addons/dockable_container/dockable_container.gd b/addons/dockable_container/dockable_container.gd index 3913a68..6aecbf4 100644 --- a/addons/dockable_container/dockable_container.gd +++ b/addons/dockable_container/dockable_container.gd @@ -2,6 +2,9 @@ class_name DockableContainer extends Container +signal window_created +signal window_closed + const SplitHandle := preload("split_handle.gd") const DockablePanel := preload("dockable_panel.gd") const DragNDropPanel := preload("drag_n_drop_panel.gd") @@ -54,6 +57,7 @@ const DragNDropPanel := preload("drag_n_drop_panel.gd") var _layout := DockableLayout.new() var _panel_container := Container.new() +var _windows_container := Container.new() var _split_container := Container.new() var _drag_n_drop_panel := DragNDropPanel.new() var _drag_panel: DockablePanel @@ -80,6 +84,8 @@ func _ready() -> void: _split_container.name = "_split_container" _split_container.mouse_filter = MOUSE_FILTER_PASS _panel_container.add_child(_split_container) + _windows_container.name = "_windows_container" + get_parent().call_deferred("add_child", _windows_container) _drag_n_drop_panel.name = "_drag_n_drop_panel" _drag_n_drop_panel.mouse_filter = MOUSE_FILTER_PASS @@ -161,6 +167,53 @@ func _drop_data(_position: Vector2, data) -> void: queue_sort() +func _add_floating_options(tab_container: TabContainer) -> void: + var options := PopupMenu.new() + options.add_item("Make Floating") + options.id_pressed.connect(_toggle_floating.bind(tab_container)) + options.size.y = 0 + _windows_container.add_child(options) + tab_container.set_popup(options) + + +## required when converting a window back to panel +func _refresh_tabs_visible() -> void: + if tabs_visible: + tabs_visible = false + await get_tree().process_frame + await get_tree().process_frame + tabs_visible = true + + +func _toggle_floating(_id: int, tab_container: TabContainer) -> void: + var node_name := tab_container.get_tab_title(tab_container.current_tab) + var node := find_child(node_name, false) + if node: + _convert_to_window(node) + + +func _convert_to_window(content: Control) -> void: + var old_owner := content.owner + var data := {} + if content.name in layout.windows: + data = layout.windows[content.name] + var window := FloatingWindow.new(content, data) + _windows_container.add_child(window) + window.show() + _refresh_tabs_visible() + window.close_requested.connect(_convert_to_panel.bind(window, old_owner)) + window.data_changed.connect(layout.save_window_properties) + + +func _convert_to_panel(window: FloatingWindow, old_owner: Node) -> void: + var content := window.window_content + window.remove_child(content) + window.destroy() + add_child(content) + content.owner = old_owner + _refresh_tabs_visible() + + func set_control_as_current_tab(control: Control) -> void: assert( control.get_parent_control() == self, @@ -195,6 +248,16 @@ func set_layout(value: DockableLayout) -> void: _layout.changed.disconnect(queue_sort) _layout = value _layout.changed.connect(queue_sort) + for window in _windows_container.get_children(): + if not window.name in _layout.windows and window is FloatingWindow: + window.prevent_data_erasure = true # We don't want to delete data + window.close_requested.emit() # Removes the window + continue + for window: String in _layout.windows.keys(): + var panel := find_child(window, false) + # Only those windows get created which were not previously created + if panel: + _convert_to_window(panel) _layout_dirty = true queue_sort() @@ -401,6 +464,7 @@ func _get_panel(idx: int) -> DockablePanel: panel.hide_single_tab = _hide_single_tab panel.use_hidden_tabs_for_min_size = _use_hidden_tabs_for_min_size panel.set_tabs_rearrange_group(maxi(0, rearrange_group)) + _add_floating_options(panel) _panel_container.add_child(panel) panel.tab_layout_changed.connect(_on_panel_tab_layout_changed.bind(panel)) return panel diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd new file mode 100644 index 0000000..8717dee --- /dev/null +++ b/addons/dockable_container/floating_window.gd @@ -0,0 +1,65 @@ +class_name FloatingWindow +extends Window + +signal data_changed + +var window_content: Control +var _is_initialized := false +var prevent_data_erasure := false + + +func _init(content: Control, data := {}) -> void: + window_content = content + title = window_content.name + name = window_content.name + min_size = window_content.get_minimum_size() + unresizable = false + wrap_controls = true + ready.connect(_deserialize.bind(data)) + + +func _ready() -> void: + set_deferred(&"size", Vector2(300, 300)) + #set_deferred(&"position", DisplayServer.window_get_size() / 2 - size / 2) + position = DisplayServer.window_get_size() / 2 - size / 2 + + +func _input(event: InputEvent) -> void: + if event is InputEventMouse: + if not window_content.get_rect().has_point(event.position) and _is_initialized: + data_changed.emit(name, serialize()) + + +func serialize() -> Dictionary: + return {"size": size, "position": position} + + +func _deserialize(data: Dictionary) -> void: + window_content.get_parent().remove_child(window_content) + window_content.visible = true + window_content.global_position = Vector2.ZERO + add_child(window_content) + size_changed.connect(window_size_changed) + if not data.is_empty(): + if "position" in data: + set_position(data["position"]) + if "size" in data: + set_deferred(&"size", data["size"]) + _is_initialized = true + + +func window_size_changed() -> void: + window_content.size = size + window_content.position = Vector2.ZERO + if _is_initialized: + data_changed.emit(name, serialize()) + + +func destroy() -> void: + size_changed.disconnect(window_size_changed) + queue_free() + + +func _exit_tree() -> void: + if _is_initialized and !prevent_data_erasure: + data_changed.emit(name, {}) diff --git a/addons/dockable_container/layout.gd b/addons/dockable_container/layout.gd index e2a8036..b02df10 100644 --- a/addons/dockable_container/layout.gd +++ b/addons/dockable_container/layout.gd @@ -23,10 +23,19 @@ enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER } if value != _hidden_tabs: _hidden_tabs = value changed.emit() +## A [Dictionary] of [StringName] and [Dictionary], containing data such as position and size. +@export var windows := {}: + get: + return _windows + set(value): + if value != _windows: + _windows = value + changed.emit() var _changed_signal_queued := false var _first_leaf: DockableLayoutPanel var _hidden_tabs: Dictionary +var _windows: Dictionary var _leaf_by_node_name: Dictionary var _root: DockableLayoutNode = DockableLayoutPanel.new() @@ -166,6 +175,15 @@ func set_tab_hidden(name: String, hidden: bool) -> void: _on_root_changed() +func save_window_properties(window_name: StringName, data: Dictionary) -> void: + var new_windows = windows.duplicate(true) + if data.is_empty(): + new_windows.erase(window_name) + else: + new_windows[window_name] = data + windows = new_windows + + func is_tab_hidden(name: String) -> bool: return _hidden_tabs.get(name, false) diff --git a/addons/dockable_container/samples/TestScene.tscn b/addons/dockable_container/samples/TestScene.tscn index 80ca9cc..311440d 100644 --- a/addons/dockable_container/samples/TestScene.tscn +++ b/addons/dockable_container/samples/TestScene.tscn @@ -31,6 +31,8 @@ resource_name = "Layout" script = ExtResource("2") root = SubResource("Resource_hl8y1") hidden_tabs = {} +windows = {} +save_on_change = false [sub_resource type="Resource" id="Resource_ntwfj"] resource_name = "Tabs" @@ -71,6 +73,8 @@ resource_name = "Layout" script = ExtResource("2") root = SubResource("Resource_jhibs") hidden_tabs = {} +windows = {} +save_on_change = false [node name="SampleScene" type="VBoxContainer"] anchors_preset = 15 From b4b74c625ce8d3375515612d87bb505018dd4ec6 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 10 Aug 2024 22:54:31 +0300 Subject: [PATCH 2/9] Remove DockablePanel's popup when exiting scene tree --- addons/dockable_container/dockable_panel.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/dockable_container/dockable_panel.gd b/addons/dockable_container/dockable_panel.gd index d522027..a32bf6b 100644 --- a/addons/dockable_container/dockable_panel.gd +++ b/addons/dockable_container/dockable_panel.gd @@ -40,6 +40,8 @@ func _exit_tree() -> void: active_tab_rearranged.disconnect(_on_tab_changed) tab_selected.disconnect(_on_tab_selected) tab_changed.disconnect(_on_tab_changed) + if is_instance_valid(get_popup()): + get_popup().queue_free() func track_nodes(nodes: Array[Control], new_leaf: DockableLayoutPanel) -> void: From d9f77950deb89fb8f898e7c85b86fffb32f11fb9 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 10 Aug 2024 23:00:33 +0300 Subject: [PATCH 3/9] Fix wrong variable definition order --- addons/dockable_container/floating_window.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd index 8717dee..90df832 100644 --- a/addons/dockable_container/floating_window.gd +++ b/addons/dockable_container/floating_window.gd @@ -4,8 +4,8 @@ extends Window signal data_changed var window_content: Control -var _is_initialized := false var prevent_data_erasure := false +var _is_initialized := false func _init(content: Control, data := {}) -> void: From 7f77574069f408a6425fa34baed39461c0fb160d Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:53:02 +0300 Subject: [PATCH 4/9] Some code and comment improvements --- addons/dockable_container/dockable_container.gd | 15 +++++++-------- addons/dockable_container/floating_window.gd | 12 ++++++------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/addons/dockable_container/dockable_container.gd b/addons/dockable_container/dockable_container.gd index 6aecbf4..6b5af8c 100644 --- a/addons/dockable_container/dockable_container.gd +++ b/addons/dockable_container/dockable_container.gd @@ -2,9 +2,6 @@ class_name DockableContainer extends Container -signal window_created -signal window_closed - const SplitHandle := preload("split_handle.gd") const DockablePanel := preload("dockable_panel.gd") const DragNDropPanel := preload("drag_n_drop_panel.gd") @@ -176,7 +173,7 @@ func _add_floating_options(tab_container: TabContainer) -> void: tab_container.set_popup(options) -## required when converting a window back to panel +## Required when converting a window back to panel. func _refresh_tabs_visible() -> void: if tabs_visible: tabs_visible = false @@ -192,6 +189,7 @@ func _toggle_floating(_id: int, tab_container: TabContainer) -> void: _convert_to_window(node) +## Converts a panel to floating window. func _convert_to_window(content: Control) -> void: var old_owner := content.owner var data := {} @@ -205,6 +203,7 @@ func _convert_to_window(content: Control) -> void: window.data_changed.connect(layout.save_window_properties) +## Converts a floating window into a panel. func _convert_to_panel(window: FloatingWindow, old_owner: Node) -> void: var content := window.window_content window.remove_child(content) @@ -250,12 +249,12 @@ func set_layout(value: DockableLayout) -> void: _layout.changed.connect(queue_sort) for window in _windows_container.get_children(): if not window.name in _layout.windows and window is FloatingWindow: - window.prevent_data_erasure = true # We don't want to delete data - window.close_requested.emit() # Removes the window + window.prevent_data_erasure = true # We don't want to delete data. + window.close_requested.emit() # Removes the window. continue for window: String in _layout.windows.keys(): var panel := find_child(window, false) - # Only those windows get created which were not previously created + # Only those windows get created which were not previously created. if panel: _convert_to_window(panel) _layout_dirty = true @@ -265,7 +264,7 @@ func set_layout(value: DockableLayout) -> void: func set_use_hidden_tabs_for_min_size(value: bool) -> void: _use_hidden_tabs_for_min_size = value for i in range(1, _panel_container.get_child_count()): - var panel = _panel_container.get_child(i) + var panel := _panel_container.get_child(i) as DockablePanel panel.use_hidden_tabs_for_min_size = value diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd index 90df832..349b48d 100644 --- a/addons/dockable_container/floating_window.gd +++ b/addons/dockable_container/floating_window.gd @@ -1,6 +1,7 @@ class_name FloatingWindow extends Window +## Emitted when the window's position or size changes, or when it's closed. signal data_changed var window_content: Control @@ -20,12 +21,12 @@ func _init(content: Control, data := {}) -> void: func _ready() -> void: set_deferred(&"size", Vector2(300, 300)) - #set_deferred(&"position", DisplayServer.window_get_size() / 2 - size / 2) position = DisplayServer.window_get_size() / 2 - size / 2 func _input(event: InputEvent) -> void: if event is InputEventMouse: + # Emit `data_changed` when the window is being moved. if not window_content.get_rect().has_point(event.position) and _is_initialized: data_changed.emit(name, serialize()) @@ -40,11 +41,10 @@ func _deserialize(data: Dictionary) -> void: window_content.global_position = Vector2.ZERO add_child(window_content) size_changed.connect(window_size_changed) - if not data.is_empty(): - if "position" in data: - set_position(data["position"]) - if "size" in data: - set_deferred(&"size", data["size"]) + if "position" in data: + set_position(data["position"]) + if "size" in data: + set_deferred(&"size", data["size"]) _is_initialized = true From e0b900d5323cc69b409a3f892a4719f3a8dca13d Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:26:35 +0300 Subject: [PATCH 5/9] Remember previous tab container when closing the floating window --- .../dockable_container/dockable_container.gd | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/addons/dockable_container/dockable_container.gd b/addons/dockable_container/dockable_container.gd index 6b5af8c..496d683 100644 --- a/addons/dockable_container/dockable_container.gd +++ b/addons/dockable_container/dockable_container.gd @@ -164,7 +164,7 @@ func _drop_data(_position: Vector2, data) -> void: queue_sort() -func _add_floating_options(tab_container: TabContainer) -> void: +func _add_floating_options(tab_container: DockablePanel) -> void: var options := PopupMenu.new() options.add_item("Make Floating") options.id_pressed.connect(_toggle_floating.bind(tab_container)) @@ -182,15 +182,18 @@ func _refresh_tabs_visible() -> void: tabs_visible = true -func _toggle_floating(_id: int, tab_container: TabContainer) -> void: +func _toggle_floating(_id: int, tab_container: DockablePanel) -> void: var node_name := tab_container.get_tab_title(tab_container.current_tab) - var node := find_child(node_name, false) - if node: - _convert_to_window(node) + var node := get_node(node_name) + if is_instance_valid(node): + var tab_position := maxi(tab_container.leaf.find_child(node), 0) + _convert_to_window(node, {"tab_position": tab_position, "tab_container": tab_container}) + else: + print("Node ", node_name, " not found!") ## Converts a panel to floating window. -func _convert_to_window(content: Control) -> void: +func _convert_to_window(content: Control, previous_data := {}) -> void: var old_owner := content.owner var data := {} if content.name in layout.windows: @@ -199,17 +202,20 @@ func _convert_to_window(content: Control) -> void: _windows_container.add_child(window) window.show() _refresh_tabs_visible() - window.close_requested.connect(_convert_to_panel.bind(window, old_owner)) + window.close_requested.connect(_convert_to_panel.bind(window, old_owner, previous_data)) window.data_changed.connect(layout.save_window_properties) ## Converts a floating window into a panel. -func _convert_to_panel(window: FloatingWindow, old_owner: Node) -> void: +func _convert_to_panel(window: FloatingWindow, old_owner: Node, previous_data := {}) -> void: var content := window.window_content window.remove_child(content) window.destroy() add_child(content) content.owner = old_owner + if previous_data.has("tab_container") and is_instance_valid(previous_data["tab_container"]): + var tab_position := previous_data.get("tab_position", 0) as int + previous_data["tab_container"].leaf.insert_node(tab_position, content) _refresh_tabs_visible() From 0614c419e125dd0bcea5a837958faea8bd4d9b0a Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:43:16 +0300 Subject: [PATCH 6/9] Set correct floating window position when embed subwindows is false --- addons/dockable_container/floating_window.gd | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd index 349b48d..2f92196 100644 --- a/addons/dockable_container/floating_window.gd +++ b/addons/dockable_container/floating_window.gd @@ -21,7 +21,12 @@ func _init(content: Control, data := {}) -> void: func _ready() -> void: set_deferred(&"size", Vector2(300, 300)) - position = DisplayServer.window_get_size() / 2 - size / 2 + await get_tree().process_frame + await get_tree().process_frame + if get_tree().current_scene.get_window().gui_embed_subwindows: + position = DisplayServer.window_get_size() / 2 - size / 2 + else: + position = DisplayServer.screen_get_usable_rect().size / 2 - size / 2 func _input(event: InputEvent) -> void: @@ -42,7 +47,9 @@ func _deserialize(data: Dictionary) -> void: add_child(window_content) size_changed.connect(window_size_changed) if "position" in data: - set_position(data["position"]) + await get_tree().process_frame + await get_tree().process_frame + position = data["position"] if "size" in data: set_deferred(&"size", data["size"]) _is_initialized = true From e852cbeeb3f06f62c559898b4cf5756858367766 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:52:17 +0300 Subject: [PATCH 7/9] Set always on top to be true for floating windows --- addons/dockable_container/floating_window.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd index 2f92196..386aa18 100644 --- a/addons/dockable_container/floating_window.gd +++ b/addons/dockable_container/floating_window.gd @@ -16,6 +16,7 @@ func _init(content: Control, data := {}) -> void: min_size = window_content.get_minimum_size() unresizable = false wrap_controls = true + always_on_top = true ready.connect(_deserialize.bind(data)) From 26da5206a92d561fb47f95b89996463f0b7588fb Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:01:28 +0200 Subject: [PATCH 8/9] Fix bug where the child windows of floating windows appear behind them --- addons/dockable_container/floating_window.gd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd index 386aa18..fcb77aa 100644 --- a/addons/dockable_container/floating_window.gd +++ b/addons/dockable_container/floating_window.gd @@ -28,6 +28,12 @@ func _ready() -> void: position = DisplayServer.window_get_size() / 2 - size / 2 else: position = DisplayServer.screen_get_usable_rect().size / 2 - size / 2 + # Enable always_on_top for all child windows. + # TODO: Remove the loop when this bug gets fixed in Godot's side. + # Probably when https://github.com/godotengine/godot/issues/92848 is closed. + for dialog_child in find_children("", "Window", true, false): + if dialog_child is Window: + dialog_child.always_on_top = always_on_top func _input(event: InputEvent) -> void: From 3f10b9b6f24311090a6010da82d6eaf110e92836 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:01:57 +0200 Subject: [PATCH 9/9] Update floating_window.gd --- addons/dockable_container/floating_window.gd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd index fcb77aa..c44a31a 100644 --- a/addons/dockable_container/floating_window.gd +++ b/addons/dockable_container/floating_window.gd @@ -28,7 +28,8 @@ func _ready() -> void: position = DisplayServer.window_get_size() / 2 - size / 2 else: position = DisplayServer.screen_get_usable_rect().size / 2 - size / 2 - # Enable always_on_top for all child windows. + # Enable always_on_top for all child windows, + # to fix a bug where the child windows of floating windows appear behind them. # TODO: Remove the loop when this bug gets fixed in Godot's side. # Probably when https://github.com/godotengine/godot/issues/92848 is closed. for dialog_child in find_children("", "Window", true, false):