Skip to content

Commit

Permalink
Implement Tree Search Functionality with Highlighting and Filtering
Browse files Browse the repository at this point in the history
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
  • Loading branch information
monxa committed Oct 6, 2024
1 parent 760af80 commit 93524dd
Show file tree
Hide file tree
Showing 9 changed files with 919 additions and 8 deletions.
27 changes: 27 additions & 0 deletions editor/limbo_ai_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
p_behavior_tree->editor_set_section_unfold("blackboard_plan", true);
p_behavior_tree->notify_property_list_changed();
#endif // LIMBOAI_MODULE
// Remember current search info.
if (idx_history >= 0 && idx_history < history.size()) {
tab_search_context.insert(history[idx_history], task_tree->tree_search_get_search_info());
}

task_tree->load_bt(p_behavior_tree);

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

// Restore search info from [tab_search_context].
if (idx_history >= 0 && idx_history < history.size()) {
// info for BehaviorTree available. Restore!
if (tab_search_context.has(history[idx_history])) {
task_tree->tree_search_set_search_info(tab_search_context[history[idx_history]]);
}
// new SearchContext.
else {
task_tree->tree_search_set_search_info(TreeSearch::SearchInfo());
}
}

_update_tabs();
}

Expand Down Expand Up @@ -457,6 +473,8 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
_on_save_pressed();
} else if (LW_IS_SHORTCUT("limbo_ai/load_behavior_tree", p_event)) {
_popup_file_dialog(load_dialog);
} else if (LW_IS_SHORTCUT("limbo_ai/find_task", p_event)) {
task_tree->tree_search_show_and_focus();
} else {
handled = false;
}
Expand Down Expand Up @@ -799,6 +817,9 @@ void LimboAIEditor::_misc_option_selected(int p_id) {
EDITOR_FILE_SYSTEM()->scan();
EDIT_SCRIPT(template_path);
} break;
case MISC_SEARCH_TREE: {
task_tree->tree_search_show_and_focus();
}
}
}

Expand Down Expand Up @@ -1319,6 +1340,9 @@ void LimboAIEditor::_update_misc_menu() {
misc_menu->add_item(
FILE_EXISTS(_get_script_template_path()) ? TTR("Edit Script Template") : TTR("Create Script Template"),
MISC_CREATE_SCRIPT_TEMPLATE);

misc_menu->add_separator();
misc_menu->add_icon_shortcut(theme_cache.search_icon, LW_GET_SHORTCUT("limbo_ai/find_task"), MISC_SEARCH_TREE);
}

void LimboAIEditor::_update_banners() {
Expand Down Expand Up @@ -1381,6 +1405,7 @@ void LimboAIEditor::_do_update_theme_item_cache() {
theme_cache.cut_icon = get_theme_icon(LW_NAME(ActionCut), LW_NAME(EditorIcons));
theme_cache.copy_icon = get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons));
theme_cache.paste_icon = get_theme_icon(LW_NAME(ActionPaste), LW_NAME(EditorIcons));
theme_cache.search_icon = get_theme_icon(LW_NAME(Search), LW_NAME(EditorIcons));

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

set_process_shortcut_input(true);

Expand Down
6 changes: 5 additions & 1 deletion editor/limbo_ai_editor_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "owner_picker.h"
#include "task_palette.h"
#include "task_tree.h"
#include "tree_search.h"

#ifdef LIMBOAI_MODULE
#include "core/object/class_db.h"
Expand Down Expand Up @@ -47,6 +48,7 @@

#ifdef LIMBOAI_GDEXTENSION
#include "godot_cpp/classes/accept_dialog.hpp"
#include <godot_cpp/classes/config_file.hpp>
#include <godot_cpp/classes/control.hpp>
#include <godot_cpp/classes/editor_plugin.hpp>
#include <godot_cpp/classes/editor_spin_slider.hpp>
Expand All @@ -63,7 +65,6 @@
#include <godot_cpp/classes/texture2d.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/variant.hpp>
#include <godot_cpp/classes/config_file.hpp>

using namespace godot;

Expand Down Expand Up @@ -100,6 +101,7 @@ class LimboAIEditor : public Control {
MISC_LAYOUT_WIDESCREEN_OPTIMIZED,
MISC_PROJECT_SETTINGS,
MISC_CREATE_SCRIPT_TEMPLATE,
MISC_SEARCH_TREE
};

