-
-
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?
Conversation
Credits to Variable for starting the initial work
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.
I have yet to test this branch to get more insights, but I already have some comments based on the code.
Sorry for taking this long to review this 😅
var new_windows = windows.duplicate(true) | ||
if data.is_empty(): | ||
new_windows.erase(window_name) | ||
else: | ||
new_windows[window_name] = data | ||
windows = new_windows |
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.
Do we really need to duplicate windows
here? If it's about emitting the changed signal, we could just modify _windows
and call _on_root_changed
instead.
@@ -0,0 +1,80 @@ | |||
class_name FloatingWindow |
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.
Not too sure about it, but maybe this class should be called DockableFloatingWindow
to namespace the type, avoiding possible name clashes? The other "private" classes in this package don't use a global class_name
, not sure if this one should too. People shouldn't be creating instances of FloatingWindow
in the editor, for example.
set_deferred(&"size", Vector2(300, 300)) | ||
await get_tree().process_frame | ||
await get_tree().process_frame |
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.
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.
@@ -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) |
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 of DockableContainer
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.
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) |
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.
This method could be implemented in DockablePanel
directly, what do you think? DockablePanel
would then have a toggle_floating_window
signal or something like this, that already passes all required values like current tab index, current tab name, leaf control, etc... and DockableContainer
simply connects it to _toggle_floating
.
By the way, I think the popup doesn't need to be a child of _windows_container
or anything else, setting a popup to a TabContainer ties the popup to it and you don't need to worry about it ever again: it will be managed by the tab container. Doesn't it? 🤔
var data := {} | ||
if content.name in layout.windows: | ||
data = layout.windows[content.name] |
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.
This could use Dictionary.get
var data := {} | |
if content.name in layout.windows: | |
data = layout.windows[content.name] | |
var data := layout.windows.get(content.name, {}) as Dictionary |
if is_instance_valid(get_popup()): | ||
get_popup().queue_free() |
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 we don't add the popup as a child of _window_container
as mentioned above, this would likely be unnecessary. As per the documentation:
This is a required internal node, removing and freeing it may cause a crash.
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. |
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.
Instead of using prevent_data_erasure
, closing all windows and reopening them later, we could use a pool of FloatingWindow nodes, much like we pool DockablePanel and SplitHandle nodes. In _resort
, we would call _untrack_children_after(_windows_container)
after setting all active floating windows, which would delete unused windows, making them close anyway.
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 |
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.
Isn't this continue
unnecessary, since it's the last statement of the for
block?
Implements #4, a continuation of #18 for Godot 4. Many thanks to @Variable-ind for starting the work!
Tab containers now have settings with a "Make Floating" option. This is what makes panels into windows. To make windows into panels again, simply close the window. If the original tab container of the panel still exists when the window closes (meaning that it has more tabs and it did not get freed), the panel returns to that tab container. If it doesn't exist, it is being added to the first available tab container.
Based on my testing, it works well with the embed subwindows option set to both true or false. Windows and their data (position and size) are being stored in the layout, and thus are being remembered between sessions.
Probably the biggest issue with this is that child controls are being reparented to window nodes. Given how windows work in Godot, I don't think soft parenting is possible, especially now that we can have separate windows. But of course I may be wrong, so ideas are welcome!