Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement floating windows #36

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion addons/dockable_container/dockable_container.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -195,14 +253,24 @@ 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()


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


Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions addons/dockable_container/dockable_panel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
73 changes: 73 additions & 0 deletions addons/dockable_container/floating_window.gd
Original file line number Diff line number Diff line change
@@ -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, {})
18 changes: 18 additions & 0 deletions addons/dockable_container/layout.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions addons/dockable_container/samples/TestScene.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading