Skip to content

Commit 6776319

Browse files
committed
Implement Tree Search Functionality with Highlighting and Filtering
This commit introduces a comprehensive Tree Search feature, including: - Tree highlighting: Highlights items that match the search query. - Tree filtering: Filters items so only matches and descendants are shown. - Counting descendants: Shows the number of matching items within collapsed branches. - Jump to next match: on enter. - (Limbo-)Shortcut: Default CTRL-F. - Menu entry: Misc->Search Tree. - Remember separate SearchInfo for each tab. Key implementation details: - Optimized performance for large trees - Implemented recursive filtering for efficiency - Added UI elements including next/previous match buttons Development History: - Initial implementation of highlighting and filtering - Multiple rounds of performance optimization - Bug fixes and refactoring for correctness - UI enhancements and polish - Code cleanup and style improvements
1 parent 760af80 commit 6776319

9 files changed

+921
-8
lines changed

editor/limbo_ai_editor_plugin.cpp

+27
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
261261
p_behavior_tree->editor_set_section_unfold("blackboard_plan", true);
262262
p_behavior_tree->notify_property_list_changed();
263263
#endif // LIMBOAI_MODULE
264+
// Remember current search info.
265+
if (idx_history >= 0 && idx_history < history.size()) {
266+
tab_search_context.insert(history[idx_history], task_tree->tree_search_get_search_info());
267+
}
264268

265269
task_tree->load_bt(p_behavior_tree);
266270

@@ -280,6 +284,18 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
280284
task_tree->show();
281285
task_palette->show();
282286

287+
// Restore search info from [tab_search_context].
288+
if (idx_history >= 0 && idx_history < history.size()) {
289+
// info for BehaviorTree available. Restore!
290+
if (tab_search_context.has(history[idx_history])) {
291+
task_tree->tree_search_set_search_info(tab_search_context[history[idx_history]]);
292+
}
293+
// new SearchContext.
294+
else {
295+
task_tree->tree_search_set_search_info(TreeSearch::SearchInfo());
296+
}
297+
}
298+
283299
_update_tabs();
284300
}
285301

@@ -457,6 +473,8 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
457473
_on_save_pressed();
458474
} else if (LW_IS_SHORTCUT("limbo_ai/load_behavior_tree", p_event)) {
459475
_popup_file_dialog(load_dialog);
476+
} else if (LW_IS_SHORTCUT("limbo_ai/find_task", p_event)) {
477+
task_tree->tree_search_show_and_focus();
460478
} else {
461479
handled = false;
462480
}
@@ -799,6 +817,9 @@ void LimboAIEditor::_misc_option_selected(int p_id) {
799817
EDITOR_FILE_SYSTEM()->scan();
800818
EDIT_SCRIPT(template_path);
801819
} break;
820+
case MISC_SEARCH_TREE: {
821+
task_tree->tree_search_show_and_focus();
822+
}
802823
}
803824
}
804825

@@ -1319,6 +1340,9 @@ void LimboAIEditor::_update_misc_menu() {
13191340
misc_menu->add_item(
13201341
FILE_EXISTS(_get_script_template_path()) ? TTR("Edit Script Template") : TTR("Create Script Template"),
13211342
MISC_CREATE_SCRIPT_TEMPLATE);
1343+
1344+
misc_menu->add_separator();
1345+
misc_menu->add_icon_shortcut(theme_cache.search_icon, LW_GET_SHORTCUT("limbo_ai/find_task"), MISC_SEARCH_TREE);
13221346
}
13231347

13241348
void LimboAIEditor::_update_banners() {
@@ -1381,6 +1405,7 @@ void LimboAIEditor::_do_update_theme_item_cache() {
13811405
theme_cache.cut_icon = get_theme_icon(LW_NAME(ActionCut), LW_NAME(EditorIcons));
13821406
theme_cache.copy_icon = get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons));
13831407
theme_cache.paste_icon = get_theme_icon(LW_NAME(ActionPaste), LW_NAME(EditorIcons));
1408+
theme_cache.search_icon = get_theme_icon(LW_NAME(Search), LW_NAME(EditorIcons));
13841409

