Fix various accessibility issues in PopupMenu

This commit is contained in:
Mikael Hermansson
2026-04-22 16:51:02 +02:00
parent 1078619908
commit ca475b9a35
2 changed files with 89 additions and 24 deletions
+71 -22
View File
@@ -534,6 +534,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
control->grab_focus();
emit_signal(SNAME("id_focused"), items[i].id);
scroll_to_item(i);
queue_accessibility_update();
@@ -550,6 +551,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
control->grab_focus();
emit_signal(SNAME("id_focused"), items[i].id);
scroll_to_item(i);
queue_accessibility_update();
@@ -577,6 +579,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
control->grab_focus();
emit_signal(SNAME("id_focused"), items[i].id);
scroll_to_item(i);
queue_accessibility_update();
@@ -593,6 +596,7 @@ void PopupMenu::_input_from_window_internal(const Ref<InputEvent> &p_event) {
if (!items[i].separator && !items[i].disabled && items[i].visible) {
prev_mouse_over = mouse_over;
mouse_over = i;
control->grab_focus();
emit_signal(SNAME("id_focused"), items[i].id);
scroll_to_item(i);
queue_accessibility_update();
@@ -1085,12 +1089,20 @@ void PopupMenu::_update_search_bar_visibility() {
}
}
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::_items_focus_entered() {
if (mouse_over != -1) {
return;
}
for (int i = 0; i < items.size(); i++) {
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);
scroll_to_item(i);
queue_accessibility_update();
control->queue_redraw();
break;
}
}
}
@@ -1105,6 +1117,13 @@ void PopupMenu::_search_bar_text_changed(const String &p_new_text) {
_menu_changed();
}
void PopupMenu::_search_bar_focus_entered() {
prev_mouse_over = mouse_over;
mouse_over = -1;
queue_accessibility_update();
control->queue_redraw();
}
void PopupMenu::_filter_items(const String &p_query) {
for (PopupMenu::Item &item : items) {
if (item.submenu) {
@@ -1369,7 +1388,14 @@ void PopupMenu::_notification(int p_what) {
ERR_FAIL_COND(ae.is_null());
AccessibilityServer::get_singleton()->update_set_role(ae, AccessibilityServerEnums::AccessibilityRole::ROLE_MENU);
AccessibilityServer::get_singleton()->update_set_list_item_count(ae, items.size());
int item_count = 0;
for (const Item &item : items) {
if (item.visible && !item.separator) {
item_count++;
}
}
AccessibilityServer::get_singleton()->update_set_list_item_count(ae, item_count);
if (accessibility_scroll_element.is_null()) {
accessibility_scroll_element = AccessibilityServer::get_singleton()->create_sub_element(ae, AccessibilityServerEnums::AccessibilityRole::ROLE_CONTAINER);
@@ -1380,25 +1406,26 @@ void PopupMenu::_notification(int p_what) {
AccessibilityServer::get_singleton()->update_set_transform(accessibility_scroll_element, scroll_xform);
AccessibilityServer::get_singleton()->update_set_bounds(accessibility_scroll_element, Rect2(0, 0, get_size().x, scroll_container->get_v_scroll_bar()->get_max()));
float scroll_width = scroll_container->get_v_scroll_bar()->is_visible_in_tree() ? scroll_container->get_v_scroll_bar()->get_size().width : 0;
float display_width = control->get_size().width - scroll_width;
Point2 ofs;
float display_width = control->get_size().width;
Point2 ofs = scroll_container->get_global_position();
int item_index = 0;
bool first_visible = true;
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);
Point2 item_ofs = ofs;
if (item.accessibility_item_element.is_null()) {
item.accessibility_item_element = AccessibilityServer::get_singleton()->create_sub_element(accessibility_scroll_element, AccessibilityServerEnums::AccessibilityRole::ROLE_MENU_ITEM);
item.accessibility_item_dirty = true;
}
if (item.visible) {
// Avoid discrepancy between min size and drawn items due to rounding.
ofs.y += first_visible ? 0 : theme_cache.v_separation;
first_visible = false;
}
Point2 item_ofs = ofs;
item_ofs.x += item.indent * theme_cache.indent;
float h = _get_item_height(i);
@@ -1418,18 +1445,26 @@ void PopupMenu::_notification(int p_what) {
}
AccessibilityServer::get_singleton()->update_add_action(item.accessibility_item_element, AccessibilityServerEnums::AccessibilityAction::ACTION_CLICK, callable_mp(this, &PopupMenu::_accessibility_action_click).bind(i));
AccessibilityServer::get_singleton()->update_set_list_item_index(item.accessibility_item_element, i);
AccessibilityServer::get_singleton()->update_set_list_item_index(item.accessibility_item_element, item_index);
AccessibilityServer::get_singleton()->update_set_list_item_level(item.accessibility_item_element, 0);
AccessibilityServer::get_singleton()->update_set_list_item_selected(item.accessibility_item_element, i == mouse_over);
AccessibilityServer::get_singleton()->update_set_name(item.accessibility_item_element, item.xl_text);
AccessibilityServer::get_singleton()->update_set_flag(item.accessibility_item_element, AccessibilityServerEnums::AccessibilityFlags::FLAG_DISABLED, item.disabled);
AccessibilityServer::get_singleton()->update_set_flag(item.accessibility_item_element, AccessibilityServerEnums::AccessibilityFlags::FLAG_HIDDEN, !item.visible);
AccessibilityServer::get_singleton()->update_set_tooltip(item.accessibility_item_element, item.tooltip);
AccessibilityServer::get_singleton()->update_set_bounds(item.accessibility_item_element, Rect2(item_ofs, Size2(display_width, h + theme_cache.v_separation)));
item.accessibility_item_dirty = false;
}
ofs.y += h;
if (item.visible) {
ofs.y += h;
if (!item.separator) {
item_index++;
}
}
}
prev_mouse_over = -1;
@@ -3705,7 +3740,7 @@ void PopupMenu::set_visible(bool p_visible) {
PopupMenu *parent_popup = Object::cast_to<PopupMenu>(get_parent());
if (!parent_popup) {
if (search_bar->is_visible()) {
search_bar->edit(true);
search_bar->edit();
}
}
}
@@ -3724,6 +3759,18 @@ void PopupMenu::set_visible(bool p_visible) {
}
}
RID PopupMenuItems::get_focused_accessibility_element() const {
return popup->get_focused_accessibility_element();
}
void PopupMenuItems::gui_input(const Ref<InputEvent> &p_event) {
if (p_event->is_action("ui_up") || p_event->is_action("ui_down")) {
// We've already handled these in `PopupMenu::_input_from_window`, and these events will be handled as focus navigation if we don't stop them from propagating, causing focus to jump to the search bar.
accept_event();
return;
}
}
PopupMenu::PopupMenu() {
set_flag(FLAG_TRANSPARENT, true);
set_default_canvas_item_texture_filter(Viewport::DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_PARENT_NODE);
@@ -3746,7 +3793,7 @@ PopupMenu::PopupMenu() {
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));
search_bar->connect(SceneStringName(focus_entered), callable_mp(this, &PopupMenu::_search_bar_focus_entered));
vbox_container->add_child(search_bar, false, INTERNAL_MODE_FRONT);
// Scroll Container
@@ -3758,13 +3805,15 @@ PopupMenu::PopupMenu() {
scroll_container->add_theme_style_override(SceneStringName(panel), memnew(StyleBoxEmpty));
// The control which will display the items
control = memnew(Control);
control = memnew(PopupMenuItems(this));
control->set_clip_contents(false);
control->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
control->set_h_size_flags(Control::SIZE_EXPAND_FILL);
control->set_v_size_flags(Control::SIZE_EXPAND_FILL);
control->set_focus_mode(Control::FOCUS_ALL);
scroll_container->add_child(control, false, INTERNAL_MODE_FRONT);
control->connect(SceneStringName(draw), callable_mp(this, &PopupMenu::_draw_items));
control->connect(SceneStringName(focus_entered), callable_mp(this, &PopupMenu::_items_focus_entered));
submenu_timer = memnew(Timer);
submenu_timer->set_wait_time(submenu_timer_popup_delay); // Default is 0.2.
+18 -2
View File
@@ -41,10 +41,13 @@ class PanelContainer;
class VBoxContainer;
class LineEdit;
class Timer;
class PopupMenuItems;
class PopupMenu : public Popup {
GDCLASS(PopupMenu, Popup);
friend class PopupMenuItems;
static HashMap<NativeMenu::SystemMenus, PopupMenu *> system_menus;
struct Item {
@@ -187,7 +190,7 @@ class PopupMenu : public Popup {
VBoxContainer *vbox_container = nullptr;
LineEdit *search_bar = nullptr;
ScrollContainer *scroll_container = nullptr;
Control *control = nullptr;
PopupMenuItems *control = nullptr;
const float DEFAULT_GAMEPAD_EVENT_DELAY_MS = 0.5;
const float GAMEPAD_EVENT_REPEAT_RATE_MS = 1.0 / 20;
@@ -243,8 +246,9 @@ class PopupMenu : public Popup {
void _draw_items();
void _update_search_bar_visibility();
void _search_bar_input(const Ref<InputEvent> &p_event);
void _items_focus_entered();
void _search_bar_text_changed(const String &p_new_text);
void _search_bar_focus_entered();
void _filter_items(const String &p_query);
void _close_pressed();
@@ -454,3 +458,15 @@ public:
PopupMenu();
~PopupMenu();
};
class PopupMenuItems : public Control {
GDCLASS(PopupMenuItems, Control);
PopupMenu *popup = nullptr;
public:
PopupMenuItems(PopupMenu *p_popup) : popup(p_popup) {}
virtual RID get_focused_accessibility_element() const override;
virtual void gui_input(const Ref<InputEvent> &p_event) override;
};