From 30b6df629d0f944c207d4158e31378882d5e6494 Mon Sep 17 00:00:00 2001 From: Chris Cranford Date: Sat, 6 Jan 2024 10:08:55 -0500 Subject: [PATCH] GH-61 Add floating window mode --- src/editor/about_dialog.cpp | 2 +- src/editor/editor.cpp | 3 + src/editor/graph/actions/action_menu.cpp | 2 +- src/editor/graph/graph_edit.cpp | 15 +- src/editor/graph/graph_node.cpp | 2 +- src/editor/graph/graph_node_pin.cpp | 2 +- src/editor/main_view.cpp | 57 ++- src/editor/main_view.h | 21 +- src/editor/script_view.cpp | 43 +- src/editor/script_view.h | 3 +- src/editor/window_wrapper.cpp | 476 +++++++++++++++++++++++ src/editor/window_wrapper.h | 104 +++++ src/plugin/plugin.cpp | 82 +++- src/plugin/plugin.h | 13 +- 14 files changed, 766 insertions(+), 59 deletions(-) create mode 100644 src/editor/window_wrapper.cpp create mode 100644 src/editor/window_wrapper.h diff --git a/src/editor/about_dialog.cpp b/src/editor/about_dialog.cpp index bc3d1ec8..609fa6c6 100644 --- a/src/editor/about_dialog.cpp +++ b/src/editor/about_dialog.cpp @@ -119,7 +119,7 @@ void OrchestratorAboutDialog::_bind_methods() void OrchestratorAboutDialog::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { connect("theme_changed", callable_mp(this, &OrchestratorAboutDialog::_on_theme_changed)); _version_btn->connect("pressed", callable_mp(this, &OrchestratorAboutDialog::_on_version_pressed)); diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 4d38576f..0756fc22 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -26,6 +26,7 @@ #include "editor/graph/nodes/graph_node_comment.h" #include "editor/graph/nodes/graph_node_default.h" #include "editor/graph/pins/graph_node_pins.h" +#include "editor/window_wrapper.h" #include "main_view.h" #include "script_view.h" @@ -54,6 +55,8 @@ void register_editor_classes() // View components ORCHESTRATOR_REGISTER_INTERNAL_CLASS(OrchestratorAboutDialog) + ORCHESTRATOR_REGISTER_INTERNAL_CLASS(OrchestratorScreenSelect) + ORCHESTRATOR_REGISTER_INTERNAL_CLASS(OrchestratorWindowWrapper) ORCHESTRATOR_REGISTER_INTERNAL_CLASS(OrchestratorMainView) ORCHESTRATOR_REGISTER_INTERNAL_CLASS(OrchestratorScriptView) ORCHESTRATOR_REGISTER_INTERNAL_CLASS(OrchestratorScriptViewSection) diff --git a/src/editor/graph/actions/action_menu.cpp b/src/editor/graph/actions/action_menu.cpp index 97e1ccc3..f8fb3998 100644 --- a/src/editor/graph/actions/action_menu.cpp +++ b/src/editor/graph/actions/action_menu.cpp @@ -34,7 +34,7 @@ void OrchestratorGraphActionMenu::_bind_methods() void OrchestratorGraphActionMenu::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { set_title("All Actions"); diff --git a/src/editor/graph/graph_edit.cpp b/src/editor/graph/graph_edit.cpp index ae7f5776..e10edb27 100644 --- a/src/editor/graph/graph_edit.cpp +++ b/src/editor/graph/graph_edit.cpp @@ -119,7 +119,7 @@ void OrchestratorGraphEdit::free_clipboard() void OrchestratorGraphEdit::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { Label* label = memnew(Label); label->set_text("Use Right Mouse Button To Add New Nodes"); @@ -182,19 +182,8 @@ void OrchestratorGraphEdit::_notification(int p_what) ProjectSettings* ps = ProjectSettings::get_singleton(); ps->connect("settings_changed", callable_mp(this, &OrchestratorGraphEdit::_on_project_settings_changed)); - } - else if (p_what == NOTIFICATION_POST_ENTER_TREE) - { + _synchronize_graph_with_script(_deferred_tween_node == -1); - } - else if (p_what == NOTIFICATION_EXIT_TREE) - { - _script_graph->disconnect("node_removed", callable_mp(this, &OrchestratorGraphEdit::_on_graph_node_removed)); - _script_graph->disconnect("node_added", callable_mp(this, &OrchestratorGraphEdit::_on_graph_node_added)); - _script->disconnect("connections_changed", callable_mp(this, &OrchestratorGraphEdit::_on_graph_connections_changed)); - } - else if (p_what == NOTIFICATION_READY) - { _focus_node(_deferred_tween_node); } } diff --git a/src/editor/graph/graph_node.cpp b/src/editor/graph/graph_node.cpp index 373953a4..3a61992a 100644 --- a/src/editor/graph/graph_node.cpp +++ b/src/editor/graph/graph_node.cpp @@ -58,7 +58,7 @@ void OrchestratorGraphNode::_bind_methods() void OrchestratorGraphNode::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { // Update the title bar widget layouts HBoxContainer* titlebar = get_titlebar_hbox(); diff --git a/src/editor/graph/graph_node_pin.cpp b/src/editor/graph/graph_node_pin.cpp index 94a84406..11ee8e71 100644 --- a/src/editor/graph/graph_node_pin.cpp +++ b/src/editor/graph/graph_node_pin.cpp @@ -54,7 +54,7 @@ String OrchestratorGraphNodePin::_get_color_name() const void OrchestratorGraphNodePin::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { _create_widgets(); diff --git a/src/editor/main_view.cpp b/src/editor/main_view.cpp index 2cfa5c6d..92bef88b 100644 --- a/src/editor/main_view.cpp +++ b/src/editor/main_view.cpp @@ -20,6 +20,7 @@ #include "common/version.h" #include "editor/about_dialog.h" #include "editor/graph/graph_edit.h" +#include "editor/window_wrapper.h" #include "plugin/plugin.h" #include "plugin/settings.h" #include "script/language.h" @@ -52,22 +53,15 @@ #define SKEY(m,k) Key(static_cast(m) | static_cast(k)) -OrchestratorMainView::OrchestratorMainView(OrchestratorPlugin* p_plugin) +OrchestratorMainView::OrchestratorMainView(OrchestratorPlugin* p_plugin, OrchestratorWindowWrapper* p_window_wrapper) { _plugin = p_plugin; + _wrapper = p_window_wrapper; } void OrchestratorMainView::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) - { - OrchestratorGraphEdit::initialize_clipboard(); - } - else if (p_what == NOTIFICATION_EXIT_TREE) - { - OrchestratorGraphEdit::free_clipboard(); - } - else if (p_what == NOTIFICATION_READY) + if (p_what == NOTIFICATION_READY) { set_anchors_preset(PRESET_FULL_RECT); set_h_size_flags(SIZE_EXPAND_FILL); @@ -172,6 +166,21 @@ void OrchestratorMainView::_notification(int p_what) version->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); right_menu_container->add_child(version); + if (_wrapper->is_window_available()) + { + vs = memnew(VSeparator); + vs->set_v_size_flags(SIZE_SHRINK_CENTER); + vs->set_custom_minimum_size(Vector2i(0, 24)); + right_menu_container->add_child(vs); + + _select = memnew(OrchestratorScreenSelect); + _select->set_flat(true); + _select->set_tooltip_text("Make the Orchestration editor floating."); + _select->connect("request_open_in_screen", callable_mp(_wrapper, &OrchestratorWindowWrapper::enable_window_on_screen).bind(true)); + right_menu_container->add_child(_select); + _wrapper->connect("window_visibility_changed", callable_mp(this, &OrchestratorMainView::_on_window_changed)); + } + _update_container = memnew(HBoxContainer); _update_container->set_visible(false); @@ -228,7 +237,7 @@ void OrchestratorMainView::_notification(int p_what) _open_dialog = memnew(FileDialog); _open_dialog->set_min_size(Vector2(700, 400)); - _open_dialog->set_initial_position(Window::WINDOW_INITIAL_POSITION_CENTER_MAIN_WINDOW_SCREEN); + _open_dialog->set_initial_position(Window::WINDOW_INITIAL_POSITION_CENTER_SCREEN_WITH_KEYBOARD_FOCUS); _open_dialog->set_title("Open Orchestration Script"); _open_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE); _open_dialog->add_filter("*.os", "Orchestrator Scripts"); @@ -237,7 +246,7 @@ void OrchestratorMainView::_notification(int p_what) _save_dialog = memnew(FileDialog); _save_dialog->set_min_size(Vector2(700, 400)); - _save_dialog->set_initial_position(Window::WINDOW_INITIAL_POSITION_CENTER_MAIN_WINDOW_SCREEN); + _save_dialog->set_initial_position(Window::WINDOW_INITIAL_POSITION_CENTER_SCREEN_WITH_KEYBOARD_FOCUS); _save_dialog->set_title("Save As Orchestration Script"); _save_dialog->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE); _save_dialog->add_filter("*.os", "Orchestrator Scripts"); @@ -253,7 +262,6 @@ void OrchestratorMainView::_notification(int p_what) add_child(_close_confirm); _goto_dialog = memnew(ConfirmationDialog); - _goto_dialog->set_initial_position(Window::WINDOW_INITIAL_POSITION_CENTER_MAIN_WINDOW_SCREEN); _goto_dialog->set_title("Go to Node"); VBoxContainer* container = memnew(VBoxContainer); @@ -300,6 +308,14 @@ void OrchestratorMainView::apply_changes() file.editor->apply_changes(); } +void OrchestratorMainView::get_window_layout(const Ref& p_configuration) +{ +} + +void OrchestratorMainView::set_window_layout(const Ref& p_configuration) +{ +} + bool OrchestratorMainView::build() { for (const ScriptFile& file : _script_files) @@ -476,6 +492,10 @@ void OrchestratorMainView::_show_create_new_script_dialog() ScriptCreateDialog* dialog = _plugin->get_script_create_dialog(); + // Cache existing position and change it for our pop-out + const Window::WindowInitialPosition initial_position = dialog->get_initial_position(); + dialog->set_initial_position(Window::WINDOW_INITIAL_POSITION_CENTER_SCREEN_WITH_KEYBOARD_FOCUS); + // Finds the LanguageMenu option button and forces the Orchestrator choice // This must be done before calling "config" to make sure that all the dialog logic for templates // and the language choice align properly. @@ -505,6 +525,9 @@ void OrchestratorMainView::_show_create_new_script_dialog() dialog->connect("script_created", callable_mp(this, &OrchestratorMainView::_on_script_file_created)); dialog->popup_centered(); + + // Restore old position + dialog->set_initial_position(initial_position); } void OrchestratorMainView::_update_files_list() @@ -626,7 +649,7 @@ void OrchestratorMainView::_on_menu_option(int p_option) break; case GOTO_NODE: { - _goto_dialog->show(); + _goto_dialog->popup_centered(); break; } default: @@ -725,6 +748,12 @@ void OrchestratorMainView::_on_goto_node_closed(LineEdit* p_edit) p_edit->set_text(""); } +void OrchestratorMainView::_on_window_changed(bool p_visible) +{ + _select->set_visible(!p_visible); + _floating = p_visible; +} + void OrchestratorMainView::_on_request_completed(int p_result, int p_code, const PackedStringArray& p_headers, const PackedByteArray& p_data) { diff --git a/src/editor/main_view.h b/src/editor/main_view.h index d67c9842..3fa9f2ad 100644 --- a/src/editor/main_view.h +++ b/src/editor/main_view.h @@ -17,6 +17,7 @@ #ifndef ORCHESTRATOR_MAIN_VIEW_H #define ORCHESTRATOR_MAIN_VIEW_H +#include #include #include #include @@ -26,7 +27,9 @@ using namespace godot; /// Forward declarations class OrchestratorPlugin; class OScript; +class OrchestratorScreenSelect; class OrchestratorScriptView; +class OrchestratorWindowWrapper; namespace godot { @@ -92,6 +95,9 @@ class OrchestratorMainView : public Control OrchestratorPlugin* _plugin{ nullptr }; //! Orchestrator plugin Timer* _update_timer{ nullptr }; //! Update timer HTTPRequest* _http_request{ nullptr }; //! HTTP request + bool _floating{ false }; //! Whether this window is floating + OrchestratorScreenSelect* _select{ nullptr }; //! Screen selection + OrchestratorWindowWrapper* _wrapper{ nullptr }; //! Window wrapper static void _bind_methods() { } OrchestratorMainView() = default; @@ -99,7 +105,8 @@ class OrchestratorMainView : public Control public: /// Creates the main view /// @param p_plugin the plugin instance - OrchestratorMainView(OrchestratorPlugin* p_plugin); + /// @param p_window_wrapper the window wrapper + OrchestratorMainView(OrchestratorPlugin* p_plugin, OrchestratorWindowWrapper* p_window_wrapper); /// Godot callback that handles notifications /// @param p_what the notification to be handled @@ -112,6 +119,14 @@ class OrchestratorMainView : public Control /// Saves all open files void apply_changes(); + /// Get the window's layout to be serialized to disk when the editor is restored + /// @param p_configuration the configuration + void get_window_layout(const Ref& p_configuration); + + /// Set the window's layout, restoring the previous editor state + /// @param p_configuration the configuration + void set_window_layout(const Ref& p_configuration); + /// Performs the build step /// @return true if the build was successful; false otherwise bool build(); @@ -214,6 +229,10 @@ class OrchestratorMainView : public Control /// @param p_edit the line edit control, should never be null void _on_goto_node_visibility_changed(LineEdit* p_edit); + /// Dispatched when the window's floating status changes + /// @param p_visible the current visibility status + void _on_window_changed(bool p_visible); + void _on_request_completed(int p_result, int p_code, const PackedStringArray& p_headers, const PackedByteArray& p_data); }; diff --git a/src/editor/script_view.cpp b/src/editor/script_view.cpp index 7b470567..35333a54 100644 --- a/src/editor/script_view.cpp +++ b/src/editor/script_view.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -45,7 +46,7 @@ OrchestratorScriptViewSection::OrchestratorScriptViewSection(const String& p_sec void OrchestratorScriptViewSection::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { set_v_size_flags(Control::SIZE_SHRINK_BEGIN); set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -213,7 +214,7 @@ String OrchestratorScriptViewSection::_create_unique_name_with_prefix(const Stri return {}; } -bool OrchestratorScriptViewSection::_find_child_and_activate(const String& p_name) +bool OrchestratorScriptViewSection::_find_child_and_activate(const String& p_name, bool p_edit) { TreeItem* root = _tree->get_root(); @@ -225,6 +226,14 @@ bool OrchestratorScriptViewSection::_find_child_and_activate(const String& p_nam { _tree->call_deferred("set_selected", child, 0); _handle_item_activated(child); + + if (p_edit) + { + Ref timer = get_tree()->create_timer(0.1f); + if (timer.is_valid()) + timer->connect("timeout", callable_mp(_tree, &Tree::edit_selected).bind(true)); + } + return true; } } @@ -470,8 +479,7 @@ void OrchestratorScriptViewGraphsSection::_handle_add_new_item() update(); - if (_find_child_and_activate(name)) - _tree->call_deferred("edit_selected", true); + _find_child_and_activate(name); } void OrchestratorScriptViewGraphsSection::_handle_item_activated(TreeItem* p_item) @@ -567,7 +575,10 @@ void OrchestratorScriptViewFunctionsSection::_on_override_virtual_function() void OrchestratorScriptViewFunctionsSection::_notification(int p_what) { - if (p_what == NOTIFICATION_POST_ENTER_TREE) + // Godot does not dispatch to parent (shrugs) + OrchestratorScriptViewSection::_notification(p_what); + + if (p_what == NOTIFICATION_READY) { HBoxContainer* container = _get_panel_hbox(); @@ -579,9 +590,6 @@ void OrchestratorScriptViewFunctionsSection::_notification(int p_what) override_button->set_tooltip_text("Override a Godot virtual function"); container->add_child(override_button); } - - // Godot does not dispatch to parent (shrugs) - OrchestratorScriptViewSection::_notification(p_what); } void OrchestratorScriptViewFunctionsSection::_show_function_graph(TreeItem* p_item) @@ -664,8 +672,7 @@ void OrchestratorScriptViewFunctionsSection::_handle_add_new_item() update(); - if (_find_child_and_activate(name)) - _tree->call_deferred("edit_selected", true); + _find_child_and_activate(name); } void OrchestratorScriptViewFunctionsSection::_handle_item_activated(TreeItem* p_item) @@ -749,7 +756,10 @@ String OrchestratorScriptViewMacrosSection::_get_tooltip_text() const void OrchestratorScriptViewMacrosSection::_notification(int p_what) { - if (p_what == NOTIFICATION_POST_ENTER_TREE) + // Godot does not dispatch to parent (shrugs) + OrchestratorScriptViewSection::_notification(p_what); + + if (p_what == NOTIFICATION_READY) { HBoxContainer* container = _get_panel_hbox(); @@ -757,9 +767,6 @@ void OrchestratorScriptViewMacrosSection::_notification(int p_what) if (button) button->set_disabled(true); } - - // Godot does not dispatch to parent (shrugs) - OrchestratorScriptViewSection::_notification(p_what); } void OrchestratorScriptViewMacrosSection::update() @@ -885,8 +892,7 @@ void OrchestratorScriptViewVariablesSection::_handle_add_new_item() update(); - if (_find_child_and_activate(name)) - _tree->call_deferred("edit_selected", true); + _find_child_and_activate(name); } void OrchestratorScriptViewVariablesSection::_handle_item_selected() @@ -1051,8 +1057,7 @@ void OrchestratorScriptViewSignalsSection::_handle_add_new_item() update(); - if (_find_child_and_activate(name)) - _tree->call_deferred("edit_selected", true); + _find_child_and_activate(name); } void OrchestratorScriptViewSignalsSection::_handle_item_selected() @@ -1143,7 +1148,7 @@ OrchestratorScriptView::OrchestratorScriptView(OrchestratorPlugin* p_plugin, con void OrchestratorScriptView::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) + if (p_what == NOTIFICATION_READY) { Node* editor_node = get_tree()->get_root()->get_child(0); editor_node->connect("script_add_function_request", callable_mp(this, &OrchestratorScriptView::_add_callback)); diff --git a/src/editor/script_view.h b/src/editor/script_view.h index c89c9ee3..5765357f 100644 --- a/src/editor/script_view.h +++ b/src/editor/script_view.h @@ -73,8 +73,9 @@ class OrchestratorScriptViewSection : public VBoxContainer /// Finds the specified child in the tree with text that matches the given name. /// @param p_name the text to find and activate in the tree control + /// @param p_edit true if the item should be edited after activation, false otherwise /// @return true if the item is found and activated, false otherwise - bool _find_child_and_activate(const String& p_name); + bool _find_child_and_activate(const String& p_name, bool p_edit = true); /// Get the panel's horizontal box control /// @return the HBoxContainer child of the top panel, never null diff --git a/src/editor/window_wrapper.cpp b/src/editor/window_wrapper.cpp new file mode 100644 index 00000000..8ec18358 --- /dev/null +++ b/src/editor/window_wrapper.cpp @@ -0,0 +1,476 @@ +// This file is part of the Godot Orchestrator project. +// +// Copyright (c) 2023-present Vahera Studios LLC and its contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "window_wrapper.h" + +#include "common/scene_utils.h" +#include "plugin/plugin.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +OrchestratorWindowWrapper::OrchestratorWindowWrapper() +{ + _window = memnew(Window); + _window->set_wrap_controls(true); + + add_child(_window); + _window->hide(); + + _window_background = memnew(Panel); + _window_background->set_anchors_and_offsets_preset(PRESET_FULL_RECT); + _window->add_child(_window_background); + + // todo: what about progress bar? +} + +void OrchestratorWindowWrapper::_bind_methods() +{ + ADD_SIGNAL(MethodInfo("window_visibility_changed", PropertyInfo(Variant::BOOL, "visible"))); + ADD_SIGNAL(MethodInfo("window_close_requested")); +} + +void OrchestratorWindowWrapper::_notification(int p_what) +{ + if (!is_window_available()) + return; + + switch (p_what) + { + // Grab the focus when WindowWrapper::set_visible(true) is called and is shown. + case NOTIFICATION_VISIBILITY_CHANGED: + if (get_window_enabled() && is_visible()) + _window->grab_focus(); + break; + + case NOTIFICATION_ENTER_TREE: + _window->connect("close_requested", callable_mp(this, &OrchestratorWindowWrapper::set_window_enabled).bind(false)); + break; + + case NOTIFICATION_READY: + break; + + case NOTIFICATION_THEME_CHANGED: + _window_background->add_theme_stylebox_override("panel", + get_theme_stylebox("PanelForeground", "EditorStyles")); + break; + } +} + +Rect2 OrchestratorWindowWrapper::_get_default_window_rect() const +{ + // Assume that the control rect is the desired one for the window. + Transform2D xform = _wrapped_control->get_screen_transform(); + return Rect2(xform.get_origin(), xform.get_scale() * get_size()); +} + +Node* OrchestratorWindowWrapper::_get_wrapped_control_parent() const +{ + if (_margin) + return _margin; + return _window; +} + +void OrchestratorWindowWrapper::_set_window_enabled_with_rect(bool p_visible, const Rect2 p_rect) +{ + ERR_FAIL_NULL(_wrapped_control); + + if (!is_window_available()) + return; + + if (_window->is_visible() == p_visible) + { + if (p_visible) + _window->grab_focus(); + return; + } + + Node* parent = _get_wrapped_control_parent(); + + if (_wrapped_control->get_parent() != parent) + { + // Move the control to the window + _wrapped_control->reparent(parent, false); + + _set_window_rect(p_rect); + _wrapped_control->set_anchors_and_offsets_preset(PRESET_FULL_RECT); + } + else if (!p_visible) + { + // Remove control from window. + _wrapped_control->reparent(this, false); + } + + _window->set_visible(p_visible); + if (!p_visible) + emit_signal("window_close_requested"); + + emit_signal("window_visibility_changed", p_visible); +} + +void OrchestratorWindowWrapper::_set_window_rect(const Rect2 p_rect) +{ + // Set the window rect even when the window is maximized to have a good default size + // when the user leaves maximized mode. + _window->set_position(p_rect.position); + _window->set_size(p_rect.size); + + Ref es = OrchestratorPlugin::get_singleton()->get_editor_interface()->get_editor_settings(); + if (es.is_valid() && es->get_setting("interface/multi_window/maximize_window")) + _window->set_mode(Window::MODE_MAXIMIZED); +} + +void OrchestratorWindowWrapper::set_wrapped_control(Control* p_control) +{ + ERR_FAIL_NULL(p_control); + ERR_FAIL_COND(_wrapped_control); + + _wrapped_control = p_control; + add_child(p_control); +} + +Control* OrchestratorWindowWrapper::get_wrapped_control() const +{ + return _wrapped_control; +} + +Control* OrchestratorWindowWrapper::release_wrapped_control() +{ + set_window_enabled(false); + if (_wrapped_control) + { + Control* old = _wrapped_control; + _wrapped_control->get_parent()->remove_child(_wrapped_control); + _wrapped_control = nullptr; + return old; + } + return nullptr; +} + +bool OrchestratorWindowWrapper::is_window_available() const +{ + return _window != nullptr; +} + +bool OrchestratorWindowWrapper::get_window_enabled() const +{ + return is_window_available() ? _window->is_visible() : false; +} + +void OrchestratorWindowWrapper::set_window_enabled(bool p_enabled) +{ + _set_window_enabled_with_rect(p_enabled, _get_default_window_rect()); +} + +Rect2i OrchestratorWindowWrapper::get_window_rect() const +{ + ERR_FAIL_COND_V(!get_window_enabled(), Rect2i()); + return Rect2i(_window->get_position(), _window->get_size()); +} + +int OrchestratorWindowWrapper::get_window_screen() const +{ + ERR_FAIL_COND_V(!get_window_enabled(), -1); + return _window->get_current_screen(); +} + +void OrchestratorWindowWrapper::restore_window(const Rect2i& p_rect, int p_screen) +{ + ERR_FAIL_COND(!is_window_available()); + ERR_FAIL_INDEX(p_screen, DisplayServer::get_singleton()->get_screen_count()); + + _set_window_enabled_with_rect(true, p_rect); + _window->set_current_screen(p_screen); +} + +void OrchestratorWindowWrapper::restore_window_from_saved_position(const Rect2 p_window_rect, int p_screen, const Rect2 p_screen_rect) +{ + ERR_FAIL_COND(!is_window_available()); + + Rect2 window_rect = p_window_rect; + int screen = p_screen; + Rect2 restored_screen_rect = p_screen_rect; + + if (screen < 0 || screen >= DisplayServer::get_singleton()->get_screen_count()) + { + // Fallback to the main window screen if the saved screen is not available. + screen = get_window()->get_window_id(); + } + + Rect2i real_screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(screen); + + if (Rect2i(restored_screen_rect) == Rect2i()) + { + // Fallback to the target screen rect. + restored_screen_rect = real_screen_rect; + } + + if (Rect2i(window_rect) == Rect2i()) + { + // Fallback to a standard rect. + window_rect = Rect2i(restored_screen_rect.position + restored_screen_rect.size / 4, restored_screen_rect.size / 2); + } + + // Adjust the window rect size in case the resolution changes. + Vector2 screen_ratio = Vector2(real_screen_rect.size) / Vector2(restored_screen_rect.size); + + // The screen positioning may change, so remove the original screen position. + window_rect.position -= restored_screen_rect.position; + window_rect = Rect2i(window_rect.position * screen_ratio, window_rect.size * screen_ratio); + window_rect.position += real_screen_rect.position; + + // All good, restore the window. + _window->set_current_screen(p_screen); + if (_window->is_visible()) + _set_window_rect(window_rect); + else + _set_window_enabled_with_rect(true, window_rect); +} + +void OrchestratorWindowWrapper::enable_window_on_screen(int p_screen, bool p_auto_scale) +{ + int current_screen = Object::cast_to(get_viewport())->get_current_screen(); + int screen = p_screen < 0 ? current_screen : p_screen; + + Ref es = OrchestratorPlugin::get_singleton()->get_editor_interface()->get_editor_settings(); + bool auto_scale = p_auto_scale && !es->get_setting("interface/multi_window/maximize_window"); + + if (auto_scale && current_screen != screen) + { + Rect2 control_rect = _get_default_window_rect(); + + Rect2i source_screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(current_screen); + Rect2i dest_screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(screen); + + // Adjust the window rect size in case the resolution changes. + Vector2 screen_ratio = Vector2(source_screen_rect.size) / Vector2(dest_screen_rect.size); + + // The screen positioning may change, so remove the original screen position. + control_rect.position -= source_screen_rect.position; + control_rect = Rect2i(control_rect.position * screen_ratio, control_rect.size * screen_ratio); + control_rect.position += dest_screen_rect.position; + + restore_window(control_rect, p_screen); + } + else + { + _window->set_current_screen(p_screen); + set_window_enabled(true); + } +} + +void OrchestratorWindowWrapper::set_window_title(const String& p_title) +{ + if (!is_window_available()) + return; + + _window->set_title(p_title); +} + +void OrchestratorWindowWrapper::set_margins_enabled(bool p_enabled) +{ + if (!is_window_available()) + return; + + if (!p_enabled && _margin) + { + _margin->queue_free(); + _margin = nullptr; + } + else if (p_enabled && !_margin) + { + Size2 borders = Size2(4, 4); + _margin = memnew(MarginContainer); + _margin->add_theme_constant_override("margin_right", borders.width); + _margin->add_theme_constant_override("margin_top", borders.height); + _margin->add_theme_constant_override("margin_left", borders.width); + _margin->add_theme_constant_override("margin_bottom", borders.height); + + _window->add_child(_margin); + _margin->set_anchors_and_offsets_preset(PRESET_FULL_RECT); + } +} + +void OrchestratorWindowWrapper::move_to_foreground() +{ + _window->move_to_foreground(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +OrchestratorScreenSelect::OrchestratorScreenSelect() +{ + set_tooltip_text("Make this panel floating."); + set_button_mask(MouseButtonMask::MOUSE_BUTTON_MASK_RIGHT); + set_flat(true); + set_toggle_mode(true); + set_focus_mode(FOCUS_NONE); + set_action_mode(ACTION_MODE_BUTTON_PRESS); + + // Create the popup. + const Size2 borders = Size2(4, 4); + + _popup = memnew(Popup); + add_child(_popup); + + _popup_background = memnew(Panel); + _popup_background->set_anchors_and_offsets_preset(PRESET_FULL_RECT); + _popup->add_child(_popup_background); + + MarginContainer* root = memnew(MarginContainer); + root->add_theme_constant_override("margin_right", borders.width); + root->add_theme_constant_override("margin_top", borders.height); + root->add_theme_constant_override("margin_left", borders.width); + root->add_theme_constant_override("margin_bottom", borders.height); + _popup->add_child(root); + + VBoxContainer* vbox = memnew(VBoxContainer); + vbox->set_alignment(BoxContainer::ALIGNMENT_CENTER); + root->add_child(vbox); + + Label* description = memnew(Label); + description->set_text("Select Screen"); + description->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + vbox->add_child(description); + + _screen_list = memnew(HBoxContainer); + _screen_list->set_alignment(BoxContainer::ALIGNMENT_CENTER); + vbox->add_child(_screen_list); + + root->set_anchors_and_offsets_preset(PRESET_FULL_RECT); +} + +void OrchestratorScreenSelect::_bind_methods() +{ + ADD_SIGNAL(MethodInfo("request_open_in_screen", PropertyInfo(Variant::INT, "screen"))); +} + +void OrchestratorScreenSelect::_notification(int p_what) +{ + switch (p_what) + { + case NOTIFICATION_READY: + _popup->connect("popup_hide", callable_mp(static_cast(this), &OrchestratorScreenSelect::set_pressed).bind(false)); + connect("gui_input", callable_mp(this, &OrchestratorScreenSelect::_handle_mouse_shortcut)); + break; + + case NOTIFICATION_THEME_CHANGED: + { + set_button_icon(SceneUtils::get_icon(this, "MakeFloating")); + _popup_background->add_theme_stylebox_override("panel", get_theme_stylebox("PanelForeground", "EditorStyles")); + + const real_t popup_height = real_t(get_theme_font_size("font_size")) * 2.0; + _popup->set_min_size(Size2(0, popup_height * 3)); + break; + } + } +} + +void OrchestratorScreenSelect::_pressed() +{ + if (_popup->is_visible()) + { + _popup->hide(); + return; + } + + _build_advanced_menu(); + _show_popup(); +} + +void OrchestratorScreenSelect::_build_advanced_menu() +{ + // Clear old screen list. + while (_screen_list->get_child_count(false) > 0) + { + Node* child = _screen_list->get_child(0); + _screen_list->remove_child(child); + child->queue_free(); + } + + // Populate screen list + const real_t height = real_t(get_theme_font_size("font_size")) * 1.5; + + int current_screen = get_window()->get_current_screen(); + for (int i = 0; i < DisplayServer::get_singleton()->get_screen_count(); i++) + { + Button* button = memnew(Button); + + Size2 screen_size = Size2(DisplayServer::get_singleton()->screen_get_size(i)); + Size2 button_size = Size2(height * (screen_size.x / screen_size.y), height); + button->set_custom_minimum_size(button_size); + _screen_list->add_child(button); + + button->set_text(itos(i)); + button->set_text_alignment(HORIZONTAL_ALIGNMENT_CENTER); + button->set_tooltip_text(vformat("Make this panel floating in the screen %d.", i)); + + if (i == current_screen) + { + Color accent_color = get_theme_color("accent_color", "Editor"); + button->add_theme_color_override("font_color", accent_color); + } + + button->connect("pressed", callable_mp(this, &OrchestratorScreenSelect::_emit_screen_signal).bind(i)); + button->connect("pressed", callable_mp(static_cast(this), &OrchestratorScreenSelect::set_pressed).bind(false)); + button->connect("pressed", callable_mp(static_cast(_popup), &Popup::hide)); + } +} + +void OrchestratorScreenSelect::_emit_screen_signal(int p_screen_index) +{ + emit_signal("request_open_in_screen", p_screen_index); +} + +void OrchestratorScreenSelect::_handle_mouse_shortcut(const Ref& p_event) +{ + const Ref mb = p_event; + if (mb.is_valid()) + { + if (mb->is_pressed() && mb->get_button_index() == MouseButton::MOUSE_BUTTON_LEFT) + { + _emit_screen_signal(get_window()->get_current_screen()); + accept_event(); + } + } +} + +void OrchestratorScreenSelect::_show_popup() +{ + if (!get_viewport()) + return; + + Size2 size = get_size() * get_viewport()->get_canvas_transform().get_scale(); + + _popup->set_size(Size2(size.width, 0)); + + Point2 gp = get_screen_position(); + gp.y += size.y; + if (is_layout_rtl()) + gp.x += size.width - _popup->get_size().width; + + _popup->set_position(gp); + _popup->popup(); +} + diff --git a/src/editor/window_wrapper.h b/src/editor/window_wrapper.h new file mode 100644 index 00000000..51904bfa --- /dev/null +++ b/src/editor/window_wrapper.h @@ -0,0 +1,104 @@ +// This file is part of the Godot Orchestrator project. +// +// Copyright (c) 2023-present Vahera Studios LLC and its contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef ORCHESTRATOR_WINDOW_WRAPPER_H +#define ORCHESTRATOR_WINDOW_WRAPPER_H + +#include +#include + +using namespace godot; + +namespace godot +{ + class HBoxContainer; + class Panel; + class Popup; +} + +/// A window wrapper implementation, based heavily off the Godot Editor's WindowWrapper class. +/// @see godot-engine/editor/window_wrapper.h +class OrchestratorWindowWrapper : public MarginContainer +{ + GDCLASS(OrchestratorWindowWrapper, MarginContainer); + + Control* _wrapped_control{ nullptr }; + MarginContainer* _margin{ nullptr }; + Window* _window{ nullptr }; + Panel* _window_background{ nullptr }; + + Rect2 _get_default_window_rect() const; + Node* _get_wrapped_control_parent() const; + + void _set_window_enabled_with_rect(bool p_visible, const Rect2 p_rect); + void _set_window_rect(const Rect2 p_rect); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + OrchestratorWindowWrapper(); + + void set_wrapped_control(Control* p_control); + Control* get_wrapped_control() const; + Control* release_wrapped_control(); + + bool is_window_available() const; + + bool get_window_enabled() const; + void set_window_enabled(bool p_enabled); + + Rect2i get_window_rect() const; + int get_window_screen() const; + + void restore_window(const Rect2i& p_rect, int p_screen = -1); + void restore_window_from_saved_position(const Rect2 p_window_rect, int p_screen, const Rect2 p_screen_rect); + void enable_window_on_screen(int p_screen = -1, bool p_auto_scale = false); + + void set_window_title(const String& p_title); + void set_margins_enabled(bool p_enabled); + + void move_to_foreground(); +}; + +/// A screen select button implementation, based heavily off the Godot Editor's ScreenSelect class. +/// @see godot-engine/editor/window_wrapper.h +class OrchestratorScreenSelect : public Button +{ + GDCLASS(OrchestratorScreenSelect, Button); + + Popup* _popup{ nullptr }; + Panel* _popup_background{ nullptr }; + HBoxContainer* _screen_list{ nullptr }; + + void _build_advanced_menu(); + + void _emit_screen_signal(int p_screen_index); + void _handle_mouse_shortcut(const Ref& p_event); + void _show_popup(); + +protected: + static void _bind_methods(); + void _notification(int p_what); + +public: + OrchestratorScreenSelect(); + + void _pressed() override; +}; + +#endif // ORCHESTRATOR_WINDOW_WRAPPER_H \ No newline at end of file diff --git a/src/plugin/plugin.cpp b/src/plugin/plugin.cpp index 1c5730e8..ac166fb7 100644 --- a/src/plugin/plugin.cpp +++ b/src/plugin/plugin.cpp @@ -17,12 +17,16 @@ #include "plugin.h" #include "common/version.h" +#include "editor/graph/graph_edit.h" #include "editor/main_view.h" +#include "editor/window_wrapper.h" #include "script/script.h" #include +#include #include #include +#include #include #include #include @@ -46,6 +50,8 @@ void OrchestratorPlugin::_notification(int p_what) { if (p_what == NOTIFICATION_ENTER_TREE) { + OrchestratorGraphEdit::initialize_clipboard(); + // Plugins only enter the tree once and this happens before the main view. // It's safe then to cache the plugin reference here. _plugin = this; @@ -55,12 +61,24 @@ void OrchestratorPlugin::_notification(int p_what) if (theme.is_valid() && !theme->has_icon(_get_plugin_name(), "EditorIcons")) theme->set_icon(_get_plugin_name(), "EditorIcons", _get_plugin_icon()); - _main_view = memnew(OrchestratorMainView(this)); - _editor.get_editor_main_screen()->add_child(_main_view); + _window_wrapper = memnew(OrchestratorWindowWrapper); + _window_wrapper->set_window_title(vformat("Orchestrator - Godot Engine")); + _window_wrapper->set_margins_enabled(true); + + _main_view = memnew(OrchestratorMainView(this, _window_wrapper)); + + _editor.get_editor_main_screen()->add_child(_window_wrapper); + _window_wrapper->set_wrapped_control(_main_view); + _window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL); + _window_wrapper->hide(); + _window_wrapper->connect("window_visibility_changed", callable_mp(this, &OrchestratorPlugin::_on_window_visibility_changed)); + _make_visible(false); } else if (p_what == NOTIFICATION_EXIT_TREE) { + OrchestratorGraphEdit::free_clipboard(); + memdelete(_main_view); _main_view = nullptr; @@ -74,7 +92,10 @@ void OrchestratorPlugin::_edit(Object* p_object) { Ref script(Object::cast_to(p_object)); if (script.is_valid()) + { _main_view->edit(script); + _window_wrapper->move_to_foreground(); + } } } @@ -90,8 +111,10 @@ bool OrchestratorPlugin::_has_main_screen() const void OrchestratorPlugin::_make_visible(bool p_visible) { - if (_main_view) - _main_view->set_visible(p_visible); + if (p_visible) + _window_wrapper->show(); + else + _window_wrapper->hide(); } String OrchestratorPlugin::_get_plugin_name() const @@ -130,6 +153,52 @@ void OrchestratorPlugin::_apply_changes() _main_view->apply_changes(); } +void OrchestratorPlugin::_set_window_layout(const Ref& p_configuration) +{ + if (_main_view) + _main_view->set_window_layout(p_configuration); + + Ref es = get_editor_interface()->get_editor_settings(); + if (es.is_valid()) + { + bool restore_on_load = es->get_setting("interface/multi_window/restore_windows_on_load"); + if (restore_on_load && _window_wrapper->is_window_available() + && p_configuration->has_section_key("Orchestrator", "window_rect")) + { + _window_wrapper->restore_window_from_saved_position( + p_configuration->get_value("Orchestrator", "window_rect", Rect2i()), + p_configuration->get_value("Orchestrator", "window_screen", -1), + p_configuration->get_value("Orchestrator", "window_screen_rect", Rect2i())); + } + else + _window_wrapper->set_window_enabled(false); + } +} + +void OrchestratorPlugin::_get_window_layout(const Ref& p_configuration) +{ + if (_main_view) + _main_view->get_window_layout(p_configuration); + + if (_window_wrapper->get_window_enabled()) + { + p_configuration->set_value("Orchestrator", "window_rect", _window_wrapper->get_window_rect()); + int screen = _window_wrapper->get_window_screen(); + p_configuration->set_value("Orchestrator", "window_screen", screen); + p_configuration->set_value("Orchestrator", "window_screen_rect", + DisplayServer::get_singleton()->screen_get_usable_rect(screen)); + } + else + { + if (p_configuration->has_section_key("Orchestrator", "window_rect")) + p_configuration->erase_section_key("Orchestrator", "window_rect"); + if (p_configuration->has_section_key("Orchestrator", "window_screen")) + p_configuration->erase_section_key("Orchestrator", "window_screen"); + if (p_configuration->has_section_key("Orchestrator", "window_screen_rect")) + p_configuration->erase_section_key("Orchestrator", "window_screen_rect"); + } +} + bool OrchestratorPlugin::_build() { if (_main_view) @@ -146,6 +215,11 @@ void OrchestratorPlugin::_disable_plugin() { } +void OrchestratorPlugin::_on_window_visibility_changed(bool p_visible) +{ + // todo: see script_editor_plugin.cpp +} + void register_plugin_classes() { ORCHESTRATOR_REGISTER_CLASS(OrchestratorPlugin) diff --git a/src/plugin/plugin.h b/src/plugin/plugin.h index 5d17dc79..370aa6b1 100644 --- a/src/plugin/plugin.h +++ b/src/plugin/plugin.h @@ -17,6 +17,7 @@ #ifndef ORCHESTRATOR_PLUGIN_H #define ORCHESTRATOR_PLUGIN_H +#include #include #include @@ -24,6 +25,7 @@ using namespace godot; /// Forward declarations class OrchestratorMainView; +class OrchestratorWindowWrapper; /// The Orchestrator editor plug-in. class OrchestratorPlugin : public EditorPlugin @@ -34,9 +36,9 @@ class OrchestratorPlugin : public EditorPlugin static OrchestratorPlugin* _plugin; - EditorInterface& _editor; //! Godot editor interface reference - OrchestratorMainView* _main_view{ nullptr }; //! Plugin's main view - + EditorInterface& _editor; //! Godot editor interface reference + OrchestratorMainView* _main_view{ nullptr }; //! Plugin's main view + OrchestratorWindowWrapper* _window_wrapper{ nullptr }; //! Window wrapper public: /// Constructor OrchestratorPlugin(); @@ -65,10 +67,15 @@ class OrchestratorPlugin : public EditorPlugin String _get_plugin_name() const override; Ref _get_plugin_icon() const override; void _apply_changes() override; + void _set_window_layout(const Ref& configuration) override; + void _get_window_layout(const Ref& configuration) override; bool _build() override; void _enable_plugin() override; void _disable_plugin() override; //~ End EditorPlugin interface + +private: + void _on_window_visibility_changed(bool p_visible); }; void register_plugin_classes();