From f3833e82ac3fc964c015552cf8a5565fd1bc8ed7 Mon Sep 17 00:00:00 2001 From: Micky Date: Wed, 12 Oct 2022 19:43:56 +0200 Subject: [PATCH] Improve Scene Tree Dock's Node filter (Allow multiple terms & more) --- editor/scene_tree_dock.cpp | 76 +++++++++++++++- editor/scene_tree_dock.h | 10 ++ editor/scene_tree_editor.cpp | 172 ++++++++++++++++++++++++++++------- editor/scene_tree_editor.h | 6 +- 4 files changed, 230 insertions(+), 34 deletions(-) diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 04646ea2c..80f5d92ec 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -1293,6 +1293,8 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { } break; default: { + _filter_option_selected(p_tool); + if (p_tool >= EDIT_SUBRESOURCE_BASE) { int idx = p_tool - EDIT_SUBRESOURCE_BASE; @@ -3041,8 +3043,69 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { menu->popup(); } +void SceneTreeDock::_update_filter_menu() { + _append_filter_options_to(filter->get_menu()); +} + void SceneTreeDock::_filter_changed(const String &p_filter) { scene_tree->set_filter(p_filter); + + String warning = scene_tree->get_filter_term_warning(); + if (!warning.empty()) { + filter->add_icon_override("clear", get_icon("NodeWarning", "EditorIcons")); + filter->set_tooltip(warning); + } else { + filter->remove_icon_override("clear"); + filter->set_tooltip(""); + } +} + +void SceneTreeDock::_filter_gui_input(const Ref &p_event) { + Ref mb = p_event; + if (mb.is_null()) { + return; + } + + if (mb->is_pressed() && mb->get_button_index() == BUTTON_MIDDLE) { + filter_quick_menu->clear(); + + _append_filter_options_to(filter_quick_menu, false); + filter_quick_menu->set_position(get_global_position() + get_local_mouse_position()); + filter_quick_menu->popup(); + filter_quick_menu->grab_focus(); + accept_event(); + } +} + +void SceneTreeDock::_filter_option_selected(int p_option) { + String filter_parameter; + switch (p_option) { + case FILTER_BY_TYPE: { + filter_parameter = "type"; + } break; + case FILTER_BY_GROUP: { + filter_parameter = "group"; + } break; + } + + if (!filter_parameter.empty()) { + set_filter((get_filter() + " " + filter_parameter + ":").strip_edges()); + filter->set_cursor_position(filter->get_text().length()); + filter->grab_focus(); + } +} + +void SceneTreeDock::_append_filter_options_to(PopupMenu *p_menu, bool p_include_separator) { + if (p_include_separator) { + p_menu->add_separator(); + } + + p_menu->add_item(TTR("Filter by Type"), FILTER_BY_TYPE); + p_menu->add_item(TTR("Filter by Group"), FILTER_BY_GROUP); + p_menu->set_item_icon(p_menu->get_item_index(FILTER_BY_TYPE), get_icon("Node", "EditorIcons")); + p_menu->set_item_icon(p_menu->get_item_index(FILTER_BY_GROUP), get_icon("Groups", "EditorIcons")); + p_menu->set_item_tooltip(p_menu->get_item_index(FILTER_BY_TYPE), TTR("Selects all Nodes of the given type.")); + p_menu->set_item_tooltip(p_menu->get_item_index(FILTER_BY_GROUP), TTR("Selects all Nodes belonging to the given group.\nIf empty, selects any Node belonging to any group.")); } String SceneTreeDock::get_filter() { @@ -3378,6 +3441,9 @@ void SceneTreeDock::_bind_methods() { ClassDB::bind_method(D_METHOD("_script_dropped"), &SceneTreeDock::_script_dropped); ClassDB::bind_method(D_METHOD("_tree_rmb"), &SceneTreeDock::_tree_rmb); ClassDB::bind_method(D_METHOD("_filter_changed"), &SceneTreeDock::_filter_changed); + ClassDB::bind_method(D_METHOD("_filter_gui_input"), &SceneTreeDock::_filter_gui_input); + ClassDB::bind_method(D_METHOD("_update_filter_menu"), &SceneTreeDock::_update_filter_menu); + ClassDB::bind_method(D_METHOD("_filter_option_selected"), &SceneTreeDock::_filter_option_selected); ClassDB::bind_method(D_METHOD("_focus_node"), &SceneTreeDock::_focus_node); ClassDB::bind_method(D_METHOD("_remote_tree_selected"), &SceneTreeDock::_remote_tree_selected); ClassDB::bind_method(D_METHOD("_local_tree_selected"), &SceneTreeDock::_local_tree_selected); @@ -3482,14 +3548,22 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel button_instance->set_tooltip(TTR("Instance a scene file as a Node. Creates an inherited scene if no root node exists.")); button_instance->set_shortcut(ED_GET_SHORTCUT("scene_tree/instance_scene")); filter_hbc->add_child(button_instance); - vbc->add_child(filter_hbc); + + // The "Filter Nodes" text input above the Scene Tree Editor. filter = memnew(LineEdit); filter->set_h_size_flags(SIZE_EXPAND_FILL); filter->set_placeholder(TTR("Filter nodes")); filter_hbc->add_child(filter); filter->add_theme_constant_override("minimum_spaces", 0); filter->connect("text_changed", this, "_filter_changed"); + filter->connect("gui_input", this, "_filter_gui_input"); + filter->get_menu()->connect("id_pressed", this, "_filter_option_selected"); + call_deferred("_update_filter_menu"); + + filter_quick_menu = memnew(PopupMenu); + filter_quick_menu->connect("id_pressed", this, "_filter_option_selected"); + filter->add_child(filter_quick_menu); button_create_script = memnew(ToolButton); button_create_script->connect("pressed", this, "_tool_selected", make_binds(TOOL_ATTACH_SCRIPT, false)); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index 98f4387f7..147ad4d20 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -180,7 +180,13 @@ class SceneTreeDock : public VBoxContainer { EditorSubScene *import_subscene_dialog; EditorFileDialog *new_scene_from_dialog; + enum FilterMenuItems { + FILTER_BY_TYPE = 64, // Used in the same menus as the Tool enum. + FILTER_BY_GROUP, + }; + LineEdit *filter; + PopupMenu *filter_quick_menu; TextureRect *filter_icon; PopupMenu *menu; @@ -264,8 +270,12 @@ class SceneTreeDock : public VBoxContainer { void _quick_open(); void _tree_rmb(const Vector2 &p_menu_pos); + void _update_filter_menu(); void _filter_changed(const String &p_filter); + void _filter_gui_input(const Ref &p_event); + void _filter_option_selected(int option); + void _append_filter_options_to(PopupMenu *p_menu, bool p_include_separator = true); void _perform_instance_scenes(const Vector &p_files, Node *parent, int p_pos); void _replace_with_branch_scene(const String &p_file, Node *base); diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index 1e458c9a0..826c03b20 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -194,9 +194,9 @@ void SceneTreeEditor::_toggle_visible(Node *p_node) { } } -bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll_to_selected) { +void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { if (!p_node) { - return false; + return; } // only owned nodes are editable, since nodes can create their own (manually owned) child nodes, @@ -209,7 +209,7 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll part_of_subscene = true; //allow } else { - return false; + return; } } else { part_of_subscene = p_node != get_scene_node() && get_scene_node()->get_scene_inherited_state().is_valid() && get_scene_node()->get_scene_inherited_state()->find_node_by_path(get_scene_node()->get_path_to(p_node)) >= 0; @@ -275,7 +275,7 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll } } else if (part_of_subscene) { if (valid_types.size() == 0) { - item->set_custom_color(0, get_theme_color("disabled_font_color", "Editor")); + item->set_custom_color(0, get_theme_color("warning_color", "Editor")); } } else if (marked.has(p_node)) { String node_name = p_node->get_name(); @@ -462,29 +462,21 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll } } - bool scroll = false; - if (editor_selection) { if (editor_selection->is_selected(p_node)) { item->select(0); - scroll = p_scroll_to_selected; } } if (selected == p_node) { if (!editor_selection) { item->select(0); - scroll = p_scroll_to_selected; } item->set_as_cursor(0); } - bool keep = (filter.is_subsequence_ofi(String(p_node->get_name()))); - for (int i = 0; i < p_node->get_child_count(); i++) { - bool child_keep = _add_nodes(p_node->get_child(i), item, p_scroll_to_selected); - - keep = keep || child_keep; + _add_nodes(p_node->get_child(i), item); } if (valid_types.size()) { @@ -497,27 +489,10 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll } if (!valid) { - //item->set_selectable(0,marked_selectable); item->set_custom_color(0, get_theme_color("disabled_font_color", "Editor")); item->set_selectable(0, false); } } - - if (!keep) { - if (editor_selection) { - Node *n = get_node(item->get_metadata(0)); - if (n) { - editor_selection->remove_node(n); - } - } - memdelete(item); - return false; - } else { - if (scroll) { - tree->scroll_to_item(item); - } - return true; - } } void SceneTreeEditor::_node_visibility_changed(Node *p_node) { @@ -634,13 +609,142 @@ void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) { updating_tree = true; tree->clear(); if (get_scene_node()) { - _add_nodes(get_scene_node(), nullptr, p_scroll_to_selected); + _add_nodes(get_scene_node(), nullptr); last_hash = hash_djb2_one_64(0); _compute_hash(get_scene_node(), last_hash); } updating_tree = false; - tree_dirty = false; + + if (!filter.strip_edges().empty()) { + _update_filter(nullptr, p_scroll_to_selected); + } +} + +bool SceneTreeEditor::_update_filter(TreeItem *p_parent, bool p_scroll_to_selected) { + if (!p_parent) { + p_parent = tree->get_root(); + filter_term_warning.clear(); + } + if (!p_parent) { + // Tree is empty, nothing to do here. + return false; + } + + bool keep_for_children = false; + for (TreeItem *child = p_parent->get_children(); child; child = child->get_next()) { + // Always keep if at least one of the children are kept. + keep_for_children = _update_filter(child, p_scroll_to_selected) || keep_for_children; + } + + // Now find other reasons to keep this Node, too. + Vector terms = filter.to_lower().split_spaces(); + bool keep = _item_matches_all_terms(p_parent, terms); + + if (keep_for_children || keep) { + if (keep_for_children) { + if (!keep) { + const Color original_color = p_parent->get_custom_color(0); + const Color disabled_color = get_color("disabled_font_color", "Editor"); + if (original_color == Color()) { + p_parent->set_custom_color(0, disabled_color); + } else { + p_parent->set_custom_color(0, original_color.linear_interpolate(disabled_color, 0.5)); + p_parent->set_selectable(0, false); + p_parent->deselect(0); + } + } + } + } else { + memdelete(p_parent); + } + + if (editor_selection) { + Node *n = get_node_or_null(p_parent->get_metadata(0)); + if (keep) { + if (p_scroll_to_selected && n && editor_selection->is_selected(n)) { + tree->scroll_to_item(p_parent); + } + } else { + if (n && p_parent->is_selected(0)) { + editor_selection->remove_node(n); + p_parent->deselect(0); + } + } + } + + return keep || keep_for_children; +} + +bool SceneTreeEditor::_item_matches_all_terms(TreeItem *p_item, Vector p_terms) { + if (p_terms.empty()) { + return true; + } + + for (int i = 0; i < p_terms.size(); i++) { + String term = p_terms[i]; + + // Recognise special filter. + if (term.find(":") > -1 && !term.get_slicec(':', 0).empty()) { + String parameter = term.get_slicec(':', 0); + String argument = term.get_slicec(':', 1); + + if (parameter == "type" || parameter == "t") { + // Filter by Type. + String type = get_node(p_item->get_metadata(0))->get_class(); + bool term_in_inherited_class = false; + // Every Node is is a Node, duh! + while (type != "Node") { + if (type.to_lower().find(argument) > -1) { + term_in_inherited_class = true; + break; + } + + type = ClassDB::get_parent_class(type); + } + if (!term_in_inherited_class) { + return false; + } + } else if (parameter == "group" || parameter == "g") { + // Filter by Group. + Node *node = get_node(p_item->get_metadata(0)); + + if (argument.empty()) { + // When argument is empty, match all Nodes belonging to any exposed group. + if (node->get_persistent_group_count() == 0) { + return false; + } + } else { + List group_info_list; + node->get_groups(&group_info_list); + + bool term_in_groups = false; + for (int j = 0; j < group_info_list.size(); j++) { + if (!group_info_list[j].persistent) { + continue; // Ignore internal groups. + } + if (String(group_info_list[j].name).to_lower().find(argument) > -1) { + term_in_groups = true; + break; + } + } + if (!term_in_groups) { + return false; + } + } + } else if (filter_term_warning.empty()) { + filter_term_warning = vformat(TTR("\"%s\" is not a known filter."), parameter); + continue; + } + } else { + // Default. + if (p_item->get_text(0).to_lower().find(term) == -1) { + return false; + } + } + } + + return true; } void SceneTreeEditor::_compute_hash(Node *p_node, uint64_t &hash) { @@ -932,6 +1036,10 @@ String SceneTreeEditor::get_filter() const { return filter; } +String SceneTreeEditor::get_filter_term_warning() const { + return filter_term_warning; +} + void SceneTreeEditor::set_display_foreign_nodes(bool p_display) { display_foreign = p_display; _update_tree(); diff --git a/editor/scene_tree_editor.h b/editor/scene_tree_editor.h index eea277ad5..7da8666ad 100644 --- a/editor/scene_tree_editor.h +++ b/editor/scene_tree_editor.h @@ -76,6 +76,7 @@ class SceneTreeEditor : public Control { ObjectID instance_node; String filter; + String filter_term_warning; AcceptDialog *error; AcceptDialog *warning; @@ -87,9 +88,11 @@ class SceneTreeEditor : public Control { void _compute_hash(Node *p_node, uint64_t &hash); - bool _add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll_to_selected = false); + void _add_nodes(Node *p_node, TreeItem *p_parent); void _test_update_tree(); void _update_tree(bool p_scroll_to_selected = false); + bool _update_filter(TreeItem *p_parent = nullptr, bool p_scroll_to_selected = false); + bool _item_matches_all_terms(TreeItem *p_item, Vector p_terms); void _tree_changed(); void _node_removed(Node *p_node); void _node_renamed(Node *p_node); @@ -149,6 +152,7 @@ class SceneTreeEditor : public Control { public: void set_filter(const String &p_filter); String get_filter() const; + String get_filter_term_warning() const; void set_undo_redo(UndoRedo *p_undo_redo) { undo_redo = p_undo_redo; }; void set_display_foreign_nodes(bool p_display);