[Accessibility] Process non-focusable windows (popups, menus) as part of the parent window tree.

This commit is contained in:
Pāvels Nadtočajevs 2025-07-28 11:27:43 +03:00
parent 0c51ede243
commit 7b47f5e8db
No known key found for this signature in database
GPG key ID: 8413210218EF35D2
8 changed files with 96 additions and 42 deletions

View file

@ -38,7 +38,7 @@
#include "scene/theme/theme_db.h" #include "scene/theme/theme_db.h"
void Popup::_input_from_window(const Ref<InputEvent> &p_event) { void Popup::_input_from_window(const Ref<InputEvent> &p_event) {
if ((ac_popup || get_flag(FLAG_POPUP)) && p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { if (get_flag(FLAG_POPUP) && p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) {
hide_reason = HIDE_REASON_CANCELED; // ESC pressed, mark as canceled unconditionally. hide_reason = HIDE_REASON_CANCELED; // ESC pressed, mark as canceled unconditionally.
_close_pressed(); _close_pressed();
} }
@ -115,7 +115,7 @@ void Popup::_notification(int p_what) {
} break; } break;
case NOTIFICATION_APPLICATION_FOCUS_OUT: { case NOTIFICATION_APPLICATION_FOCUS_OUT: {
if (!is_in_edited_scene_root() && (get_flag(FLAG_POPUP) || ac_popup)) { if (!is_in_edited_scene_root() && get_flag(FLAG_POPUP)) {
if (hide_reason == HIDE_REASON_NONE) { if (hide_reason == HIDE_REASON_NONE) {
hide_reason = HIDE_REASON_UNFOCUSED; hide_reason = HIDE_REASON_UNFOCUSED;
} }
@ -126,7 +126,7 @@ void Popup::_notification(int p_what) {
} }
void Popup::_parent_focused() { void Popup::_parent_focused() {
if (popped_up && (get_flag(FLAG_POPUP) || ac_popup)) { if (popped_up && get_flag(FLAG_POPUP)) {
if (hide_reason == HIDE_REASON_NONE) { if (hide_reason == HIDE_REASON_NONE) {
hide_reason = HIDE_REASON_UNFOCUSED; hide_reason = HIDE_REASON_UNFOCUSED;
} }

View file

@ -40,7 +40,6 @@ class Popup : public Window {
GDCLASS(Popup, Window); GDCLASS(Popup, Window);
LocalVector<Window *> visible_parents; LocalVector<Window *> visible_parents;
bool ac_popup = false;
bool popped_up = false; bool popped_up = false;
public: public:
@ -60,7 +59,6 @@ protected:
void _close_pressed(); void _close_pressed();
virtual Rect2i _popup_adjust_rect() const override; virtual Rect2i _popup_adjust_rect() const override;
virtual void _input_from_window(const Ref<InputEvent> &p_event) override; virtual void _input_from_window(const Ref<InputEvent> &p_event) override;
void set_ac_popup() { ac_popup = true; }
void _notification(int p_what); void _notification(int p_what);
void _validate_property(PropertyInfo &p_property) const; void _validate_property(PropertyInfo &p_property) const;

View file

@ -3209,13 +3209,8 @@ void PopupMenu::popup(const Rect2i &p_bounds) {
_native_popup(p_bounds != Rect2i() ? p_bounds : Rect2i(get_position(), Size2i())); _native_popup(p_bounds != Rect2i() ? p_bounds : Rect2i(get_position(), Size2i()));
} else { } else {
if (is_inside_tree()) { if (is_inside_tree()) {
bool ac = get_tree()->is_accessibility_enabled(); set_flag(FLAG_POPUP, true);
// Note: Native popup menus need keyboard focus to work with screen reader. set_flag(FLAG_NO_FOCUS, !is_embedded());
set_flag(FLAG_POPUP, !ac);
set_flag(FLAG_NO_FOCUS, !is_embedded() && !ac);
if (ac) {
set_ac_popup();
}
} }
moved = Vector2(); moved = Vector2();
@ -3253,13 +3248,8 @@ void PopupMenu::set_visible(bool p_visible) {
} }
} else { } else {
if (is_inside_tree()) { if (is_inside_tree()) {
bool ac = get_tree()->is_accessibility_enabled(); set_flag(FLAG_POPUP, true);
// Note: Native popup menus need keyboard focus to work with screen reader. set_flag(FLAG_NO_FOCUS, !is_embedded());
set_flag(FLAG_POPUP, !ac);
set_flag(FLAG_NO_FOCUS, !is_embedded() && !ac);
if (ac) {
set_ac_popup();
}
} }
Popup::set_visible(p_visible); Popup::set_visible(p_visible);

View file

@ -68,7 +68,7 @@ void Node::_notification(int p_notification) {
for (int i = 0; i < get_child_count(); i++) { for (int i = 0; i < get_child_count(); i++) {
Node *child_node = get_child(i); Node *child_node = get_child(i);
Window *child_wnd = Object::cast_to<Window>(child_node); Window *child_wnd = Object::cast_to<Window>(child_node);
if (child_wnd && !child_wnd->is_embedded()) { if (child_wnd && !(child_wnd->is_visible() && (child_wnd->is_embedded() || child_wnd->is_popup()))) {
continue; continue;
} }
if (child_node->is_part_of_edited_scene()) { if (child_node->is_part_of_edited_scene()) {
@ -2055,6 +2055,14 @@ Window *Node::get_window() const {
return nullptr; return nullptr;
} }
Window *Node::get_non_popup_window() const {
Window *w = get_window();
while (w && w->is_popup()) {
w = w->get_parent_visible_window();
}
return w;
}
Window *Node::get_last_exclusive_window() const { Window *Node::get_last_exclusive_window() const {
Window *w = get_window(); Window *w = get_window();
while (w && w->get_exclusive_child()) { while (w && w->get_exclusive_child()) {
@ -3687,8 +3695,9 @@ RID Node::get_accessibility_element() const {
return RID(); return RID();
} }
if (unlikely(data.accessibility_element.is_null())) { if (unlikely(data.accessibility_element.is_null())) {
if (get_window() && get_window()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { Window *w = get_non_popup_window();
data.accessibility_element = DisplayServer::get_singleton()->accessibility_create_element(get_window()->get_window_id(), DisplayServer::ROLE_CONTAINER); if (w && w->get_window_id() != DisplayServer::INVALID_WINDOW_ID && get_window()->is_visible()) {
data.accessibility_element = DisplayServer::get_singleton()->accessibility_create_element(w->get_window_id(), DisplayServer::ROLE_CONTAINER);
} }
} }
return data.accessibility_element; return data.accessibility_element;

View file

@ -500,6 +500,7 @@ public:
Node *find_parent(const String &p_pattern) const; Node *find_parent(const String &p_pattern) const;
Window *get_window() const; Window *get_window() const;
Window *get_non_popup_window() const;
Window *get_last_exclusive_window() const; Window *get_last_exclusive_window() const;
_FORCE_INLINE_ SceneTree *get_tree() const { _FORCE_INLINE_ SceneTree *get_tree() const {

View file

@ -246,10 +246,10 @@ void SceneTree::_process_accessibility_changes(DisplayServer::WindowID p_window_
Vector<ObjectID> processed; Vector<ObjectID> processed;
for (const ObjectID &id : accessibility_change_queue) { for (const ObjectID &id : accessibility_change_queue) {
Node *node = Object::cast_to<Node>(ObjectDB::get_instance(id)); Node *node = Object::cast_to<Node>(ObjectDB::get_instance(id));
if (!node || !node->get_window()) { if (!node || !node->get_non_popup_window() || !node->get_window()->is_visible()) {
processed.push_back(id); processed.push_back(id);
continue; // Invalid node, remove from list and skip. continue; // Invalid node, remove from list and skip.
} else if (node->get_window()->get_window_id() != p_window_id) { } else if (node->get_non_popup_window()->get_window_id() != p_window_id) {
continue; // Another window, skip. continue; // Another window, skip.
} }
node->notification(Node::NOTIFICATION_ACCESSIBILITY_UPDATE); node->notification(Node::NOTIFICATION_ACCESSIBILITY_UPDATE);
@ -267,6 +267,15 @@ void SceneTree::_process_accessibility_changes(DisplayServer::WindowID p_window_
w_this = w_focus; w_this = w_focus;
} }
// Popups have no native window focus, but have focused element.
DisplayServer::WindowID popup_id = DisplayServer::get_singleton()->window_get_active_popup();
if (popup_id != DisplayServer::INVALID_WINDOW_ID) {
Window *popup_w = Window::get_from_id(popup_id);
if (popup_w && w_this->is_ancestor_of(popup_w)) {
w_this = popup_w;
}
}
RID new_focus_element; RID new_focus_element;
Control *n_focus = w_this->gui_get_focus_owner(); Control *n_focus = w_this->gui_get_focus_owner();
if (n_focus && !n_focus->is_part_of_edited_scene()) { if (n_focus && !n_focus->is_part_of_edited_scene()) {

View file

@ -570,6 +570,10 @@ bool Window::get_flag(Flags p_flag) const {
return flags[p_flag]; return flags[p_flag];
} }
bool Window::is_popup() const {
return get_flag(Window::FLAG_POPUP) || get_flag(Window::FLAG_NO_FOCUS);
}
bool Window::is_maximize_allowed() const { bool Window::is_maximize_allowed() const {
ERR_READ_THREAD_GUARD_V(false); ERR_READ_THREAD_GUARD_V(false);
if (window_id != DisplayServer::INVALID_WINDOW_ID) { if (window_id != DisplayServer::INVALID_WINDOW_ID) {
@ -680,11 +684,6 @@ void Window::_make_window() {
} }
} }
if (get_tree() && get_tree()->is_accessibility_supported()) {
get_tree()->_accessibility_force_update();
_accessibility_notify_enter(this);
}
_update_window_callbacks(); _update_window_callbacks();
RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_WHEN_VISIBLE); RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_WHEN_VISIBLE);
@ -716,10 +715,6 @@ void Window::_clear_window() {
_update_from_window(); _update_from_window();
if (get_tree() && get_tree()->is_accessibility_supported()) {
_accessibility_notify_exit(this);
}
DisplayServer::get_singleton()->delete_sub_window(window_id); DisplayServer::get_singleton()->delete_sub_window(window_id);
window_id = DisplayServer::INVALID_WINDOW_ID; window_id = DisplayServer::INVALID_WINDOW_ID;
@ -886,7 +881,7 @@ void Window::_accessibility_notify_enter(Node *p_node) {
if (p_node != this) { if (p_node != this) {
const Window *window = Object::cast_to<Window>(p_node); const Window *window = Object::cast_to<Window>(p_node);
if (window && !window->is_embedded()) { if (window) {
return; return;
} }
} }
@ -901,7 +896,7 @@ void Window::_accessibility_notify_exit(Node *p_node) {
if (p_node != this) { if (p_node != this) {
const Window *window = Object::cast_to<Window>(p_node); const Window *window = Object::cast_to<Window>(p_node);
if (window && !window->is_embedded()) { if (window) {
return; return;
} }
} }
@ -950,23 +945,35 @@ void Window::set_visible(bool p_visible) {
} }
} }
embedder->_sub_window_register(this); embedder->_sub_window_register(this);
embedder->queue_accessibility_update();
RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_WHEN_PARENT_VISIBLE); RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_WHEN_PARENT_VISIBLE);
} else { } else {
embedder->_sub_window_remove(this); embedder->_sub_window_remove(this);
embedder->queue_accessibility_update();
embedder = nullptr; embedder = nullptr;
RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_DISABLED); RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_DISABLED);
} }
_update_window_size(); _update_window_size();
} }
if (!visible) { if (visible) {
if (get_tree() && get_tree()->is_accessibility_supported()) {
get_tree()->_accessibility_force_update();
_accessibility_notify_enter(this);
}
} else {
if (get_tree() && get_tree()->is_accessibility_supported()) {
_accessibility_notify_exit(this);
}
focused = false; focused = false;
if (focused_window == this) { if (focused_window == this) {
focused_window = nullptr; focused_window = nullptr;
} }
} }
if (get_parent()) {
get_parent()->queue_accessibility_update();
}
if (embedder) {
embedder->queue_accessibility_update();
}
notification(NOTIFICATION_VISIBILITY_CHANGED); notification(NOTIFICATION_VISIBILITY_CHANGED);
emit_signal(SceneStringName(visibility_changed)); emit_signal(SceneStringName(visibility_changed));
@ -1371,10 +1378,10 @@ Viewport *Window::get_embedder() const {
} }
RID Window::get_accessibility_element() const { RID Window::get_accessibility_element() const {
if (is_part_of_edited_scene()) { if (!visible || is_part_of_edited_scene()) {
return RID(); return RID();
} }
if (get_embedder()) { if (get_embedder() || is_popup()) {
return Node::get_accessibility_element(); return Node::get_accessibility_element();
} else if (window_id != DisplayServer::INVALID_WINDOW_ID) { } else if (window_id != DisplayServer::INVALID_WINDOW_ID) {
return DisplayServer::get_singleton()->accessibility_get_window_root(window_id); return DisplayServer::get_singleton()->accessibility_get_window_root(window_id);
@ -1415,11 +1422,18 @@ void Window::_notification(int p_what) {
DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &Window::_accessibility_action_grab_focus)); DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &Window::_accessibility_action_grab_focus));
DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, !visible); DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, !visible);
if (get_embedder()) { if (get_embedder() || is_popup()) {
Control *parent_ctrl = Object::cast_to<Control>(get_parent()); Control *parent_ctrl = Object::cast_to<Control>(get_parent());
Transform2D parent_tr = parent_ctrl ? parent_ctrl->get_global_transform() : Transform2D(); Transform2D parent_tr = parent_ctrl ? parent_ctrl->get_global_transform() : Transform2D();
Transform2D tr; Transform2D tr;
if (window_id == DisplayServer::INVALID_WINDOW_ID) {
tr.set_origin(position); tr.set_origin(position);
} else {
Window *np = get_non_popup_window();
if (np) {
tr.set_origin(get_position() - np->get_position());
}
}
DisplayServer::get_singleton()->accessibility_update_set_transform(ae, parent_tr.affine_inverse() * tr); DisplayServer::get_singleton()->accessibility_update_set_transform(ae, parent_tr.affine_inverse() * tr);
DisplayServer::get_singleton()->accessibility_update_set_bounds(ae, Rect2(Point2(), size)); DisplayServer::get_singleton()->accessibility_update_set_bounds(ae, Rect2(Point2(), size));
@ -1540,9 +1554,19 @@ void Window::_notification(int p_what) {
_make_transient(); _make_transient();
} }
if (visible) { if (visible) {
if (window_id != DisplayServer::MAIN_WINDOW_ID && get_tree() && get_tree()->is_accessibility_supported()) {
get_tree()->_accessibility_force_update();
_accessibility_notify_enter(this);
}
notification(NOTIFICATION_VISIBILITY_CHANGED); notification(NOTIFICATION_VISIBILITY_CHANGED);
emit_signal(SceneStringName(visibility_changed)); emit_signal(SceneStringName(visibility_changed));
RS::get_singleton()->viewport_set_active(get_viewport_rid(), true); RS::get_singleton()->viewport_set_active(get_viewport_rid(), true);
if (get_parent()) {
get_parent()->queue_accessibility_update();
}
if (embedder) {
embedder->queue_accessibility_update();
}
} }
// Emits NOTIFICATION_THEME_CHANGED internally. // Emits NOTIFICATION_THEME_CHANGED internally.
@ -1584,6 +1608,18 @@ void Window::_notification(int p_what) {
set_theme_context(nullptr, false); set_theme_context(nullptr, false);
if (visible && window_id != DisplayServer::MAIN_WINDOW_ID) {
if (get_tree() && get_tree()->is_accessibility_supported()) {
_accessibility_notify_exit(this);
if (get_parent()) {
get_parent()->queue_accessibility_update();
}
if (embedder) {
embedder->queue_accessibility_update();
}
}
}
accessibility_title_element = RID(); accessibility_title_element = RID();
accessibility_announcement_element = RID(); accessibility_announcement_element = RID();
@ -1816,6 +1852,14 @@ Viewport *Window::get_parent_viewport() const {
} }
} }
Window *Window::get_non_popup_window() const {
Window *w = const_cast<Window *>(this);
while (w && w->is_popup()) {
w = w->get_parent_visible_window();
}
return w;
}
Window *Window::get_parent_visible_window() const { Window *Window::get_parent_visible_window() const {
ERR_READ_THREAD_GUARD_V(nullptr); ERR_READ_THREAD_GUARD_V(nullptr);
Viewport *vp = get_parent_viewport(); Viewport *vp = get_parent_viewport();

View file

@ -332,6 +332,8 @@ public:
void set_flag(Flags p_flag, bool p_enabled); void set_flag(Flags p_flag, bool p_enabled);
bool get_flag(Flags p_flag) const; bool get_flag(Flags p_flag) const;
bool is_popup() const;
bool is_maximize_allowed() const; bool is_maximize_allowed() const;
void request_attention(); void request_attention();
@ -399,6 +401,7 @@ public:
Window *get_exclusive_child() const { return exclusive_child; } Window *get_exclusive_child() const { return exclusive_child; }
HashSet<Window *> get_transient_children() const { return transient_children; } HashSet<Window *> get_transient_children() const { return transient_children; }
Window *get_parent_visible_window() const; Window *get_parent_visible_window() const;
Window *get_non_popup_window() const;
Viewport *get_parent_viewport() const; Viewport *get_parent_viewport() const;
virtual void popup(const Rect2i &p_screen_rect = Rect2i()); virtual void popup(const Rect2i &p_screen_rect = Rect2i());