From 2f1e8dad746953d4829bc10862f4f96861a434cc Mon Sep 17 00:00:00 2001 From: Malcolm Anderson Date: Thu, 29 May 2025 17:46:37 -0700 Subject: [PATCH] Add Instant Preview to Quick Open dialog Add toggle for instant preview Always keep search box selected so that keyboard navigation works Add default setting for Instant Preview Directly set property value for resource via Quick Load menu (no undo/redo or dirty-scene functionality yet) Add undo/redo functionality Update class reference Update doc/classes/EditorSettings.xml Co-authored-by: Micky <66727710+Mickeon@users.noreply.github.com> Slight improvement(?) to wording of setting Allow previewing without committing change Address various suggestions/improvements Only allow Instant Preview to be used if Quick Open menu is being used to modify a property Only allow property-based Quick Load when resource to modify is defined (otherwise default to old behavior) Apply suggestions from code review Co-authored-by: Tomasz Chabora Address comments/suggestions Get rid of duplicated code and use original callback strategy (Attempt to) fix Instant Preview for editing multiple nodes at once and undo/redo stack for single nodes Fix cancelling Quick Open when multiple nodes are selected Prevent initially selected item in Quick Open dialog from overwriting the currently selected property Apply suggestions from code review Co-authored-by: Tomasz Chabora Co-authored-by: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com> Make a few changes/improvements based on feedback - Combine some duplicated code into `_finish_dialog_setup()` - Move `ERR_FAIL_NULL(p_obj);` to top of checks - Fix renaming of `is_instant_preview_enabled()` across code, and remove now-redundant conditions where it is used - Make `EditorResourcePicker::property_path` be `StringName` not `String` --- doc/classes/EditorSettings.xml | 3 + editor/gui/editor_quick_open_dialog.cpp | 134 ++++++++++++++++++-- editor/gui/editor_quick_open_dialog.h | 17 +++ editor/inspector/editor_properties.cpp | 1 + editor/inspector/editor_resource_picker.cpp | 8 +- editor/inspector/editor_resource_picker.h | 2 + editor/inspector/multi_node_edit.cpp | 29 ++++- editor/inspector/multi_node_edit.h | 4 +- editor/settings/editor_settings.cpp | 1 + 9 files changed, 183 insertions(+), 16 deletions(-) diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index f11bd0f0202..fba930d12f1 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -795,6 +795,9 @@ If [code]true[/code], results will include files located in the [code]addons[/code] folder. + + If [code]true[/code], highlighting a resource will preview it quickly without confirming the selection or closing the dialog. + The number of missed query characters allowed in a match when fuzzy matching is enabled. For example, with the default value of [code]2[/code], [code]"normal"[/code] would match [code]"narmal"[/code] and [code]"norma"[/code] but not [code]"nor"[/code]. diff --git a/editor/gui/editor_quick_open_dialog.cpp b/editor/gui/editor_quick_open_dialog.cpp index 41218992b1c..58765ad7cd3 100644 --- a/editor/gui/editor_quick_open_dialog.cpp +++ b/editor/gui/editor_quick_open_dialog.cpp @@ -35,9 +35,11 @@ #include "editor/docks/filesystem_dock.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" +#include "editor/editor_undo_redo_manager.h" #include "editor/file_system/editor_file_system.h" #include "editor/file_system/editor_paths.h" #include "editor/inspector/editor_resource_preview.h" +#include "editor/inspector/multi_node_edit.h" #include "editor/settings/editor_settings.h" #include "editor/themes/editor_scale.h" #include "scene/gui/center_container.h" @@ -123,7 +125,8 @@ EditorQuickOpenDialog::EditorQuickOpenDialog() { { container = memnew(QuickOpenResultContainer); - container->connect("result_clicked", callable_mp(this, &EditorQuickOpenDialog::ok_pressed)); + container->connect("selection_changed", callable_mp(this, &EditorQuickOpenDialog::selection_changed)); + container->connect("result_clicked", callable_mp(this, &EditorQuickOpenDialog::item_pressed)); vbc->add_child(container); } @@ -149,26 +152,117 @@ void EditorQuickOpenDialog::popup_dialog(const Vector &p_base_types, ERR_FAIL_COND(p_base_types.is_empty()); ERR_FAIL_COND(!p_item_selected_callback.is_valid()); + property_object = nullptr; + property_path = ""; item_selected_callback = p_item_selected_callback; container->init(p_base_types); - get_ok_button()->set_disabled(container->has_nothing_selected()); + container->set_instant_preview_toggle_visible(false); + _finish_dialog_setup(p_base_types); +} +void EditorQuickOpenDialog::popup_dialog_for_property(const Vector &p_base_types, Object *p_obj, const StringName &p_path, const Callable &p_item_selected_callback) { + ERR_FAIL_NULL(p_obj); + ERR_FAIL_COND(p_base_types.is_empty()); + ERR_FAIL_COND(!p_item_selected_callback.is_valid()); + + property_object = p_obj; + property_path = p_path; + item_selected_callback = p_item_selected_callback; + initial_property_value = property_object->get(property_path); + + // Reset this, so that the property isn't updated immediately upon opening + // the window. + initial_selection_performed = false; + + container->init(p_base_types); + container->set_instant_preview_toggle_visible(true); + _finish_dialog_setup(p_base_types); +} + +void EditorQuickOpenDialog::_finish_dialog_setup(const Vector &p_base_types) { + get_ok_button()->set_disabled(container->has_nothing_selected()); set_title(get_dialog_title(p_base_types)); popup_centered_clamped(Size2(780, 650) * EDSCALE, 0.8f); search_box->grab_focus(); } void EditorQuickOpenDialog::ok_pressed() { - item_selected_callback.call(container->get_selected()); - - container->save_selected_item(); + update_property(); container->cleanup(); search_box->clear(); hide(); } +bool EditorQuickOpenDialog::_is_instant_preview_active() const { + return property_object != nullptr && container->is_instant_preview_enabled(); +} + +void EditorQuickOpenDialog::selection_changed() { + if (!_is_instant_preview_active()) { + return; + } + + // This prevents the property from being changed the first time the Quick Open + // window is opened. + if (!initial_selection_performed) { + initial_selection_performed = true; + } else { + preview_property(); + } +} + +void EditorQuickOpenDialog::item_pressed(bool p_double_click) { + // A double-click should always be taken as a "confirm" action. + if (p_double_click) { + container->save_selected_item(); + ok_pressed(); + return; + } + + // Single-clicks should be taken as a "confirm" action only if Instant Preview + // isn't currently enabled, or the property object is null for some reason. + if (!_is_instant_preview_active()) { + container->save_selected_item(); + ok_pressed(); + } +} + +void EditorQuickOpenDialog::preview_property() { + Ref loaded_resource = ResourceLoader::load(container->get_selected()); + ERR_FAIL_COND_MSG(loaded_resource.is_null(), "Cannot load resource from path '" + container->get_selected() + "'."); + + // MultiNodeEdit has adding to the undo/redo stack baked into its set function. + // As such, we have to specifically call a version of its setter that doesn't + // create undo/redo actions. + if (Object::cast_to(property_object)) { + Object::cast_to(property_object)->_set_impl(property_path, loaded_resource, "", false); + } else { + property_object->set(property_path, loaded_resource); + } +} + +void EditorQuickOpenDialog::update_property() { + // Set the property back to the initial value first, so that the undo action + // has the correct object. + if (property_object) { + if (Object::cast_to(property_object)) { + Object::cast_to(property_object)->_set_impl(property_path, initial_property_value, "", false); + } else { + property_object->set(property_path, initial_property_value); + } + } + item_selected_callback.call(container->get_selected()); +} + void EditorQuickOpenDialog::cancel_pressed() { + if (property_object) { + if (Object::cast_to(property_object)) { + Object::cast_to(property_object)->_set_impl(property_path, initial_property_value, "", false); + } else { + property_object->set(property_path, initial_property_value); + } + } container->cleanup(); search_box->clear(); } @@ -262,6 +356,13 @@ QuickOpenResultContainer::QuickOpenResultContainer() { bottom_bar->add_theme_constant_override("separation", 3); add_child(bottom_bar); + instant_preview_toggle = memnew(CheckButton); + style_button(instant_preview_toggle); + instant_preview_toggle->set_text(TTRC("Instant Preview")); + instant_preview_toggle->set_tooltip_text(TTRC("Selected resource will be previewed in the editor before accepting.")); + instant_preview_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_instant_preview)); + bottom_bar->add_child(instant_preview_toggle); + fuzzy_search_toggle = memnew(CheckButton); style_button(fuzzy_search_toggle); fuzzy_search_toggle->set_text(TTR("Fuzzy Search")); @@ -333,8 +434,10 @@ void QuickOpenResultContainer::init(const Vector &p_base_types) { _set_display_mode((QuickOpenDisplayMode)last); } + const bool do_instant_preview = EDITOR_GET("filesystem/quick_open_dialog/instant_preview"); const bool fuzzy_matching = EDITOR_GET("filesystem/quick_open_dialog/enable_fuzzy_matching"); const bool include_addons = EDITOR_GET("filesystem/quick_open_dialog/include_addons"); + instant_preview_toggle->set_pressed_no_signal(do_instant_preview); fuzzy_search_toggle->set_pressed_no_signal(fuzzy_matching); include_addons_toggle->set_pressed_no_signal(include_addons); never_opened = false; @@ -679,6 +782,8 @@ void QuickOpenResultContainer::_select_item(int p_index) { bool in_history = history_set.has(candidates[selection_index].file_path); file_details_path->set_text(get_selected() + (in_history ? TTR(" (recently opened)") : "")); + emit_signal(SNAME("selection_changed")); + const QuickOpenResultItem *item = result_items[selection_index]; // Copied from Tree. @@ -700,7 +805,7 @@ void QuickOpenResultContainer::_item_input(const Ref &p_ev, int p_in if (mb.is_valid() && mb->is_pressed()) { if (mb->get_button_index() == MouseButton::LEFT) { _select_item(p_index); - emit_signal(SNAME("result_clicked")); + emit_signal(SNAME("result_clicked"), mb->is_double_click()); } else if (mb->get_button_index() == MouseButton::RIGHT) { _select_item(p_index); file_context_menu->set_position(result_items[p_index]->get_screen_position() + mb->get_position()); @@ -710,6 +815,10 @@ void QuickOpenResultContainer::_item_input(const Ref &p_ev, int p_in } } +void QuickOpenResultContainer::_toggle_instant_preview(bool p_pressed) { + EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/instant_preview", p_pressed); +} + void QuickOpenResultContainer::_toggle_fuzzy_search(bool p_pressed) { EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/enable_fuzzy_matching", p_pressed); update_results(); @@ -810,6 +919,14 @@ String _get_uid_string(const String &p_filepath) { return id == ResourceUID::INVALID_ID ? p_filepath : ResourceUID::get_singleton()->id_to_text(id); } +bool QuickOpenResultContainer::is_instant_preview_enabled() const { + return instant_preview_toggle && instant_preview_toggle->is_visible() && instant_preview_toggle->is_pressed(); +} + +void QuickOpenResultContainer::set_instant_preview_toggle_visible(bool p_visible) { + instant_preview_toggle->set_visible(p_visible); +} + void QuickOpenResultContainer::save_selected_item() { if (base_types.size() > 1) { // Getting the type of the file and checking which base type it belongs to should be possible. @@ -885,13 +1002,14 @@ void QuickOpenResultContainer::_notification(int p_what) { } void QuickOpenResultContainer::_bind_methods() { - ADD_SIGNAL(MethodInfo("result_clicked")); + ADD_SIGNAL(MethodInfo("selection_changed")); + ADD_SIGNAL(MethodInfo("result_clicked", PropertyInfo(Variant::BOOL, "double_click"))); } //------------------------- Result Item QuickOpenResultItem::QuickOpenResultItem() { - set_focus_mode(FocusMode::FOCUS_ALL); + set_focus_mode(FocusMode::FOCUS_NONE); _set_enabled(false); set_default_cursor_shape(CURSOR_POINTING_HAND); diff --git a/editor/gui/editor_quick_open_dialog.h b/editor/gui/editor_quick_open_dialog.h index c0953e06f5d..025b0f4e1ed 100644 --- a/editor/gui/editor_quick_open_dialog.h +++ b/editor/gui/editor_quick_open_dialog.h @@ -97,6 +97,9 @@ public: bool has_nothing_selected() const; String get_selected() const; + bool is_instant_preview_enabled() const; + void set_instant_preview_toggle_visible(bool p_visible); + void save_selected_item(); void cleanup(); @@ -139,6 +142,7 @@ private: Label *file_details_path = nullptr; Button *display_mode_toggle = nullptr; + CheckButton *instant_preview_toggle = nullptr; CheckButton *include_addons_toggle = nullptr; CheckButton *fuzzy_search_toggle = nullptr; @@ -168,6 +172,7 @@ private: void _layout_result_item(QuickOpenResultItem *p_item); void _set_display_mode(QuickOpenDisplayMode p_display_mode); void _toggle_display_mode(); + void _toggle_instant_preview(bool p_pressed); void _toggle_include_addons(bool p_pressed); void _toggle_fuzzy_search(bool p_pressed); void _menu_option(int p_option); @@ -252,11 +257,14 @@ class EditorQuickOpenDialog : public AcceptDialog { public: void popup_dialog(const Vector &p_base_types, const Callable &p_item_selected_callback); + void popup_dialog_for_property(const Vector &p_base_types, Object *p_obj, const StringName &p_path, const Callable &p_item_selected_callback); EditorQuickOpenDialog(); protected: virtual void cancel_pressed() override; virtual void ok_pressed() override; + void item_pressed(bool p_double_click); + void selection_changed(); private: static String get_dialog_title(const Vector &p_base_types); @@ -266,5 +274,14 @@ private: Callable item_selected_callback; + Object *property_object = nullptr; + StringName property_path; + Variant initial_property_value; + bool initial_selection_performed = false; + bool _is_instant_preview_active() const; void _search_box_text_changed(const String &p_query); + void _finish_dialog_setup(const Vector &p_base_types); + + void preview_property(); + void update_property(); }; diff --git a/editor/inspector/editor_properties.cpp b/editor/inspector/editor_properties.cpp index 1ae68011e60..ba3a7ccc84e 100644 --- a/editor/inspector/editor_properties.cpp +++ b/editor/inspector/editor_properties.cpp @@ -3441,6 +3441,7 @@ void EditorPropertyResource::setup(Object *p_object, const String &p_path, const resource_picker->set_base_type(p_base_type); resource_picker->set_resource_owner(p_object); + resource_picker->set_property_path(p_path); resource_picker->set_editable(true); resource_picker->set_h_size_flags(SIZE_EXPAND_FILL); add_child(resource_picker); diff --git a/editor/inspector/editor_resource_picker.cpp b/editor/inspector/editor_resource_picker.cpp index 88a641b5cf9..2d5b3e4ee65 100644 --- a/editor/inspector/editor_resource_picker.cpp +++ b/editor/inspector/editor_resource_picker.cpp @@ -361,7 +361,13 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { base_types.push_back(type); } - EditorNode::get_singleton()->get_quick_open_dialog()->popup_dialog(base_types, callable_mp(this, &EditorResourcePicker::_file_selected)); + EditorQuickOpenDialog *quick_open = EditorNode::get_singleton()->get_quick_open_dialog(); + if (resource_owner) { + quick_open->popup_dialog_for_property(base_types, resource_owner, property_path, callable_mp(this, &EditorResourcePicker::_file_selected)); + } else { + quick_open->popup_dialog(base_types, callable_mp(this, &EditorResourcePicker::_file_selected)); + } + } break; case OBJ_MENU_INSPECT: { diff --git a/editor/inspector/editor_resource_picker.h b/editor/inspector/editor_resource_picker.h index a08a93d197f..2fbb028ca34 100644 --- a/editor/inspector/editor_resource_picker.h +++ b/editor/inspector/editor_resource_picker.h @@ -83,6 +83,7 @@ class EditorResourcePicker : public HBoxContainer { }; Object *resource_owner = nullptr; + StringName property_path; PopupMenu *edit_menu = nullptr; @@ -143,6 +144,7 @@ public: bool is_toggle_pressed() const; void set_resource_owner(Object *p_object); + void set_property_path(const StringName &p_path) { property_path = p_path; } void set_editable(bool p_editable); bool is_editable() const; diff --git a/editor/inspector/multi_node_edit.cpp b/editor/inspector/multi_node_edit.cpp index 1b32a2890bb..de778409560 100644 --- a/editor/inspector/multi_node_edit.cpp +++ b/editor/inspector/multi_node_edit.cpp @@ -38,7 +38,7 @@ bool MultiNodeEdit::_set(const StringName &p_name, const Variant &p_value) { return _set_impl(p_name, p_value, ""); } -bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value, const String &p_field) { +bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value, const String &p_field, bool p_undo_redo) { Node *es = EditorNode::get_singleton()->get_edited_scene(); if (!es) { return false; @@ -59,7 +59,10 @@ bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value, EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); - ur->create_action(vformat(TTR("Set %s on %d nodes"), name, get_node_count()), UndoRedo::MERGE_ENDS); + if (p_undo_redo) { + ur->create_action(vformat(TTR("Set %s on %d nodes"), name, get_node_count()), UndoRedo::MERGE_ENDS); + } + for (const NodePath &E : nodes) { Node *n = es->get_node_or_null(E); if (!n) { @@ -71,7 +74,12 @@ bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value, if (node_path_target) { path = n->get_path_to(node_path_target); } - ur->add_do_property(n, name, path); + + if (p_undo_redo) { + ur->add_do_property(n, name, path); + } else { + n->set(name, path); + } } else { Variant new_value; if (p_field.is_empty()) { @@ -81,13 +89,22 @@ bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value, // only one field new_value = fieldwise_assign(n->get(name), p_value, p_field); } - ur->add_do_property(n, name, new_value); + + if (p_undo_redo) { + ur->add_do_property(n, name, new_value); + } else { + n->set(name, new_value); + } } - ur->add_undo_property(n, name, n->get(name)); + if (p_undo_redo) { + ur->add_undo_property(n, name, n->get(name)); + } } - ur->commit_action(); + if (p_undo_redo) { + ur->commit_action(); + } return true; } diff --git a/editor/inspector/multi_node_edit.h b/editor/inspector/multi_node_edit.h index bfc8d1d0b9f..9426cf066e5 100644 --- a/editor/inspector/multi_node_edit.h +++ b/editor/inspector/multi_node_edit.h @@ -35,6 +35,8 @@ class MultiNodeEdit : public RefCounted { GDCLASS(MultiNodeEdit, RefCounted); + friend class EditorQuickOpenDialog; + LocalVector nodes; bool notify_property_list_changed_pending = false; struct PLData { @@ -42,7 +44,7 @@ class MultiNodeEdit : public RefCounted { PropertyInfo info; }; - bool _set_impl(const StringName &p_name, const Variant &p_value, const String &p_field); + bool _set_impl(const StringName &p_name, const Variant &p_value, const String &p_field, bool p_undo_redo = true); void _queue_notify_property_list_changed(); void _notify_property_list_changed(); diff --git a/editor/settings/editor_settings.cpp b/editor/settings/editor_settings.cpp index 1ac5e4ca4d1..75418072210 100644 --- a/editor/settings/editor_settings.cpp +++ b/editor/settings/editor_settings.cpp @@ -656,6 +656,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { // Quick Open dialog EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/quick_open_dialog/max_results", 100, "0,10000,1", PROPERTY_USAGE_DEFAULT) + _initial_set("filesystem/quick_open_dialog/instant_preview", false); _initial_set("filesystem/quick_open_dialog/show_search_highlight", true); _initial_set("filesystem/quick_open_dialog/enable_fuzzy_matching", true); EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/quick_open_dialog/max_fuzzy_misses", 2, "0,10,1", PROPERTY_USAGE_DEFAULT)