diff --git a/doc/classes/OptionButton.xml b/doc/classes/OptionButton.xml
index 728bc23c4a..6c72fad04a 100644
--- a/doc/classes/OptionButton.xml
+++ b/doc/classes/OptionButton.xml
@@ -140,6 +140,12 @@
Returns [code]true[/code] if the item at index [param idx] is marked as a separator.
+
+
+
+ Returns [code]true[/code] if the search bar is enabled.
+
+
@@ -233,6 +239,9 @@
If [code]true[/code], the currently selected item can be selected again.
+
+ Enables the [PopupMenu] search bar if the item count is greater than [code]0[/code].
+
If [code]true[/code], minimum size will be determined by the longest item's text, instead of the currently selected one's.
[b]Note:[/b] For performance reasons, the minimum size doesn't update immediately when adding, removing or modifying items.
diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml
index 8676051bc7..fb6a05c5fe 100644
--- a/doc/classes/PopupMenu.xml
+++ b/doc/classes/PopupMenu.xml
@@ -408,6 +408,12 @@
Returns [code]true[/code] if the system native menu is supported and currently used by this [PopupMenu].
+
+
+
+ Returns [code]true[/code] if search bar is currently enabled.
+
+
@@ -669,6 +675,10 @@
If [code]true[/code], [MenuBar] will use native menu when supported.
[b]Note:[/b] If [PopupMenu] is linked to [StatusIndicator], [MenuBar], or another [PopupMenu] item it can use native menu regardless of this property, use [method is_native_menu] to check it.
+
+ 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], shrinks [PopupMenu] to minimum height when it's shown.
@@ -791,6 +801,9 @@
[Texture2D] icon for the unchecked radio button items when they are disabled.
+
+ [Texture2D] icon for the search bar's search icon.
+
[Texture2D] icon for the submenu arrow (for left-to-right layouts).
diff --git a/editor/inspector/editor_properties.cpp b/editor/inspector/editor_properties.cpp
index f02dbf5463..80876daa99 100644
--- a/editor/inspector/editor_properties.cpp
+++ b/editor/inspector/editor_properties.cpp
@@ -562,6 +562,7 @@ EditorPropertyTextEnum::EditorPropertyTextEnum() {
option_button->set_h_size_flags(SIZE_EXPAND_FILL);
option_button->set_clip_text(true);
option_button->set_flat(true);
+ option_button->set_search_bar_enabled_on_item_count(10);
option_button->set_theme_type_variation(SNAME("EditorInspectorButton"));
option_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
default_layout->add_child(option_button);
@@ -969,6 +970,7 @@ EditorPropertyEnum::EditorPropertyEnum() {
options->set_flat(true);
options->set_theme_type_variation(SNAME("EditorInspectorButton"));
options->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
+ options->set_search_bar_enabled_on_item_count(10);
add_child(options);
add_focusable(options);
options->connect(SceneStringName(item_selected), callable_mp(this, &EditorPropertyEnum::_option_selected));
diff --git a/editor/themes/theme_classic.cpp b/editor/themes/theme_classic.cpp
index 5d70852f9a..0f89d097fd 100644
--- a/editor/themes/theme_classic.cpp
+++ b/editor/themes/theme_classic.cpp
@@ -1119,6 +1119,7 @@ void ThemeClassic::populate_standard_styles(const Ref &p_theme, Edi
p_theme->set_icon("radio_unchecked_disabled", "PopupMenu", p_theme->get_icon(SNAME("GuiRadioUncheckedDisabled"), EditorStringName(EditorIcons)));
p_theme->set_icon("submenu", "PopupMenu", p_theme->get_icon(SNAME("ArrowRight"), EditorStringName(EditorIcons)));
p_theme->set_icon("submenu_mirrored", "PopupMenu", p_theme->get_icon(SNAME("ArrowLeft"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("search", "PopupMenu", p_theme->get_icon(SNAME("Search"), EditorStringName(EditorIcons)));
int v_sep = (p_config.enable_touch_optimizations ? 12 : p_config.forced_even_separation) * EDSCALE;
p_theme->set_constant("v_separation", "PopupMenu", v_sep);
diff --git a/editor/themes/theme_modern.cpp b/editor/themes/theme_modern.cpp
index e3496c28bb..32419c2fca 100644
--- a/editor/themes/theme_modern.cpp
+++ b/editor/themes/theme_modern.cpp
@@ -1094,6 +1094,7 @@ void ThemeModern::populate_standard_styles(const Ref &p_theme, Edit
p_theme->set_icon("radio_unchecked_disabled", "PopupMenu", p_theme->get_icon(SNAME("GuiRadioUncheckedDisabled"), EditorStringName(EditorIcons)));
p_theme->set_icon("submenu", "PopupMenu", p_theme->get_icon(SNAME("ArrowRight"), EditorStringName(EditorIcons)));
p_theme->set_icon("submenu_mirrored", "PopupMenu", p_theme->get_icon(SNAME("ArrowLeft"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("search", "PopupMenu", p_theme->get_icon(SNAME("Search"), EditorStringName(EditorIcons)));
p_theme->set_constant("h_separation", "PopupMenu", p_config.base_margin * 1.75 * EDSCALE);
int v_sep = (p_config.enable_touch_optimizations ? 12 : p_config.base_margin * 1.75) * EDSCALE;
diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp
index d0b376736a..ddf8805fef 100644
--- a/scene/gui/option_button.cpp
+++ b/scene/gui/option_button.cpp
@@ -387,6 +387,18 @@ bool OptionButton::get_allow_reselect() const {
return allow_reselect;
}
+bool OptionButton::is_search_bar_enabled() const {
+ return popup->is_search_bar_enabled();
+}
+
+void OptionButton::set_search_bar_enabled_on_item_count(int p_count) {
+ popup->set_search_bar_enabled_on_item_count(p_count);
+}
+
+int OptionButton::get_search_bar_enabled_on_item_count() const {
+ return popup->get_search_bar_enabled_on_item_count();
+}
+
void OptionButton::add_separator(const String &p_text) {
popup->add_separator(p_text);
}
@@ -567,6 +579,7 @@ void OptionButton::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_item_metadata", "idx", "metadata"), &OptionButton::set_item_metadata);
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("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);
@@ -576,6 +589,8 @@ void OptionButton::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_item_auto_translate_mode", "idx"), &OptionButton::get_item_auto_translate_mode);
ClassDB::bind_method(D_METHOD("is_item_disabled", "idx"), &OptionButton::is_item_disabled);
ClassDB::bind_method(D_METHOD("is_item_separator", "idx"), &OptionButton::is_item_separator);
+ ClassDB::bind_method(D_METHOD("is_search_bar_enabled"), &OptionButton::is_search_bar_enabled);
+ ClassDB::bind_method(D_METHOD("get_search_bar_enabled_on_item_count"), &OptionButton::get_search_bar_enabled_on_item_count);
ClassDB::bind_method(D_METHOD("add_separator", "text"), &OptionButton::add_separator, DEFVAL(String()));
ClassDB::bind_method(D_METHOD("clear"), &OptionButton::clear);
ClassDB::bind_method(D_METHOD("select", "idx"), &OptionButton::select);
@@ -601,6 +616,7 @@ void OptionButton::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::INT, "selected"), "_select_int", "get_selected");
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_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 e35bb7fd0c..9f2e785e13 100644
--- a/scene/gui/option_button.h
+++ b/scene/gui/option_button.h
@@ -107,6 +107,7 @@ public:
void set_item_disabled(int p_idx, bool p_disabled);
void set_item_tooltip(int p_idx, const String &p_tooltip);
void set_item_auto_translate_mode(int p_idx, AutoTranslateMode p_mode);
+ void set_search_bar_enabled_on_item_count(int p_count);
String get_item_text(int p_idx) const;
Ref get_item_icon(int p_idx) const;
@@ -117,6 +118,8 @@ public:
bool is_item_separator(int p_idx) const;
String get_item_tooltip(int p_idx) const;
AutoTranslateMode get_item_auto_translate_mode(int p_idx) const;
+ bool is_search_bar_enabled() const;
+ int get_search_bar_enabled_on_item_count() 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 7c3592c588..9f4684572b 100644
--- a/scene/gui/popup_menu.cpp
+++ b/scene/gui/popup_menu.cpp
@@ -35,7 +35,9 @@
#include "core/input/input.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
+#include "scene/gui/box_container.h"
#include "scene/gui/graph_edit.h"
+#include "scene/gui/line_edit.h"
#include "scene/gui/menu_bar.h"
#include "scene/gui/panel_container.h"
#include "scene/main/timer.h"
@@ -315,6 +317,9 @@ int PopupMenu::_get_items_total_height() const {
// Get total height of all items by taking max of icon height and font height
int items_total_height = 0;
for (int i = 0; i < items.size(); i++) {
+ if (!items[i].visible) {
+ continue;
+ }
items_total_height += _get_item_height(i) + theme_cache.v_separation;
}
@@ -339,6 +344,9 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const {
const float over_control_y = control->get_transform().xform_inv(over_scroll_container).y;
float bottom_edge = 0;
for (int i = 0; i < items.size(); i++) {
+ if (!items[i].visible) {
+ continue;
+ }
bottom_edge += theme_cache.v_separation;
bottom_edge += _get_item_height(i);
if (bottom_edge > over_control_y) {
@@ -374,8 +382,8 @@ void PopupMenu::_activate_submenu(int p_over, bool p_by_keyboard) {
Rect2i screen_rect = is_embedded() ? Rect2i(get_embedder()->get_visible_rect()) : get_parent_rect();
active_submenu_target_line.clear();
- panel_offset_start = Point2(panel->get_offset(SIDE_LEFT), panel->get_offset(SIDE_TOP)) * win_scale;
- const Point2 panel_offset_end = Point2(-panel->get_offset(SIDE_RIGHT), -panel->get_offset(SIDE_BOTTOM)) * win_scale;
+ scroll_container_offset_start = Point2(scroll_container->get_offset(SIDE_LEFT), scroll_container->get_offset(SIDE_TOP)) * win_scale;
+ const Point2 scroll_container_offset_end = Point2(-scroll_container->get_offset(SIDE_RIGHT), -scroll_container->get_offset(SIDE_BOTTOM)) * win_scale;
const Vector2 scaled_this_size = this_rect.size * win_scale;
const float scaled_theme_v_separation = theme_cache.v_separation * win_scale;
const float scroll_offset = control->get_position().y;
@@ -384,17 +392,17 @@ void PopupMenu::_activate_submenu(int p_over, bool p_by_keyboard) {
if (is_layout_rtl()) {
is_active_submenu_left = true;
- submenu_pos += this_pos + Point2(-submenu_size.width + panel_offset_end.x, scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + panel_offset_start.y);
+ submenu_pos += this_pos + Point2(-submenu_size.width + scroll_container_offset_end.x, scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + scroll_container_offset_start.y);
if (submenu_pos.x < screen_rect.position.x) {
- submenu_pos.x = this_pos.x + this_rect.size.width - panel_offset_start.x;
+ submenu_pos.x = this_pos.x + this_rect.size.width - scroll_container_offset_start.x;
is_active_submenu_left = false;
}
} else {
is_active_submenu_left = false;
- submenu_pos += this_pos + Point2(scaled_this_size.x + panel_offset_start.x, scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + panel_offset_start.y);
+ submenu_pos += this_pos + Point2(scaled_this_size.x + scroll_container_offset_start.x, scaled_ofs_cache + scroll_offset - int(scaled_theme_v_separation * 0.5) + scroll_container_offset_start.y);
if (submenu_pos.x + submenu_size.width > screen_rect.position.x + screen_rect.size.width) {
- submenu_pos.x = this_pos.x - submenu_size.width + panel_offset_end.x;
+ submenu_pos.x = this_pos.x - submenu_size.width + scroll_container_offset_end.x;
is_active_submenu_left = true;
}
}
@@ -509,7 +517,7 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
bool match_found = false;
for (int i = search_from; i < items.size(); i++) {
- if (!items[i].separator && !items[i].disabled) {
+ if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
emit_signal(SNAME("id_focused"), items[i].id);
@@ -525,7 +533,7 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
if (!match_found) {
// If the last item is not selectable, try re-searching from the start.
for (int i = 0; i < search_from; i++) {
- if (!items[i].separator && !items[i].disabled) {
+ if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
emit_signal(SNAME("id_focused"), items[i].id);
@@ -552,7 +560,7 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
bool match_found = false;
for (int i = search_from; i >= 0; i--) {
- if (!items[i].separator && !items[i].disabled) {
+ if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
emit_signal(SNAME("id_focused"), items[i].id);
@@ -568,7 +576,7 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
if (!match_found) {
// If the first item is not selectable, try re-searching from the end.
for (int i = items.size() - 1; i >= search_from; i--) {
- if (!items[i].separator && !items[i].disabled) {
+ if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
emit_signal(SNAME("id_focused"), items[i].id);
@@ -688,6 +696,9 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
int over = _get_mouse_over(b->get_position());
if (over < 0) {
+ if (search_bar->is_visible() && search_bar->get_global_rect().has_point(b->get_position() / win_scale)) {
+ return;
+ }
if (!was_during_grabbed_click) {
hide();
}
@@ -720,7 +731,7 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
if (this_submenu_index != -1) { // Is a submenu.
PopupMenu *parent_popup = Object::cast_to(get_parent());
ERR_FAIL_NULL(parent_popup);
- Point2 areas_mouse_pos = get_mouse_position() - parent_popup->panel_offset_start;
+ Point2 areas_mouse_pos = get_mouse_position() - parent_popup->scroll_container_offset_start;
for (const Rect2 &E : autohide_areas) {
if (!scroll_container->get_global_rect().has_point(m->get_position()) && E.has_point(areas_mouse_pos)) {
_close_or_suspend();
@@ -754,7 +765,7 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) {
Ref k = p_event;
- if (allow_search && k.is_valid() && k->get_unicode() && k->is_pressed()) {
+ if (!search_bar->is_visible() && allow_search && k.is_valid() && k->get_unicode() && k->is_pressed()) {
uint64_t now = OS::get_singleton()->get_ticks_msec();
uint64_t diff = now - search_time_msec;
uint64_t max_interval = uint64_t(GLOBAL_GET_CACHED(uint64_t, "gui/timers/incremental_search_max_interval_msec"));
@@ -868,7 +879,7 @@ void PopupMenu::_draw_items() {
bool has_check_gutter = false;
bool gutter_compact = theme_cache.gutter_compact;
for (int i = 0; i < items.size(); i++) {
- if (items[i].separator) {
+ if (items[i].separator || !items[i].visible) {
continue;
}
@@ -892,6 +903,10 @@ void PopupMenu::_draw_items() {
// Loop through all items and draw each.
for (int i = 0; i < items.size(); i++) {
+ if (!items[i].visible) {
+ continue;
+ }
+
// For the first item only add half a separation. For all other items, add a whole separation to the offset.
ofs.y += i > 0 ? theme_cache.v_separation : theme_cache.v_separation / 2;
@@ -1048,6 +1063,59 @@ void PopupMenu::_draw_items() {
}
}
+void PopupMenu::_search_bar_input(const Ref &p_event) {
+ // Redirect navigational key events to the tree.
+ Ref key = p_event;
+ if (key.is_valid()) {
+ if (key->is_action("ui_up", true) || key->is_action("ui_down", true) || key->is_action("ui_page_up") || key->is_action("ui_page_down")) {
+ search_bar->accept_event();
+ }
+ }
+}
+
+void PopupMenu::_search_bar_text_changed(const String &p_new_text) {
+ _filter_items(p_new_text);
+
+ queue_accessibility_update();
+ control->queue_redraw();
+ child_controls_changed();
+ notify_property_list_changed();
+ _menu_changed();
+}
+
+void PopupMenu::_filter_items(const String &p_query) {
+ for (PopupMenu::Item &item : items) {
+ bool all_sub_items_invisible = true;
+ if (item.submenu) {
+ item.submenu->_filter_items(p_query);
+ for (const PopupMenu::Item &submenu_item : item.submenu->items) {
+ if (submenu_item.visible) {
+ all_sub_items_invisible = false;
+ break;
+ }
+ }
+ }
+
+ if (p_query.length() > 0) {
+ bool contains = item.text.containsn(p_query);
+ if (item.submenu) {
+ if (contains) {
+ item.visible = true;
+ for (PopupMenu::Item &submenu_item : item.submenu->items) {
+ submenu_item.visible = true;
+ }
+ } else {
+ item.visible = !all_sub_items_invisible;
+ }
+ } else {
+ item.visible = contains;
+ }
+ } else {
+ item.visible = true;
+ }
+ }
+}
+
void PopupMenu::_close_pressed() {
if (this_submenu_index != -1 && active_submenu_index != -1) { // Close secondary submenus on first submenu close.
ERR_FAIL_INDEX_MSG(active_submenu_index, items.size(), vformat("Invalid active_submenu_index index %d in _close_pressed.", active_submenu_index));
@@ -1264,6 +1332,9 @@ void PopupMenu::_notification(int p_what) {
for (int i = 0; i < items.size(); i++) {
const Item &item = items.write[i];
+ if (!item.visible) {
+ continue;
+ }
// Avoid discrepancy between min size and drawn items due to rounding.
ofs.y += i > 0 ? theme_cache.v_separation : theme_cache.v_separation - (theme_cache.v_separation / 2);
@@ -1324,6 +1395,8 @@ void PopupMenu::_notification(int p_what) {
case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED: {
+ search_bar->set_right_icon(get_theme_icon(SNAME("search")));
+
panel->add_theme_style_override(SceneStringName(panel), theme_cache.panel_style);
if (is_visible()) {
@@ -1360,6 +1433,9 @@ void PopupMenu::_notification(int p_what) {
} break;
case NOTIFICATION_WM_MOUSE_ENTER: {
+ if (is_embedded() && search_bar->is_menu_visible()) {
+ return;
+ }
grab_focus();
} break;
@@ -1407,7 +1483,7 @@ void PopupMenu::_notification(int p_what) {
bool match_found = false;
for (int i = search_from; i < items.size(); i++) {
- if (!items[i].separator && !items[i].disabled) {
+ if (!items[i].separator && !items[i].disabled && items[i].visible) {
mouse_over = i;
emit_signal(SNAME("id_focused"), items[i].id);
scroll_to_item(i);
@@ -1472,7 +1548,7 @@ void PopupMenu::_notification(int p_what) {
ERR_FAIL_NULL(parent_popup);
const float win_scale = get_content_scale_factor();
Point2 mouse_pos = DisplayServer::get_singleton()->mouse_get_position() - get_position();
- Point2 areas_mouse_pos = mouse_pos - parent_popup->panel_offset_start;
+ Point2 areas_mouse_pos = mouse_pos - parent_popup->scroll_container_offset_start;
for (const Rect2 &E : autohide_areas) {
if (!Rect2(Point2(), control->get_size() * win_scale).has_point(areas_mouse_pos) && E.has_point(areas_mouse_pos)) {
_close_or_suspend();
@@ -1486,6 +1562,21 @@ void PopupMenu::_notification(int p_what) {
if (!is_visible()) {
this_submenu_index = -1;
+ if (!search_bar->get_text().is_empty()) {
+ search_bar->clear();
+ _search_bar_text_changed("");
+ }
+
+ if (is_embedded()) {
+ LineEdit *parent_search_bar = Object::cast_to(get_parent());
+ if (parent_search_bar && parent_search_bar->is_visible()) {
+ Window *window_parent = Object::cast_to(parent_search_bar->get_viewport());
+ if (window_parent) {
+ window_parent->grab_focus();
+ }
+ }
+ }
+
if (mouse_over >= 0) {
prev_mouse_over = mouse_over;
mouse_over = -1;
@@ -3098,6 +3189,18 @@ String PopupMenu::get_tooltip(const Point2 &p_pos) const {
return items[over].tooltip;
}
+bool PopupMenu::is_search_bar_enabled() const {
+ return search_bar_enabled_on_item_count > 0 && items.size() >= search_bar_enabled_on_item_count;
+}
+
+void PopupMenu::set_search_bar_enabled_on_item_count(int p_count) {
+ search_bar_enabled_on_item_count = p_count;
+}
+
+int PopupMenu::get_search_bar_enabled_on_item_count() const {
+ return search_bar_enabled_on_item_count;
+}
+
#ifdef TOOLS_ENABLED
PackedStringArray PopupMenu::get_configuration_warnings() const {
PackedStringArray warnings = Popup::get_configuration_warnings();
@@ -3281,6 +3384,9 @@ 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_shrink_height", "shrink"), &PopupMenu::set_shrink_height);
ClassDB::bind_method(D_METHOD("get_shrink_height"), &PopupMenu::get_shrink_height);
@@ -3298,6 +3404,8 @@ void PopupMenu::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shrink_height"), "set_shrink_height", "get_shrink_height");
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_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "item_");
ADD_SIGNAL(MethodInfo("id_pressed", PropertyInfo(Variant::INT, "id")));
@@ -3329,6 +3437,7 @@ void PopupMenu::_bind_methods() {
BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, radio_unchecked);
BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, radio_unchecked_disabled);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, search);
BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, submenu);
BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, PopupMenu, submenu_mirrored);
@@ -3497,6 +3606,16 @@ void PopupMenu::set_visible(bool p_visible) {
}
#endif
+ if (p_visible) {
+ PopupMenu *parent_popup = Object::cast_to(get_parent());
+ if (!parent_popup) {
+ search_bar->set_visible(is_search_bar_enabled());
+ if (search_bar->is_visible()) {
+ search_bar->edit(true);
+ }
+ }
+ }
+
if (native) {
if (p_visible) {
_native_popup(Rect2i(get_position(), get_size()));
@@ -3518,10 +3637,27 @@ PopupMenu::PopupMenu() {
panel->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
add_child(panel, false, INTERNAL_MODE_FRONT);
+ vbox_container = memnew(VBoxContainer);
+ vbox_container->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+ vbox_container->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ vbox_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ panel->add_child(vbox_container, false, INTERNAL_MODE_FRONT);
+
+ search_bar = memnew(LineEdit);
+ search_bar->set_visible(is_search_bar_enabled());
+ search_bar->set_clear_button_enabled(true);
+ search_bar->set_placeholder(ETR("Search"));
+ search_bar->set_keep_editing_on_text_submit(true);
+ search_bar->connect(SceneStringName(text_changed), callable_mp(this, &PopupMenu::_search_bar_text_changed));
+ search_bar->connect(SceneStringName(gui_input), callable_mp(this, &PopupMenu::_search_bar_input));
+ vbox_container->add_child(search_bar, false, INTERNAL_MODE_FRONT);
+
// Scroll Container
scroll_container = memnew(ScrollContainer);
scroll_container->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
- panel->add_child(scroll_container, false, INTERNAL_MODE_FRONT);
+ scroll_container->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ scroll_container->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+ vbox_container->add_child(scroll_container, false, INTERNAL_MODE_FRONT);
// The control which will display the items
control = memnew(Control);
diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h
index ef8ccd3b6e..bd6a02c1b9 100644
--- a/scene/gui/popup_menu.h
+++ b/scene/gui/popup_menu.h
@@ -38,6 +38,8 @@
#include "servers/display/native_menu.h"
class PanelContainer;
+class VBoxContainer;
+class LineEdit;
class Timer;
class PopupMenu : public Popup {
@@ -62,6 +64,7 @@ class PopupMenu : public Popup {
AutoTranslateMode auto_translate_mode = AUTO_TRANSLATE_MODE_INHERIT;
bool checked = false;
+ bool visible = true;
enum {
CHECKABLE_TYPE_NONE,
CHECKABLE_TYPE_CHECK_BOX,
@@ -137,7 +140,7 @@ class PopupMenu : public Popup {
Point2 last_submenu_mouse_position;
int submenu_mouse_exited_ticks_msec = -1;
bool mouse_movement_was_tested = false;
- Point2 panel_offset_start;
+ Point2 scroll_container_offset_start;
float submenu_timer_popup_delay = 0.2;
const float CLOSE_SUSPENDED_TIMER_DELAY = 0.5;
String _get_accel_text(const Item &p_item) const;
@@ -176,7 +179,10 @@ class PopupMenu : public Popup {
uint64_t search_time_msec = 0;
String search_string = "";
+ int search_bar_enabled_on_item_count = 0;
PanelContainer *panel = nullptr;
+ VBoxContainer *vbox_container = nullptr;
+ LineEdit *search_bar = nullptr;
ScrollContainer *scroll_container = nullptr;
Control *control = nullptr;
@@ -210,6 +216,7 @@ class PopupMenu : public Popup {
Ref radio_unchecked;
Ref radio_unchecked_disabled;
+ Ref search;
Ref submenu;
Ref submenu_mirrored;
@@ -231,6 +238,9 @@ class PopupMenu : public Popup {
} theme_cache;
void _draw_items();
+ void _search_bar_input(const Ref &p_event);
+ void _search_bar_text_changed(const String &p_new_text);
+ void _filter_items(const String &p_query);
void _close_pressed();
void _menu_changed();
@@ -374,6 +384,11 @@ public:
void set_prefer_native_menu(bool p_enabled);
bool is_prefer_native_menu() const;
+ bool is_search_bar_enabled() const;
+
+ void set_search_bar_enabled_on_item_count(int p_count);
+ int get_search_bar_enabled_on_item_count() const;
+
bool is_native_menu() const;
void scroll_to_item(int p_idx);
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index 51afdd6c53..4eaeee967f 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -760,6 +760,7 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const
theme->set_icon("radio_unchecked_disabled", "PopupMenu", icons["radio_unchecked_disabled"]);
theme->set_icon("submenu", "PopupMenu", icons["popup_menu_arrow_right"]);
theme->set_icon("submenu_mirrored", "PopupMenu", icons["popup_menu_arrow_left"]);
+ theme->set_icon("search", "PopupMenu", icons["search"]);
theme->set_font(SceneStringName(font), "PopupMenu", Ref());
theme->set_font("font_separator", "PopupMenu", Ref());
diff --git a/scene/theme/icons/search.svg b/scene/theme/icons/search.svg
new file mode 100644
index 0000000000..736d55358b
--- /dev/null
+++ b/scene/theme/icons/search.svg
@@ -0,0 +1 @@
+