From 33628f79ff2f8cb18c59b5b88a8a3d230cfce9d8 Mon Sep 17 00:00:00 2001 From: Mikael Hermansson Date: Thu, 30 Apr 2026 15:38:11 +0200 Subject: [PATCH] Fix `PopupMenu` fuzzy searching and expose fuzzy search properties --- doc/classes/OptionButton.xml | 7 +++ doc/classes/PopupMenu.xml | 7 +++ scene/gui/option_button.cpp | 29 ++++++++++++ scene/gui/option_button.h | 6 +++ scene/gui/popup_menu.cpp | 92 +++++++++++++++++++++++++++++------- scene/gui/popup_menu.h | 9 ++++ 6 files changed, 132 insertions(+), 18 deletions(-) diff --git a/doc/classes/OptionButton.xml b/doc/classes/OptionButton.xml index 78aad3abf56..404c831e3c8 100644 --- a/doc/classes/OptionButton.xml +++ b/doc/classes/OptionButton.xml @@ -269,6 +269,13 @@ The text of the item at [code]index[/code]. [b]Note:[/b] [code]index[/code] is a value in the [code]0 .. item_count - 1[/code] range. + + If [code]true[/code], enables fuzzy searching in the [PopupMenu] search bar. This allows the search results to include items that almost match the search query, as well items that match the individual characters of the search query, but not in sequence. + Use [member search_bar_fuzzy_search_max_misses] to set the maximum number of mismatches allowed in the search results. + + + Sets the maximum number of mismatches allowed in each search result when fuzzy searching is enabled for the [PopupMenu] search bar. Any item with more mismatches will be hidden from the search results. + The index of the currently selected item, or [code]-1[/code] if no item is selected. diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index ddcb40c9a82..416d25831dd 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -709,6 +709,13 @@ Enables the [PopupMenu] search bar if the item count is greater than [code]0[/code]. [b]Note:[/b] When enabled, [member allow_search] is ignored. + + If [code]true[/code], enables fuzzy searching in the [PopupMenu] search bar. This allows the search results to include items that almost match the search query, as well items that match the individual characters of the search query, but not in sequence. + Use [member search_bar_fuzzy_search_max_misses] to set the maximum number of mismatches allowed in the search results. + + + Sets the maximum number of mismatches allowed in each search result when fuzzy searching is enabled for the [PopupMenu] search bar. Any item with more mismatches will be hidden from the search results. + If [code]true[/code], shrinks [PopupMenu] to minimum height when it's shown. diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index bc95bbf1fa5..e1663f21610 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -396,12 +396,29 @@ bool OptionButton::is_search_bar_enabled() const { void OptionButton::set_search_bar_enabled_on_item_count(int p_count) { popup->set_search_bar_enabled_on_item_count(p_count); + notify_property_list_changed(); } int OptionButton::get_search_bar_enabled_on_item_count() const { return popup->get_search_bar_enabled_on_item_count(); } +void OptionButton::set_search_bar_fuzzy_search_enabled(bool p_enabled) { + popup->set_search_bar_fuzzy_search_enabled(p_enabled); +} + +bool OptionButton::is_search_bar_fuzzy_search_enabled() const { + return popup->is_search_bar_fuzzy_search_enabled(); +} + +void OptionButton::set_search_bar_fuzzy_search_max_misses(int p_max_misses) { + popup->set_search_bar_fuzzy_search_max_misses(p_max_misses); +} + +int OptionButton::get_search_bar_fuzzy_search_max_misses() const { + return popup->get_search_bar_fuzzy_search_max_misses(); +} + void OptionButton::add_separator(const String &p_text) { popup->add_separator(p_text); } @@ -570,6 +587,12 @@ void OptionButton::_validate_property(PropertyInfo &p_property) const { if (p_property.name == "text" || p_property.name == "icon") { p_property.usage = PROPERTY_USAGE_NONE; } + + if (popup->get_search_bar_enabled_on_item_count() == 0) { + if (p_property.name == "search_bar_fuzzy_search_enabled" || p_property.name == "search_bar_fuzzy_search_max_misses") { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } + } } void OptionButton::_bind_methods() { @@ -583,6 +606,10 @@ void OptionButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_item_tooltip", "idx", "tooltip"), &OptionButton::set_item_tooltip); ClassDB::bind_method(D_METHOD("set_item_auto_translate_mode", "idx", "mode"), &OptionButton::set_item_auto_translate_mode); ClassDB::bind_method(D_METHOD("set_search_bar_enabled_on_item_count", "counts"), &OptionButton::set_search_bar_enabled_on_item_count); + ClassDB::bind_method(D_METHOD("set_search_bar_fuzzy_search_enabled", "enabled"), &OptionButton::set_search_bar_fuzzy_search_enabled); + ClassDB::bind_method(D_METHOD("is_search_bar_fuzzy_search_enabled"), &OptionButton::is_search_bar_fuzzy_search_enabled); + ClassDB::bind_method(D_METHOD("set_search_bar_fuzzy_search_max_misses", "max_misses"), &OptionButton::set_search_bar_fuzzy_search_max_misses); + ClassDB::bind_method(D_METHOD("get_search_bar_fuzzy_search_max_misses"), &OptionButton::get_search_bar_fuzzy_search_max_misses); ClassDB::bind_method(D_METHOD("get_item_text", "idx"), &OptionButton::get_item_text); ClassDB::bind_method(D_METHOD("get_item_icon", "idx"), &OptionButton::get_item_icon); ClassDB::bind_method(D_METHOD("get_item_id", "idx"), &OptionButton::get_item_id); @@ -620,6 +647,8 @@ void OptionButton::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fit_to_longest_item"), "set_fit_to_longest_item", "is_fit_to_longest_item"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_reselect"), "set_allow_reselect", "get_allow_reselect"); ADD_PROPERTY(PropertyInfo(Variant::INT, "enable_search_bar_on_item_count", PROPERTY_HINT_RANGE, "0,20,1,or_greater"), "set_search_bar_enabled_on_item_count", "get_search_bar_enabled_on_item_count"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "search_bar_fuzzy_search_enabled"), "set_search_bar_fuzzy_search_enabled", "is_search_bar_fuzzy_search_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "search_bar_fuzzy_search_max_misses", PROPERTY_HINT_RANGE, "0,2,1,or_greater"), "set_search_bar_fuzzy_search_max_misses", "get_search_bar_fuzzy_search_max_misses"); ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "popup/item_"); ADD_SIGNAL(MethodInfo("item_selected", PropertyInfo(Variant::INT, "index"))); diff --git a/scene/gui/option_button.h b/scene/gui/option_button.h index 9f2e785e130..7f713c9bbb0 100644 --- a/scene/gui/option_button.h +++ b/scene/gui/option_button.h @@ -121,6 +121,12 @@ public: bool is_search_bar_enabled() const; int get_search_bar_enabled_on_item_count() const; + void set_search_bar_fuzzy_search_enabled(bool p_enabled); + bool is_search_bar_fuzzy_search_enabled() const; + + void set_search_bar_fuzzy_search_max_misses(int p_max_misses); + int get_search_bar_fuzzy_search_max_misses() const; + bool has_selectable_items() const; int get_selectable_item(bool p_from_last = false) const; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 68fe74518f1..005eb04b75a 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -1100,35 +1100,58 @@ void PopupMenu::_search_bar_text_changed(const String &p_new_text) { } void PopupMenu::_filter_items(const String &p_query) { - PackedStringArray search_names; - for (int i = 0; i < items.size(); i++) { - search_names.append(items[i].text); - } - - Vector results; - FuzzySearch fuzzy; - fuzzy.set_query(p_query, false); - fuzzy.search_all(search_names, results); - for (PopupMenu::Item &item : items) { - bool submenu_visible = false; if (item.submenu) { item.submenu->_filter_items(p_query); - for (PopupMenu::Item &submenu_item : item.submenu->items) { + } + } + + for (PopupMenu::Item &item : items) { + item.visible = true; + } + + if (p_query.is_empty()) { + return; + } + + PackedStringArray search_candidates; + search_candidates.reserve(items.size()); + + Vector search_candidate_to_item; + search_candidate_to_item.reserve(items.size()); + + for (int i = 0; i < items.size(); i++) { + Item &item = items.write[i]; + item.visible = false; + + if (item.submenu) { + for (const PopupMenu::Item &submenu_item : item.submenu->items) { if (submenu_item.visible) { - submenu_visible = true; + item.visible = true; break; } } } - item.visible = p_query.length() == 0 || submenu_visible; + if (!item.separator) { + search_candidates.append(item.text); + search_candidate_to_item.append(i); + } } - for (const FuzzySearchResult &res : results) { - items.write[res.original_index].visible = res.score > 0; - if (items[res.original_index].visible && items[res.original_index].submenu) { - for (PopupMenu::Item &submenu_item : items[res.original_index].submenu->items) { + Vector results; + FuzzySearch fuzzy; + fuzzy.max_results = search_candidates.size(); + fuzzy.max_misses = search_bar_fuzzy_search_max_misses; + fuzzy.allow_subsequences = search_bar_fuzzy_search_enabled; + fuzzy.set_query(p_query, false); + fuzzy.search_all(search_candidates, results); + + for (const FuzzySearchResult &result : results) { + PopupMenu::Item &item = items.write[search_candidate_to_item[result.original_index]]; + item.visible = true; + if (item.submenu) { + for (PopupMenu::Item &submenu_item : item.submenu->items) { submenu_item.visible = true; } } @@ -3220,12 +3243,30 @@ bool PopupMenu::is_search_bar_enabled() const { void PopupMenu::set_search_bar_enabled_on_item_count(int p_count) { search_bar_enabled_on_item_count = p_count; + notify_property_list_changed(); } int PopupMenu::get_search_bar_enabled_on_item_count() const { return search_bar_enabled_on_item_count; } +void PopupMenu::set_search_bar_fuzzy_search_enabled(bool p_enabled) { + search_bar_fuzzy_search_enabled = p_enabled; +} + +bool PopupMenu::is_search_bar_fuzzy_search_enabled() const { + return search_bar_fuzzy_search_enabled; +} + +void PopupMenu::set_search_bar_fuzzy_search_max_misses(int p_max_misses) { + ERR_FAIL_COND(p_max_misses < 0); + search_bar_fuzzy_search_max_misses = p_max_misses; +} + +int PopupMenu::get_search_bar_fuzzy_search_max_misses() const { + return search_bar_fuzzy_search_max_misses; +} + #ifdef TOOLS_ENABLED PackedStringArray PopupMenu::get_configuration_warnings() const { PackedStringArray warnings = Popup::get_configuration_warnings(); @@ -3299,6 +3340,14 @@ bool PopupMenu::_set(const StringName &p_name, const Variant &p_value) { return false; } +void PopupMenu::_validate_property(PropertyInfo &p_property) const { + if (search_bar_enabled_on_item_count == 0) { + if (p_property.name == "search_bar_fuzzy_search_enabled" || p_property.name == "search_bar_fuzzy_search_max_misses") { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } + } +} + void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("activate_item_by_event", "event", "for_global_only"), &PopupMenu::activate_item_by_event, DEFVAL(false)); @@ -3409,9 +3458,14 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("is_system_menu"), &PopupMenu::is_system_menu); ClassDB::bind_method(D_METHOD("set_system_menu", "system_menu_id"), &PopupMenu::set_system_menu); ClassDB::bind_method(D_METHOD("get_system_menu"), &PopupMenu::get_system_menu); + ClassDB::bind_method(D_METHOD("is_search_bar_enabled"), &PopupMenu::is_search_bar_enabled); ClassDB::bind_method(D_METHOD("set_search_bar_enabled_on_item_count", "count"), &PopupMenu::set_search_bar_enabled_on_item_count); ClassDB::bind_method(D_METHOD("get_search_bar_enabled_on_item_count"), &PopupMenu::get_search_bar_enabled_on_item_count); + ClassDB::bind_method(D_METHOD("set_search_bar_fuzzy_search_enabled", "enabled"), &PopupMenu::set_search_bar_fuzzy_search_enabled); + ClassDB::bind_method(D_METHOD("is_search_bar_fuzzy_search_enabled"), &PopupMenu::is_search_bar_fuzzy_search_enabled); + ClassDB::bind_method(D_METHOD("set_search_bar_fuzzy_search_max_misses", "max_misses"), &PopupMenu::set_search_bar_fuzzy_search_max_misses); + ClassDB::bind_method(D_METHOD("get_search_bar_fuzzy_search_max_misses"), &PopupMenu::get_search_bar_fuzzy_search_max_misses); ClassDB::bind_method(D_METHOD("set_shrink_height", "shrink"), &PopupMenu::set_shrink_height); ClassDB::bind_method(D_METHOD("get_shrink_height"), &PopupMenu::get_shrink_height); @@ -3430,6 +3484,8 @@ void PopupMenu::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shrink_width"), "set_shrink_width", "get_shrink_width"); ADD_PROPERTY(PropertyInfo(Variant::INT, "search_bar_enabled_on_item_count", PROPERTY_HINT_RANGE, "0,20,1,or_greater"), "set_search_bar_enabled_on_item_count", "get_search_bar_enabled_on_item_count"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "search_bar_fuzzy_search_enabled"), "set_search_bar_fuzzy_search_enabled", "is_search_bar_fuzzy_search_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "search_bar_fuzzy_search_max_misses", PROPERTY_HINT_RANGE, "0,2,1,or_greater"), "set_search_bar_fuzzy_search_max_misses", "get_search_bar_fuzzy_search_max_misses"); ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "item_"); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index 515ddfa1605..0f432798ed8 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -180,6 +180,8 @@ class PopupMenu : public Popup { String search_string = ""; int search_bar_enabled_on_item_count = 0; + bool search_bar_fuzzy_search_enabled = true; + int search_bar_fuzzy_search_max_misses = 2; PanelContainer *panel = nullptr; VBoxContainer *vbox_container = nullptr; LineEdit *search_bar = nullptr; @@ -271,6 +273,7 @@ protected: void _get_property_list(List *p_list) const { property_helper.get_property_list(p_list); } bool _property_can_revert(const StringName &p_name) const { return property_helper.property_can_revert(p_name); } bool _property_get_revert(const StringName &p_name, Variant &r_property) const { return property_helper.property_get_revert(p_name, r_property); } + void _validate_property(PropertyInfo &p_property) const; static void _bind_methods(); virtual String _get_accessibility_name() const override; @@ -392,6 +395,12 @@ public: void set_search_bar_enabled_on_item_count(int p_count); int get_search_bar_enabled_on_item_count() const; + void set_search_bar_fuzzy_search_enabled(bool p_enabled); + bool is_search_bar_fuzzy_search_enabled() const; + + void set_search_bar_fuzzy_search_max_misses(int p_max_misses); + int get_search_bar_fuzzy_search_max_misses() const; + bool is_native_menu() const; void scroll_to_item(int p_idx);