From ae4b5046edacceaa70b08b6cc7d1d9c8703c9068 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:13:13 +0300 Subject: [PATCH] Implement support for floating windows --- addons/README.md | 2 +- .../dockable_container/dockable_container.gd | 71 +++++++++++++++++- addons/dockable_container/dockable_panel.gd | 2 + addons/dockable_container/floating_window.gd | 73 +++++++++++++++++++ addons/dockable_container/layout.gd | 18 +++++ .../dockable_container/samples/TestScene.tscn | 4 + assets/layouts/Default.tres | 1 + assets/layouts/Tallscreen.tres | 1 + 8 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 addons/dockable_container/floating_window.gd diff --git a/addons/README.md b/addons/README.md index e42e3318d..263765b85 100644 --- a/addons/README.md +++ b/addons/README.md @@ -24,7 +24,7 @@ Files extracted from source: ## godot-dockable-container - Upstream: https://github.com/gilzoide/godot-dockable-container -- Version: Based on [ddff84aa31e466101b4a75c7ff68d3a82701e387](https://github.com/gilzoide/godot-dockable-container/commit/ddff84aa31e466101b4a75c7ff68d3a82701e387), but with changes in layout.gd that add a `save_on_change` variable and a `save()` method. +- Version: Based on [e852cbeeb3f06f62c559898b4cf5756858367766](https://github.com/OverloadedOrama/godot-dockable-container/commit/e852cbeeb3f06f62c559898b4cf5756858367766), but with changes in layout.gd that add a `save_on_change` variable and a `save()` method. - License: [CC0-1.0](https://github.com/gilzoide/godot-dockable-container/blob/main/LICENSE) ## SmartSlicer diff --git a/addons/dockable_container/dockable_container.gd b/addons/dockable_container/dockable_container.gd index 3913a68f5..496d683ab 100644 --- a/addons/dockable_container/dockable_container.gd +++ b/addons/dockable_container/dockable_container.gd @@ -54,6 +54,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 +81,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 +164,61 @@ func _drop_data(_position: Vector2, data) -> void: queue_sort() +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)) + 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: DockablePanel) -> void: + var node_name := tab_container.get_tab_title(tab_container.current_tab) + 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, previous_data := {}) -> 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, 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, 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() + + func set_control_as_current_tab(control: Control) -> void: assert( control.get_parent_control() == self, @@ -195,6 +253,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() @@ -202,7 +270,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 @@ -401,6 +469,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/dockable_panel.gd b/addons/dockable_container/dockable_panel.gd index d522027dd..a32bf6b28 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: diff --git a/addons/dockable_container/floating_window.gd b/addons/dockable_container/floating_window.gd new file mode 100644 index 000000000..386aa18b6 --- /dev/null +++ b/addons/dockable_container/floating_window.gd @@ -0,0 +1,73 @@ +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 +var prevent_data_erasure := false +var _is_initialized := 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 + always_on_top = true + ready.connect(_deserialize.bind(data)) + + +func _ready() -> void: + set_deferred(&"size", Vector2(300, 300)) + 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: + 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()) + + +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 "position" in data: + 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 + + +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 d0eb28938..409418185 100644 --- a/addons/dockable_container/layout.gd +++ b/addons/dockable_container/layout.gd @@ -23,6 +23,14 @@ 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() @export var save_on_change := false: set(value): save_on_change = value @@ -36,6 +44,7 @@ enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER } 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() @@ -182,6 +191,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 80ca9cc6a..311440da1 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 diff --git a/assets/layouts/Default.tres b/assets/layouts/Default.tres index 95b086465..6f753dda1 100644 --- a/assets/layouts/Default.tres +++ b/assets/layouts/Default.tres @@ -175,4 +175,5 @@ hidden_tabs = { "Recorder": true, "Second Canvas": true } +windows = {} save_on_change = false diff --git a/assets/layouts/Tallscreen.tres b/assets/layouts/Tallscreen.tres index 99b36b946..cf1252892 100644 --- a/assets/layouts/Tallscreen.tres +++ b/assets/layouts/Tallscreen.tres @@ -145,4 +145,5 @@ hidden_tabs = { "Recorder": true, "Second Canvas": true } +windows = {} save_on_change = false