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 @@ +