Fix PopupMenu fuzzy searching and expose fuzzy search properties

This commit is contained in:
Mikael Hermansson
2026-04-30 15:38:11 +02:00
parent bc192293b1
commit 33628f79ff
6 changed files with 132 additions and 18 deletions
+7
View File
@@ -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.
</member>
<member name="search_bar_fuzzy_search_enabled" type="bool" setter="set_search_bar_fuzzy_search_enabled" getter="is_search_bar_fuzzy_search_enabled" default="true">
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.
</member>
<member name="search_bar_fuzzy_search_max_misses" type="int" setter="set_search_bar_fuzzy_search_max_misses" getter="get_search_bar_fuzzy_search_max_misses" default="2">
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.
</member>
<member name="selected" type="int" setter="_select_int" getter="get_selected" default="-1">
The index of the currently selected item, or [code]-1[/code] if no item is selected.
</member>
+7
View File
@@ -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.
</member>
<member name="search_bar_fuzzy_search_enabled" type="bool" setter="set_search_bar_fuzzy_search_enabled" getter="is_search_bar_fuzzy_search_enabled" default="true">
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.
</member>
<member name="search_bar_fuzzy_search_max_misses" type="int" setter="set_search_bar_fuzzy_search_max_misses" getter="get_search_bar_fuzzy_search_max_misses" default="2">
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.
</member>
<member name="shrink_height" type="bool" setter="set_shrink_height" getter="get_shrink_height" default="true">
If [code]true[/code], shrinks [PopupMenu] to minimum height when it's shown.
</member>
+29
View File
@@ -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")));
+6
View File
@@ -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;
+74 -18
View File
@@ -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<FuzzySearchResult> 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<int> 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<FuzzySearchResult> 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_");
+9
View File
@@ -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<PropertyInfo> *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);