Merge pull request #114236 from warriormaster12/san-popupmenu-search-bar

Implement search bar for `PopupMenu`
This commit is contained in:
Thaddeus Crews
2026-03-04 11:20:49 -06:00
11 changed files with 215 additions and 17 deletions
+9
View File
@@ -140,6 +140,12 @@
Returns [code]true[/code] if the item at index [param idx] is marked as a separator.
</description>
</method>
<method name="is_search_bar_enabled" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if the search bar is enabled.
</description>
</method>
<method name="remove_item">
<return type="void" />
<param index="0" name="idx" type="int" />
@@ -233,6 +239,9 @@
<member name="allow_reselect" type="bool" setter="set_allow_reselect" getter="get_allow_reselect" default="false">
If [code]true[/code], the currently selected item can be selected again.
</member>
<member name="enable_search_bar_on_item_count" type="int" setter="set_search_bar_enabled_on_item_count" getter="get_search_bar_enabled_on_item_count" default="0">
Enables the [PopupMenu] search bar if the item count is greater than [code]0[/code].
</member>
<member name="fit_to_longest_item" type="bool" setter="set_fit_to_longest_item" getter="is_fit_to_longest_item" default="true">
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.
+13
View File
@@ -408,6 +408,12 @@
Returns [code]true[/code] if the system native menu is supported and currently used by this [PopupMenu].
</description>
</method>
<method name="is_search_bar_enabled" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if search bar is currently enabled.
</description>
</method>
<method name="is_system_menu" qualifiers="const">
<return type="bool" />
<description>
@@ -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.
</member>
<member name="search_bar_enabled_on_item_count" type="int" setter="set_search_bar_enabled_on_item_count" getter="get_search_bar_enabled_on_item_count" default="0">
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="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>
@@ -791,6 +801,9 @@
<theme_item name="radio_unchecked_disabled" data_type="icon" type="Texture2D">
[Texture2D] icon for the unchecked radio button items when they are disabled.
</theme_item>
<theme_item name="search" data_type="icon" type="Texture2D">
[Texture2D] icon for the search bar's search icon.
</theme_item>
<theme_item name="submenu" data_type="icon" type="Texture2D">
[Texture2D] icon for the submenu arrow (for left-to-right layouts).
</theme_item>
+2
View File
@@ -578,6 +578,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);
@@ -985,6 +986,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));
+1
View File
@@ -1126,6 +1126,7 @@ void ThemeClassic::populate_standard_styles(const Ref<EditorTheme> &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);
+1
View File
@@ -1101,6 +1101,7 @@ void ThemeModern::populate_standard_styles(const Ref<EditorTheme> &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;
+16
View File
@@ -390,6 +390,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);
}
@@ -570,6 +582,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);
@@ -579,6 +592,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);
@@ -604,6 +619,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")));
+3
View File
@@ -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<Texture2D> 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;
+152 -16
View File
@@ -37,7 +37,9 @@
#include "core/object/class_db.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"
@@ -319,6 +321,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;
}
@@ -343,6 +348,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) {
@@ -378,8 +386,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;
@@ -388,17 +396,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;
}
}
@@ -513,7 +521,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &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);
@@ -529,7 +537,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &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);
@@ -556,7 +564,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &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);
@@ -572,7 +580,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &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);
@@ -692,6 +700,9 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &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();
}
@@ -724,7 +735,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
if (this_submenu_index != -1) { // Is a submenu.
PopupMenu *parent_popup = Object::cast_to<PopupMenu>(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();
@@ -758,7 +769,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
Ref<InputEventKey> 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"));
@@ -872,7 +883,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;
}
@@ -896,6 +907,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;
@@ -1052,6 +1067,59 @@ void PopupMenu::_draw_items() {
}
}
void PopupMenu::_search_bar_input(const Ref<InputEvent> &p_event) {
// Redirect navigational key events to the tree.
Ref<InputEventKey> 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));
@@ -1268,6 +1336,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);
@@ -1328,6 +1399,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()) {
@@ -1364,6 +1437,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;
@@ -1411,7 +1487,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);
@@ -1476,7 +1552,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();
@@ -1490,6 +1566,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<LineEdit>(get_parent());
if (parent_search_bar && parent_search_bar->is_visible()) {
Window *window_parent = Object::cast_to<Window>(parent_search_bar->get_viewport());
if (window_parent) {
window_parent->grab_focus();
}
}
}
if (mouse_over >= 0) {
prev_mouse_over = mouse_over;
mouse_over = -1;
@@ -3102,6 +3193,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();
@@ -3285,6 +3388,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);
@@ -3302,6 +3408,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")));
@@ -3333,6 +3441,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);
@@ -3501,6 +3610,16 @@ void PopupMenu::set_visible(bool p_visible) {
}
#endif
if (p_visible) {
PopupMenu *parent_popup = Object::cast_to<PopupMenu>(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()));
@@ -3522,10 +3641,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);
+16 -1
View File
@@ -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<Texture2D> radio_unchecked;
Ref<Texture2D> radio_unchecked_disabled;
Ref<Texture2D> search;
Ref<Texture2D> submenu;
Ref<Texture2D> submenu_mirrored;
@@ -231,6 +238,9 @@ class PopupMenu : public Popup {
} theme_cache;
void _draw_items();
void _search_bar_input(const Ref<InputEvent> &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);
+1
View File
@@ -760,6 +760,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &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<Font>());
theme->set_font("font_separator", "PopupMenu", Ref<Font>());
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M10.168 8.754a5 5 0 1 0-1.414 1.414l4.316 4.316 1.414-1.414zM6 3a3 3 0 0 1 0 6 3 3 0 0 1 0-6z"/></svg>

After

Width:  |  Height:  |  Size: 190 B