13851410
theme_cache.behavior_tree_icon = LimboUtility::get_singleton()->get_task_icon("BehaviorTree");
13861411
theme_cache.percent_icon = LimboUtility::get_singleton()->get_task_icon("LimboPercent");
@@ -1512,6 +1537,8 @@ LimboAIEditor::LimboAIEditor() {
15121537
LW_SHORTCUT("limbo_ai/open_debugger", TTR("Open Debugger"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(D)));
15131538
LW_SHORTCUT("limbo_ai/jump_to_owner", TTR("Jump to Owner"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(J)));
15141539
LW_SHORTCUT("limbo_ai/close_tab", TTR("Close Tab"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(W)));
1540+
LW_SHORTCUT("limbo_ai/find_task", TTR("Find Task"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(F)));
1541+
LW_SHORTCUT("limbo_ai/hide_tree_search", TTR("Close Search"), (Key)(LW_KEY(ESCAPE)));
15151542

15161543
set_process_shortcut_input(true);
15171544

editor/limbo_ai_editor_plugin.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "owner_picker.h"
2121
#include "task_palette.h"
2222
#include "task_tree.h"
23+
#include "tree_search.h"
2324

2425
#ifdef LIMBOAI_MODULE
2526
#include "core/object/class_db.h"
@@ -47,6 +48,7 @@
4748

4849
#ifdef LIMBOAI_GDEXTENSION
4950
#include "godot_cpp/classes/accept_dialog.hpp"
51+
#include <godot_cpp/classes/config_file.hpp>
5052
#include <godot_cpp/classes/control.hpp>
5153
#include <godot_cpp/classes/editor_plugin.hpp>
5254
#include <godot_cpp/classes/editor_spin_slider.hpp>
@@ -63,7 +65,6 @@
6365
#include <godot_cpp/classes/texture2d.hpp>
6466
#include <godot_cpp/variant/packed_string_array.hpp>
6567
#include <godot_cpp/variant/variant.hpp>
66-
#include <godot_cpp/classes/config_file.hpp>
6768

6869
using namespace godot;
6970

@@ -100,6 +101,7 @@ class LimboAIEditor : public Control {
100101
MISC_LAYOUT_WIDESCREEN_OPTIMIZED,
101102
MISC_PROJECT_SETTINGS,
102103
MISC_CREATE_SCRIPT_TEMPLATE,
104+
MISC_SEARCH_TREE
103105
};
104106

105107
enum TabMenu {
@@ -134,12 +136,14 @@ class LimboAIEditor : public Control {
134136
Ref<Texture2D> cut_icon;
135137
Ref<Texture2D> copy_icon;
136138
Ref<Texture2D> paste_icon;
139+
Ref<Texture2D> search_icon;
137140
} theme_cache;
138141

139142
EditorPlugin *plugin;
140143
EditorLayout editor_layout;
141144
Vector<Ref<BehaviorTree>> history;
142145
int idx_history;
146+
HashMap<Ref<BehaviorTree>, TreeSearch::SearchInfo> tab_search_context;
143147
bool updating_tabs = false;
144148
bool request_update_tabs = false;
145149
HashSet<Ref<BehaviorTree>> dirty;

editor/task_tree.cpp

+46-6
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,23 @@
1717
#include "../bt/tasks/composites/bt_probability_selector.h"
1818
#include "../util/limbo_compat.h"
1919
#include "../util/limbo_utility.h"
20+
#include "tree_search.h"
2021

2122
#ifdef LIMBOAI_MODULE
2223
#include "core/object/script_language.h"
2324
#include "editor/themes/editor_scale.h"
2425
#include "scene/gui/box_container.h"
25-
#include "scene/gui/texture_rect.h"
2626
#include "scene/gui/label.h"
27+
#include "scene/gui/texture_rect.h"
2728
#endif // LIMBOAI_MODULE
2829

2930
#ifdef LIMBOAI_GDEXTENSION
3031
#include <godot_cpp/classes/editor_interface.hpp>
31-
#include <godot_cpp/classes/script.hpp>
3232
#include <godot_cpp/classes/h_box_container.hpp>
33-
#include <godot_cpp/classes/v_box_container.hpp>
34-
#include <godot_cpp/classes/texture_rect.hpp>
3533
#include <godot_cpp/classes/label.hpp>
34+
#include <godot_cpp/classes/script.hpp>
35+
#include <godot_cpp/classes/texture_rect.hpp>
36+
#include <godot_cpp/classes/v_box_container.hpp>
3637
using namespace godot;
3738
#endif // LIMBOAI_GDEXTENSION
3839

@@ -46,6 +47,12 @@ TreeItem *TaskTree::_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent,
4647
_create_tree(p_task->get_child(i), item);
4748
}
4849
_update_item(item);
50+
51+
// update TreeSearch if root task was created
52+
if (tree->get_root() == item) {
53+
tree_search->update_search(tree);
54+
}
55+
4956
return item;
5057
}
5158

@@ -105,6 +112,7 @@ void TaskTree::_update_item(TreeItem *p_item) {
105112
if (!warning_text.is_empty()) {
106113
p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text);
107114
}
115+
tree_search->notify_item_edited(p_item); // this is necessary to preserve custom drawing from tree search.
108116
}
109117