enum TabMenu {
Expand Down Expand Up @@ -134,12 +136,14 @@ class LimboAIEditor : public Control {
Ref<Texture2D> cut_icon;
Ref<Texture2D> copy_icon;
Ref<Texture2D> paste_icon;
Ref<Texture2D> search_icon;
} theme_cache;

EditorPlugin *plugin;
EditorLayout editor_layout;
Vector<Ref<BehaviorTree>> history;
int idx_history;
HashMap<Ref<BehaviorTree>, TreeSearch::SearchInfo> tab_search_context;
bool updating_tabs = false;
bool request_update_tabs = false;
HashSet<Ref<BehaviorTree>> dirty;
Expand Down
52 changes: 46 additions & 6 deletions editor/task_tree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@
#include "../bt/tasks/composites/bt_probability_selector.h"
#include "../util/limbo_compat.h"
#include "../util/limbo_utility.h"
#include "tree_search.h"

#ifdef LIMBOAI_MODULE
#include "core/object/script_language.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#endif // LIMBOAI_MODULE

#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/label.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
using namespace godot;
#endif // LIMBOAI_GDEXTENSION

Expand All @@ -46,6 +47,12 @@ TreeItem *TaskTree::_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent,
_create_tree(p_task->get_child(i), item);
}
_update_item(item);

// update TreeSearch if root task was created
if (tree->get_root() == item) {
tree_search->update_search(tree);
}

return item;
}

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

void TaskTree::_update_tree() {
Expand Down Expand Up @@ -434,7 +442,7 @@ void TaskTree::_normalize_drop(TreeItem *item, int type, int &to_pos, Ref<BTTask
to_pos = to_task->get_index();
{
Vector<Ref<BTTask>> selected = get_selected_tasks();
if (to_task == selected[selected.size()-1]) {
if (to_task == selected[selected.size() - 1]) {
to_pos += 1;
}
}
Expand Down Expand Up @@ -530,6 +538,8 @@ void TaskTree::_notification(int p_what) {
tree->connect("multi_selected", callable_mp(this, &TaskTree::_on_item_selected).unbind(3), CONNECT_DEFERRED);
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated));
tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed));
tree_search_panel->connect("update_requested", callable_mp(tree_search.ptr(), &TreeSearch::update_search).bind(tree));
tree_search_panel->connect("visibility_changed", callable_mp(tree_search.ptr(), &TreeSearch::update_search).bind(tree));
} break;
case NOTIFICATION_THEME_CHANGED: {
_do_update_theme_item_cache();
Expand Down Expand Up @@ -562,12 +572,38 @@ void TaskTree::_bind_methods() {
PropertyInfo(Variant::INT, "type")));
}

// TreeSearch API
void TaskTree::tree_search_show_and_focus() {
ERR_FAIL_NULL(tree_search);
tree_search_panel->set_visible(true);
tree_search_panel->focus_editor();
}

TreeSearch::SearchInfo TaskTree::tree_search_get_search_info() const {
if (!tree_search.is_valid()) {
return TreeSearch::SearchInfo();
}
return tree_search_panel->get_search_info();
}

void TaskTree::tree_search_set_search_info(const TreeSearch::SearchInfo &p_search_info) {
ERR_FAIL_NULL(tree_search);
tree_search_panel->set_search_info(p_search_info);
}

// TreeSearch Api ^

TaskTree::TaskTree() {
editable = true;
updating_tree = false;

VBoxContainer *vbox_container = memnew(VBoxContainer);
add_child(vbox_container);
vbox_container->set_anchors_preset(PRESET_FULL_RECT);

tree = memnew(Tree);
add_child(tree);
tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
vbox_container->add_child(tree);
tree->set_columns(2);
tree->set_column_expand(0, true);
tree->set_column_expand(1, false);
Expand All @@ -578,6 +614,10 @@ TaskTree::TaskTree() {
tree->set_select_mode(Tree::SelectMode::SELECT_MULTI);

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));

tree_search_panel = memnew(TreeSearchPanel);
tree_search = Ref(memnew(TreeSearch(tree_search_panel)));
vbox_container->add_child(tree_search_panel);
}

TaskTree::~TaskTree() {
Expand Down
14 changes: 13 additions & 1 deletion editor/task_tree.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
* =============================================================================
*/

#ifndef TASK_TREE_H
#define TASK_TREE_H

#ifdef TOOLS_ENABLED

#include "../bt/behavior_tree.h"
#include "tree_search.h"

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

Ref<TreeSearch> tree_search;
TreeSearchPanel *tree_search_panel;

struct ThemeCache {
Ref<Font> comment_font;
Ref<Font> name_font;
Expand Down Expand Up @@ -96,16 +103,21 @@ class TaskTree : public Control {
Ref<BTTask> get_selected() const;
Vector<Ref<BTTask>> get_selected_tasks() const;
void clear_selection();

Rect2 get_selected_probability_rect() const;
double get_selected_probability_weight() const;
double get_selected_probability_percent() const;
bool selected_has_probability() const;

// TreeSearch API
void tree_search_show_and_focus();
TreeSearch::SearchInfo tree_search_get_search_info() const;
void tree_search_set_search_info(const TreeSearch::SearchInfo &p_search_info);

virtual bool editor_can_reload_from_file() { return false; }

TaskTree();
~TaskTree();
};

#endif // ! TOOLS_ENABLED
#endif // ! TASK_TREE_H
Loading

0 comments on commit 93524dd

Please sign in to comment.