GUI: Add accessibility region role for landmark navigation

Adds ROLE_REGION to allow controls to be marked as accessibility
regions/landmarks.

- Add `accessibility_region` property to Control
- Add ROLE_REGION to DisplayServer and AccessKit mapping
- Prevent Container/ScrollContainer from overriding region role
- Fix TabContainer to update accessibility when tabs change
- Mark editor docks, main screen, bottom panel, and scene tabs as regions
This commit is contained in:
Nolan Darilek 2025-12-29 17:30:55 -05:00
parent 5f9a510441
commit d53ab67b83
13 changed files with 57 additions and 2 deletions

View file

@ -40,6 +40,9 @@
</method>
</methods>
<members>
<member name="accessibility_region" type="bool" setter="set_accessibility_region" getter="is_accessibility_region" default="false">
If [code]true[/code], this container is marked as a region for accessibility. Use [member Control.accessibility_name] to give the region a descriptive name. Screen readers can navigate between regions using landmark navigation.
</member>
<member name="mouse_filter" type="int" setter="set_mouse_filter" getter="get_mouse_filter" overrides="Control" enum="Control.MouseFilter" default="1" />
</members>
<signals>

View file

@ -2775,6 +2775,9 @@
<constant name="ROLE_TOOLTIP" value="45" enum="AccessibilityRole">
Tooltip element.
</constant>
<constant name="ROLE_REGION" value="46" enum="AccessibilityRole">
Region/landmark element. Screen readers can navigate between regions using landmark navigation.
</constant>
<constant name="POPUP_MENU" value="0" enum="AccessibilityPopupType">
Popup menu.
</constant>

View file

@ -1657,6 +1657,7 @@ AccessibilityDriverAccessKit::AccessibilityDriverAccessKit() {
role_map[DisplayServer::AccessibilityRole::ROLE_TITLE_BAR] = ACCESSKIT_ROLE_TITLE_BAR;
role_map[DisplayServer::AccessibilityRole::ROLE_DIALOG] = ACCESSKIT_ROLE_DIALOG;
role_map[DisplayServer::AccessibilityRole::ROLE_TOOLTIP] = ACCESSKIT_ROLE_TOOLTIP;
role_map[DisplayServer::AccessibilityRole::ROLE_REGION] = ACCESSKIT_ROLE_REGION;
action_map[DisplayServer::AccessibilityAction::ACTION_CLICK] = ACCESSKIT_ACTION_CLICK;
action_map[DisplayServer::AccessibilityAction::ACTION_FOCUS] = ACCESSKIT_ACTION_FOCUS;

View file

@ -43,6 +43,15 @@ void EditorDock::_emit_changed() {
emit_signal(SNAME("_tab_style_changed"));
}
void EditorDock::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
set_accessibility_region(true);
set_accessibility_name(get_display_title());
} break;
}
}
void EditorDock::_bind_methods() {
ClassDB::bind_method(D_METHOD("open"), &EditorDock::open);
ClassDB::bind_method(D_METHOD("make_visible"), &EditorDock::make_visible);
@ -142,6 +151,7 @@ void EditorDock::set_title(const String &p_title) {
return;
}
title = p_title;
set_accessibility_name(get_display_title());
_emit_changed();
}

View file

@ -94,6 +94,7 @@ private:
void _emit_changed();
protected:
void _notification(int p_what);
static void _bind_methods();
GDVIRTUAL1(_update_layout, int)

View file