110118
void TaskTree::_update_tree() {
@@ -434,7 +442,7 @@ void TaskTree::_normalize_drop(TreeItem *item, int type, int &to_pos, Ref<BTTask
434442
to_pos = to_task->get_index();
435443
{
436444
Vector<Ref<BTTask>> selected = get_selected_tasks();
437-
if (to_task == selected[selected.size()-1]) {
445+
if (to_task == selected[selected.size() - 1]) {
438446
to_pos += 1;
439447
}
440448
}
@@ -530,6 +538,8 @@ void TaskTree::_notification(int p_what) {
530538
tree->connect("multi_selected", callable_mp(this, &TaskTree::_on_item_selected).unbind(3), CONNECT_DEFERRED);
531539
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated));
532540
tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed));
541+
tree_search_panel->connect("update_requested", callable_mp(tree_search.ptr(), &TreeSearch::update_search).bind(tree));
542+
tree_search_panel->connect("visibility_changed", callable_mp(tree_search.ptr(), &TreeSearch::update_search).bind(tree));
533543
} break;
534544
case NOTIFICATION_THEME_CHANGED: {
535545
_do_update_theme_item_cache();
@@ -562,12 +572,38 @@ void TaskTree::_bind_methods() {
562572
PropertyInfo(Variant::INT, "type")));
563573
}
564574

575+
// TreeSearch API
576+
void TaskTree::tree_search_show_and_focus() {
577+
ERR_FAIL_NULL(tree_search);
578+
tree_search_panel->set_visible(true);
579+
tree_search_panel->focus_editor();
580+
}
581+
582+
TreeSearch::SearchInfo TaskTree::tree_search_get_search_info() const {
583+
if (!tree_search.is_valid()) {
584+
return TreeSearch::SearchInfo();
585+
}
586+
return tree_search_panel->get_search_info();
587+
}
588+
589+
void TaskTree::tree_search_set_search_info(const TreeSearch::SearchInfo &p_search_info) {
590+
ERR_FAIL_NULL(tree_search);
591+
tree_search_panel->set_search_info(p_search_info);
592+
}
593+
594+
// TreeSearch Api ^
595+
565596
TaskTree::TaskTree() {
566597
editable = true;
567598
updating_tree = false;
568599

600+
VBoxContainer *vbox_container = memnew(VBoxContainer);
601+
add_child(vbox_container);
602+
vbox_container->set_anchors_preset(PRESET_FULL_RECT);
603+
569604
tree = memnew(Tree);
570-
add_child(tree);
605+
tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
606+
vbox_container->add_child(tree);
571607
tree->set_columns(2);
572608
tree->set_column_expand(0, true);
573609
tree->set_column_expand(1, false);
@@ -578,6 +614,10 @@ TaskTree::TaskTree() {
578614
tree->set_select_mode(Tree::SelectMode::SELECT_MULTI);
579615

580616
tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw));
617+
618+
tree_search_panel = memnew(TreeSearchPanel);
619+
tree_search = Ref(memnew(TreeSearch(tree_search_panel)));
620+
vbox_container->add_child(tree_search_panel);
581621
}
582622

583623
TaskTree::~TaskTree() {

editor/task_tree.h

+13-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
* =============================================================================
1010
*/
1111

12+
#ifndef TASK_TREE_H
13+
#define TASK_TREE_H
14+
1215
#ifdef TOOLS_ENABLED
1316

1417
#include "../bt/behavior_tree.h"
18+
#include "tree_search.h"
1519

1620
#ifdef LIMBOAI_MODULE
1721
#include "scene/gui/control.h"
@@ -43,6 +47,9 @@ class TaskTree : public Control {
4347
bool updating_tree;
4448
HashMap<RECT_CACHE_KEY, Rect2> probability_rect_cache;
4549

50+
Ref<TreeSearch> tree_search;
51+
TreeSearchPanel *tree_search_panel;
52+
4653
struct ThemeCache {
4754
Ref<Font> comment_font;
4855
Ref<Font> name_font;
@@ -96,16 +103,21 @@ class TaskTree : public Control {
96103
Ref<BTTask> get_selected() const;
97104
Vector<Ref<BTTask>> get_selected_tasks() const;
98105
void clear_selection();
99-
100106
Rect2 get_selected_probability_rect() const;
101107
double get_selected_probability_weight() const;
102108
double get_selected_probability_percent() const;
103109
bool selected_has_probability() const;
104110

111+
// TreeSearch API
112+
void tree_search_show_and_focus();
113+
TreeSearch::SearchInfo tree_search_get_search_info() const;
114+
void tree_search_set_search_info(const TreeSearch::SearchInfo &p_search_info);
115+
105116
virtual bool editor_can_reload_from_file() { return false; }
106117

107118
TaskTree();
108119
~TaskTree();
109120
};
110121

111122
#endif // ! TOOLS_ENABLED
123+
#endif // ! TASK_TREE_H

0 commit comments

Comments
 (0)