-
-
Notifications
You must be signed in to change notification settings - Fork 21
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
base: main
Are you sure you want to change the base?
Changes from all commits
3dabc1a
b4b74c6
d9f7795
7f77574
e0b900d
0614c41
e852cbe
26da520
3f10b9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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) | ||||||||||
Comment on lines
+167
to
+173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method could be implemented in By the way, I think the popup doesn't need to be a child of |
||||||||||
|
||||||||||
|
||||||||||
## 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] | ||||||||||
Comment on lines
+198
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could use
Suggested change
|
||||||||||
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,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. | ||||||||||
Comment on lines
+257
to
+259
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using |
||||||||||
continue | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this |
||||||||||
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 | ||||||||||
|
||||||||||
|
||||||||||
|
@@ -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 | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't add the popup as a child of
|
||
|
||
|
||
func track_nodes(nodes: Array[Control], new_leaf: DockableLayoutPanel) -> void: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
class_name FloatingWindow | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not too sure about it, but maybe this class should be called |
||
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 | ||
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe there is another signal that makes more sense that we could wait here instead of generically waiting for 2 frames? If there isn't any, just ignore this comment. |
||
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 | ||
# 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): | ||
if dialog_child is Window: | ||
dialog_child.always_on_top = always_on_top | ||
|
||
|
||
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, {}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+179
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need to duplicate |
||
|
||
|
||
func is_tab_hidden(name: String) -> bool: | ||
return _hidden_tabs.get(name, false) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this script creates
_windows_container
, it should also destroy it, likely in_exit_tree
.By the way, can't
_windows_container
be a direct child ofDockableContainer
instead of a sibling? It could be a special child that is not docked, just as_panel_container
,_split_container
and_drag_n_drop_panel
are.