@ -41,6 +41,7 @@
void EditorMainScreen::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
set_accessibility_region(true);
if (EDITOR_3D < buttons.size() && buttons[EDITOR_3D]->is_visible()) {
// If the 3D editor is enabled, use this as the default.
select(EDITOR_3D);
@ -194,6 +195,7 @@ void EditorMainScreen::select(int p_index) {
selected_plugin = new_editor;
selected_plugin->make_visible(true);
selected_plugin->selected_notify();
set_accessibility_name(selected_plugin->get_plugin_name());
EditorData &editor_data = EditorNode::get_editor_data();
int plugin_count = editor_data.get_editor_plugin_count();

View file

@ -47,6 +47,7 @@
void EditorBottomPanel::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
set_accessibility_region(true);
layout_popup = get_popup();
} break;
@ -60,6 +61,9 @@ void EditorBottomPanel::_notification(int p_what) {
void EditorBottomPanel::_on_tab_changed(int p_idx) {
_update_center_split_offset();
_repaint();
if (p_idx >= 0 && p_idx < get_tab_count()) {
set_accessibility_name(get_tab_title(p_idx));
}
}
void EditorBottomPanel::_theme_changed() {

View file

@ -188,7 +188,11 @@ void Container::_notification(int p_what) {
RID ae = get_accessibility_element();
ERR_FAIL_COND(ae.is_null());
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER);
if (accessibility_region) {
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_REGION);
} else {
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER);
}
} break;
case NOTIFICATION_RESIZED:
@ -204,6 +208,18 @@ void Container::_notification(int p_what) {
}
}
void Container::set_accessibility_region(bool p_region) {
ERR_MAIN_THREAD_GUARD;
if (accessibility_region != p_region) {
accessibility_region = p_region;
queue_accessibility_update();
}
}
bool Container::is_accessibility_region() const {
return accessibility_region;
}
PackedStringArray Container::get_configuration_warnings() const {
PackedStringArray warnings = Control::get_configuration_warnings();
@ -217,6 +233,8 @@ PackedStringArray Container::get_configuration_warnings() const {
void Container::_bind_methods() {
ClassDB::bind_method(D_METHOD("queue_sort"), &Container::queue_sort);
ClassDB::bind_method(D_METHOD("fit_child_in_rect", "child", "rect"), &Container::fit_child_in_rect);
ClassDB::bind_method(D_METHOD("set_accessibility_region", "region"), &Container::set_accessibility_region);
ClassDB::bind_method(D_METHOD("is_accessibility_region"), &Container::is_accessibility_region);
GDVIRTUAL_BIND(_get_allowed_size_flags_horizontal);
GDVIRTUAL_BIND(_get_allowed_size_flags_vertical);
@ -226,6 +244,8 @@ void Container::_bind_methods() {
ADD_SIGNAL(MethodInfo("pre_sort_children"));
ADD_SIGNAL(MethodInfo("sort_children"));
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "accessibility_region"), "set_accessibility_region", "is_accessibility_region");
}
Container::Container() {

View file

@ -36,6 +36,7 @@ class Container : public Control {
GDCLASS(Container, Control);
bool pending_sort = false;
bool accessibility_region = false;
void _sort_children();
void _child_minsize_changed();
@ -72,5 +73,8 @@ public:
PackedStringArray get_configuration_warnings() const override;
void set_accessibility_region(bool p_region);
bool is_accessibility_region() const;
Container();
};

View file

@ -434,7 +434,11 @@ void ScrollContainer::_notification(int p_what) {
RID ae = get_accessibility_element();
ERR_FAIL_COND(ae.is_null());
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SCROLL_VIEW);
if (is_accessibility_region()) {
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_REGION);
} else {
DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SCROLL_VIEW);
}
DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_DOWN, callable_mp(this, &ScrollContainer::_accessibility_action_scroll_down));
DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_LEFT, callable_mp(this, &ScrollContainer::_accessibility_action_scroll_left));

View file

@ -565,6 +565,7 @@ void TabContainer::_on_tab_hovered(int p_tab) {
void TabContainer::_on_tab_changed(int p_tab) {
callable_mp(this, &TabContainer::_repaint).call_deferred();
queue_redraw();
queue_accessibility_update();
emit_signal(SNAME("tab_changed"), p_tab);
}

View file

@ -1706,6 +1706,7 @@ void DisplayServer::_bind_methods() {
BIND_ENUM_CONSTANT(ROLE_TITLE_BAR);
BIND_ENUM_CONSTANT(ROLE_DIALOG);
BIND_ENUM_CONSTANT(ROLE_TOOLTIP);
BIND_ENUM_CONSTANT(ROLE_REGION);
BIND_ENUM_CONSTANT(POPUP_MENU);
BIND_ENUM_CONSTANT(POPUP_LIST);

View file

@ -606,6 +606,7 @@ public:
ROLE_TITLE_BAR,
ROLE_DIALOG,
ROLE_TOOLTIP,
ROLE_REGION,
};
enum AccessibilityPopupType {