From b106dfd4f9acbc910e327a10ffdb86fabefcaa61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pa=CC=84vels=20Nadtoc=CC=8Cajevs?= <7645683+bruvzg@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:42:23 +0200 Subject: [PATCH] Base accessibility API. --- .pre-commit-config.yaml | 2 + core/config/project_settings.cpp | 3 + core/input/input_map.cpp | 50 +- core/input/input_map.h | 2 + core/string/ustring.cpp | 27 +- core/string/ustring.h | 2 +- doc/classes/Control.xml | 24 + doc/classes/DisplayServer.xml | 894 ++++++++++++++++++ doc/classes/GraphEdit.xml | 6 + doc/classes/GraphNode.xml | 7 + doc/classes/InputMap.xml | 7 + doc/classes/Label.xml | 4 + doc/classes/LinkButton.xml | 2 +- doc/classes/MenuBar.xml | 1 + doc/classes/MenuButton.xml | 2 +- doc/classes/Node.xml | 60 ++ doc/classes/ProjectSettings.xml | 32 + doc/classes/RichTextLabel.xml | 14 +- doc/classes/SceneTree.xml | 12 + doc/classes/ScrollBar.xml | 1 + doc/classes/TextEdit.xml | 3 + doc/classes/TextLine.xml | 6 + doc/classes/TextParagraph.xml | 12 + doc/classes/TextServer.xml | 86 ++ doc/classes/TextServerExtension.xml | 97 ++ doc/classes/TreeItem.xml | 27 +- doc/classes/Viewport.xml | 13 + doc/classes/Window.xml | 6 + main/main.cpp | 55 +- .../4.4-stable.expected | 10 + modules/text_server_adv/text_server_adv.cpp | 220 +++++ modules/text_server_adv/text_server_adv.h | 24 + modules/text_server_fb/text_server_fb.cpp | 217 +++++ modules/text_server_fb/text_server_fb.h | 23 + scene/2d/animated_sprite_2d.cpp | 11 + scene/2d/gpu_particles_2d.cpp | 4 +- scene/2d/node_2d.cpp | 7 + scene/2d/physics/touch_screen_button.cpp | 19 + scene/2d/physics/touch_screen_button.h | 2 + scene/2d/sprite_2d.cpp | 11 + scene/3d/decal.cpp | 2 +- scene/3d/gpu_particles_3d.cpp | 4 +- scene/3d/light_3d.cpp | 4 +- scene/3d/node_3d.cpp | 7 + scene/3d/voxel_gi.cpp | 2 + scene/audio/audio_stream_player.cpp | 9 +- scene/gui/base_button.cpp | 72 +- scene/gui/base_button.h | 1 + scene/gui/button.cpp | 22 + scene/gui/check_box.cpp | 13 +- scene/gui/check_box.h | 2 +- scene/gui/check_button.cpp | 7 + scene/gui/color_picker.cpp | 50 +- scene/gui/color_rect.cpp | 8 + scene/gui/container.cpp | 7 + scene/gui/control.cpp | 194 +++- scene/gui/control.h | 19 +- scene/gui/dialogs.cpp | 6 + scene/gui/file_dialog.cpp | 24 +- scene/gui/graph_edit.cpp | 246 ++++- scene/gui/graph_edit.h | 15 + scene/gui/graph_node.cpp | 358 +++++++ scene/gui/graph_node.h | 16 + scene/gui/item_list.cpp | 208 +++- scene/gui/item_list.h | 16 + scene/gui/label.cpp | 26 +- scene/gui/label.h | 1 + scene/gui/line_edit.cpp | 143 ++- scene/gui/line_edit.h | 6 + scene/gui/link_button.cpp | 20 +- scene/gui/menu_bar.cpp | 20 + scene/gui/menu_button.cpp | 10 +- scene/gui/option_button.cpp | 8 + scene/gui/panel.cpp | 7 + scene/gui/popup.cpp | 6 +- scene/gui/popup.h | 2 + scene/gui/popup_menu.cpp | 210 +++- scene/gui/popup_menu.h | 9 + scene/gui/progress_bar.cpp | 8 + scene/gui/range.cpp | 50 + scene/gui/range.h | 5 + scene/gui/rich_text_label.compat.inc | 12 +- scene/gui/rich_text_label.cpp | 721 +++++++++++++- scene/gui/rich_text_label.h | 64 +- scene/gui/scroll_bar.cpp | 9 + scene/gui/scroll_container.cpp | 35 + scene/gui/scroll_container.h | 6 + scene/gui/slider.cpp | 6 + scene/gui/spin_box.cpp | 46 +- scene/gui/spin_box.h | 17 +- scene/gui/split_container.cpp | 55 ++ scene/gui/split_container.h | 6 + scene/gui/tab_bar.cpp | 117 ++- scene/gui/tab_bar.h | 9 + scene/gui/tab_container.cpp | 47 + scene/gui/tab_container.h | 4 + scene/gui/text_edit.cpp | 282 +++++- scene/gui/text_edit.h | 30 +- scene/gui/texture_progress_bar.cpp | 7 + scene/gui/tree.compat.inc | 41 + scene/gui/tree.cpp | 570 ++++++++++- scene/gui/tree.h | 65 +- scene/gui/video_stream_player.cpp | 7 + scene/main/canvas_item.cpp | 7 + scene/main/node.cpp | 269 +++++- scene/main/node.h | 48 + scene/main/scene_tree.cpp | 105 ++ scene/main/scene_tree.h | 12 + scene/main/viewport.cpp | 85 +- scene/main/viewport.h | 11 + scene/main/window.cpp | 156 ++- scene/main/window.h | 21 +- scene/resources/text_line.cpp | 5 + scene/resources/text_line.h | 1 + scene/resources/text_paragraph.cpp | 16 + scene/resources/text_paragraph.h | 3 + scene/theme/default_theme.cpp | 5 + servers/display_server.cpp | 628 ++++++++++++ servers/display_server.h | 303 ++++++ servers/text/text_server_extension.cpp | 78 ++ servers/text/text_server_extension.h | 24 + servers/text_server.cpp | 12 + servers/text_server.h | 12 + tests/scene/test_control.h | 7 +- 124 files changed, 7631 insertions(+), 181 deletions(-) create mode 100644 scene/gui/tree.compat.inc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8100f0ed2e..f1bc349fb90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,8 @@ exclude: | (?x)^( .*thirdparty/.*| .*-so_wrap\.(h|c)| + .*-dll_wrap\.(h|c)| + .*-dylib_wrap\.(h|c)| platform/android/java/editor/src/main/java/com/android/.*| platform/android/java/lib/src/com/google/.* )$ diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index c328332387d..34a4cab8015 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1491,6 +1491,9 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF("application/config/auto_accept_quit", true); GLOBAL_DEF("application/config/quit_on_go_back", true); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "accessibility/general/accessibility_support", PROPERTY_HINT_ENUM, "Auto (When Screen Reader is Running),Always Active,Disabled"), 0); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "accessibility/general/updates_per_second", PROPERTY_HINT_RANGE, "1,100,1"), 60); + // The default window size is tuned to: // - Have a 16:9 aspect ratio, // - Have both dimensions divisible by 8 to better play along with video recording, diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index a15e4f18761..ea03c25c729 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -47,6 +47,8 @@ void InputMap::_bind_methods() { ClassDB::bind_method(D_METHOD("add_action", "action", "deadzone"), &InputMap::add_action, DEFVAL(DEFAULT_DEADZONE)); ClassDB::bind_method(D_METHOD("erase_action", "action"), &InputMap::erase_action); + ClassDB::bind_method(D_METHOD("get_action_description", "action"), &InputMap::get_action_description); + ClassDB::bind_method(D_METHOD("action_set_deadzone", "action", "deadzone"), &InputMap::action_set_deadzone); ClassDB::bind_method(D_METHOD("action_get_deadzone", "action"), &InputMap::action_get_deadzone); ClassDB::bind_method(D_METHOD("action_add_event", "action", "event"), &InputMap::action_add_event); @@ -181,6 +183,25 @@ bool InputMap::has_action(const StringName &p_action) const { return input_map.has(p_action); } +String InputMap::get_action_description(const StringName &p_action) const { + ERR_FAIL_COND_V_MSG(!input_map.has(p_action), String(), suggest_actions(p_action)); + + String ret; + const List> &inputs = input_map[p_action].inputs; + for (Ref iek : inputs) { + if (iek.is_valid()) { + if (!ret.is_empty()) { + ret += RTR(" or "); + } + ret += iek->as_text(); + } + } + if (ret.is_empty()) { + ret = RTR("Action has no bound inputs"); + } + return ret; +} + float InputMap::action_get_deadzone(const StringName &p_action) { ERR_FAIL_COND_V_MSG(!input_map.has(p_action), 0.0f, suggest_actions(p_action)); @@ -344,6 +365,7 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = { { "ui_cut", TTRC("Cut") }, { "ui_copy", TTRC("Copy") }, { "ui_paste", TTRC("Paste") }, + { "ui_focus_mode", TTRC("Toggle Tab Focus Mode") }, { "ui_undo", TTRC("Undo") }, { "ui_redo", TTRC("Redo") }, { "ui_text_completion_query", TTRC("Completion Query") }, @@ -397,12 +419,15 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = { { "ui_text_submit", TTRC("Submit Text") }, { "ui_graph_duplicate", TTRC("Duplicate Nodes") }, { "ui_graph_delete", TTRC("Delete Nodes") }, + { "ui_graph_follow_left", TTRC("Follow Input Port Connection") }, + { "ui_graph_follow_right", TTRC("Follow Output Port Connection") }, { "ui_filedialog_up_one_level", TTRC("Go Up One Level") }, { "ui_filedialog_refresh", TTRC("Refresh") }, { "ui_filedialog_show_hidden", TTRC("Show Hidden") }, { "ui_swap_input_direction ", TTRC("Swap Input Direction") }, { "ui_unicode_start", TTRC("Start Unicode Character Input") }, - { "ui_colorpicker_delete_preset", TTRC("Toggle License Notices") }, + { "ui_colorpicker_delete_preset", TTRC("Toggle License Notices") }, + { "ui_accessibility_drag_and_drop", TTRC("Accessibility: Keyboard Drag and Drop") }, { "", ""} /* clang-format on */ }; @@ -488,6 +513,9 @@ const HashMap>> &InputMap::get_builtins() { inputs.push_back(InputEventKey::create_reference(Key::END)); default_builtin_cache.insert("ui_end", inputs); + inputs = List>(); + default_builtin_cache.insert("ui_accessibility_drag_and_drop", inputs); + // ///// UI basic Shortcuts ///// inputs = List>(); @@ -500,6 +528,10 @@ const HashMap>> &InputMap::get_builtins() { inputs.push_back(InputEventKey::create_reference(Key::INSERT | KeyModifierMask::CMD_OR_CTRL)); default_builtin_cache.insert("ui_copy", inputs); + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::M | KeyModifierMask::CTRL)); + default_builtin_cache.insert("ui_focus_mode", inputs); + inputs = List>(); inputs.push_back(InputEventKey::create_reference(Key::V | KeyModifierMask::CMD_OR_CTRL)); inputs.push_back(InputEventKey::create_reference(Key::INSERT | KeyModifierMask::SHIFT)); @@ -773,6 +805,22 @@ const HashMap>> &InputMap::get_builtins() { inputs.push_back(InputEventKey::create_reference(Key::KEY_DELETE)); default_builtin_cache.insert("ui_graph_delete", inputs); + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::LEFT | KeyModifierMask::CMD_OR_CTRL)); + default_builtin_cache.insert("ui_graph_follow_left", inputs); + + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::LEFT | KeyModifierMask::ALT)); + default_builtin_cache.insert("ui_graph_follow_left.macos", inputs); + + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL)); + default_builtin_cache.insert("ui_graph_follow_right", inputs); + + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::RIGHT | KeyModifierMask::ALT)); + default_builtin_cache.insert("ui_graph_follow_right.macos", inputs); + // ///// UI File Dialog Shortcuts ///// inputs = List>(); inputs.push_back(InputEventKey::create_reference(Key::BACKSPACE)); diff --git a/core/input/input_map.h b/core/input/input_map.h index 9b89281b5cf..00bc1550a2b 100644 --- a/core/input/input_map.h +++ b/core/input/input_map.h @@ -85,6 +85,8 @@ public: void add_action(const StringName &p_action, float p_deadzone = DEFAULT_DEADZONE); void erase_action(const StringName &p_action); + String get_action_description(const StringName &p_action) const; + float action_get_deadzone(const StringName &p_action); void action_set_deadzone(const StringName &p_action, float p_deadzone); void action_add_event(const StringName &p_action, const Ref &p_event); diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 854d32af967..a7ec4d17e99 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -2073,34 +2073,45 @@ Error String::append_utf8(const char *p_utf8, int p_len, bool p_skip_cr) { return result; } -CharString String::utf8() const { +CharString String::utf8(Vector *r_ch_length_map) const { int l = length(); if (!l) { return CharString(); } + uint8_t *map_ptr = nullptr; + if (r_ch_length_map) { + r_ch_length_map->resize(l); + map_ptr = r_ch_length_map->ptrw(); + } + const char32_t *d = &operator[](0); int fl = 0; for (int i = 0; i < l; i++) { uint32_t c = d[i]; + int ch_w = 1; if (c <= 0x7f) { // 7 bits. - fl += 1; + ch_w = 1; } else if (c <= 0x7ff) { // 11 bits - fl += 2; + ch_w = 2; } else if (c <= 0xffff) { // 16 bits - fl += 3; + ch_w = 3; } else if (c <= 0x001fffff) { // 21 bits - fl += 4; + ch_w = 4; } else if (c <= 0x03ffffff) { // 26 bits - fl += 5; + ch_w = 5; print_unicode_error(vformat("Invalid unicode codepoint (%x)", c)); } else if (c <= 0x7fffffff) { // 31 bits - fl += 6; + ch_w = 6; print_unicode_error(vformat("Invalid unicode codepoint (%x)", c)); } else { - fl += 1; + ch_w = 1; print_unicode_error(vformat("Invalid unicode codepoint (%x), cannot represent as UTF-8", c), true); } + fl += ch_w; + if (map_ptr) { + map_ptr[i] = ch_w; + } } CharString utf8s; diff --git a/core/string/ustring.h b/core/string/ustring.h index e828456df12..145240e4d54 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -511,7 +511,7 @@ public: return string; } - CharString utf8() const; + CharString utf8(Vector *r_ch_length_map = nullptr) const; Error append_utf8(const char *p_utf8, int p_len = -1, bool p_skip_cr = false); Error append_utf8(const Span &p_range, bool p_skip_cr = false) { return append_utf8(p_range.ptr(), p_range.size(), p_skip_cr); diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index 63c13437327..5cc12077acb 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -24,6 +24,12 @@ https://github.com/godotengine/godot-demo-projects/tree/master/gui + + + + Return the description of the keyboard shortcuts and other contextual help for this control. + + @@ -31,6 +37,7 @@ Godot calls this method to test if [param data] from a control's [method _get_drag_data] can be dropped at [param at_position]. [param at_position] is local to this control. This method should only be used to test the data. Process the data in [method _drop_data]. + [b]Note:[/b] If drag was initiated by keyboard shortcut or [method accessibility_drag], [param at_position] is set to [code]Vector2(INFINITY, INFINITY)[/code] and the currently selected item/text position should be used as drop position. [codeblocks] [gdscript] func _can_drop_data(position, data): @@ -55,6 +62,7 @@ Godot calls this method to pass you the [param data] from a control's [method _get_drag_data] result. Godot first calls [method _can_drop_data] to test if [param data] is allowed to drop at [param at_position] where [param at_position] is local to this control. + [b]Note:[/b] If drag was initiated by keyboard shortcut or [method accessibility_drag], [param at_position] is set to [code]Vector2(INFINITY, INFINITY)[/code] and the currently selected item/text position should be used as drop position. [codeblocks] [gdscript] func _can_drop_data(position, data): @@ -83,6 +91,7 @@ Godot calls this method to get data that can be dragged and dropped onto controls that expect drop data. Returns [code]null[/code] if there is no data to drag. Controls that want to receive drop data should implement [method _can_drop_data] and [method _drop_data]. [param at_position] is local to this control. Drag may be forced with [method force_drag]. A preview that will follow the mouse that should represent the data can be set with [method set_drag_preview]. A good time to set the preview is in this method. + [b]Note:[/b] If drag was initiated by keyboard shortcut or [method accessibility_drag], [param at_position] is set to [code]Vector2(INFINITY, INFINITY)[/code] and the currently selected item/text position should be used as drop position. [codeblocks] [gdscript] func _get_drag_data(position): @@ -223,6 +232,18 @@ [b]Note:[/b] This does not affect the methods in [Input], only the way events are propagated. + + + + Starts drag-and-drop operation without using a mouse. + + + + + + Ends drag-and-drop operation without using a mouse. + + @@ -1174,6 +1195,9 @@ The node can grab focus on mouse click, using the arrows and the Tab keys on the keyboard, or using the D-pad buttons on a gamepad. Use with [member focus_mode]. + + The node can grab focus only when screen reader is active. Use with [member focus_mode]. + Inherits the associated behavior from the control's parent. This is the default for any newly created control. diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 0103cff1df4..1cb908e3e30 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -10,6 +10,630 @@ + + + + + + Creates a new, empty accessibility element resource. + [b]Note:[/b] An accessibility element is created and freed automatically for each [Node]. In general, this function should not be called manually. + + + + + + + + + Creates a new, empty accessibility sub-element resource. Sub-elements can be used to provide accessibility information for objects which are not [Node]s, such as list items, table cells, or menu items. Sub-elements are freed automatically when the parent element is freed, or can be freed early using the [method accessibility_free_element] method. + + + + + + + + + + Creates a new, empty accessibility sub-element from the shaped text buffer. Sub-elements are freed automatically when the parent element is freed, or can be freed early using the [method accessibility_free_element] method. + + + + + + + Returns the metadata of the accessibility element. + + + + + + + + Sets the metadata of the accessibility element. + + + + + + + Frees an object created by [method accessibility_create_element], [method accessibility_create_sub_element], or [method accessibility_create_sub_text_edit_elements]. + + + + + + + Returns the main accessibility element of the OS native window. + + + + + + + Returns [code]true[/code] if [param id] is a valid accessibility element. + + + + + + Returns [code]1[/code] if a screen reader, Braille display or other assistive app is active, [code]0[/code] otherwise. Returns [code]-1[/code] if status is unknown. + [b]Note:[/b] This method is implemented on Linux, macOS, and Windows. + [b]Note:[/b] Accessibility debugging tools, such as Accessibility Insights for Windows, macOS Accessibility Inspector, or AT-SPI Browser do not count as assistive apps and will not affect this value. To test your app with these tools, set [member ProjectSettings.accessibility/general/accessibility_support] to [code]1[/code]. + + + + + + + + Sets the window focused state for assistive apps. + [b]Note:[/b] This method is implemented on Linux, macOS, and Windows. + [b]Note:[/b] Advanced users only! [Window] objects call this method automatically. + + + + + + + + + Sets window outer (with decorations) and inner (without decorations) bounds for assistive apps. + [b]Note:[/b] This method is implemented on Linux, macOS, and Windows. + [b]Note:[/b] Advanced users only! [Window] objects call this method automatically. + + + + + + Returns [code]1[/code] if a high-contrast user interface theme should be used, [code]0[/code] otherwise. Returns [code]-1[/code] if status is unknown. + [b]Note:[/b] This method is implemented on Linux (X11/Wayland, GNOME), macOS, and Windows. + + + + + + Returns [code]1[/code] if flashing, blinking, and other moving content that can cause seizures in users with photosensitive epilepsy should be disabled, [code]0[/code] otherwise. Returns [code]-1[/code] if status is unknown. + [b]Note:[/b] This method is implemented on macOS and Windows. + + + + + + Returns [code]1[/code] if background images, transparency, and other features that can reduce the contrast between the foreground and background should be disabled, [code]0[/code] otherwise. Returns [code]-1[/code] if status is unknown. + [b]Note:[/b] This method is implemented on macOS and Windows. + + + + + + + + + Adds a callback for the accessibility action (action which can be performed by using a special screen reader command or buttons on the Braille display), and marks this action as supported. The action callback receives one [Variant] argument, which value depends on action type. + + + + + + + + Adds a child accessibility element. + [b]Note:[/b] [Node] children and sub-elements are added to the child list automatically. + + + + + + + + + Adds support for a custom accessibility action. [param action_id] is passed as an argument to the callback of [constant ACTION_CUSTOM] action. + + + + + + + + Adds an element that is controlled by this element. + + + + + + + + Adds an element that describes this element. + + + + + + + + Adds an element that details this element. + + + + + + + + Adds an element that this element flow into. + + + + + + + + Adds an element that labels this element. + + + + + + + + Adds an element that is part of the same radio group. + [b]Note:[/b] This method should be called on each element of the group, using all other elements as [param related_id]. + + + + + + + + Adds an element that is an active descendant of this element. + + + + + + + + Sets element background color. + + + + + + + + Sets element bounding box, relative to the node position. + + + + + + + + Sets element checked state. + + + + + + + + Sets element class name. + + + + + + + + Sets element color value. + + + + + + + + Sets element accessibility description. + + + + + + + + Sets an element which contains an error message for this element. + + + + + + + + Sets element accessibility extra information added to the element name. + + + + + + + + + Sets element flag, see [enum DisplayServer.AccessibilityFlags]. + + + + + + + Sets currently focused element. + + + + + + + + Sets element foreground color. + + + + + + + + Sets target element for the link. + + + + + + + + Sets element text language. + + + + + + + + Sets number of items in the list. + + + + + + + + Sets list/tree item expanded status. + + + + + + + + Sets the position of the element in the list. + + + + + + + + Sets the hierarchical level of the element in the list. + + + + + + + + Sets list/tree item selected status. + + + + + + + + Sets the orientation of the list elements. + + + + + + + + Sets the priority of the live region updates. + + + + + + + + Sets the element to be a member of the group. + + + + + + + + Sets element accessibility name. + + + + + + + + Sets next element on the line. + + + + + + + + Sets numeric value jump. + + + + + + + + + Sets numeric value range. + + + + + + + + Sets numeric value step. + + + + + + + + Sets numeric value. + + + + + + + + Sets placeholder text. + + + + + + + + Sets popup type for popup buttons. + + + + + + + + Sets previous element on the line. + + + + + + + + Sets element accessibility role. + + + + + + + + Sets element accessibility role description text. + + + + + + + + Sets scroll bar x position. + + + + + + + + + Sets scroll bar x range. + + + + + + + + Sets scroll bar y position. + + + + + + + + + Sets scroll bar y range. + + + + + + + + Sets the list of keyboard shortcuts used by element. + + + + + + + + Sets human-readable description of the current checked state. + + + + + + + + + Sets cell position in the table. + + + + + + + + + Sets cell row/column span. + + + + + + + + Sets number of columns in the table. + + + + + + + + Sets position of the column. + + + + + + + + Sets number of rows in the table. + + + + + + + + Sets position of the row in the table. + + + + + + + + Sets element text alignment. + + + + + + + + + + Sets text underline/overline/strikethrough. + + + + + + + + Sets text orientation. + + + + + + + + + + + Sets text selection to the text field. [param text_start_id] and [param text_end_id] should be elements created by [method accessibility_create_sub_text_edit_elements]. Character offsets are relative to the corresponding element. + + + + + + + + Sets tooltip text. + + + + + + + + Sets element 2D transform. + + + + + + + + Sets link URL. + + + + + + + + Sets element text value. + + @@ -1969,6 +2593,276 @@ Display server automatically fits popups according to the screen boundaries. Window nodes should not attempt to do that themselves. + + Display server supports interaction with screen reader or Braille display. [b]Linux (X11/Wayland), macOS, Windows[/b] + + + Unknown or custom role. + + + Default dialog button element. + + + Audio player element. + + + Video player element. + + + Non-editable text label. + + + Container element. Elements with this role are used for internal structure and ignored by screen readers. + + + Panel container element. + + + Button element. + + + Link element. + + + Check box element. + + + Radio button element. + + + Check button element. + + + Scroll bar element. + + + Scroll container element. + + + Container splitter handle element. + + + Slider element. + + + Spin box element. + + + Progress indicator element. + + + Editable text field element. + + + Multiline editable text field element. + + + Color picker element. + + + Table element. + + + Table/tree cell element. + + + Table/tree row element. + + + Table/tree row group element. + + + Table/tree row header element. + + + Table/tree column header element. + + + Tree view element. + + + Tree view item element. + + + List element. + + + List item element. + + + List view element. + + + List view item element. + + + Tab bar element. + + + Tab bar item element. + + + Tab panel element. + + + Menu bar element. + + + Popup menu element. + + + Popup menu item element. + + + Popup menu check button item element. + + + Popup menu radio button item element. + + + Image element. + + + Window element. + + + Embedded window title bar element. + + + Dialog window element. + + + Tooltip element. + + + Other/unknown popup type. + + + Popup menu. + + + Popup list. + + + Popup tree view. + + + Popup dialog. + + + Element is hidden for accessibility tools. + + + + + Element is support multiple item selection. + + + Element require user input. + + + Element is a visited link. + + + Element content is not ready (e.g. loading). + + + Element is modal window. + + + Element allows touches to be passed through when a screen reader is in touch exploration mode. + + + Element is text field with selectable but read-only text. + + + Element is disabled. + + + Element clips children. + + + Single click action, callback argument is not set. + + + Focus action, callback argument is not set. + + + Blur action, callback argument is not set. + + + Collapse action, callback argument is not set. + + + Expand action, callback argument is not set. + + + Decrement action, callback argument is not set. + + + Increment action, callback argument is not set. + + + Hide tooltip action, callback argument is not set. + + + Show tooltip action, callback argument is not set. + + + Set text selection action, callback argument is set to [Dictionary] with the following keys: + - [code]"start_element"[/code] accessibility element of the selection start. + - [code]"start_char"[/code] character offset relative to the accessibility element of the selection start. + - [code]"end_element"[/code] accessibility element of the selection end. + - [code]"end_char"[/code] character offset relative to the accessibility element of the selection end. + + + Replace text action, callback argument is set to [String] with the replacement text. + + + Scroll backward action, callback argument is not set. + + + Scroll down action, callback argument is not set. + + + Scroll forward action, callback argument is not set. + + + Scroll left action, callback argument is not set. + + + Scroll right action, callback argument is not set. + + + Scroll up action, callback argument is not set. + + + Scroll into view action, callback argument is not set. + + + Scroll to point action, callback argument is set to [Vector2] with the relative point coordinates. + + + Set scroll offset action, callback argument is set to [Vector2] with the scroll offset. + + + Set value action action, callback argument is set to [String] or number with the new value. + + + Show context menu action, callback argument is not set. + + + Custom action, callback argument is set to the integer action id. + + + Indicates that updates to the live region should not be presented. + + + Indicates that updates to the live region should be presented at the next opportunity (for example at the end of speaking the current sentence). + + + Indicates that updates to the live region have the highest priority and should be presented immediately. + Makes the mouse cursor visible if it is hidden. diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml index 4c0847871b4..39d3af920f9 100644 --- a/doc/classes/GraphEdit.xml +++ b/doc/classes/GraphEdit.xml @@ -391,6 +391,9 @@ If [code]true[/code], enables snapping. + + [Dictionary] of human readable port type names. + The current zoom value. @@ -603,5 +606,8 @@ The background drawn under the grid. + + [StyleBox] used when the [GraphEdit] is focused (when used with assistive apps). + diff --git a/doc/classes/GraphNode.xml b/doc/classes/GraphNode.xml index fc8bc15b5c2..a9445c33197 100644 --- a/doc/classes/GraphNode.xml +++ b/doc/classes/GraphNode.xml @@ -267,6 +267,7 @@ + If [code]true[/code], you can connect ports with different types, even if the connection was not explicitly allowed in the parent [GraphEdit]. @@ -299,12 +300,18 @@ The default background for the slot area of the [GraphNode]. + + [StyleBox] used when the [GraphNode] is focused (when used with assistive apps). + The [StyleBox] used for the slot area when selected. The [StyleBox] used for each slot of the [GraphNode]. + + [StyleBox] used when the slot is focused (when used with assistive apps). + The [StyleBox] used for the title bar of the [GraphNode]. diff --git a/doc/classes/InputMap.xml b/doc/classes/InputMap.xml index 0abd7c69742..c4b9695a164 100644 --- a/doc/classes/InputMap.xml +++ b/doc/classes/InputMap.xml @@ -90,6 +90,13 @@ If [param exact_match] is [code]false[/code], it ignores additional input modifiers for [InputEventKey] and [InputEventMouseButton] events, and the direction for [InputEventJoypadMotion] events. + + + + + Returns the human-readable description of the given action. + + diff --git a/doc/classes/Label.xml b/doc/classes/Label.xml index 6f99d27cb16..2e39d9539c1 100644 --- a/doc/classes/Label.xml +++ b/doc/classes/Label.xml @@ -58,6 +58,7 @@ Ellipsis character used for text clipping. + Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants. @@ -153,6 +154,9 @@ Font size of the [Label]'s text. + + [StyleBox] used when the [Label] is focused (when used with assistive apps). + Background [StyleBox] for the [Label]. diff --git a/doc/classes/LinkButton.xml b/doc/classes/LinkButton.xml index b1b3d747111..802899858ac 100644 --- a/doc/classes/LinkButton.xml +++ b/doc/classes/LinkButton.xml @@ -10,7 +10,7 @@ - + Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead. diff --git a/doc/classes/MenuBar.xml b/doc/classes/MenuBar.xml index d2303622bfa..2fecdd2381b 100644 --- a/doc/classes/MenuBar.xml +++ b/doc/classes/MenuBar.xml @@ -100,6 +100,7 @@ Flat [MenuBar] don't display item decoration. + Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead. diff --git a/doc/classes/MenuButton.xml b/doc/classes/MenuButton.xml index 16b3772fa90..a1e0b34ffc4 100644 --- a/doc/classes/MenuButton.xml +++ b/doc/classes/MenuButton.xml @@ -34,7 +34,7 @@ - + The number of items currently in the list. diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index 1e27ed48602..8521acd2223 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -36,6 +36,20 @@ Corresponds to the [constant NOTIFICATION_EXIT_TREE] notification in [method Object._notification] and signal [signal tree_exiting]. To get notified when the node has already left the active tree, connect to the [signal tree_exited]. + + + + The elements in the array returned from this method are displayed as warnings in the Scene dock if the script that overrides it is a [code]tool[/code] script, and accessibility warnings are enabled in the editor settings. + Returning an empty array produces no warnings. + + + + + + + Return a human-readable description of the position of [param node] child in the custom container, added to the node name. + + @@ -56,6 +70,12 @@ [/codeblock] + + + + Called during accessibility information updates to determine the currently focused sub-element, should return a sub-element RID or the value returned by [method get_accessibility_element]. + + @@ -299,6 +319,13 @@ [b]Note:[/b] As this method walks upwards in the scene tree, it can be slow in large, deeply nested nodes. Consider storing a reference to the found node in a variable. Alternatively, use [method get_node] with unique names (see [member unique_name_in_owner]). + + + + Returns main accessibility element RID. + [b]Note:[/b] This method should be called only during accessibility information updates ([constant NOTIFICATION_ACCESSIBILITY_UPDATE]). + + @@ -777,6 +804,12 @@ Calls [method Object.notification] with [param what] on this node and all of its children, recursively. + + + + Queues an accessibility information update for this node. + + @@ -994,6 +1027,27 @@ + + The list of nodes which are controlled by this node. + + + The list of nodes which are describing this node. + + + The human-readable node description that is reported to assistive apps. + + + The list of nodes which this node flows into. + + + The list of nodes which label this node. + + + Live region update mode, a live region is [Node] that is updated as a result of an external event when user focus may be elsewhere. + + + The human-readable node name that is reported to assistive apps. + Defines if any text should automatically change to its translated version depending on the current locale (for nodes such as [Label], [RichTextLabel], [Window], etc.). Also decides if the node's strings should be parsed for POT generation. [b]Note:[/b] For the root node, auto translate mode can also be set via [member ProjectSettings.internationalization/rendering/root_node_auto_translate]. @@ -1276,6 +1330,12 @@ Notification received when the [TextServer] is changed. + + Notification received when an accessibility information update is required. + + + Notification received when accessibility elements are invalidated. All node accessibility elements are automatically deleted after receiving this message, therefore all existing references to such elements should be discarded. + Inherits [member process_mode] from the node's parent. This is the default for any newly created node. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index a156c0ad275..e878c245e78 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -235,6 +235,16 @@ + + Accessibility support mode: + - [b]Auto[/b] ([code]0[/code]): accessibility support is enabled, but accessibility information updates are processed only if an assistive app (e.g. screen reader or Braille display) is active (default). + - [b]Always Active[/b] ([code]1[/code]): accessibility support is enabled, and accessibility information updates are processed regardless of current assistive apps' status. + - [b]Disabled[/b] ([code]2[/code]): accessibility support is fully disabled. + [b]Note:[/b] Accessibility debugging tools, such as Accessibility Insights for Windows, macOS Accessibility Inspector, or AT-SPI Browser do not count as assistive apps. To test your app with these tools, use [code]1[/code]. + + + The number of accessibility information updates per second. + If [code]true[/code], [AnimationMixer] prints the warning of interpolation being forced to choose the shortest rotation path due to multiple angle interpolation types being mixed in the [AnimationMixer] cache. @@ -1191,6 +1201,10 @@ Default [InputEventAction] to confirm a focused button, menu or list item, or validate input. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + Default [InputEventAction] to start or end a drag-and-drop operation without using mouse. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + Default [InputEventAction] to discard a modal or pending input. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. @@ -1227,6 +1241,10 @@ Default [InputEventAction] to go up one directory in a [FileDialog]. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + Default [InputEventAction] to switch [TextEdit] [member input/ui_text_indent] between moving keyboard focus to the next [Control] in the scene and inputting a [code]Tab[/code] character. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + Default [InputEventAction] to focus the next [Control] in the scene. The focus behavior can be configured via [member Control.focus_next]. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. @@ -1243,6 +1261,20 @@ Default [InputEventAction] to duplicate a [GraphNode] in a [GraphEdit]. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + Default [InputEventAction] to follow a [GraphNode] input port connection. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + + macOS specific override for the shortcut to follow a [GraphNode] input port connection. + + + Default [InputEventAction] to follow a [GraphNode] output port connection. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + + macOS specific override for the shortcut to follow a [GraphNode] output port connection. + Default [InputEventAction] to go to the start position of a [Control] (e.g. first item in an [ItemList] or a [Tree]), matching the behavior of [constant KEY_HOME] on typical desktop UI systems. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml index 8ef40f777bc..2786879d352 100644 --- a/doc/classes/RichTextLabel.xml +++ b/doc/classes/RichTextLabel.xml @@ -29,6 +29,7 @@ + Adds an image's opening and closing tags to the tag stack, optionally providing a [param width] and [param height] to resize the image, a [param color] to tint the image and a [param region] to only use parts of the image. If [param width] or [param height] is set to 0, the image size will be adjusted in order to keep the original aspect ratio. @@ -36,6 +37,7 @@ [param key] is an optional identifier, that can be used to modify the image via [method update_image]. If [param pad] is set, and the image is smaller than the size specified by [param width] and [param height], the image padding is added to match the size instead of upscaling. If [param size_in_percent] is set, [param width] and [param height] values are percentages of the control width instead of pixels. + [param alt_text] is used as the image description for assistive apps. @@ -517,8 +519,9 @@ + - Adds a [code skip-lint][table=columns,inline_align][/code] tag to the tag stack. Use [method set_table_column_expand] to set column expansion ratio. Use [method push_cell] to add cells. + Adds a [code skip-lint][table=columns,inline_align][/code] tag to the tag stack. Use [method set_table_column_expand] to set column expansion ratio. Use [method push_cell] to add cells. [param name] is used as the table name for assistive apps. @@ -612,6 +615,14 @@ If [param expand] is [code]false[/code], the column will not contribute to the total ratio. + + + + + + Sets table column name for assistive apps. + + @@ -658,6 +669,7 @@ If [code]true[/code], the label's minimum size will be automatically updated to fit its content, matching the behavior of [Label]. + If [code]true[/code], the label underlines hint tags such as [code skip-lint][hint=description]{text}[/hint][/code]. diff --git a/doc/classes/SceneTree.xml b/doc/classes/SceneTree.xml index 5ae21ad138d..31459dcdddd 100644 --- a/doc/classes/SceneTree.xml +++ b/doc/classes/SceneTree.xml @@ -152,6 +152,18 @@ Returns [code]true[/code] if a node added to the given group [param name] exists in the tree. + + + + Returns [code]true[/code] if accessibility features are enabled, and accessibility information updates are actively processed. + + + + + + Returns [code]true[/code] if accessibility features are supported by the OS and enabled in project settings. + + diff --git a/doc/classes/ScrollBar.xml b/doc/classes/ScrollBar.xml index 4ee05632a59..d9f0217638a 100644 --- a/doc/classes/ScrollBar.xml +++ b/doc/classes/ScrollBar.xml @@ -12,6 +12,7 @@ Overrides the step used when clicking increment and decrement buttons or when using arrow keys when the [ScrollBar] is focused. + diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 330ea49292f..aa591c2e38a 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -1380,6 +1380,9 @@ The syntax highlighter to use. [b]Note:[/b] A [SyntaxHighlighter] instance should not be used across multiple [TextEdit] nodes. + + If [code]true[/code], [member ProjectSettings.input/ui_text_indent] input [code]Tab[/code] character, otherwise it moves keyboard focus to the next [Control] in the scene. + String value of the [TextEdit]. diff --git a/doc/classes/TextLine.xml b/doc/classes/TextLine.xml index 2e4a3f7e4c8..f5164ad61c9 100644 --- a/doc/classes/TextLine.xml +++ b/doc/classes/TextLine.xml @@ -56,6 +56,12 @@ Draw text into a canvas item at a given position, with [param color]. [param pos] specifies the top left corner of the bounding box. + + + + Returns the text writing direction inferred by the BiDi algorithm. + + diff --git a/doc/classes/TextParagraph.xml b/doc/classes/TextParagraph.xml index c9ac660b8c4..2b590f32db4 100644 --- a/doc/classes/TextParagraph.xml +++ b/doc/classes/TextParagraph.xml @@ -122,6 +122,12 @@ Returns drop cap bounding box size. + + + + Returns the text writing direction inferred by the BiDi algorithm. + + @@ -205,6 +211,12 @@ Returns the size of the bounding box of the paragraph, without line breaks. + + + + Returns the character range of the paragraph. + + diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index 2d6c3445aa0..09b3300a488 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -1215,6 +1215,69 @@ [b]Note:[/b] This function is used by during project export, to include TextServer database. + + + + + Returns the number of uniform text runs in the buffer. + + + + + + + + Returns the direction of the [param index] text run (in visual order). + + + + + + + + Returns the font RID of the [param index] text run (in visual order). + + + + + + + + Returns the font size of the [param index] text run (in visual order). + + + + + + + + Returns the language of the [param index] text run (in visual order). + + + + + + + + Returns the embedded object of the [param index] text run (in visual order). + + + + + + + + Returns the source text range of the [param index] text run (in visual order). + + + + + + + + Returns the source text of the [param index] text run (in visual order). + + @@ -1238,6 +1301,29 @@ Returns text span metadata. + + + + + + Returns the text span embedded object key. + + + + + + + + Returns the text span source text. + + + + + + + Returns the text buffer source text, including object replacement characters. + + diff --git a/doc/classes/TextServerExtension.xml b/doc/classes/TextServerExtension.xml index 22bc39a4f18..ef00e7cf78f 100644 --- a/doc/classes/TextServerExtension.xml +++ b/doc/classes/TextServerExtension.xml @@ -1323,6 +1323,77 @@ Saves optional TextServer database (e.g. ICU break iterators and dictionaries) to the file. + + + + + [b]Required.[/b] + Returns the number of uniform text runs in the buffer. + + + + + + + + [b]Required.[/b] + Returns the direction of the [param index] text run (in visual order). + + + + + + + + [b]Required.[/b] + Returns the font RID of the [param index] text run (in visual order). + + + + + + + + [b]Required.[/b] + Returns the font size of the [param index] text run (in visual order). + + + + + + + + [b]Required.[/b] + Returns the language of the [param index] text run (in visual order). + + + + + + + + [b]Required.[/b] + Returns the embedded object of the [param index] text run (in visual order). + + + + + + + + [b]Required.[/b] + Returns the source text range of the [param index] text run (in visual order). + + + + + + + + [b]Required.[/b] + Returns the source text of the [param index] text run (in visual order). + + @@ -1349,6 +1420,32 @@ Returns text span metadata. + + + + + + [b]Required.[/b] + Returns the text span embedded object key. + + + + + + + + [b]Required.[/b] + Returns the text span source text. + + + + + + + [b]Required.[/b] + Returns the text buffer source text, including object replacement characters. + + diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml index cce31b4bb1d..7901d3d0913 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -18,8 +18,9 @@ + - Adds a button with [Texture2D] [param button] to the end of the cell at column [param column]. The [param id] is used to identify the button in the according [signal Tree.button_clicked] signal and can be different from the buttons index. If not specified, the next available index is used, which may be retrieved by calling [method get_button_count] immediately before this method. Optionally, the button can be [param disabled] and have a [param tooltip_text]. + Adds a button with [Texture2D] [param button] to the end of the cell at column [param column]. The [param id] is used to identify the button in the according [signal Tree.button_clicked] signal and can be different from the buttons index. If not specified, the next available index is used, which may be retrieved by calling [method get_button_count] immediately before this method. Optionally, the button can be [param disabled] and have a [param tooltip_text]. [param alt_text] is used as the button description for assistive apps. @@ -79,6 +80,13 @@ Removes the button at index [param button_index] in column [param column]. + + + + + Returns the given column's alternative text. + + @@ -506,6 +514,14 @@ Selects the given [param column]. + + + + + + Sets the given column's alternative (description) text for assistive apps. + + @@ -532,6 +548,15 @@ Sets the given column's button [Texture2D] at index [param button_index] to [param button]. + + + + + + + Sets the given column's button alternative text (description) at index [param button_index] for assistive apps. + + diff --git a/doc/classes/Viewport.xml b/doc/classes/Viewport.xml index 644aa8e1784..87462cabc78 100644 --- a/doc/classes/Viewport.xml +++ b/doc/classes/Viewport.xml @@ -148,6 +148,12 @@ Returns the drag data from the GUI, that was previously returned by [method Control._get_drag_data]. + + + + Returns the drag data human-readable description. + + @@ -180,6 +186,13 @@ Removes the focus from the currently focused [Control] within this viewport. If no [Control] has the focus, does nothing. + + + + + Sets the drag data human-readable description. + + diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml index 4016f05628f..29e9b61c6ac 100644 --- a/doc/classes/Window.xml +++ b/doc/classes/Window.xml @@ -108,6 +108,12 @@ Returns [code]true[/code] if the [param flag] is set. + + + + Returns the focused window. + + diff --git a/main/main.cpp b/main/main.cpp index 0cea51cacf9..fdc04b86dd2 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -202,6 +202,8 @@ static int audio_driver_idx = -1; // Engine config/tools +static DisplayServer::AccessibilityMode accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_AUTO; +static bool accessibility_mode_set = false; static bool single_window = false; static bool editor = false; static bool project_manager = false; @@ -616,6 +618,7 @@ void Main::print_help(const char *p_binary) { print_help_option("--xr-mode ", "Select XR (Extended Reality) mode [\"default\", \"off\", \"on\"].\n"); #endif print_help_option("--wid ", "Request parented to window.\n"); + print_help_option("--accessibility ", "Select accessibility mode ['auto' (when screen reader is running, default), 'always', 'disabled'].\n"); print_help_title("Debug options"); print_help_option("-d, --debug", "Debug (local stdout debugger).\n"); @@ -1300,6 +1303,27 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } else if (arg == "--single-window") { // force single window single_window = true; + } else if (arg == "--accessibility") { + if (N) { + String string = N->get(); + if (string == "auto") { + accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_AUTO; + accessibility_mode_set = true; + } else if (string == "always") { + accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_ALWAYS; + accessibility_mode_set = true; + } else if (string == "disabled") { + accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_DISABLED; + accessibility_mode_set = true; + } else { + OS::get_singleton()->print("Accessibility mode argument not recognized, aborting.\n"); + goto error; + } + N = N->next(); + } else { + OS::get_singleton()->print("Missing accessibility mode argument, aborting.\n"); + goto error; + } } else if (arg == "-t" || arg == "--always-on-top") { // force always-on-top window init_always_on_top = true; @@ -2360,14 +2384,16 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph default_renderer_mobile = "gl_compatibility"; } #endif - if (renderer_hints.is_empty()) { - ERR_PRINT("No renderers available."); + if (!renderer_hints.is_empty()) { + renderer_hints += ","; } + renderer_hints += "dummy"; if (!rendering_method.is_empty()) { if (rendering_method != "forward_plus" && rendering_method != "mobile" && - rendering_method != "gl_compatibility") { + rendering_method != "gl_compatibility" && + rendering_method != "dummy") { OS::get_singleton()->print("Unknown rendering method '%s', aborting.\nValid options are ", rendering_method.utf8().get_data()); @@ -2435,7 +2461,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph // Set a default renderer if none selected. Try to choose one that matches the driver. if (rendering_method.is_empty()) { - if (rendering_driver == "opengl3" || rendering_driver == "opengl3_angle" || rendering_driver == "opengl3_es") { + if (rendering_driver == "dummy") { + rendering_method = "dummy"; + } else if (rendering_driver == "opengl3" || rendering_driver == "opengl3_angle" || rendering_driver == "opengl3_es") { rendering_method = "gl_compatibility"; } else { rendering_method = "forward_plus"; @@ -2463,6 +2491,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph available_drivers.push_back("opengl3_es"); } #endif + if (rendering_method == "dummy") { + available_drivers.push_back("dummy"); + } if (available_drivers.is_empty()) { OS::get_singleton()->print("Unknown renderer name '%s', aborting.\n", rendering_method.utf8().get_data()); goto error; @@ -2499,7 +2530,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } if (rendering_driver.is_empty()) { - if (rendering_method == "gl_compatibility") { + if (rendering_method == "dummy") { + rendering_driver = "dummy"; + } else if (rendering_method == "gl_compatibility") { rendering_driver = GLOBAL_GET("rendering/gl_compatibility/driver"); } else { rendering_driver = GLOBAL_GET("rendering/rendering_device/driver"); @@ -3095,6 +3128,11 @@ Error Main::setup2(bool p_show_boot_logo) { } #endif + if (!accessibility_mode_set) { + accessibility_mode = (DisplayServer::AccessibilityMode)GLOBAL_GET("accessibility/general/accessibility_support").operator int64_t(); + } + DisplayServer::accessibility_set_mode(accessibility_mode); + // rendering_driver now held in static global String in main and initialized in setup() Error err; display_server = DisplayServer::create(display_driver_idx, rendering_driver, window_mode, window_vsync_mode, window_flags, window_position, window_size, init_screen, context, init_embed_parent_window_id, err); @@ -4791,7 +4829,12 @@ bool Main::iteration() { return exit; } - OS::get_singleton()->add_frame_delay(DisplayServer::get_singleton()->window_can_draw()); + SceneTree *scene_tree = SceneTree::get_singleton(); + bool skip_delay = scene_tree && scene_tree->is_accessibility_enabled(); + + if (!skip_delay) { + OS::get_singleton()->add_frame_delay(DisplayServer::get_singleton()->window_can_draw()); + } #ifdef TOOLS_ENABLED if (auto_build_solutions) { diff --git a/misc/extension_api_validation/4.4-stable.expected b/misc/extension_api_validation/4.4-stable.expected index a7b4de86184..ceafe8269b5 100644 --- a/misc/extension_api_validation/4.4-stable.expected +++ b/misc/extension_api_validation/4.4-stable.expected @@ -31,3 +31,13 @@ GH-104890 Validate extension JSON: API was removed: classes/JSONRPC/methods/set_scope Replaced `set_scope` with `set_method`. Compatibility method registered for binary compatibility. Manual upgrade required by users to retain functionality. + + +GH-76829 +-------- +Validate extension JSON: Error: Field 'classes/RichTextLabel/methods/add_image/arguments': size changed value in new API, from 6 to 11. +Validate extension JSON: Error: Field 'classes/RichTextLabel/methods/add_image/arguments': size changed value in new API, from 10 to 11. +Validate extension JSON: Error: Field 'classes/RichTextLabel/methods/push_table/arguments': size changed value in new API, from 3 to 4. +Validate extension JSON: Error: Field 'classes/TreeItem/methods/add_button/arguments': size changed value in new API, from 5 to 6. + +Added optional arguments. Compatibility methods registered. diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index e229af0418f..68db69d3960 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -4210,6 +4210,8 @@ void TextServerAdvanced::invalidate(TextServerAdvanced::ShapedTextDataAdvanced * p_shaped->uthk = 0.0; p_shaped->glyphs.clear(); p_shaped->glyphs_logical.clear(); + p_shaped->runs.clear(); + p_shaped->runs_dirty = true; p_shaped->overrun_trim_data = TrimData(); p_shaped->utf16 = Char16String(); for (int i = 0; i < p_shaped->bidi_iter.size(); i++) { @@ -4492,6 +4494,213 @@ Variant TextServerAdvanced::_shaped_get_span_embedded_object(const RID &p_shaped } } +String TextServerAdvanced::_shaped_get_span_text(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + ShapedTextDataAdvanced *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, String()); + } + ERR_FAIL_INDEX_V(p_index, span_sd->spans.size(), String()); + return span_sd->text.substr(span_sd->spans[p_index].start, span_sd->spans[p_index].end - span_sd->spans[p_index].start); +} + +Variant TextServerAdvanced::_shaped_get_span_object(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, Variant()); + ShapedTextDataAdvanced *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, Variant()); + } + ERR_FAIL_INDEX_V(p_index, span_sd->spans.size(), Variant()); + return span_sd->spans[p_index].embedded_key; +} + +void TextServerAdvanced::_generate_runs(ShapedTextDataAdvanced *p_sd) const { + ERR_FAIL_NULL(p_sd); + p_sd->runs.clear(); + + ShapedTextDataAdvanced *span_sd = p_sd; + if (p_sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(p_sd->parent); + ERR_FAIL_NULL(span_sd); + } + + int sd_size = p_sd->glyphs.size(); + const Glyph *sd_gl = p_sd->glyphs.ptr(); + + int span_count = span_sd->spans.size(); + int span = -1; + int span_start = -1; + int span_end = -1; + + TextRun run; + for (int i = 0; i < sd_size; i += sd_gl[i].count) { + const Glyph &gl = sd_gl[i]; + if (gl.start < 0 || gl.end < 0) { + continue; + } + if (gl.start < span_start || gl.start >= span_end) { + span = -1; + span_start = -1; + span_end = -1; + for (int j = 0; j < span_count; j++) { + if (gl.start >= span_sd->spans[j].start && gl.end <= span_sd->spans[j].end) { + span = j; + span_start = span_sd->spans[j].start; + span_end = span_sd->spans[j].end; + break; + } + } + } + if (run.font_rid != gl.font_rid || run.font_size != gl.font_size || run.span_index != span || run.rtl != bool(gl.flags & GRAPHEME_IS_RTL)) { + if (run.span_index >= 0) { + p_sd->runs.push_back(run); + } + run.range = Vector2i(gl.start, gl.end); + run.font_rid = gl.font_rid; + run.font_size = gl.font_size; + run.rtl = bool(gl.flags & GRAPHEME_IS_RTL); + run.span_index = span; + } + run.range.x = MIN(run.range.x, gl.start); + run.range.y = MAX(run.range.y, gl.end); + } + if (run.span_index >= 0) { + p_sd->runs.push_back(run); + } + p_sd->runs_dirty = false; +} + +int64_t TextServerAdvanced::_shaped_get_run_count(const RID &p_shaped) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, 0); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + return sd->runs.size(); +} + +String TextServerAdvanced::_shaped_get_run_text(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), String()); + return sd->text.substr(sd->runs[p_index].range.x - sd->start, sd->runs[p_index].range.y - sd->runs[p_index].range.x); +} + +Vector2i TextServerAdvanced::_shaped_get_run_range(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, Vector2i()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), Vector2i()); + return sd->runs[p_index].range; +} + +RID TextServerAdvanced::_shaped_get_run_font_rid(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, RID()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), RID()); + return sd->runs[p_index].font_rid; +} + +int TextServerAdvanced::_shaped_get_run_font_size(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, 0); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), 0); + return sd->runs[p_index].font_size; +} + +String TextServerAdvanced::_shaped_get_run_language(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), String()); + + int span_idx = sd->runs[p_index].span_index; + ShapedTextDataAdvanced *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, String()); + } + ERR_FAIL_INDEX_V(span_idx, span_sd->spans.size(), String()); + return span_sd->spans[span_idx].language; +} + +TextServer::Direction TextServerAdvanced::_shaped_get_run_direction(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, TextServer::DIRECTION_LTR); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), TextServer::DIRECTION_LTR); + return sd->runs[p_index].rtl ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; +} + +Variant TextServerAdvanced::_shaped_get_run_object(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, Variant()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), Variant()); + + int span_idx = sd->runs[p_index].span_index; + ShapedTextDataAdvanced *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, Variant()); + } + ERR_FAIL_INDEX_V(span_idx, span_sd->spans.size(), Variant()); + return span_sd->spans[span_idx].embedded_key; +} + void TextServerAdvanced::_shaped_set_span_update_font(const RID &p_shaped, int64_t p_index, const TypedArray &p_fonts, int64_t p_size, const Dictionary &p_opentype_features) { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL(sd); @@ -4575,6 +4784,13 @@ bool TextServerAdvanced::_shaped_text_add_object(const RID &p_shaped, const Vari return true; } +String TextServerAdvanced::_shaped_get_text(const RID &p_shaped) const { + const ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + + return sd->text; +} + bool TextServerAdvanced::_shaped_text_resize_object(const RID &p_shaped, const Variant &p_key, const Size2 &p_size, InlineAlignment p_inline_align, double p_baseline) { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, false); @@ -4771,6 +4987,8 @@ bool TextServerAdvanced::_shape_substr(ShapedTextDataAdvanced *p_new_sd, const S p_new_sd->sort_valid = false; p_new_sd->upos = p_sd->upos; p_new_sd->uthk = p_sd->uthk; + p_new_sd->runs.clear(); + p_new_sd->runs_dirty = true; if (p_length > 0) { p_new_sd->text = p_sd->text.substr(p_start - p_sd->start, p_length); @@ -6560,6 +6778,8 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { } else { bidi_ranges = sd->bidi_override; } + sd->runs.clear(); + sd->runs_dirty = true; for (int ov = 0; ov < bidi_ranges.size(); ov++) { // Create BiDi iterator. diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index 2c2b52bc34d..deb6e675c0e 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -458,6 +458,14 @@ class TextServerAdvanced : public TextServerExtension { Vector ellipsis_glyph_buf; }; + struct TextRun { + Vector2i range; + RID font_rid; + int font_size = 0; + bool rtl = false; + int64_t span_index = -1; + }; + struct ShapedTextDataAdvanced { Mutex mutex; @@ -489,6 +497,9 @@ class TextServerAdvanced : public TextServerExtension { int first_span = 0; // First span in the parent ShapedTextData. int last_span = 0; + Vector runs; + bool runs_dirty = true; + struct EmbeddedObject { int start = -1; int end = -1; @@ -664,6 +675,7 @@ class TextServerAdvanced : public TextServerExtension { mutable HashMap system_font_data; void _update_chars(ShapedTextDataAdvanced *p_sd) const; + void _generate_runs(ShapedTextDataAdvanced *p_sd) const; void _realign(ShapedTextDataAdvanced *p_sd) const; int64_t _convert_pos(const String &p_utf32, const Char16String &p_utf16, int64_t p_pos) const; int64_t _convert_pos(const ShapedTextDataAdvanced *p_sd, int64_t p_pos) const; @@ -956,12 +968,24 @@ public: MODBIND7R(bool, shaped_text_add_string, const RID &, const String &, const TypedArray &, int64_t, const Dictionary &, const String &, const Variant &); MODBIND6R(bool, shaped_text_add_object, const RID &, const Variant &, const Size2 &, InlineAlignment, int64_t, double); MODBIND5R(bool, shaped_text_resize_object, const RID &, const Variant &, const Size2 &, InlineAlignment, double); + MODBIND1RC(String, shaped_get_text, const RID &); MODBIND1RC(int64_t, shaped_get_span_count, const RID &); MODBIND2RC(Variant, shaped_get_span_meta, const RID &, int64_t); MODBIND2RC(Variant, shaped_get_span_embedded_object, const RID &, int64_t); + MODBIND2RC(String, shaped_get_span_text, const RID &, int64_t); + MODBIND2RC(Variant, shaped_get_span_object, const RID &, int64_t); MODBIND5(shaped_set_span_update_font, const RID &, int64_t, const TypedArray &, int64_t, const Dictionary &); + MODBIND1RC(int64_t, shaped_get_run_count, const RID &); + MODBIND2RC(String, shaped_get_run_text, const RID &, int64_t); + MODBIND2RC(Vector2i, shaped_get_run_range, const RID &, int64_t); + MODBIND2RC(RID, shaped_get_run_font_rid, const RID &, int64_t); + MODBIND2RC(int, shaped_get_run_font_size, const RID &, int64_t); + MODBIND2RC(String, shaped_get_run_language, const RID &, int64_t); + MODBIND2RC(Direction, shaped_get_run_direction, const RID &, int64_t); + MODBIND2RC(Variant, shaped_get_run_object, const RID &, int64_t); + MODBIND3RC(RID, shaped_text_substr, const RID &, int64_t, int64_t); MODBIND1RC(RID, shaped_text_get_parent, const RID &); diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 7e31ae17c8c..541ad65f55b 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -3105,6 +3105,8 @@ void TextServerFallback::invalidate(ShapedTextDataFallback *p_shaped) { p_shaped->uthk = 0.0; p_shaped->glyphs.clear(); p_shaped->glyphs_logical.clear(); + p_shaped->runs.clear(); + p_shaped->runs_dirty = true; } void TextServerFallback::full_copy(ShapedTextDataFallback *p_shaped) { @@ -3339,6 +3341,212 @@ Variant TextServerFallback::_shaped_get_span_embedded_object(const RID &p_shaped } } +String TextServerFallback::_shaped_get_span_text(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + ShapedTextDataFallback *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, String()); + } + ERR_FAIL_INDEX_V(p_index, span_sd->spans.size(), String()); + return span_sd->text.substr(span_sd->spans[p_index].start, span_sd->spans[p_index].end - span_sd->spans[p_index].start); +} + +Variant TextServerFallback::_shaped_get_span_object(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, Variant()); + ShapedTextDataFallback *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, Variant()); + } + ERR_FAIL_INDEX_V(p_index, span_sd->spans.size(), Variant()); + return span_sd->spans[p_index].embedded_key; +} + +void TextServerFallback::_generate_runs(ShapedTextDataFallback *p_sd) const { + ERR_FAIL_NULL(p_sd); + p_sd->runs.clear(); + + ShapedTextDataFallback *span_sd = p_sd; + if (p_sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(p_sd->parent); + ERR_FAIL_NULL(span_sd); + } + + int sd_size = p_sd->glyphs.size(); + Glyph *sd_gl = p_sd->glyphs.ptrw(); + + int span_count = span_sd->spans.size(); + int span = -1; + int span_start = -1; + int span_end = -1; + + TextRun run; + for (int i = 0; i < sd_size; i += sd_gl[i].count) { + const Glyph &gl = sd_gl[i]; + if (gl.start < 0 || gl.end < 0) { + continue; + } + if (gl.start < span_start || gl.start >= span_end) { + span = -1; + span_start = -1; + span_end = -1; + for (int j = 0; j < span_count; j++) { + if (gl.start >= span_sd->spans[j].start && gl.end <= span_sd->spans[j].end) { + span = j; + span_start = span_sd->spans[j].start; + span_end = span_sd->spans[j].end; + break; + } + } + } + if (run.font_rid != gl.font_rid || run.font_size != gl.font_size || run.span_index != span) { + if (run.span_index >= 0) { + p_sd->runs.push_back(run); + } + run.range = Vector2i(gl.start, gl.end); + run.font_rid = gl.font_rid; + run.font_size = gl.font_size; + run.span_index = span; + } + run.range.x = MIN(run.range.x, gl.start); + run.range.y = MAX(run.range.y, gl.end); + } + if (run.span_index >= 0) { + p_sd->runs.push_back(run); + } + p_sd->runs_dirty = false; +} + +int64_t TextServerFallback::_shaped_get_run_count(const RID &p_shaped) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, 0); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + return sd->runs.size(); +} + +String TextServerFallback::_shaped_get_run_text(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), String()); + return sd->text.substr(sd->runs[p_index].range.x - sd->start, sd->runs[p_index].range.y - sd->runs[p_index].range.x); +} + +Vector2i TextServerFallback::_shaped_get_run_range(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, Vector2i()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), Vector2i()); + return sd->runs[p_index].range; +} + +RID TextServerFallback::_shaped_get_run_font_rid(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, RID()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), RID()); + return sd->runs[p_index].font_rid; +} + +int TextServerFallback::_shaped_get_run_font_size(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, 0); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), 0); + return sd->runs[p_index].font_size; +} + +String TextServerFallback::_shaped_get_run_language(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), String()); + + int span_idx = sd->runs[p_index].span_index; + ShapedTextDataFallback *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, String()); + } + ERR_FAIL_INDEX_V(span_idx, span_sd->spans.size(), String()); + return span_sd->spans[span_idx].language; +} + +TextServer::Direction TextServerFallback::_shaped_get_run_direction(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, TextServer::DIRECTION_LTR); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), TextServer::DIRECTION_LTR); + return TextServer::DIRECTION_LTR; +} + +Variant TextServerFallback::_shaped_get_run_object(const RID &p_shaped, int64_t p_index) const { + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, Variant()); + MutexLock lock(sd->mutex); + if (!sd->valid.is_set()) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + if (sd->runs_dirty) { + _generate_runs(sd); + } + ERR_FAIL_INDEX_V(p_index, sd->runs.size(), Variant()); + + int span_idx = sd->runs[p_index].span_index; + ShapedTextDataFallback *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, Variant()); + } + ERR_FAIL_INDEX_V(span_idx, span_sd->spans.size(), Variant()); + return span_sd->spans[span_idx].embedded_key; +} + void TextServerFallback::_shaped_set_span_update_font(const RID &p_shaped, int64_t p_index, const TypedArray &p_fonts, int64_t p_size, const Dictionary &p_opentype_features) { ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL(sd); @@ -3450,6 +3658,13 @@ bool TextServerFallback::_shaped_text_add_object(const RID &p_shaped, const Vari return true; } +String TextServerFallback::_shaped_get_text(const RID &p_shaped) const { + const ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, String()); + + return sd->text; +} + bool TextServerFallback::_shaped_text_resize_object(const RID &p_shaped, const Variant &p_key, const Size2 &p_size, InlineAlignment p_inline_align, double p_baseline) { ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, false); @@ -4385,6 +4600,8 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) { sd->descent = 0.0; sd->width = 0.0; sd->glyphs.clear(); + sd->runs.clear(); + sd->runs_dirty = true; if (sd->text.length() == 0) { sd->valid.set(); diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 971ae2cb7f8..28ce1789c41 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -400,6 +400,13 @@ class TextServerFallback : public TextServerExtension { Vector ellipsis_glyph_buf; }; + struct TextRun { + Vector2i range; + RID font_rid; + int font_size = 0; + int64_t span_index = -1; + }; + struct ShapedTextDataFallback { Mutex mutex; @@ -431,6 +438,9 @@ class TextServerFallback : public TextServerExtension { int first_span = 0; // First span in the parent ShapedTextData. int last_span = 0; + Vector runs; + bool runs_dirty = true; + struct EmbeddedObject { int start = -1; int end = -1; @@ -575,6 +585,7 @@ class TextServerFallback : public TextServerExtension { mutable HashMap system_fonts; mutable HashMap system_font_data; + void _generate_runs(ShapedTextDataFallback *p_sd) const; void _realign(ShapedTextDataFallback *p_sd) const; _FORCE_INLINE_ RID _find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text); @@ -820,12 +831,24 @@ public: MODBIND7R(bool, shaped_text_add_string, const RID &, const String &, const TypedArray &, int64_t, const Dictionary &, const String &, const Variant &); MODBIND6R(bool, shaped_text_add_object, const RID &, const Variant &, const Size2 &, InlineAlignment, int64_t, double); MODBIND5R(bool, shaped_text_resize_object, const RID &, const Variant &, const Size2 &, InlineAlignment, double); + MODBIND1RC(String, shaped_get_text, const RID &); MODBIND1RC(int64_t, shaped_get_span_count, const RID &); MODBIND2RC(Variant, shaped_get_span_meta, const RID &, int64_t); MODBIND2RC(Variant, shaped_get_span_embedded_object, const RID &, int64_t); + MODBIND2RC(String, shaped_get_span_text, const RID &, int64_t); + MODBIND2RC(Variant, shaped_get_span_object, const RID &, int64_t); MODBIND5(shaped_set_span_update_font, const RID &, int64_t, const TypedArray &, int64_t, const Dictionary &); + MODBIND1RC(int64_t, shaped_get_run_count, const RID &); + MODBIND2RC(String, shaped_get_run_text, const RID &, int64_t); + MODBIND2RC(Vector2i, shaped_get_run_range, const RID &, int64_t); + MODBIND2RC(RID, shaped_get_run_font_rid, const RID &, int64_t); + MODBIND2RC(int, shaped_get_run_font_size, const RID &, int64_t); + MODBIND2RC(String, shaped_get_run_language, const RID &, int64_t); + MODBIND2RC(Direction, shaped_get_run_direction, const RID &, int64_t); + MODBIND2RC(Variant, shaped_get_run_object, const RID &, int64_t); + MODBIND3RC(RID, shaped_text_substr, const RID &, int64_t, int64_t); MODBIND1RC(RID, shaped_text_get_parent, const RID &); diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index e9a8c11b2a5..bdcb2b83d0b 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -167,6 +167,17 @@ void AnimatedSprite2D::_validate_property(PropertyInfo &p_property) const { void AnimatedSprite2D::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + Rect2 dst_rect = _get_rect(); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_IMAGE); + DisplayServer::get_singleton()->accessibility_update_set_transform(ae, get_transform()); + DisplayServer::get_singleton()->accessibility_update_set_bounds(ae, dst_rect); + } break; + case NOTIFICATION_READY: { if (!Engine::get_singleton()->is_editor_hint() && frames.is_valid() && frames->has_animation(autoplay)) { play(autoplay); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 5a3cb7be0ad..73ccf953d24 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -388,11 +388,11 @@ PackedStringArray GPUParticles2D::get_configuration_warnings() const { } } - if (trail_enabled && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (trail_enabled && (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy")) { warnings.push_back(RTR("Particle trails are only available when using the Forward+ or Mobile renderers.")); } - if (sub_emitter != NodePath() && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (sub_emitter != NodePath() && (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy")) { warnings.push_back(RTR("Particle sub-emitters are not available when using the Compatibility renderer.")); } diff --git a/scene/2d/node_2d.cpp b/scene/2d/node_2d.cpp index 304e29253e6..e970656c4af 100644 --- a/scene/2d/node_2d.cpp +++ b/scene/2d/node_2d.cpp @@ -429,6 +429,13 @@ Point2 Node2D::to_global(Point2 p_local) const { void Node2D::_notification(int p_notification) { switch (p_notification) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } break; + case NOTIFICATION_ENTER_TREE: { ERR_MAIN_THREAD_GUARD; diff --git a/scene/2d/physics/touch_screen_button.cpp b/scene/2d/physics/touch_screen_button.cpp index 88fe2bc7055..93ff87d7590 100644 --- a/scene/2d/physics/touch_screen_button.cpp +++ b/scene/2d/physics/touch_screen_button.cpp @@ -112,8 +112,27 @@ bool TouchScreenButton::is_shape_centered() const { return shape_centered; } +void TouchScreenButton::_accessibility_action_click(const Variant &p_data) { + _press(0); + _release(); +} + void TouchScreenButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + Rect2 dst_rect(Point2(), texture_normal.is_valid() ? texture_normal->get_size() : Size2()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_BUTTON); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_CLICK, callable_mp(this, &TouchScreenButton::_accessibility_action_click)); + + DisplayServer::get_singleton()->accessibility_update_set_transform(ae, get_transform()); + DisplayServer::get_singleton()->accessibility_update_set_bounds(ae, dst_rect); + } break; + case NOTIFICATION_DRAW: { if (!is_inside_tree()) { return; diff --git a/scene/2d/physics/touch_screen_button.h b/scene/2d/physics/touch_screen_button.h index bee5715b651..d9474ec830f 100644 --- a/scene/2d/physics/touch_screen_button.h +++ b/scene/2d/physics/touch_screen_button.h @@ -74,6 +74,8 @@ protected: bool _set(const StringName &p_name, const Variant &p_value); #endif // DISABLE_DEPRECATED + void _accessibility_action_click(const Variant &p_data); + public: #ifdef DEBUG_ENABLED virtual Rect2 _edit_get_rect() const override; diff --git a/scene/2d/sprite_2d.cpp b/scene/2d/sprite_2d.cpp index 01b3c6cef09..4114720b77f 100644 --- a/scene/2d/sprite_2d.cpp +++ b/scene/2d/sprite_2d.cpp @@ -115,6 +115,17 @@ void Sprite2D::_get_rects(Rect2 &r_src_rect, Rect2 &r_dst_rect, bool &r_filter_c void Sprite2D::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + Rect2 dst_rect = get_rect(); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_IMAGE); + DisplayServer::get_singleton()->accessibility_update_set_transform(ae, get_transform()); + DisplayServer::get_singleton()->accessibility_update_set_bounds(ae, dst_rect); + } break; + case NOTIFICATION_DRAW: { if (texture.is_null()) { return; diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index 82d72f876d9..0e2dce74317 100644 --- a/scene/3d/decal.cpp +++ b/scene/3d/decal.cpp @@ -179,7 +179,7 @@ void Decal::_validate_property(PropertyInfo &p_property) const { PackedStringArray Decal::get_configuration_warnings() const { PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); - if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy") { warnings.push_back(RTR("Decals are only available when using the Forward+ or Mobile renderers.")); return warnings; } diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp index 4ae2ca3f8f7..4ed17df02f1 100644 --- a/scene/3d/gpu_particles_3d.cpp +++ b/scene/3d/gpu_particles_3d.cpp @@ -421,12 +421,12 @@ PackedStringArray GPUParticles3D::get_configuration_warnings() const { if ((dp_count || skin.is_valid()) && (missing_trails || no_materials)) { warnings.push_back(RTR("Trails enabled, but one or more mesh materials are either missing or not set for trails rendering.")); } - if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy") { warnings.push_back(RTR("Particle trails are only available when using the Forward+ or Mobile renderers.")); } } - if (sub_emitter != NodePath() && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (sub_emitter != NodePath() && (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy")) { warnings.push_back(RTR("Particle sub-emitters are only available when using the Forward+ or Mobile renderers.")); } diff --git a/scene/3d/light_3d.cpp b/scene/3d/light_3d.cpp index f4e7815e03f..51826f7597a 100644 --- a/scene/3d/light_3d.cpp +++ b/scene/3d/light_3d.cpp @@ -632,7 +632,7 @@ PackedStringArray OmniLight3D::get_configuration_warnings() const { warnings.push_back(RTR("Projector texture only works with shadows active.")); } - if (get_projector().is_valid() && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (get_projector().is_valid() && (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy")) { warnings.push_back(RTR("Projector textures are not supported when using the Compatibility renderer yet. Support will be added in a future release.")); } @@ -668,7 +668,7 @@ PackedStringArray SpotLight3D::get_configuration_warnings() const { warnings.push_back(RTR("Projector texture only works with shadows active.")); } - if (get_projector().is_valid() && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { + if (get_projector().is_valid() && (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "dummy")) { warnings.push_back(RTR("Projector textures are not supported when using the Compatibility renderer yet. Support will be added in a future release.")); } diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp index 1efa1d9457b..4d8037b1406 100644 --- a/scene/3d/node_3d.cpp +++ b/scene/3d/node_3d.cpp @@ -131,6 +131,13 @@ void Node3D::_propagate_transform_changed(Node3D *p_origin) { void Node3D::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } break; + case NOTIFICATION_ENTER_TREE: { ERR_MAIN_THREAD_GUARD; ERR_FAIL_NULL(get_tree()); diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp index 25f5d13f0ce..232df7ce858 100644 --- a/scene/3d/voxel_gi.cpp +++ b/scene/3d/voxel_gi.cpp @@ -542,6 +542,8 @@ PackedStringArray VoxelGI::get_configuration_warnings() const { if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { warnings.push_back(RTR("VoxelGI nodes are not supported when using the Compatibility renderer yet. Support will be added in a future release.")); + } else if (OS::get_singleton()->get_current_rendering_method() == "dummy") { + warnings.push_back(RTR("VoxelGI nodes are not supported when using the Dummy renderer.")); } else if (probe_data.is_null()) { warnings.push_back(RTR("No VoxelGI data set, so this node is disabled. Bake static objects to enable GI.")); } diff --git a/scene/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp index 4fc0f23d5db..6fbfd25451b 100644 --- a/scene/audio/audio_stream_player.cpp +++ b/scene/audio/audio_stream_player.cpp @@ -35,7 +35,14 @@ #include "servers/audio/audio_stream.h" void AudioStreamPlayer::_notification(int p_what) { - internal->notification(p_what); + if (p_what == NOTIFICATION_ACCESSIBILITY_UPDATE) { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_AUDIO); + } else { + internal->notification(p_what); + } } void AudioStreamPlayer::set_stream(Ref p_stream) { diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index 1502579a6a3..9ec61494efa 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -41,6 +41,7 @@ void BaseButton::_unpress_group() { if (toggle_mode && !button_group->is_allow_unpress()) { status.pressed = true; + queue_accessibility_update(); } for (BaseButton *E : button_group->buttons) { @@ -83,15 +84,66 @@ void BaseButton::gui_input(const Ref &p_event) { } } +void BaseButton::_accessibility_action_click(const Variant &p_data) { + if (toggle_mode) { + status.pressed = !status.pressed; + + if (status.pressed) { + _unpress_group(); + if (button_group.is_valid()) { + button_group->emit_signal(SceneStringName(pressed), this); + } + } + + _toggled(status.pressed); + _pressed(); + } else { + _pressed(); + } + queue_accessibility_update(); + queue_redraw(); +} + void BaseButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_BUTTON); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_CLICK, callable_mp(this, &BaseButton::_accessibility_action_click)); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_DISABLED, status.disabled); + if (toggle_mode) { + DisplayServer::get_singleton()->accessibility_update_set_checked(ae, status.pressed); + } + if (button_group.is_valid()) { + for (const BaseButton *btn : button_group->buttons) { + if (btn->is_part_of_edited_scene()) { + continue; + } + DisplayServer::get_singleton()->accessibility_update_add_related_radio_group(ae, btn->get_accessibility_element()); + } + } + if (shortcut_in_tooltip && shortcut.is_valid() && shortcut->has_valid_event()) { + String text = atr(shortcut->get_name()) + " (" + shortcut->get_as_text() + ")"; + String tooltip = get_tooltip_text(); + if (!tooltip.is_empty() && shortcut->get_name().nocasecmp_to(tooltip) != 0) { + text += "\n" + atr(tooltip); + } + DisplayServer::get_singleton()->accessibility_update_set_tooltip(ae, text); + } + } break; + case NOTIFICATION_MOUSE_ENTER: { status.hovering = true; + queue_accessibility_update(); queue_redraw(); } break; case NOTIFICATION_MOUSE_EXIT: { status.hovering = false; + queue_accessibility_update(); queue_redraw(); } break; @@ -175,6 +227,7 @@ void BaseButton::on_action_event(Ref p_event) { } _toggled(status.pressed); _pressed(); + queue_accessibility_update(); } } else { if ((p_event->is_pressed() && action_mode == ACTION_MODE_BUTTON_PRESS) || (!p_event->is_pressed() && action_mode == ACTION_MODE_BUTTON_RELEASE)) { @@ -214,6 +267,7 @@ void BaseButton::set_disabled(bool p_disabled) { status.press_attempt = false; status.pressing_inside = false; } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -247,7 +301,7 @@ void BaseButton::set_pressed_no_signal(bool p_pressed) { return; } status.pressed = p_pressed; - + queue_accessibility_update(); queue_redraw(); } @@ -303,6 +357,7 @@ void BaseButton::set_toggle_mode(bool p_on) { if (!p_on) { set_pressed(false); } + queue_accessibility_update(); toggle_mode = p_on; update_configuration_warnings(); @@ -313,7 +368,10 @@ bool BaseButton::is_toggle_mode() const { } void BaseButton::set_shortcut_in_tooltip(bool p_on) { - shortcut_in_tooltip = p_on; + if (shortcut_in_tooltip != p_on) { + shortcut_in_tooltip = p_on; + queue_accessibility_update(); + } } bool BaseButton::is_shortcut_in_tooltip_enabled() const { @@ -353,8 +411,11 @@ bool BaseButton::is_shortcut_feedback() const { } void BaseButton::set_shortcut(const Ref &p_shortcut) { - shortcut = p_shortcut; - set_process_shortcut_input(shortcut.is_valid()); + if (shortcut != p_shortcut) { + shortcut = p_shortcut; + set_process_shortcut_input(shortcut.is_valid()); + queue_accessibility_update(); + } } Ref BaseButton::get_shortcut() const { @@ -380,7 +441,7 @@ void BaseButton::shortcut_input(const Ref &p_event) { _toggled(status.pressed); _pressed(); - + queue_accessibility_update(); } else { _pressed(); } @@ -440,6 +501,7 @@ void BaseButton::set_button_group(const Ref &p_group) { button_group->buttons.insert(this); } + queue_accessibility_update(); queue_redraw(); //checkbox changes to radio if set a buttongroup update_configuration_warnings(); } diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 76dba0badfe..60e5a8ab794 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -86,6 +86,7 @@ protected: void _notification(int p_what); bool _was_pressed_by_mouse() const; + void _accessibility_action_click(const Variant &p_data); GDVIRTUAL0(_pressed) GDVIRTUAL1(_toggled, bool) diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index 958e1f3a270..4c090cb1f60 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -30,6 +30,8 @@ #include "button.h" +#include "scene/gui/dialogs.h" + #include "scene/theme/theme_db.h" Size2 Button::get_minimum_size() const { @@ -185,6 +187,21 @@ Ref Button::_get_current_stylebox() const { void Button::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + if (!xl_text.is_empty() && get_accessibility_name().is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_name(ae, xl_text); + } else if (!xl_text.is_empty() && !get_accessibility_name().is_empty() && get_accessibility_name() != xl_text) { + DisplayServer::get_singleton()->accessibility_update_set_name(ae, get_accessibility_name() + ": " + xl_text); + } + AcceptDialog *dlg = Object::cast_to(get_parent()); + if (dlg && dlg->get_ok_button() == this) { + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_DEFAULT_BUTTON); + } + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { queue_redraw(); } break; @@ -194,6 +211,7 @@ void Button::_notification(int p_what) { _shape(); update_minimum_size(); + queue_accessibility_update(); queue_redraw(); } break; @@ -610,6 +628,7 @@ void Button::set_text(const String &p_text) { xl_text = translated_text; _shape(); + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -649,6 +668,7 @@ void Button::set_text_direction(Control::TextDirection p_text_direction) { if (text_direction != p_text_direction) { text_direction = p_text_direction; _shape(); + queue_accessibility_update(); queue_redraw(); } } @@ -661,6 +681,7 @@ void Button::set_language(const String &p_language) { if (language != p_language) { language = p_language; _shape(); + queue_accessibility_update(); queue_redraw(); } } @@ -738,6 +759,7 @@ bool Button::get_clip_text() const { void Button::set_text_alignment(HorizontalAlignment p_alignment) { if (alignment != p_alignment) { alignment = p_alignment; + queue_accessibility_update(); queue_redraw(); } } diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index 475dd1f4652..03dab7448be 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -81,6 +81,17 @@ Size2 CheckBox::get_minimum_size() const { void CheckBox::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + if (is_radio()) { + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_RADIO_BUTTON); + } else { + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CHECK_BOX); + } + } break; + case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { @@ -134,7 +145,7 @@ void CheckBox::_notification(int p_what) { } } -bool CheckBox::is_radio() { +bool CheckBox::is_radio() const { return get_button_group().is_valid(); } diff --git a/scene/gui/check_box.h b/scene/gui/check_box.h index 62f6aa85322..fcbacfcb18b 100644 --- a/scene/gui/check_box.h +++ b/scene/gui/check_box.h @@ -60,7 +60,7 @@ protected: void _notification(int p_what); static void _bind_methods(); - bool is_radio(); + bool is_radio() const; public: CheckBox(const String &p_text = String()); diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index 78daaf2629e..d90f12e48e7 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -85,6 +85,13 @@ Size2 CheckButton::get_minimum_size() const { void CheckButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CHECK_BUTTON); + } break; + case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index 91195b42963..162d1409e05 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -56,20 +56,31 @@ void ColorPicker::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_COLOR_PICKER); + DisplayServer::get_singleton()->accessibility_update_set_color_value(ae, color); + } break; + case NOTIFICATION_ENTER_TREE: { _update_color(); } break; case NOTIFICATION_READY: { - // FIXME: The embedding check is needed to fix a bug in single-window mode (GH-93718). if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_COLOR_PICKER)) { + btn_pick->set_accessibility_name(ETR("Pick Color From Screen")); btn_pick->set_tooltip_text(ETR("Pick a color from the screen.")); btn_pick->connect(SceneStringName(pressed), callable_mp(this, &ColorPicker::_pick_button_pressed_native)); } else if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_SCREEN_CAPTURE) && !get_tree()->get_root()->is_embedding_subwindows()) { + // FIXME: The embedding check is needed to fix a bug in single-window mode (GH-93718). + btn_pick->set_accessibility_name(ETR("Pick Color From Screen")); btn_pick->set_tooltip_text(ETR("Pick a color from the screen.")); btn_pick->connect(SceneStringName(pressed), callable_mp(this, &ColorPicker::_pick_button_pressed)); } else { // On unsupported platforms, use a legacy method for color picking. + btn_pick->set_accessibility_name(ETR("Pick Color From Window")); btn_pick->set_tooltip_text(ETR("Pick a color from the application window.")); btn_pick->connect(SceneStringName(pressed), callable_mp(this, &ColorPicker::_pick_button_pressed_legacy)); } @@ -338,8 +349,12 @@ void ColorPicker::_update_controls() { for (int i = 0; i < current_slider_count; i++) { labels[i]->set_text(modes[current_mode]->get_slider_label(i)); + sliders[i]->set_accessibility_name(modes[current_mode]->get_slider_label(i)); + values[i]->set_accessibility_name(modes[current_mode]->get_slider_label(i)); } alpha_label->set_text("A"); + alpha_slider->set_accessibility_name(ETR("Alpha")); + alpha_value->set_accessibility_name(ETR("Alpha")); slider_theme_modified = modes[current_mode]->apply_theme(); @@ -726,6 +741,7 @@ void ColorPicker::_update_color(bool p_update_sliders) { } alpha_slider->queue_redraw(); updating = false; + queue_accessibility_update(); } void ColorPicker::_update_presets() { @@ -862,6 +878,7 @@ inline int ColorPicker::_get_preset_size() { void ColorPicker::_add_preset_button(int p_size, const Color &p_color) { ColorPresetButton *btn_preset_new = memnew(ColorPresetButton(p_color, p_size)); btn_preset_new->set_tooltip_text(vformat(atr(ETR("Color: #%s\nLMB: Apply color\nRMB: Remove preset")), p_color.to_html(p_color.a < 1))); + btn_preset_new->set_accessibility_name(vformat(atr(ETR("Color: #%")), p_color.to_html(p_color.a < 1))); SET_DRAG_FORWARDING_GCDU(btn_preset_new, ColorPicker); btn_preset_new->set_button_group(preset_group); preset_container->add_child(btn_preset_new); @@ -872,6 +889,7 @@ void ColorPicker::_add_preset_button(int p_size, const Color &p_color) { void ColorPicker::_add_recent_preset_button(int p_size, const Color &p_color) { ColorPresetButton *btn_preset_new = memnew(ColorPresetButton(p_color, p_size)); btn_preset_new->set_tooltip_text(vformat(atr(ETR("Color: #%s\nLMB: Apply color")), p_color.to_html(p_color.a < 1))); + btn_preset_new->set_accessibility_name(vformat(atr(ETR("Color: #%s")), p_color.to_html(p_color.a < 1))); btn_preset_new->set_button_group(recent_preset_group); recent_preset_hbc->add_child(btn_preset_new); recent_preset_hbc->move_child(btn_preset_new, 0); @@ -2000,6 +2018,7 @@ ColorPicker::ColorPicker() { btn_pick = memnew(Button); btn_pick->set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER); + btn_pick->set_accessibility_name(ETR("Pick")); sample_hbc->add_child(btn_pick); sample = memnew(TextureRect); @@ -2012,6 +2031,7 @@ ColorPicker::ColorPicker() { btn_shape->set_flat(false); sample_hbc->add_child(btn_shape); btn_shape->set_toggle_mode(true); + btn_shape->set_accessibility_name(ETR("Picker Shape")); btn_shape->set_tooltip_text(ETR("Select a picker shape.")); btn_shape->set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER); btn_shape->set_focus_mode(FOCUS_ALL); @@ -2061,6 +2081,7 @@ ColorPicker::ColorPicker() { btn_mode->set_flat(false); mode_hbc->add_child(btn_mode); btn_mode->set_toggle_mode(true); + btn_mode->set_accessibility_name(ETR("Picker Mode")); btn_mode->set_tooltip_text(ETR("Select a picker mode.")); btn_mode->set_focus_mode(FOCUS_ALL); @@ -2101,7 +2122,8 @@ ColorPicker::ColorPicker() { text_type = memnew(Button); hex_hbc->add_child(text_type); text_type->set_text("#"); - text_type->set_tooltip_text(RTR("Switch between hexadecimal and code values.")); + text_type->set_accessibility_name(ETR("Hexadecimal/Code Values")); + text_type->set_tooltip_text(ETR("Switch between hexadecimal and code values.")); if (Engine::get_singleton()->is_editor_hint()) { text_type->connect(SceneStringName(pressed), callable_mp(this, &ColorPicker::_text_type_toggled)); } else { @@ -2114,6 +2136,7 @@ ColorPicker::ColorPicker() { hex_hbc->add_child(c_text); c_text->set_h_size_flags(SIZE_EXPAND_FILL); c_text->set_select_all_on_focus(true); + c_text->set_accessibility_name(ETR("Hex Code or Name")); c_text->set_tooltip_text(ETR("Enter a hex code (\"#ff0000\") or named color (\"red\").")); c_text->set_placeholder(ETR("Hex code or named color")); c_text->connect(SceneStringName(text_submitted), callable_mp(this, &ColorPicker::_html_submitted)); @@ -2151,6 +2174,7 @@ ColorPicker::ColorPicker() { menu_btn->set_flat(true); menu_btn->set_focus_mode(FOCUS_ALL); menu_btn->set_tooltip_text(ETR("Show all options available.")); + menu_btn->set_accessibility_name(ETR("All Options")); menu_btn->connect("about_to_popup", callable_mp(this, &ColorPicker::_update_menu_items)); palette_box->add_child(menu_btn); @@ -2187,6 +2211,7 @@ ColorPicker::ColorPicker() { btn_add_preset = memnew(Button); btn_add_preset->set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER); btn_add_preset->set_tooltip_text(ETR("Add current color as a preset.")); + btn_add_preset->set_accessibility_name(ETR("Add Preset")); btn_add_preset->connect(SceneStringName(pressed), callable_mp(this, &ColorPicker::_add_preset_pressed)); preset_container->add_child(btn_add_preset); } @@ -2223,6 +2248,7 @@ void ColorPickerButton::_about_to_popup() { void ColorPickerButton::_color_changed(const Color &p_color) { color = p_color; + queue_accessibility_update(); queue_redraw(); emit_signal(SNAME("color_changed"), color); } @@ -2269,6 +2295,15 @@ void ColorPickerButton::pressed() { void ColorPickerButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_BUTTON); + DisplayServer::get_singleton()->accessibility_update_set_popup_type(ae, DisplayServer::AccessibilityPopupType::POPUP_DIALOG); + DisplayServer::get_singleton()->accessibility_update_set_color_value(ae, color); + } break; + case NOTIFICATION_DRAW: { const Rect2 r = Rect2(theme_cache.normal_style->get_offset(), get_size() - theme_cache.normal_style->get_minimum_size()); draw_texture_rect(theme_cache.background_icon, r, true); @@ -2302,7 +2337,7 @@ void ColorPickerButton::set_pick_color(const Color &p_color) { if (picker) { picker->set_pick_color(p_color); } - + queue_accessibility_update(); queue_redraw(); } @@ -2382,6 +2417,14 @@ ColorPickerButton::ColorPickerButton(const String &p_text) : void ColorPresetButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_BUTTON); + DisplayServer::get_singleton()->accessibility_update_set_color_value(ae, preset_color); + } break; + case NOTIFICATION_DRAW: { const Rect2 r = Rect2(Point2(0, 0), get_size()); Ref sb_raw = theme_cache.foreground_style->duplicate(); @@ -2439,6 +2482,7 @@ void ColorPresetButton::_notification(int p_what) { void ColorPresetButton::set_preset_color(const Color &p_color) { preset_color = p_color; + queue_accessibility_update(); } Color ColorPresetButton::get_preset_color() const { diff --git a/scene/gui/color_rect.cpp b/scene/gui/color_rect.cpp index fc334325b67..141581c22d6 100644 --- a/scene/gui/color_rect.cpp +++ b/scene/gui/color_rect.cpp @@ -35,6 +35,7 @@ void ColorRect::set_color(const Color &p_color) { return; } color = p_color; + queue_accessibility_update(); queue_redraw(); } @@ -44,6 +45,13 @@ Color ColorRect::get_color() const { void ColorRect::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_color_value(ae, color); + } break; + case NOTIFICATION_DRAW: { draw_rect(Rect2(Point2(), get_size()), color); } break; diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index eb69b2dfb8b..303ceb8d3a2 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -184,6 +184,13 @@ Vector Container::get_allowed_size_flags_vertical() const { void Container::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } break; + case NOTIFICATION_RESIZED: case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 6f004dae7b7..c8bbb3db4f2 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -32,8 +32,12 @@ #include "container.h" #include "core/config/project_settings.h" +#include "core/input/input_map.h" +#include "core/math/geometry_2d.h" #include "core/os/os.h" #include "core/string/translation_server.h" +#include "scene/gui/label.h" +#include "scene/gui/panel.h" #include "scene/gui/scroll_container.h" #include "scene/main/canvas_layer.h" #include "scene/main/window.h" @@ -246,6 +250,27 @@ PackedStringArray Control::get_configuration_warnings() const { return warnings; } +PackedStringArray Control::get_accessibility_configuration_warnings() const { + ERR_READ_THREAD_GUARD_V(PackedStringArray()); + PackedStringArray warnings = Node::get_accessibility_configuration_warnings(); + + String ac_name = get_accessibility_name().strip_edges(); + if (ac_name.is_empty()) { + warnings.push_back(RTR("Accessibility Name must not be empty, or contain only spaces.")); + } + if (ac_name.contains(get_class_name())) { + warnings.push_back(RTR("Accessibility Name must not include Node class name.")); + } + for (int i = 0; i < ac_name.length(); i++) { + if (is_control(ac_name[i])) { + warnings.push_back(RTR("Accessibility Name must not include control character.")); + break; + } + } + + return warnings; +} + bool Control::is_text_field() const { ERR_READ_THREAD_GUARD_V(false); return false; @@ -1534,6 +1559,7 @@ void Control::set_scale(const Vector2 &p_scale) { } queue_redraw(); _notify_transform(); + queue_accessibility_update(); } Vector2 Control::get_scale() const { @@ -1550,6 +1576,7 @@ void Control::set_rotation(real_t p_radians) { data.rotation = p_radians; queue_redraw(); _notify_transform(); + queue_accessibility_update(); } void Control::set_rotation_degrees(real_t p_degrees) { @@ -1576,6 +1603,7 @@ void Control::set_pivot_offset(const Vector2 &p_pivot) { data.pivot_offset = p_pivot; queue_redraw(); _notify_transform(); + queue_accessibility_update(); } Vector2 Control::get_pivot_offset() const { @@ -1749,6 +1777,8 @@ void Control::_size_changed() { if (pos_changed && !size_changed) { _update_canvas_item_transform(); } + + queue_accessibility_update(); } else if (pos_changed) { _notify_transform(); } @@ -2018,7 +2048,40 @@ void Control::force_drag(const Variant &p_data, Control *p_control) { ERR_FAIL_COND(!is_inside_tree()); ERR_FAIL_COND(p_data.get_type() == Variant::NIL); - get_viewport()->_gui_force_drag(this, p_data, p_control); + Viewport *vp = get_viewport(); + + vp->_gui_force_drag_start(); + vp->_gui_force_drag(this, p_data, p_control); +} + +void Control::accessibility_drag() { + ERR_MAIN_THREAD_GUARD; + ERR_FAIL_COND(!is_inside_tree()); + + Viewport *vp = get_viewport(); + + vp->_gui_force_drag_start(); + Variant dnd_data = get_drag_data(Vector2(INFINITY, INFINITY)); + if (dnd_data.get_type() != Variant::NIL) { + Window *w = Window::get_from_id(get_window()->get_window_id()); + if (w) { + w->accessibility_announcement(vformat(RTR("%s grabbed. Select target and use %s to drop, use %s to cancel."), vp->gui_get_drag_description(), InputMap::get_singleton()->get_action_description("ui_accessibility_drag_and_drop"), InputMap::get_singleton()->get_action_description("ui_cancel"))); + } + vp->_gui_force_drag(this, dnd_data, nullptr); + queue_accessibility_update(); + } else { + vp->_gui_force_drag_cancel(); + } +} + +void Control::accessibility_drop() { + ERR_MAIN_THREAD_GUARD; + ERR_FAIL_COND(!is_inside_tree()); + ERR_FAIL_COND(!get_viewport()->gui_is_dragging()); + + get_viewport()->gui_perform_drop_at(Vector2(INFINITY, INFINITY), this); + + queue_accessibility_update(); } void Control::set_drag_preview(Control *p_control) { @@ -2037,7 +2100,7 @@ bool Control::is_drag_successful() const { void Control::set_focus_mode(FocusMode p_focus_mode) { ERR_MAIN_THREAD_GUARD; - ERR_FAIL_INDEX((int)p_focus_mode, 3); + ERR_FAIL_INDEX((int)p_focus_mode, 4); if (is_inside_tree() && p_focus_mode == FOCUS_NONE && data.focus_mode != FOCUS_NONE && has_focus()) { release_focus(); @@ -2163,6 +2226,10 @@ Control *Control::find_next_valid_focus() const { } Control *from = const_cast(this); + bool ac_enabled = get_tree() && get_tree()->is_accessibility_enabled(); + + // Index of the current `Control` subtree within the containing `Window`. + int window_next = -1; while (true) { // Find next child. @@ -2194,6 +2261,25 @@ Control *Control::find_next_valid_focus() const { } next_child = next_child->data.parent_control; } + + Window *win = next_child == nullptr ? nullptr : next_child->data.parent_window; + if (win) { // Cycle through `Control` subtrees of the parent window + if (window_next == -1) { + window_next = next_child->get_index(); + ERR_FAIL_INDEX_V(window_next, win->get_child_count(), nullptr); + } + + for (int i = 1; i < win->get_child_count() + 1; i++) { + int next = Math::wrapi(window_next + i, 0, win->get_child_count()); + Control *c = Object::cast_to(win->get_child(next)); + if (!c || !c->is_visible_in_tree() || c->is_set_as_top_level()) { + continue; + } + window_next = next; + next_child = c; + break; + } + } } } @@ -2201,7 +2287,7 @@ Control *Control::find_next_valid_focus() const { break; } - if (next_child->get_focus_mode_with_recursive() == FOCUS_ALL) { + if ((next_child->get_focus_mode_with_recursive() == FOCUS_ALL) || (ac_enabled && next_child->get_focus_mode_with_recursive() == FOCUS_ACCESSIBILITY)) { return next_child; } @@ -2244,6 +2330,10 @@ Control *Control::find_prev_valid_focus() const { } Control *from = const_cast(this); + bool ac_enabled = get_tree() && get_tree()->is_accessibility_enabled(); + + // Index of the current `Control` subtree within the containing `Window`. + int window_prev = -1; while (true) { // Find prev child. @@ -2253,7 +2343,28 @@ Control *Control::find_prev_valid_focus() const { if (from->is_set_as_top_level() || !from->data.parent_control) { // Find last of the children. - prev_child = _prev_control(from); // Wrap start here. + Window *win = from->data.parent_window; + if (win) { // Cycle through `Control` subtrees of the parent window + if (window_prev == -1) { + window_prev = from->get_index(); + ERR_FAIL_INDEX_V(window_prev, win->get_child_count(), nullptr); + } + + for (int i = 1; i < win->get_child_count() + 1; i++) { + int prev = Math::wrapi(window_prev - i, 0, win->get_child_count()); + Control *c = Object::cast_to(win->get_child(prev)); + if (!c || !c->is_visible_in_tree() || c->is_set_as_top_level()) { + continue; + } + window_prev = prev; + prev_child = _prev_control(c); + break; + } + } + + if (!prev_child) { + prev_child = _prev_control(from); // Wrap start here. + } } else { for (int i = (from->get_index() - 1); i >= 0; i--) { @@ -2274,7 +2385,7 @@ Control *Control::find_prev_valid_focus() const { } } - if (prev_child->get_focus_mode_with_recursive() == FOCUS_ALL) { + if ((prev_child->get_focus_mode_with_recursive() == FOCUS_ALL) || (ac_enabled && prev_child->get_focus_mode_with_recursive() == FOCUS_ACCESSIBILITY)) { return prev_child; } @@ -2509,11 +2620,13 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons return; // Bye. } + bool ac_enabled = get_tree() && get_tree()->is_accessibility_enabled(); + Control *c = Object::cast_to(p_at); Container *container = Object::cast_to(p_at); bool in_container = container ? container->is_ancestor_of(this) : false; - if (c && c != this && c->get_focus_mode_with_recursive() == FOCUS_ALL && !in_container && p_clamp.intersects(c->get_global_rect())) { + if (c && c != this && ((c->get_focus_mode_with_recursive() == FOCUS_ALL) || (ac_enabled && c->get_focus_mode_with_recursive() == FOCUS_ACCESSIBILITY)) && !in_container && p_clamp.intersects(c->get_global_rect())) { Rect2 r_c = c->get_global_rect(); r_c = r_c.intersection(p_clamp); real_t begin_d = p_dir.dot(r_c.get_position()); @@ -3419,6 +3532,13 @@ String Control::get_tooltip(const Point2 &p_pos) const { return data.tooltip; } +String Control::accessibility_get_contextual_info() const { + ERR_READ_THREAD_GUARD_V(String()); + String ret; + GDVIRTUAL_CALL(_accessibility_get_contextual_info, ret); + return ret; +} + Control *Control::make_custom_tooltip(const String &p_text) const { ERR_READ_THREAD_GUARD_V(nullptr); Object *ret = nullptr; @@ -3428,6 +3548,35 @@ Control *Control::make_custom_tooltip(const String &p_text) const { // Base object overrides. +void Control::_accessibility_action_foucs(const Variant &p_data) { + grab_focus(); +} + +void Control::_accessibility_action_blur(const Variant &p_data) { + release_focus(); +} + +void Control::_accessibility_action_show_tooltip(const Variant &p_data) { + Viewport *vp = get_viewport(); + if (vp) { + vp->show_tooltip(this); + } +} + +void Control::_accessibility_action_hide_tooltip(const Variant &p_data) { + Viewport *vp = get_viewport(); + if (vp) { + vp->cancel_tooltip(); + } +} + +void Control::_accessibility_action_scroll_into_view(const Variant &p_data) { + ScrollContainer *sc = Object::cast_to(get_parent()); + if (sc) { + sc->ensure_control_visible(this); + } +} + void Control::_notification(int p_notification) { ERR_MAIN_THREAD_GUARD; switch (p_notification) { @@ -3439,6 +3588,31 @@ void Control::_notification(int p_notification) { saving = false; } break; #endif // TOOLS_ENABLED + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_transform(ae, get_transform()); + DisplayServer::get_singleton()->accessibility_update_set_bounds(ae, Rect2(Vector2(), data.size_cache)); + DisplayServer::get_singleton()->accessibility_update_set_tooltip(ae, data.tooltip); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_CLIPS_CHILDREN, data.clip_contents); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_TOUCH_PASSTHROUGH, data.mouse_filter == MOUSE_FILTER_PASS); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &Control::_accessibility_action_foucs)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &Control::_accessibility_action_blur)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SHOW_TOOLTIP, callable_mp(this, &Control::_accessibility_action_show_tooltip)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_HIDE_TOOLTIP, callable_mp(this, &Control::_accessibility_action_hide_tooltip)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &Control::_accessibility_action_scroll_into_view)); + if (is_inside_tree() && get_viewport()->gui_is_dragging()) { + if (can_drop_data(Vector2(INFINITY, INFINITY), get_viewport()->gui_get_drag_data())) { + DisplayServer::get_singleton()->accessibility_update_set_extra_info(ae, vformat(RTR("%s can be dropped here. Use %s to drop, use %s to cancel."), get_viewport()->gui_get_drag_description(), InputMap::get_singleton()->get_action_description("ui_accessibility_drag_and_drop"), InputMap::get_singleton()->get_action_description("ui_cancel"))); + } else { + DisplayServer::get_singleton()->accessibility_update_set_extra_info(ae, vformat(RTR("%s can not be dropped here. Use %s to cancel."), get_viewport()->gui_get_drag_description(), InputMap::get_singleton()->get_action_description("ui_cancel"))); + } + } + } break; + case NOTIFICATION_POSTINITIALIZE: { data.initialized = true; @@ -3773,6 +3947,9 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("force_drag", "data", "preview"), &Control::force_drag); + ClassDB::bind_method(D_METHOD("accessibility_drag"), &Control::accessibility_drag); + ClassDB::bind_method(D_METHOD("accessibility_drop"), &Control::accessibility_drop); + ClassDB::bind_method(D_METHOD("set_mouse_filter", "filter"), &Control::set_mouse_filter); ClassDB::bind_method(D_METHOD("get_mouse_filter"), &Control::get_mouse_filter); ClassDB::bind_method(D_METHOD("get_mouse_filter_with_recursive"), &Control::get_mouse_filter_with_recursive); @@ -3874,7 +4051,7 @@ void Control::_bind_methods() { ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_bottom", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_BOTTOM); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_next", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_next", "get_focus_next"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_previous", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_previous", "get_focus_previous"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_mode", PROPERTY_HINT_ENUM, "None,Click,All"), "set_focus_mode", "get_focus_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_mode", PROPERTY_HINT_ENUM, "None,Click,All,Accessibility"), "set_focus_mode", "get_focus_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_recursive_behavior", PROPERTY_HINT_ENUM, "Inherited,Disabled,Enabled"), "set_focus_recursive_behavior", "get_focus_recursive_behavior"); ADD_GROUP("Mouse", "mouse_"); @@ -3893,6 +4070,7 @@ void Control::_bind_methods() { BIND_ENUM_CONSTANT(FOCUS_NONE); BIND_ENUM_CONSTANT(FOCUS_CLICK); BIND_ENUM_CONSTANT(FOCUS_ALL); + BIND_ENUM_CONSTANT(FOCUS_ACCESSIBILITY); BIND_ENUM_CONSTANT(RECURSIVE_BEHAVIOR_INHERITED); BIND_ENUM_CONSTANT(RECURSIVE_BEHAVIOR_DISABLED); @@ -4003,6 +4181,8 @@ void Control::_bind_methods() { GDVIRTUAL_BIND(_drop_data, "at_position", "data"); GDVIRTUAL_BIND(_make_custom_tooltip, "for_text"); + GDVIRTUAL_BIND(_accessibility_get_contextual_info); + GDVIRTUAL_BIND(_gui_input, "event"); } diff --git a/scene/gui/control.h b/scene/gui/control.h index b2001fdd259..1e35b064a68 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -64,7 +64,8 @@ public: enum FocusMode { FOCUS_NONE, FOCUS_CLICK, - FOCUS_ALL + FOCUS_ALL, + FOCUS_ACCESSIBILITY, }; enum RecursiveBehavior { @@ -345,8 +346,6 @@ private: static int root_layout_direction; - String get_tooltip_text() const; - protected: // Dynamic properties. @@ -371,6 +370,12 @@ protected: void _notification(int p_notification); static void _bind_methods(); + void _accessibility_action_foucs(const Variant &p_data); + void _accessibility_action_blur(const Variant &p_data); + void _accessibility_action_show_tooltip(const Variant &p_data); + void _accessibility_action_hide_tooltip(const Variant &p_data); + void _accessibility_action_scroll_into_view(const Variant &p_data); + // Exposed virtual methods. GDVIRTUAL1RC(bool, _has_point, Vector2) @@ -383,6 +388,8 @@ protected: GDVIRTUAL2(_drop_data, Vector2, Variant) GDVIRTUAL1RC(Object *, _make_custom_tooltip, String) + GDVIRTUAL0RC(String, _accessibility_get_contextual_info); + GDVIRTUAL1(_gui_input, Ref) public: @@ -438,6 +445,7 @@ public: static void set_root_layout_direction(int p_root_dir); PackedStringArray get_configuration_warnings() const override; + PackedStringArray get_accessibility_configuration_warnings() const override; #ifdef TOOLS_ENABLED virtual void get_argument_options(const StringName &p_function, int p_idx, List *r_options) const override; #endif //TOOLS_ENABLED @@ -556,6 +564,8 @@ public: virtual void drop_data(const Point2 &p_point, const Variant &p_data); void set_drag_preview(Control *p_control); void force_drag(const Variant &p_data, Control *p_control); + void accessibility_drag(); + void accessibility_drop(); bool is_drag_successful() const; // Focus. @@ -674,10 +684,13 @@ public: // Extra properties. + String get_tooltip_text() const; void set_tooltip_text(const String &text); virtual String get_tooltip(const Point2 &p_pos) const; virtual Control *make_custom_tooltip(const String &p_text) const; + virtual String accessibility_get_contextual_info() const; + Control(); ~Control(); }; diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index feea863e5ae..3c88da75273 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -51,6 +51,12 @@ void AcceptDialog::_parent_focused() { void AcceptDialog::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_DIALOG); + } break; case NOTIFICATION_POST_ENTER_TREE: { if (is_visible()) { get_ok_button()->grab_focus(); diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index c33eea4b879..4fe705cdd56 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -1442,9 +1442,11 @@ void FileDialog::_update_option_controls() { for (const FileDialog::Option &opt : options) { Label *lbl = memnew(Label); lbl->set_text(opt.name); + lbl->set_focus_mode(Control::FOCUS_NONE); grid_options->add_child(lbl); if (opt.values.is_empty()) { CheckBox *cb = memnew(CheckBox); + cb->set_accessibility_name(opt.name); cb->set_pressed(opt.default_idx); grid_options->add_child(cb); cb->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::_option_changed_checkbox_toggled).bind(opt.name)); @@ -1454,6 +1456,7 @@ void FileDialog::_update_option_controls() { for (const String &val : opt.values) { ob->add_item(val); } + ob->set_accessibility_name(opt.name); ob->select(opt.default_idx); grid_options->add_child(ob); ob->connect(SceneStringName(item_selected), callable_mp(this, &FileDialog::_option_changed_item_selected).bind(opt.name)); @@ -1732,11 +1735,14 @@ FileDialog::FileDialog() { dir_prev = memnew(Button); dir_prev->set_theme_type_variation(SceneStringName(FlatButton)); + dir_prev->set_accessibility_name(ETR("Previous")); dir_prev->set_tooltip_text(ETR("Go to previous folder.")); dir_next = memnew(Button); + dir_next->set_accessibility_name(ETR("Next")); dir_next->set_theme_type_variation(SceneStringName(FlatButton)); dir_next->set_tooltip_text(ETR("Go to next folder.")); dir_up = memnew(Button); + dir_up->set_accessibility_name(ETR("Parent Folder")); dir_up->set_theme_type_variation(SceneStringName(FlatButton)); dir_up->set_tooltip_text(ETR("Go to parent folder.")); hbc->add_child(dir_prev); @@ -1746,22 +1752,27 @@ FileDialog::FileDialog() { dir_next->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_go_forward)); dir_up->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_go_up)); - hbc->add_child(memnew(Label(ETR("Path:")))); + Label *lbl_path = memnew(Label(ETR("Path:"))); + lbl_path->set_focus_mode(Control::FOCUS_NONE); + hbc->add_child(lbl_path); drives_container = memnew(HBoxContainer); hbc->add_child(drives_container); drives = memnew(OptionButton); drives->connect(SceneStringName(item_selected), callable_mp(this, &FileDialog::_select_drive)); + drives->set_accessibility_name(ETR("Drive")); hbc->add_child(drives); dir = memnew(LineEdit); + dir->set_accessibility_name(ETR("Directory Path")); dir->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE); hbc->add_child(dir); dir->set_h_size_flags(Control::SIZE_EXPAND_FILL); refresh = memnew(Button); refresh->set_theme_type_variation(SceneStringName(FlatButton)); + refresh->set_accessibility_name(ETR("Refresh")); refresh->set_tooltip_text(ETR("Refresh files.")); refresh->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::update_file_list)); hbc->add_child(refresh); @@ -1770,6 +1781,7 @@ FileDialog::FileDialog() { show_hidden->set_theme_type_variation(SceneStringName(FlatButton)); show_hidden->set_toggle_mode(true); show_hidden->set_pressed(is_showing_hidden_files()); + show_hidden->set_accessibility_name(ETR("Show Hidden Files")); show_hidden->set_tooltip_text(ETR("Toggle the visibility of hidden files.")); show_hidden->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_hidden_files)); hbc->add_child(show_hidden); @@ -1778,6 +1790,7 @@ FileDialog::FileDialog() { show_filename_filter_button->set_theme_type_variation(SceneStringName(FlatButton)); show_filename_filter_button->set_toggle_mode(true); show_filename_filter_button->set_pressed(false); + show_filename_filter_button->set_accessibility_name(ETR("Filter File Names")); show_filename_filter_button->set_tooltip_text(ETR("Toggle the visibility of the filter for file names.")); show_filename_filter_button->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_filename_filter)); hbc->add_child(show_filename_filter_button); @@ -1787,6 +1800,7 @@ FileDialog::FileDialog() { makedir = memnew(Button); makedir->set_theme_type_variation(SceneStringName(FlatButton)); + makedir->set_accessibility_name(ETR("Create New Folder")); makedir->set_tooltip_text(ETR("Create a new folder.")); makedir->connect(SceneStringName(pressed), callable_mp(this, &FileDialog::_make_dir)); hbc->add_child(makedir); @@ -1794,6 +1808,7 @@ FileDialog::FileDialog() { tree = memnew(Tree); tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + tree->set_accessibility_name(ETR("Directories and Files")); tree->set_hide_root(true); vbox->add_margin_child(ETR("Directories & Files:"), tree, true); @@ -1811,13 +1826,18 @@ FileDialog::FileDialog() { filename_filter->set_stretch_ratio(4); filename_filter->set_h_size_flags(Control::SIZE_EXPAND_FILL); filename_filter->set_clear_button_enabled(true); + filename_filter->set_accessibility_name(ETR("Filename Filter")); filename_filter_box->add_child(filename_filter); filename_filter_box->set_visible(false); vbox->add_child(filename_filter_box); file_box = memnew(HBoxContainer); - file_box->add_child(memnew(Label(ETR("File:")))); + Label *lbl_file = memnew(Label(ETR("File:"))); + lbl_file->set_focus_mode(Control::FOCUS_NONE); + file_box->add_child(lbl_file); + file = memnew(LineEdit); + file->set_accessibility_name(ETR("File Name")); file->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE); file->set_stretch_ratio(4); file->set_h_size_flags(Control::SIZE_EXPAND_FILL); diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index ccca775f0a9..c1ab57ecb77 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -353,6 +353,62 @@ int GraphEdit::get_connection_count(const StringName &p_node, int p_port) { return count; } +GraphNode *GraphEdit::get_input_connection_target(const StringName &p_node, int p_port) { + for (const Ref &conn : connections) { + if (conn->to_node == p_node && conn->to_port == p_port) { + GraphNode *from = Object::cast_to(get_node(NodePath(conn->from_node))); + if (from) { + return from; + } + } + } + return nullptr; +} + +GraphNode *GraphEdit::get_output_connection_target(const StringName &p_node, int p_port) { + for (const Ref &conn : connections) { + if (conn->from_node == p_node && conn->from_port == p_port) { + GraphNode *to = Object::cast_to(get_node(NodePath(conn->to_node))); + if (to) { + return to; + } + } + } + return nullptr; +} + +String GraphEdit::get_connections_description(const StringName &p_node, int p_port) { + String out; + for (const Ref &conn : connections) { + if (conn->from_node == p_node && conn->from_port == p_port) { + GraphNode *to = Object::cast_to(get_node(NodePath(conn->to_node))); + if (to) { + if (!out.is_empty()) { + out += ", "; + } + String name = to->get_accessibility_name(); + if (name.is_empty()) { + name = to->get_name(); + } + out += vformat(ETR("connection to %s (%s) port %d"), name, to->get_title(), conn->to_port); + } + } else if (conn->to_node == p_node && conn->to_port == p_port) { + GraphNode *from = Object::cast_to(get_node(NodePath(conn->from_node))); + if (from) { + if (!out.is_empty()) { + out += ", "; + } + String name = from->get_accessibility_name(); + if (name.is_empty()) { + name = from->get_name(); + } + out += vformat(ETR("connection from %s (%s) port %d"), name, from->get_title(), conn->from_port); + } + } + } + return out; +} + void GraphEdit::set_scroll_offset(const Vector2 &p_offset) { setting_scroll_offset = true; h_scrollbar->set_value(p_offset.x); @@ -780,6 +836,10 @@ void GraphEdit::_notification(int p_what) { // Draw background fill. draw_style_box(theme_cache.panel, Rect2(Point2(), get_size())); + if (has_focus()) { + draw_style_box(theme_cache.panel_focus, Rect2(Point2(), get_size())); + } + // Draw background grid. if (show_grid) { _draw_grid(); @@ -958,9 +1018,172 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { return false; } +void GraphEdit::start_keyboard_connecting(GraphNode *p_node, int p_in_port, int p_out_port) { + if (!p_node || p_in_port == p_out_port || (p_in_port != -1 && p_out_port != -1)) { + return; + } + connecting_valid = false; + keyboard_connecting = true; + if (p_in_port != -1) { + Vector2 pos = p_node->get_input_port_position(p_in_port) * zoom + p_node->get_position(); + + if (right_disconnects || valid_right_disconnect_types.has(p_node->get_input_port_type(p_in_port))) { + // Check disconnect. + for (const Ref &conn : connection_map[p_node->get_name()]) { + if (conn->to_node == p_node->get_name() && conn->to_port == p_in_port) { + Node *fr = get_node(NodePath(conn->from_node)); + if (Object::cast_to(fr)) { + connecting_from_node = conn->from_node; + connecting_from_port_index = conn->from_port; + connecting_from_output = true; + connecting_type = Object::cast_to(fr)->get_output_port_type(conn->from_port); + connecting_color = Object::cast_to(fr)->get_output_port_color(conn->from_port); + connecting_target_valid = false; + connecting_to_point = pos; + just_disconnected = true; + + if (connecting_type >= 0) { + emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port); + fr = get_node(NodePath(connecting_from_node)); + if (Object::cast_to(fr)) { + connecting = true; + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true); + } + } + return; + } + } + } + } + + connecting_from_node = p_node->get_name(); + connecting_from_port_index = p_in_port; + connecting_from_output = false; + connecting_type = p_node->get_input_port_type(p_in_port); + connecting_color = p_node->get_input_port_color(p_in_port); + connecting_target_valid = false; + connecting_to_point = pos; + if (connecting_type >= 0) { + connecting = true; + just_disconnected = false; + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false); + } + return; + } + if (p_out_port != -1) { + Vector2 pos = p_node->get_output_port_position(p_out_port) * zoom + p_node->get_position(); + + if (valid_left_disconnect_types.has(p_node->get_output_port_type(p_out_port))) { + // Check disconnect. + for (const Ref &conn : connection_map[p_node->get_name()]) { + if (conn->from_node == p_node->get_name() && conn->from_port == p_out_port) { + Node *to = get_node(NodePath(conn->to_node)); + if (Object::cast_to(to)) { + connecting_from_node = conn->to_node; + connecting_from_port_index = conn->to_port; + connecting_from_output = false; + connecting_type = Object::cast_to(to)->get_input_port_type(conn->to_port); + connecting_color = Object::cast_to(to)->get_input_port_color(conn->to_port); + connecting_target_valid = false; + connecting_to_point = pos; + + if (connecting_type >= 0) { + just_disconnected = true; + + emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port); + to = get_node(NodePath(connecting_from_node)); // Maybe it was erased. + if (Object::cast_to(to)) { + connecting = true; + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false); + } + } + return; + } + } + } + } + + connecting_from_node = p_node->get_name(); + connecting_from_port_index = p_out_port; + connecting_from_output = true; + connecting_type = p_node->get_output_port_type(p_out_port); + connecting_color = p_node->get_output_port_color(p_out_port); + connecting_target_valid = false; + connecting_to_point = pos; + if (connecting_type >= 0) { + connecting = true; + just_disconnected = false; + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true); + } + return; + } +} + +void GraphEdit::end_keyboard_connecting(GraphNode *p_node, int p_in_port, int p_out_port) { + if (!p_node) { + return; + } + connecting_valid = true; + connecting_target_valid = false; + if (p_in_port != -1) { + Vector2 pos = p_node->get_input_port_position(p_in_port) * zoom + p_node->get_position(); + + int type = p_node->get_input_port_type(p_in_port); + if (type == connecting_type || p_node->is_ignoring_valid_connection_type() || valid_connection_types.has(ConnectionType(connecting_type, type))) { + connecting_target_valid = true; + connecting_to_point = pos; + connecting_target_node = p_node->get_name(); + connecting_target_port_index = p_in_port; + } + } + if (p_out_port != -1) { + Vector2 pos = p_node->get_output_port_position(p_out_port) * zoom + p_node->get_position(); + + int type = p_node->get_output_port_type(p_out_port); + if (type == connecting_type || p_node->is_ignoring_valid_connection_type() || valid_connection_types.has(ConnectionType(type, connecting_type))) { + connecting_target_valid = true; + connecting_to_point = pos; + connecting_target_node = p_node->get_name(); + connecting_target_port_index = p_out_port; + } + } + if (connecting_valid) { + if (connecting && connecting_target_valid) { + if (connecting_from_output) { + emit_signal(SNAME("connection_request"), connecting_from_node, connecting_from_port_index, connecting_target_node, connecting_target_port_index); + } else { + emit_signal(SNAME("connection_request"), connecting_target_node, connecting_target_port_index, connecting_from_node, connecting_from_port_index); + } + } else if (!just_disconnected) { + if (connecting_from_output) { + emit_signal(SNAME("connection_to_empty"), connecting_from_node, connecting_from_port_index, Vector2()); + } else { + emit_signal(SNAME("connection_from_empty"), connecting_from_node, connecting_from_port_index, Vector2()); + } + } + } + + keyboard_connecting = false; + if (connecting) { + force_connection_drag_end(); + } +} + +Dictionary GraphEdit::get_type_names() const { + return type_names; +} + +void GraphEdit::set_type_names(const Dictionary &p_names) { + type_names = p_names; +} + void GraphEdit::_top_connection_layer_input(const Ref &p_ev) { Ref mb = p_ev; if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) { + if (keyboard_connecting) { + force_connection_drag_end(); + keyboard_connecting = false; + } connecting_valid = false; click_pos = mb->get_position() / zoom; for (int i = get_child_count() - 1; i >= 0; i--) { @@ -1086,7 +1309,7 @@ void GraphEdit::_top_connection_layer_input(const Ref &p_ev) { } Ref mm = p_ev; - if (mm.is_valid() && connecting) { + if (mm.is_valid() && connecting && !keyboard_connecting) { connecting_to_point = mm->get_position(); minimap->queue_redraw(); callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); @@ -2142,6 +2365,7 @@ void GraphEdit::force_connection_drag_end() { connecting = false; connecting_valid = false; + keyboard_connecting = false; minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); @@ -2793,6 +3017,9 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_right_disconnects", "enable"), &GraphEdit::set_right_disconnects); ClassDB::bind_method(D_METHOD("is_right_disconnects_enabled"), &GraphEdit::is_right_disconnects_enabled); + ClassDB::bind_method(D_METHOD("set_type_names", "type_names"), &GraphEdit::set_type_names); + ClassDB::bind_method(D_METHOD("get_type_names"), &GraphEdit::get_type_names); + GDVIRTUAL_BIND(_is_in_input_hotzone, "in_node", "in_port", "mouse_position"); GDVIRTUAL_BIND(_is_in_output_hotzone, "in_node", "in_port", "mouse_position"); @@ -2813,6 +3040,8 @@ void GraphEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "panning_scheme", PROPERTY_HINT_ENUM, "Scroll Zooms,Scroll Pans"), "set_panning_scheme", "get_panning_scheme"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "right_disconnects"), "set_right_disconnects", "is_right_disconnects_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "type_names", PROPERTY_HINT_DICTIONARY_TYPE, "int;String"), "set_type_names", "get_type_names"); + ADD_GROUP("Connection Lines", "connection_lines"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_curvature"), "set_connection_lines_curvature", "get_connection_lines_curvature"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_RANGE, "0,100,0.1,suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness"); @@ -2869,6 +3098,7 @@ void GraphEdit::_bind_methods() { BIND_ENUM_CONSTANT(GRID_PATTERN_DOTS); BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphEdit, panel); + BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphEdit, panel_focus); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, grid_major); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, grid_minor); @@ -2977,7 +3207,8 @@ GraphEdit::GraphEdit() { zoom_minus_button->set_theme_type_variation(SceneStringName(FlatButton)); zoom_minus_button->set_visible(show_zoom_buttons); zoom_minus_button->set_tooltip_text(ETR("Zoom Out")); - zoom_minus_button->set_focus_mode(FOCUS_NONE); + zoom_minus_button->set_accessibility_name(ETR("Zoom Out")); + zoom_minus_button->set_focus_mode(FOCUS_ACCESSIBILITY); menu_hbox->add_child(zoom_minus_button); zoom_minus_button->connect(SceneStringName(pressed), callable_mp(this, &GraphEdit::_zoom_minus)); @@ -2985,7 +3216,8 @@ GraphEdit::GraphEdit() { zoom_reset_button->set_theme_type_variation(SceneStringName(FlatButton)); zoom_reset_button->set_visible(show_zoom_buttons); zoom_reset_button->set_tooltip_text(ETR("Zoom Reset")); - zoom_reset_button->set_focus_mode(FOCUS_NONE); + zoom_reset_button->set_accessibility_name(ETR("Zoom Reset")); + zoom_reset_button->set_focus_mode(FOCUS_ACCESSIBILITY); menu_hbox->add_child(zoom_reset_button); zoom_reset_button->connect(SceneStringName(pressed), callable_mp(this, &GraphEdit::_zoom_reset)); @@ -2993,7 +3225,8 @@ GraphEdit::GraphEdit() { zoom_plus_button->set_theme_type_variation(SceneStringName(FlatButton)); zoom_plus_button->set_visible(show_zoom_buttons); zoom_plus_button->set_tooltip_text(ETR("Zoom In")); - zoom_plus_button->set_focus_mode(FOCUS_NONE); + zoom_plus_button->set_accessibility_name(ETR("Zoom In")); + zoom_plus_button->set_focus_mode(FOCUS_ACCESSIBILITY); menu_hbox->add_child(zoom_plus_button); zoom_plus_button->connect(SceneStringName(pressed), callable_mp(this, &GraphEdit::_zoom_plus)); @@ -3005,6 +3238,7 @@ GraphEdit::GraphEdit() { toggle_grid_button->set_toggle_mode(true); toggle_grid_button->set_pressed(true); toggle_grid_button->set_tooltip_text(ETR("Toggle the visual grid.")); + toggle_grid_button->set_accessibility_name(ETR("Grid")); toggle_grid_button->set_focus_mode(FOCUS_NONE); menu_hbox->add_child(toggle_grid_button); toggle_grid_button->connect(SceneStringName(pressed), callable_mp(this, &GraphEdit::_show_grid_toggled)); @@ -3014,6 +3248,7 @@ GraphEdit::GraphEdit() { toggle_snapping_button->set_visible(show_grid_buttons); toggle_snapping_button->set_toggle_mode(true); toggle_snapping_button->set_tooltip_text(ETR("Toggle snapping to the grid.")); + toggle_snapping_button->set_accessibility_name(ETR("Snap to Grid")); toggle_snapping_button->set_pressed(snapping_enabled); toggle_snapping_button->set_focus_mode(FOCUS_NONE); menu_hbox->add_child(toggle_snapping_button); @@ -3026,6 +3261,7 @@ GraphEdit::GraphEdit() { snapping_distance_spinbox->set_step(1); snapping_distance_spinbox->set_value(snapping_distance); snapping_distance_spinbox->set_tooltip_text(ETR("Change the snapping distance.")); + snapping_distance_spinbox->set_accessibility_name(ETR("Snapping Distance")); menu_hbox->add_child(snapping_distance_spinbox); snapping_distance_spinbox->connect(SceneStringName(value_changed), callable_mp(this, &GraphEdit::_snapping_distance_changed)); @@ -3036,6 +3272,7 @@ GraphEdit::GraphEdit() { minimap_button->set_visible(show_minimap_button); minimap_button->set_toggle_mode(true); minimap_button->set_tooltip_text(ETR("Toggle the graph minimap.")); + minimap_button->set_accessibility_name(ETR("Minimap")); minimap_button->set_pressed(show_grid); minimap_button->set_focus_mode(FOCUS_NONE); menu_hbox->add_child(minimap_button); @@ -3044,6 +3281,7 @@ GraphEdit::GraphEdit() { arrange_button = memnew(Button); arrange_button->set_theme_type_variation(SceneStringName(FlatButton)); arrange_button->set_visible(show_arrange_button); + arrange_button->set_accessibility_name(ETR("Auto Arrange")); arrange_button->connect(SceneStringName(pressed), callable_mp(this, &GraphEdit::arrange_nodes)); arrange_button->set_focus_mode(FOCUS_NONE); menu_hbox->add_child(arrange_button); diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index 303b5637494..cbc71997058 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -30,6 +30,7 @@ #pragma once +#include "core/variant/typed_dictionary.h" #include "scene/gui/box_container.h" #include "scene/gui/graph_frame.h" #include "scene/gui/graph_node.h" @@ -198,6 +199,7 @@ private: bool show_grid = true; GridPattern grid_pattern = GRID_PATTERN_LINES; + bool keyboard_connecting = false; bool connecting = false; StringName connecting_from_node; bool connecting_from_output = false; @@ -269,6 +271,7 @@ private: float base_scale = 1.0; Ref panel; + Ref panel_focus; Color grid_major; Color grid_minor; @@ -303,6 +306,8 @@ private: HashMap> frame_attached_nodes; HashMap linked_parent_map; + Dictionary type_names; + void _pan_callback(Vector2 p_scroll_vec, Ref p_event); void _zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref p_event); @@ -404,6 +409,9 @@ public: Error connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, bool keep_alive = false); bool is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); int get_connection_count(const StringName &p_node, int p_port); + GraphNode *get_input_connection_target(const StringName &p_node, int p_port); + GraphNode *get_output_connection_target(const StringName &p_node, int p_port); + String get_connections_description(const StringName &p_node, int p_port); void disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void force_connection_drag_end(); @@ -413,6 +421,13 @@ public: Ref get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const; List> get_connections_intersecting_with_rect(const Rect2 &p_rect) const; + bool is_keyboard_connecting() const { return keyboard_connecting; } + void start_keyboard_connecting(GraphNode *p_node, int p_in_port, int p_out_port); + void end_keyboard_connecting(GraphNode *p_node, int p_in_port, int p_out_port); + + Dictionary get_type_names() const; + void set_type_names(const Dictionary &p_names); + virtual bool is_node_hover_valid(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity); diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp index 3e0803eb6f1..69b26173d67 100644 --- a/scene/gui/graph_node.cpp +++ b/scene/gui/graph_node.cpp @@ -31,6 +31,7 @@ #include "graph_node.h" #include "scene/gui/box_container.h" +#include "scene/gui/graph_edit.h" #include "scene/gui/label.h" #include "scene/theme/theme_db.h" @@ -196,6 +197,10 @@ void GraphNode::_resort() { children_count++; } + slot_count = children_count; + if (selected_slot >= slot_count) { + selected_slot = -1; + } if (children_count == 0) { return; @@ -285,6 +290,7 @@ void GraphNode::_resort() { valid_children_idx++; } + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; } @@ -306,8 +312,308 @@ void GraphNode::draw_port(int p_slot_index, Point2i p_pos, bool p_left, const Co port_icon->draw(get_canvas_item(), p_pos + icon_offset, p_color); } +void GraphNode::_accessibility_action_slot(const Variant &p_data) { + CustomAccessibilityAction action = (CustomAccessibilityAction)p_data.operator int(); + switch (action) { + case ACTION_CONNECT_INPUT: { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_left) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < left_port_cache.size(); i++) { + if (left_port_cache[i].slot_index == selected_slot) { + if (graph->is_keyboard_connecting()) { + graph->end_keyboard_connecting(this, i, -1); + } else { + graph->start_keyboard_connecting(this, i, -1); + } + queue_accessibility_update(); + queue_redraw(); + break; + } + } + } + } + } + } break; + + case ACTION_CONNECT_OUTPUT: { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_right) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < right_port_cache.size(); i++) { + if (right_port_cache[i].slot_index == selected_slot) { + if (graph->is_keyboard_connecting()) { + graph->end_keyboard_connecting(this, -1, i); + } else { + graph->start_keyboard_connecting(this, -1, i); + } + queue_accessibility_update(); + queue_redraw(); + break; + } + } + } + } + } + } break; + + case ACTION_FOLLOW_INPUT: { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_left) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < left_port_cache.size(); i++) { + if (left_port_cache[i].slot_index == selected_slot) { + GraphNode *target = graph->get_input_connection_target(get_name(), i); + if (target) { + target->grab_focus(); + break; + } + } + } + } + } + } + } break; + + case ACTION_FOLLOW_OUTPUT: { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_right) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < right_port_cache.size(); i++) { + if (right_port_cache[i].slot_index == selected_slot) { + GraphNode *target = graph->get_output_connection_target(get_name(), i); + if (target) { + target->grab_focus(); + break; + } + } + } + } + } + } + } break; + } +} + +void GraphNode::gui_input(const Ref &p_event) { + if (port_pos_dirty) { + _port_pos_update(); + } + + if (p_event->is_pressed() && slot_count > 0) { + if (p_event->is_action("ui_up", true)) { + selected_slot--; + if (selected_slot < 0) { + selected_slot = -1; + } else { + accept_event(); + } + } else if (p_event->is_action("ui_down", true)) { + selected_slot++; + if (selected_slot >= slot_count) { + selected_slot = -1; + } else { + accept_event(); + } + } else if (p_event->is_action("ui_cancel", true)) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph && graph->is_keyboard_connecting()) { + graph->force_connection_drag_end(); + accept_event(); + } + } else if (p_event->is_action("ui_graph_delete", true)) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph && graph->is_keyboard_connecting()) { + graph->end_keyboard_connecting(this, -1, -1); + accept_event(); + } + } else if (p_event->is_action("ui_graph_follow_left", true)) { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_left) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < left_port_cache.size(); i++) { + if (left_port_cache[i].slot_index == selected_slot) { + GraphNode *target = graph->get_input_connection_target(get_name(), i); + if (target) { + target->grab_focus(); + accept_event(); + break; + } + } + } + } + } + } + } else if (p_event->is_action("ui_graph_follow_right", true)) { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_right) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < right_port_cache.size(); i++) { + if (right_port_cache[i].slot_index == selected_slot) { + GraphNode *target = graph->get_output_connection_target(get_name(), i); + if (target) { + target->grab_focus(); + accept_event(); + break; + } + } + } + } + } + } + } else if (p_event->is_action("ui_left", true)) { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_left) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < left_port_cache.size(); i++) { + if (left_port_cache[i].slot_index == selected_slot) { + if (graph->is_keyboard_connecting()) { + graph->end_keyboard_connecting(this, i, -1); + } else { + graph->start_keyboard_connecting(this, i, -1); + } + accept_event(); + break; + } + } + } + } + } + } else if (p_event->is_action("ui_right", true)) { + if (slot_table.has(selected_slot)) { + const Slot &slot = slot_table[selected_slot]; + if (slot.enable_right) { + GraphEdit *graph = Object::cast_to(get_parent()); + if (graph) { + for (int i = 0; i < right_port_cache.size(); i++) { + if (right_port_cache[i].slot_index == selected_slot) { + if (graph->is_keyboard_connecting()) { + graph->end_keyboard_connecting(this, -1, i); + } else { + graph->start_keyboard_connecting(this, -1, i); + } + accept_event(); + break; + } + } + } + } + } + } else if (p_event->is_action("ui_accept", true)) { + if (slot_table.has(selected_slot)) { + int idx = 0; + for (int i = 0; i < get_child_count(false); i++) { + Control *child = as_sortable_control(get_child(i, false), SortableVisibilityMode::IGNORE); + if (!child) { + continue; + } + if (idx == selected_slot) { + selected_slot = -1; + child->grab_focus(); + break; + } + idx++; + } + accept_event(); + } + } + queue_accessibility_update(); + queue_redraw(); + } +} + void GraphNode::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + String name = get_accessibility_name(); + if (name.is_empty()) { + name = get_name(); + } + name = vformat(ETR("graph node %s (%s)"), name, get_title()); + + if (slot_table.has(selected_slot)) { + GraphEdit *graph = Object::cast_to(get_parent()); + Dictionary type_info; + if (graph) { + type_info = graph->get_type_names(); + } + const Slot &slot = slot_table[selected_slot]; + name += ", " + vformat(ETR("slot %d of %d"), selected_slot + 1, slot_count); + if (slot.enable_left) { + if (type_info.has(slot.type_left)) { + name += "," + vformat(ETR("input port, type: %s"), type_info[slot.type_left]); + } else { + name += "," + vformat(ETR("input port, type: %d"), slot.type_left); + } + if (graph) { + for (int i = 0; i < left_port_cache.size(); i++) { + if (left_port_cache[i].slot_index == selected_slot) { + String cd = graph->get_connections_description(get_name(), i); + if (cd.is_empty()) { + name += " " + ETR("no connections"); + } else { + name += " " + cd; + } + break; + } + } + } + } + if (slot.enable_left) { + if (type_info.has(slot.type_right)) { + name += "," + vformat(ETR("output port, type: %s"), type_info[slot.type_right]); + } else { + name += "," + vformat(ETR("output port, type: %d"), slot.type_right); + } + if (graph) { + for (int i = 0; i < right_port_cache.size(); i++) { + if (right_port_cache[i].slot_index == selected_slot) { + String cd = graph->get_connections_description(get_name(), i); + if (cd.is_empty()) { + name += " " + ETR("no connections"); + } else { + name += " " + cd; + } + break; + } + } + } + } + if (graph && graph->is_keyboard_connecting()) { + name += ", " + ETR("currently selecting target port"); + } + } else { + name += ", " + vformat(ETR("has %d slots"), slot_count); + } + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_LIST); + DisplayServer::get_singleton()->accessibility_update_set_name(ae, name); + DisplayServer::get_singleton()->accessibility_update_add_custom_action(ae, CustomAccessibilityAction::ACTION_CONNECT_INPUT, ETR("Edit Input Port Connection")); + DisplayServer::get_singleton()->accessibility_update_add_custom_action(ae, CustomAccessibilityAction::ACTION_CONNECT_OUTPUT, ETR("Edit Output Port Connection")); + DisplayServer::get_singleton()->accessibility_update_add_custom_action(ae, CustomAccessibilityAction::ACTION_FOLLOW_INPUT, ETR("Follow Input Port Connection")); + DisplayServer::get_singleton()->accessibility_update_add_custom_action(ae, CustomAccessibilityAction::ACTION_FOLLOW_OUTPUT, ETR("Follow Output Port Connection")); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_CUSTOM, callable_mp(this, &GraphNode::_accessibility_action_slot)); + } break; + case NOTIFICATION_FOCUS_EXIT: { + selected_slot = -1; + queue_redraw(); + } break; case NOTIFICATION_DRAW: { // Used for layout calculations. Ref sb_panel = theme_cache.panel; @@ -317,6 +623,7 @@ void GraphNode::_notification(int p_what) { Ref sb_to_draw_titlebar = selected ? theme_cache.titlebar_selected : theme_cache.titlebar; Ref sb_slot = theme_cache.slot; + Ref sb_slot_selected = theme_cache.slot_selected; int port_h_offset = theme_cache.port_h_offset; @@ -329,6 +636,10 @@ void GraphNode::_notification(int p_what) { // Draw body (slots area) stylebox. draw_style_box(sb_to_draw_panel, body_rect); + if (has_focus()) { + draw_style_box(theme_cache.panel_focus, body_rect); + } + // Draw title bar stylebox above. draw_style_box(sb_to_draw_titlebar, titlebar_rect); @@ -356,6 +667,12 @@ void GraphNode::_notification(int p_what) { draw_port(slot_index, Point2i(get_size().x - port_h_offset, slot_y_cache[E.key]), false, slot.color_right); } + if (slot_index == selected_slot) { + Size2i port_sz = theme_cache.port->get_size(); + draw_style_box(sb_slot_selected, Rect2i(port_h_offset - port_sz.x, slot_y_cache[E.key] + sb_panel->get_margin(SIDE_TOP) - port_sz.y, port_sz.x * 2, port_sz.y * 2)); + draw_style_box(sb_slot_selected, Rect2i(get_size().x - port_h_offset - port_sz.x, slot_y_cache[E.key] + sb_panel->get_margin(SIDE_TOP) - port_sz.y, port_sz.x * 2, port_sz.y * 2)); + } + // Draw slot stylebox. if (slot.draw_stylebox) { Control *child = Object::cast_to(get_child(E.key, false)); @@ -400,6 +717,8 @@ void GraphNode::set_slot(int p_slot_index, bool p_enable_left, int p_type_left, slot.custom_port_icon_right = p_custom_right; slot.draw_stylebox = p_draw_stylebox; slot_table[p_slot_index] = slot; + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; @@ -408,12 +727,16 @@ void GraphNode::set_slot(int p_slot_index, bool p_enable_left, int p_type_left, void GraphNode::clear_slot(int p_slot_index) { slot_table.erase(p_slot_index); + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; } void GraphNode::clear_all_slots() { slot_table.clear(); + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; } @@ -433,6 +756,8 @@ void GraphNode::set_slot_enabled_left(int p_slot_index, bool p_enable) { } slot_table[p_slot_index].enable_left = p_enable; + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; @@ -447,6 +772,8 @@ void GraphNode::set_slot_type_left(int p_slot_index, int p_type) { } slot_table[p_slot_index].type_left = p_type; + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; @@ -517,6 +844,8 @@ void GraphNode::set_slot_enabled_right(int p_slot_index, bool p_enable) { } slot_table[p_slot_index].enable_right = p_enable; + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; @@ -531,6 +860,8 @@ void GraphNode::set_slot_type_right(int p_slot_index, int p_type) { } slot_table[p_slot_index].type_right = p_type; + + queue_accessibility_update(); queue_redraw(); port_pos_dirty = true; @@ -681,6 +1012,10 @@ void GraphNode::_port_pos_update() { slot_index++; } + slot_count = slot_index; + if (selected_slot >= slot_count) { + selected_slot = -1; + } port_pos_dirty = false; } @@ -775,6 +1110,25 @@ int GraphNode::get_output_port_slot(int p_port_idx) { return right_port_cache[p_port_idx].slot_index; } +String GraphNode::get_accessibility_container_name(const Node *p_node) const { + int idx = 0; + for (int i = 0; i < get_child_count(false); i++) { + Control *child = as_sortable_control(get_child(i, false), SortableVisibilityMode::IGNORE); + if (!child) { + continue; + } + if (child == p_node) { + String name = get_accessibility_name(); + if (name.is_empty()) { + name = get_name(); + } + return vformat(ETR(", in slot %d of graph node %s (%s)"), idx + 1, name, get_title()); + } + idx++; + } + return String(); +} + void GraphNode::set_title(const String &p_title) { if (title == p_title) { return; @@ -884,9 +1238,11 @@ void GraphNode::_bind_methods() { BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, panel); BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, panel_selected); + BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, panel_focus); BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, titlebar); BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, titlebar_selected); BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, slot); + BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, GraphNode, slot_selected); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, GraphNode, separation); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, GraphNode, port_h_offset); @@ -904,7 +1260,9 @@ GraphNode::GraphNode() { title_label = memnew(Label); title_label->set_theme_type_variation("GraphNodeTitleLabel"); title_label->set_h_size_flags(SIZE_EXPAND_FILL); + title_label->set_focus_mode(Control::FOCUS_NONE); titlebar_hbox->add_child(title_label); set_mouse_filter(MOUSE_FILTER_STOP); + set_focus_mode(FOCUS_ACCESSIBILITY); } diff --git a/scene/gui/graph_node.h b/scene/gui/graph_node.h index 01196da2cba..f1831cc9ca0 100644 --- a/scene/gui/graph_node.h +++ b/scene/gui/graph_node.h @@ -66,6 +66,14 @@ class GraphNode : public GraphElement { int final_size = 0; }; + enum CustomAccessibilityAction { + ACTION_CONNECT_INPUT, + ACTION_CONNECT_OUTPUT, + ACTION_FOLLOW_INPUT, + ACTION_FOLLOW_OUTPUT, + }; + void _accessibility_action_slot(const Variant &p_data); + HBoxContainer *titlebar_hbox = nullptr; Label *title_label = nullptr; @@ -77,12 +85,17 @@ class GraphNode : public GraphElement { HashMap slot_table; Vector slot_y_cache; + int slot_count = 0; + int selected_slot = -1; + struct ThemeCache { Ref panel; Ref panel_selected; + Ref panel_focus; Ref titlebar; Ref titlebar_selected; Ref slot; + Ref slot_selected; int separation = 0; int port_h_offset = 0; @@ -112,6 +125,9 @@ protected: void _get_property_list(List *p_list) const; public: + virtual String get_accessibility_container_name(const Node *p_node) const override; + virtual void gui_input(const Ref &p_event) override; + void set_title(const String &p_title); String get_title() const; diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index b40746562c5..7f2604b4a07 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -64,6 +64,7 @@ int ItemList::add_item(const String &p_item, const Ref &p_texture, bo items.write[item_id].xl_text = _atr(item_id, p_item); _shape_text(item_id); + queue_accessibility_update(); queue_redraw(); shape_changed = true; notify_property_list_changed(); @@ -77,6 +78,7 @@ int ItemList::add_icon_item(const Ref &p_item, bool p_selectable) { items.push_back(item); int item_id = items.size() - 1; + queue_accessibility_update(); queue_redraw(); shape_changed = true; notify_property_list_changed(); @@ -96,6 +98,7 @@ void ItemList::set_item_text(int p_idx, const String &p_text) { items.write[p_idx].text = p_text; items.write[p_idx].xl_text = _atr(p_idx, p_text); _shape_text(p_idx); + queue_accessibility_update(); queue_redraw(); shape_changed = true; } @@ -114,6 +117,7 @@ void ItemList::set_item_text_direction(int p_idx, Control::TextDirection p_text_ if (items[p_idx].text_direction != p_text_direction) { items.write[p_idx].text_direction = p_text_direction; _shape_text(p_idx); + queue_accessibility_update(); queue_redraw(); } } @@ -131,6 +135,7 @@ void ItemList::set_item_language(int p_idx, const String &p_language) { if (items[p_idx].language != p_language) { items.write[p_idx].language = p_language; _shape_text(p_idx); + queue_accessibility_update(); queue_redraw(); } } @@ -149,6 +154,7 @@ void ItemList::set_item_auto_translate_mode(int p_idx, AutoTranslateMode p_mode) items.write[p_idx].auto_translate_mode = p_mode; items.write[p_idx].xl_text = _atr(p_idx, items[p_idx].text); _shape_text(p_idx); + queue_accessibility_update(); queue_redraw(); } } @@ -163,7 +169,11 @@ void ItemList::set_item_tooltip_enabled(int p_idx, const bool p_enabled) { p_idx += get_item_count(); } ERR_FAIL_INDEX(p_idx, items.size()); - items.write[p_idx].tooltip_enabled = p_enabled; + if (items[p_idx].tooltip_enabled != p_enabled) { + items.write[p_idx].tooltip_enabled = p_enabled; + items.write[p_idx].accessibility_item_dirty = true; + queue_accessibility_update(); + } } bool ItemList::is_item_tooltip_enabled(int p_idx) const { @@ -182,6 +192,7 @@ void ItemList::set_item_tooltip(int p_idx, const String &p_tooltip) { } items.write[p_idx].tooltip = p_tooltip; + queue_accessibility_update(); queue_redraw(); shape_changed = true; } @@ -348,6 +359,8 @@ void ItemList::set_item_selectable(int p_idx, bool p_selectable) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].selectable = p_selectable; + items.write[p_idx].accessibility_item_dirty = true; + queue_accessibility_update(); } bool ItemList::is_item_selectable(int p_idx) const { @@ -366,6 +379,8 @@ void ItemList::set_item_disabled(int p_idx, bool p_disabled) { } items.write[p_idx].disabled = p_disabled; + items.write[p_idx].accessibility_item_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -403,7 +418,10 @@ void ItemList::select(int p_idx, bool p_single) { } for (int i = 0; i < items.size(); i++) { - items.write[i].selected = p_idx == i; + if (items.write[i].selected != (p_idx == i)) { + items.write[i].selected = (p_idx == i); + items.write[i].accessibility_item_dirty = true; + } } current = p_idx; @@ -411,8 +429,10 @@ void ItemList::select(int p_idx, bool p_single) { } else { if (items[p_idx].selectable && !items[p_idx].disabled) { items.write[p_idx].selected = true; + items.write[p_idx].accessibility_item_dirty = true; } } + queue_accessibility_update(); queue_redraw(); } @@ -425,6 +445,8 @@ void ItemList::deselect(int p_idx) { } else { items.write[p_idx].selected = false; } + items.write[p_idx].accessibility_item_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -434,9 +456,13 @@ void ItemList::deselect_all() { } for (int i = 0; i < items.size(); i++) { - items.write[i].selected = false; + if (items.write[i].selected) { + items.write[i].selected = false; + items.write[i].accessibility_item_dirty = true; + } } current = -1; + queue_accessibility_update(); queue_redraw(); } @@ -457,6 +483,7 @@ void ItemList::set_current(int p_current) { select(p_current, true); } else { current = p_current; + queue_accessibility_update(); queue_redraw(); } } @@ -477,6 +504,7 @@ void ItemList::move_item(int p_from_idx, int p_to_idx) { items.remove_at(p_from_idx); items.insert(p_to_idx, item); + queue_accessibility_update(); queue_redraw(); shape_changed = true; notify_property_list_changed(); @@ -489,7 +517,17 @@ void ItemList::set_item_count(int p_count) { return; } + if (items.size() > p_count) { + for (int i = p_count; i < items.size(); i++) { + if (items[i].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(items.write[i].accessibility_item_element); + items.write[i].accessibility_item_element = RID(); + } + } + } + items.resize(p_count); + queue_accessibility_update(); queue_redraw(); shape_changed = true; notify_property_list_changed(); @@ -502,10 +540,15 @@ int ItemList::get_item_count() const { void ItemList::remove_item(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); + if (items[p_idx].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(items.write[p_idx].accessibility_item_element); + items.write[p_idx].accessibility_item_element = RID(); + } items.remove_at(p_idx); if (current == p_idx) { current = -1; } + queue_accessibility_update(); queue_redraw(); shape_changed = true; defer_select_single = -1; @@ -513,9 +556,16 @@ void ItemList::remove_item(int p_idx) { } void ItemList::clear() { + for (int i = 0; i < items.size(); i++) { + if (items[i].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(items.write[i].accessibility_item_element); + items.write[i].accessibility_item_element = RID(); + } + } items.clear(); current = -1; ensure_selected_visible = false; + queue_accessibility_update(); queue_redraw(); shape_changed = true; defer_select_single = -1; @@ -565,6 +615,7 @@ void ItemList::set_max_text_lines(int p_lines) { } } shape_changed = true; + queue_accessibility_update(); queue_redraw(); } } @@ -581,6 +632,7 @@ void ItemList::set_max_columns(int p_amount) { } max_columns = p_amount; + queue_accessibility_update(); queue_redraw(); shape_changed = true; } @@ -595,6 +647,7 @@ void ItemList::set_select_mode(SelectMode p_mode) { } select_mode = p_mode; + queue_accessibility_update(); queue_redraw(); } @@ -694,7 +747,9 @@ void ItemList::gui_input(const Ref &p_event) { if (mm.is_valid()) { int closest = get_item_at_position(mm->get_position(), true); if (closest != hovered) { + prev_hovered = hovered; hovered = closest; + queue_accessibility_update(); queue_redraw(); } } @@ -834,6 +889,21 @@ void ItemList::gui_input(const Ref &p_event) { } if (p_event->is_pressed() && items.size() > 0) { + if (p_event->is_action("ui_menu", true)) { + if (current != -1 && allow_rmb_select) { + int i = current; + + if (items[i].disabled) { + // Don't emit any signal or do any action with clicked item when disabled. + return; + } + + emit_signal(SNAME("item_clicked"), i, get_item_rect(i).position, MouseButton::RIGHT); + + accept_event(); + return; + } + } if (p_event->is_action("ui_up", true)) { if (!search_string.is_empty()) { uint64_t now = OS::get_singleton()->get_ticks_msec(); @@ -1076,8 +1146,131 @@ static Rect2 _adjust_to_max_size(Size2 p_size, Size2 p_max_size) { return Rect2(ofs_x, ofs_y, tex_width, tex_height); } +RID ItemList::get_focused_accessibility_element() const { + if (current == -1) { + return get_accessibility_element(); + } else { + const Item &item = items[current]; + return item.accessibility_item_element; + } +} + +void ItemList::_accessibility_action_scroll_set(const Variant &p_data) { + const Point2 &pos = p_data; + scroll_bar_h->set_value(pos.x); + scroll_bar_v->set_value(pos.y); +} + +void ItemList::_accessibility_action_scroll_up(const Variant &p_data) { + scroll_bar_v->set_value(scroll_bar_v->get_value() - scroll_bar_v->get_page() / 4); +} + +void ItemList::_accessibility_action_scroll_down(const Variant &p_data) { + scroll_bar_v->set_value(scroll_bar_v->get_value() + scroll_bar_v->get_page() / 4); +} + +void ItemList::_accessibility_action_scroll_left(const Variant &p_data) { + scroll_bar_h->set_value(scroll_bar_h->get_value() - scroll_bar_h->get_page() / 4); +} + +void ItemList::_accessibility_action_scroll_right(const Variant &p_data) { + scroll_bar_h->set_value(scroll_bar_h->get_value() + scroll_bar_h->get_page() / 4); +} + +void ItemList::_accessibility_action_scroll_into_view(const Variant &p_data, int p_index) { + ERR_FAIL_INDEX(p_index, items.size()); + + Rect2 r = items[p_index].rect_cache; + int from_v = scroll_bar_v->get_value(); + int to_v = from_v + scroll_bar_v->get_page(); + int from_h = scroll_bar_h->get_value(); + int to_h = from_h + scroll_bar_h->get_page(); + + if (r.position.y < from_v) { + scroll_bar_v->set_value(r.position.y); + } else if (r.position.y + r.size.y > to_v) { + scroll_bar_v->set_value(r.position.y + r.size.y - (to_v - from_v)); + } + if (r.position.x < from_h) { + scroll_bar_h->set_value(r.position.x); + } else if (r.position.x + r.size.x > to_h) { + scroll_bar_h->set_value(r.position.x + r.size.x - (to_h - from_h)); + } +} + +void ItemList::_accessibility_action_focus(const Variant &p_data, int p_index) { + select(p_index); +} + +void ItemList::_accessibility_action_blur(const Variant &p_data, int p_index) { + deselect(p_index); +} + void ItemList::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_EXIT_TREE: + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + for (int i = 0; i < items.size(); i++) { + items.write[i].accessibility_item_element = RID(); + } + accessibility_scroll_element = RID(); + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + force_update_list_size(); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_LIST_BOX); + DisplayServer::get_singleton()->accessibility_update_set_list_item_count(ae, items.size()); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_MULTISELECTABLE, select_mode == SELECT_MULTI); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_DOWN, callable_mp(this, &ItemList::_accessibility_action_scroll_down)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_UP, callable_mp(this, &ItemList::_accessibility_action_scroll_up)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_LEFT, callable_mp(this, &ItemList::_accessibility_action_scroll_left)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_RIGHT, callable_mp(this, &ItemList::_accessibility_action_scroll_right)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_SCROLL_OFFSET, callable_mp(this, &ItemList::_accessibility_action_scroll_set)); + + if (accessibility_scroll_element.is_null()) { + accessibility_scroll_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } + + Transform2D scroll_xform; + scroll_xform.set_origin(Vector2i(-scroll_bar_h->get_value(), -scroll_bar_v->get_value())); + DisplayServer::get_singleton()->accessibility_update_set_transform(accessibility_scroll_element, scroll_xform); + DisplayServer::get_singleton()->accessibility_update_set_bounds(accessibility_scroll_element, Rect2(0, 0, scroll_bar_h->get_max(), scroll_bar_v->get_max())); + + for (int i = 0; i < items.size(); i++) { + const Item &item = items.write[i]; + + if (item.accessibility_item_element.is_null()) { + item.accessibility_item_element = DisplayServer::get_singleton()->accessibility_create_sub_element(accessibility_scroll_element, DisplayServer::AccessibilityRole::ROLE_LIST_BOX_OPTION); + item.accessibility_item_dirty = true; + } + if (item.accessibility_item_dirty || i == hovered || i == prev_hovered) { + DisplayServer::get_singleton()->accessibility_update_add_action(item.accessibility_item_element, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &ItemList::_accessibility_action_scroll_into_view).bind(i)); + DisplayServer::get_singleton()->accessibility_update_add_action(item.accessibility_item_element, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &ItemList::_accessibility_action_focus).bind(i)); + DisplayServer::get_singleton()->accessibility_update_add_action(item.accessibility_item_element, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &ItemList::_accessibility_action_blur).bind(i)); + + DisplayServer::get_singleton()->accessibility_update_set_list_item_index(item.accessibility_item_element, i); + DisplayServer::get_singleton()->accessibility_update_set_list_item_level(item.accessibility_item_element, 0); + DisplayServer::get_singleton()->accessibility_update_set_list_item_selected(item.accessibility_item_element, item.selected); + DisplayServer::get_singleton()->accessibility_update_set_name(item.accessibility_item_element, item.xl_text); + DisplayServer::get_singleton()->accessibility_update_set_flag(item.accessibility_item_element, DisplayServer::AccessibilityFlags::FLAG_DISABLED, item.disabled); + if (item.tooltip_enabled) { + DisplayServer::get_singleton()->accessibility_update_set_tooltip(item.accessibility_item_element, item.tooltip); + } + + Rect2 r = get_item_rect(i); + DisplayServer::get_singleton()->accessibility_update_set_bounds(item.accessibility_item_element, Rect2(r.position, r.size)); + + item.accessibility_item_dirty = false; + } + } + prev_hovered = -1; + + } break; + case NOTIFICATION_RESIZED: { shape_changed = true; queue_redraw(); @@ -1089,6 +1282,7 @@ void ItemList::_notification(int p_what) { _shape_text(i); } shape_changed = true; + queue_accessibility_update(); queue_redraw(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { @@ -1097,6 +1291,7 @@ void ItemList::_notification(int p_what) { _shape_text(i); } shape_changed = true; + queue_accessibility_update(); queue_redraw(); } break; @@ -1537,6 +1732,8 @@ void ItemList::force_update_list_size() { items.write[i].rect_cache.size = minsize; items.write[i].min_rect_cache.size = minsize; + + items.write[i].accessibility_item_dirty = true; } int fit_size = size.x - theme_cache.panel_style->get_minimum_size().width; @@ -1661,7 +1858,9 @@ void ItemList::_scroll_changed(double) { void ItemList::_mouse_exited() { if (hovered > -1) { + prev_hovered = hovered; hovered = -1; + queue_accessibility_update(); queue_redraw(); } } @@ -1763,6 +1962,7 @@ String ItemList::get_tooltip(const Point2 &p_pos) const { void ItemList::sort_items_by_text() { items.sort(); + queue_accessibility_update(); queue_redraw(); shape_changed = true; @@ -1872,6 +2072,7 @@ void ItemList::set_auto_width(bool p_enable) { auto_width = p_enable; shape_changed = true; + queue_accessibility_update(); queue_redraw(); } @@ -1886,6 +2087,7 @@ void ItemList::set_auto_height(bool p_enable) { auto_height = p_enable; shape_changed = true; + queue_accessibility_update(); queue_redraw(); } diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index 5180222512a..26f21410fe8 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -52,6 +52,9 @@ public: private: struct Item { + mutable RID accessibility_item_element; + mutable bool accessibility_item_dirty = true; + Ref icon; bool icon_transposed = false; Rect2i icon_region; @@ -87,12 +90,14 @@ private: Item(bool p_dummy) {} }; + RID accessibility_scroll_element; static inline PropertyListHelper base_property_helper; PropertyListHelper property_helper; int current = -1; int hovered = -1; + int prev_hovered = -1; bool shape_changed = true; @@ -180,7 +185,18 @@ protected: bool _property_get_revert(const StringName &p_name, Variant &r_property) const { return property_helper.property_get_revert(p_name, r_property); } static void _bind_methods(); + void _accessibility_action_scroll_set(const Variant &p_data); + void _accessibility_action_scroll_up(const Variant &p_data); + void _accessibility_action_scroll_down(const Variant &p_data); + void _accessibility_action_scroll_left(const Variant &p_data); + void _accessibility_action_scroll_right(const Variant &p_data); + void _accessibility_action_scroll_into_view(const Variant &p_data, int p_index); + void _accessibility_action_focus(const Variant &p_data, int p_index); + void _accessibility_action_blur(const Variant &p_data, int p_index); + public: + virtual RID get_focused_accessibility_element() const override; + virtual void gui_input(const Ref &p_event) override; int add_item(const String &p_item, const Ref &p_texture = Ref(), bool p_selectable = true); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index f763cad51cc..6418125fc50 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -100,6 +100,7 @@ void Label::set_uppercase(bool p_uppercase) { uppercase = p_uppercase; text_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -692,6 +693,15 @@ PackedStringArray Label::get_configuration_warnings() const { void Label::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT); + DisplayServer::get_singleton()->accessibility_update_set_value(ae, xl_text); + DisplayServer::get_singleton()->accessibility_update_set_text_align(ae, horizontal_alignment); + } break; + case NOTIFICATION_TRANSLATION_CHANGED: { String new_text = atr(text); if (new_text == xl_text) { @@ -703,6 +713,7 @@ void Label::_notification(int p_what) { } text_dirty = true; + queue_accessibility_update(); queue_redraw(); update_configuration_warnings(); } break; @@ -749,7 +760,11 @@ void Label::_notification(int p_what) { int shadow_outline_size = has_settings ? settings->get_shadow_size() : theme_cache.font_shadow_outline_size; bool rtl_layout = is_layout_rtl(); - style->draw(ci, Rect2(Point2(0, 0), get_size())); + if (has_focus()) { + theme_cache.focus_style->draw(ci, Rect2(Point2(0, 0), get_size())); + } else { + theme_cache.normal_style->draw(ci, Rect2(Point2(0, 0), get_size())); + } bool trim_chars = (visible_chars >= 0) && (visible_chars_behavior == TextServer::VC_CHARS_AFTER_SHAPING); bool trim_glyphs_ltr = (visible_chars >= 0) && ((visible_chars_behavior == TextServer::VC_GLYPHS_LTR) || ((visible_chars_behavior == TextServer::VC_GLYPHS_AUTO) && !rtl_layout)); @@ -1070,7 +1085,7 @@ void Label::set_horizontal_alignment(HorizontalAlignment p_alignment) { } } horizontal_alignment = p_alignment; - + queue_accessibility_update(); queue_redraw(); } @@ -1103,6 +1118,7 @@ void Label::set_text(const String &p_string) { if (visible_ratio < 1) { visible_chars = get_total_character_count() * visible_ratio; } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); update_configuration_warnings(); @@ -1193,6 +1209,7 @@ void Label::set_paragraph_separator(const String &p_paragraph_separator) { if (paragraph_separator != p_paragraph_separator) { paragraph_separator = p_paragraph_separator; text_dirty = true; + queue_accessibility_update(); queue_redraw(); } } @@ -1285,6 +1302,7 @@ void Label::set_visible_characters(int p_amount) { } if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { text_dirty = true; + queue_accessibility_update(); } queue_redraw(); } @@ -1309,6 +1327,7 @@ void Label::set_visible_ratio(float p_ratio) { if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { text_dirty = true; + queue_accessibility_update(); } queue_redraw(); } @@ -1326,6 +1345,7 @@ void Label::set_visible_characters_behavior(TextServer::VisibleCharactersBehavio if (visible_chars_behavior != p_behavior) { if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING || p_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { text_dirty = true; + queue_accessibility_update(); } visible_chars_behavior = p_behavior; queue_redraw(); @@ -1448,6 +1468,7 @@ void Label::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options"); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, Label, normal_style, "normal"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, Label, focus_style, "focus"); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Label, line_spacing); BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Label, paragraph_spacing); @@ -1463,6 +1484,7 @@ void Label::_bind_methods() { } Label::Label(const String &p_text) { + set_focus_mode(FOCUS_ACCESSIBILITY); set_mouse_filter(MOUSE_FILTER_IGNORE); set_text(p_text); set_v_size_flags(SIZE_SHRINK_CENTER); diff --git a/scene/gui/label.h b/scene/gui/label.h index 34b25d77262..b4fa53e1b23 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -90,6 +90,7 @@ private: struct ThemeCache { Ref normal_style; + Ref focus_style; Ref font; int font_size = 0; diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 4eace744e1c..d0993c554ce 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -33,6 +33,7 @@ #include "core/input/input_map.h" #include "core/os/keyboard.h" #include "core/os/os.h" +#include "core/string/translation_server.h" #include "scene/gui/label.h" #include "scene/main/window.h" #include "scene/theme/theme_db.h" @@ -486,6 +487,7 @@ void LineEdit::gui_input(const Ref &p_event) { if (!pass && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(text); } + queue_accessibility_update(); } else if (b->is_double_click()) { // Double-click select word. last_dblclk = OS::get_singleton()->get_ticks_msec(); @@ -500,6 +502,7 @@ void LineEdit::gui_input(const Ref &p_event) { selection.creating = true; selection.start_column = caret_column; set_caret_column(selection.end); + queue_accessibility_update(); break; } } @@ -973,7 +976,9 @@ void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { if (p_data.is_string() && is_editable()) { apply_ime(); - set_caret_at_pixel_pos(p_point.x); + if (p_point != Vector2(INFINITY, INFINITY)) { + set_caret_at_pixel_pos(p_point.x); + } int caret_column_tmp = caret_column; bool is_inside_sel = selection.enabled && caret_column >= selection.begin && caret_column <= selection.end; if (Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { @@ -1009,6 +1014,7 @@ void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { } text_changed_dirty = true; } + queue_accessibility_update(); queue_redraw(); } } @@ -1035,6 +1041,33 @@ void LineEdit::_update_theme_item_cache() { theme_cache.base_scale = get_theme_default_base_scale(); } +void LineEdit::_accessibility_action_set_selection(const Variant &p_data) { + Dictionary new_selection = p_data; + int sel_start_pos = new_selection["start_char"]; + int sel_end_pos = new_selection["end_char"]; + select(sel_start_pos, sel_end_pos); +} + +void LineEdit::_accessibility_action_replace_selected(const Variant &p_data) { + String new_text = p_data; + insert_text_at_caret(new_text); +} + +void LineEdit::_accessibility_action_set_value(const Variant &p_data) { + String new_text = p_data; + set_text(new_text); +} + +void LineEdit::_accessibility_action_menu(const Variant &p_data) { + _update_context_menu(); + + Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + theme_cache.font->get_height(theme_cache.font_size)) / 2); + menu->set_position(get_screen_position() + pos); + menu->reset_size(); + menu->popup(); + menu->grab_focus(); +} + void LineEdit::_notification(int p_what) { switch (p_what) { #ifdef TOOLS_ENABLED @@ -1049,6 +1082,98 @@ void LineEdit::_notification(int p_what) { } } break; #endif + case NOTIFICATION_EXIT_TREE: + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + accessibility_text_root_element = RID(); + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_TEXT_FIELD); + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + if (using_placeholder && !placeholder.is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_placeholder(ae, atr(placeholder)); + } + if (!placeholder.is_empty() && get_accessibility_name().is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_name(ae, atr(placeholder)); + } + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_READONLY, !editable); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_TEXT_SELECTION, callable_mp(this, &LineEdit::_accessibility_action_set_selection)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_REPLACE_SELECTED_TEXT, callable_mp(this, &LineEdit::_accessibility_action_replace_selected)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &LineEdit::_accessibility_action_set_value)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SHOW_CONTEXT_MENU, callable_mp(this, &LineEdit::_accessibility_action_menu)); + if (!language.is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_language(ae, language); + } else { + DisplayServer::get_singleton()->accessibility_update_set_language(ae, TranslationServer::get_singleton()->get_tool_locale()); + } + + bool rtl = is_layout_rtl(); + Ref style = theme_cache.normal; + Ref font = theme_cache.font; + + Size2 size = get_size(); + + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; + float text_height = TS->shaped_text_get_size(text_rid).y; + int y_area = size.height - style->get_minimum_size().height; + int y_ofs = style->get_offset().y + (y_area - text_height) / 2; + + switch (alignment) { + case HORIZONTAL_ALIGNMENT_FILL: + case HORIZONTAL_ALIGNMENT_LEFT: { + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - Math::ceil(style->get_margin(SIDE_RIGHT) + (text_width)))); + } else { + x_ofs = style->get_offset().x; + } + } break; + case HORIZONTAL_ALIGNMENT_CENTER: { + if (!Math::is_zero_approx(scroll_offset)) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - (text_width)) / 2); + } + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - Math::ceil(style->get_margin(SIDE_RIGHT) + (text_width)))); + } + } break; + } + bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; + if (right_icon.is_valid() || display_clear_icon) { + Ref r_icon = display_clear_icon ? theme_cache.clear_icon : right_icon; + if (alignment == HORIZONTAL_ALIGNMENT_CENTER) { + if (Math::is_zero_approx(scroll_offset)) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); + } + } + + float text_off_x = x_ofs + scroll_offset; + + if (accessibility_text_root_element.is_null()) { + accessibility_text_root_element = DisplayServer::get_singleton()->accessibility_create_sub_text_edit_elements(ae, using_placeholder ? RID() : text_rid, text_height); + } + + Transform2D text_xform; + text_xform.set_origin(Vector2i(text_off_x, y_ofs)); + DisplayServer::get_singleton()->accessibility_update_set_transform(accessibility_text_root_element, text_xform); + if (selection.enabled) { + DisplayServer::get_singleton()->accessibility_update_set_text_selection(ae, accessibility_text_root_element, selection.begin, accessibility_text_root_element, selection.end); + } else { + DisplayServer::get_singleton()->accessibility_update_set_text_selection(ae, accessibility_text_root_element, caret_column, accessibility_text_root_element, caret_column); + } + } break; case NOTIFICATION_RESIZED: { _fit_to_width(); @@ -1209,6 +1334,7 @@ void LineEdit::_notification(int p_what) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_color); } } + const Glyph *glyphs = TS->shaped_text_get_glyphs(text_rid); int gl_size = TS->shaped_text_get_glyph_count(text_rid); @@ -1921,6 +2047,8 @@ void LineEdit::set_caret_column(int p_column) { caret_column = p_column; + queue_accessibility_update(); + // Fit to window. if (!is_inside_tree()) { @@ -1989,6 +2117,7 @@ void LineEdit::set_caret_column(int p_column) { scroll_offset = MIN(0, scroll_offset); + queue_accessibility_update(); queue_redraw(); } @@ -2083,6 +2212,7 @@ void LineEdit::deselect() { selection.enabled = false; selection.creating = false; selection.double_click = false; + queue_accessibility_update(); queue_redraw(); } @@ -2141,6 +2271,7 @@ void LineEdit::selection_fill_at_caret() { } selection.enabled = (selection.begin != selection.end); + queue_accessibility_update(); } void LineEdit::select_all() { @@ -2156,6 +2287,7 @@ void LineEdit::select_all() { selection.begin = 0; selection.end = text.length(); selection.enabled = true; + queue_accessibility_update(); queue_redraw(); } @@ -2173,6 +2305,7 @@ void LineEdit::set_editable(bool p_editable) { _validate_caret_can_draw(); update_minimum_size(); + queue_accessibility_update(); queue_redraw(); } @@ -2242,6 +2375,7 @@ void LineEdit::select(int p_from, int p_to) { selection.end = p_to; selection.creating = false; selection.double_click = false; + queue_accessibility_update(); queue_redraw(); } @@ -2635,6 +2769,13 @@ void LineEdit::_shape() { if ((expand_to_text_length && old_size.x != size.x) || (old_size.y != size.y)) { update_minimum_size(); } + + if (accessibility_text_root_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(accessibility_text_root_element); + accessibility_text_root_element = RID(); + } + + queue_accessibility_update(); } void LineEdit::_fit_to_width() { diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index b9d2fa4ee54..13affe6a560 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -105,6 +105,7 @@ private: Point2 ime_selection; RID text_rid; + RID accessibility_text_root_element; float full_width = 0.0; bool selecting_enabled = true; @@ -264,6 +265,11 @@ protected: virtual void unhandled_key_input(const Ref &p_event) override; virtual void gui_input(const Ref &p_event) override; + void _accessibility_action_set_selection(const Variant &p_data); + void _accessibility_action_replace_selected(const Variant &p_data); + void _accessibility_action_set_value(const Variant &p_data); + void _accessibility_action_menu(const Variant &p_data); + public: void edit(); void unedit(); diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index 8b330555221..46f364f0c73 100644 --- a/scene/gui/link_button.cpp +++ b/scene/gui/link_button.cpp @@ -44,6 +44,8 @@ void LinkButton::_shape() { } TS->shaped_text_set_bidi_override(text_buf->get_rid(), structured_text_parser(st_parser, st_args, xl_text)); text_buf->add_string(xl_text, font, font_size, language); + + queue_accessibility_update(); } void LinkButton::set_text(const String &p_text) { @@ -109,7 +111,10 @@ String LinkButton::get_language() const { } void LinkButton::set_uri(const String &p_uri) { - uri = p_uri; + if (uri != p_uri) { + uri = p_uri; + queue_accessibility_update(); + } } String LinkButton::get_uri() const { @@ -147,6 +152,17 @@ Size2 LinkButton::get_minimum_size() const { void LinkButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_LINK); + if (!xl_text.is_empty() && get_accessibility_name().is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_name(ae, xl_text); + } + DisplayServer::get_singleton()->accessibility_update_set_url(ae, uri); + } break; + case NOTIFICATION_TRANSLATION_CHANGED: { xl_text = atr(text); _shape(); @@ -288,7 +304,7 @@ void LinkButton::_bind_methods() { LinkButton::LinkButton(const String &p_text) { text_buf.instantiate(); - set_focus_mode(FOCUS_NONE); + set_focus_mode(FOCUS_ACCESSIBILITY); set_default_cursor_shape(CURSOR_POINTING_HAND); set_text(p_text); diff --git a/scene/gui/menu_bar.cpp b/scene/gui/menu_bar.cpp index 81742fc9165..16a13812f62 100644 --- a/scene/gui/menu_bar.cpp +++ b/scene/gui/menu_bar.cpp @@ -85,6 +85,15 @@ void MenuBar::gui_input(const Ref &p_event) { _open_popup(selected_menu, true); } return; + } else if (p_event->is_action("ui_accept", true) && p_event->is_pressed()) { + if (focused_menu == -1) { + focused_menu = 0; + } + selected_menu = focused_menu; + if (active_menu >= 0) { + get_menu_popup(active_menu)->hide(); + } + _open_popup(selected_menu, true); } Ref mm = p_event; @@ -276,6 +285,12 @@ void MenuBar::unbind_global_menu() { void MenuBar::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_MENU_BAR); + } break; case NOTIFICATION_ENTER_TREE: { if (get_menu_count() > 0) { _refresh_menu_names(); @@ -408,6 +423,10 @@ void MenuBar::_draw_menu_item(int p_index) { bool pressed = (active_menu == p_index); bool rtl = is_layout_rtl(); + if (has_focus() && focused_menu == -1 && p_index == 0) { + hovered = true; + } + if (menu_cache[p_index].hidden) { return; } @@ -950,6 +969,7 @@ String MenuBar::get_tooltip(const Point2 &p_pos) const { } MenuBar::MenuBar() { + set_focus_mode(FOCUS_ALL); set_process_shortcut_input(true); } diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index dee54d9b728..d9f35f44d37 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -126,6 +126,14 @@ int MenuButton::get_item_count() const { void MenuButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_BUTTON); + DisplayServer::get_singleton()->accessibility_update_set_popup_type(ae, DisplayServer::AccessibilityPopupType::POPUP_MENU); + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { popup->set_layout_direction((Window::LayoutDirection)get_layout_direction()); } break; @@ -218,7 +226,7 @@ MenuButton::MenuButton(const String &p_text) : set_toggle_mode(true); set_disable_shortcuts(false); set_process_shortcut_input(true); - set_focus_mode(FOCUS_NONE); + set_focus_mode(FOCUS_ACCESSIBILITY); set_action_mode(ACTION_MODE_BUTTON_PRESS); popup = memnew(PopupMenu); diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index af2cc8ffde4..9d9d04424b9 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -73,6 +73,14 @@ Size2 OptionButton::get_minimum_size() const { void OptionButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_BUTTON); + DisplayServer::get_singleton()->accessibility_update_set_popup_type(ae, DisplayServer::AccessibilityPopupType::POPUP_LIST); + } break; + case NOTIFICATION_POSTINITIALIZE: { _refresh_size_cache(); if (has_theme_icon(SNAME("arrow"))) { diff --git a/scene/gui/panel.cpp b/scene/gui/panel.cpp index 8096191933d..9f175ea34fe 100644 --- a/scene/gui/panel.cpp +++ b/scene/gui/panel.cpp @@ -33,6 +33,13 @@ void Panel::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_PANEL); + } break; + case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); theme_cache.panel_style->draw(ci, Rect2(Point2(), get_size())); diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index cb88492d2c3..22323e1890c 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -38,7 +38,7 @@ #include "scene/theme/theme_db.h" void Popup::_input_from_window(const Ref &p_event) { - if (get_flag(FLAG_POPUP) && p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { + if ((ac_popup || 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. _close_pressed(); } @@ -115,7 +115,7 @@ void Popup::_notification(int p_what) { } break; case NOTIFICATION_APPLICATION_FOCUS_OUT: { - if (!is_in_edited_scene_root() && get_flag(FLAG_POPUP)) { + if (!is_in_edited_scene_root() && (get_flag(FLAG_POPUP) || ac_popup)) { if (hide_reason == HIDE_REASON_NONE) { hide_reason = HIDE_REASON_UNFOCUSED; } @@ -126,7 +126,7 @@ void Popup::_notification(int p_what) { } void Popup::_parent_focused() { - if (popped_up && get_flag(FLAG_POPUP)) { + if (popped_up && (get_flag(FLAG_POPUP) || ac_popup)) { if (hide_reason == HIDE_REASON_NONE) { hide_reason = HIDE_REASON_UNFOCUSED; } diff --git a/scene/gui/popup.h b/scene/gui/popup.h index 1afc9a74586..de03ec64b72 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -40,6 +40,7 @@ class Popup : public Window { GDCLASS(Popup, Window); LocalVector visible_parents; + bool ac_popup = false; bool popped_up = false; public: @@ -59,6 +60,7 @@ protected: void _close_pressed(); virtual Rect2i _popup_adjust_rect() const override; virtual void _input_from_window(const Ref &p_event) override; + void set_ac_popup() { ac_popup = true; } void _notification(int p_what); void _validate_property(PropertyInfo &p_property) const; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 2baaa580c0c..3ce63969657 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -499,9 +499,11 @@ 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) { + 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(); set_input_as_handled(); match_found = true; @@ -513,9 +515,11 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) { // 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) { + 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(); set_input_as_handled(); break; @@ -537,9 +541,11 @@ 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) { + 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(); set_input_as_handled(); match_found = true; @@ -551,9 +557,11 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) { // 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) { + 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(); set_input_as_handled(); break; @@ -738,9 +746,11 @@ void PopupMenu::_input_from_window_internal(const Ref &p_event) { } if (items[i].text.findn(search_string) == 0) { + 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(); set_input_as_handled(); break; @@ -755,6 +765,7 @@ void PopupMenu::_mouse_over_update(const Point2 &p_over) { if (id < 0) { mouse_over = -1; + queue_accessibility_update(); control->queue_redraw(); return; } @@ -766,6 +777,7 @@ void PopupMenu::_mouse_over_update(const Point2 &p_over) { if (over != mouse_over) { mouse_over = over; + queue_accessibility_update(); control->queue_redraw(); } } @@ -1104,8 +1116,105 @@ void PopupMenu::remove_child_notify(Node *p_child) { _menu_changed(); } +void PopupMenu::_accessibility_action_click(const Variant &p_data, int p_idx) { + activate_item(p_idx); +} + +RID PopupMenu::get_focused_accessibility_element() const { + if (mouse_over == -1) { + return get_accessibility_element(); + } else { + const Item &item = items[mouse_over]; + return item.accessibility_item_element; + } +} + void PopupMenu::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_EXIT_TREE: { + if (system_menu_id != NativeMenu::INVALID_MENU_ID) { + unbind_global_menu(); + } + [[fallthrough]]; + } + + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + for (int i = 0; i < items.size(); i++) { + items.write[i].accessibility_item_element = RID(); + } + accessibility_scroll_element = RID(); + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + if (has_meta("_menu_name")) { + DisplayServer::get_singleton()->accessibility_update_set_name(ae, get_meta("_menu_name", get_name())); + } + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_MENU); + DisplayServer::get_singleton()->accessibility_update_set_list_item_count(ae, items.size()); + + if (accessibility_scroll_element.is_null()) { + accessibility_scroll_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } + + Transform2D scroll_xform; + scroll_xform.set_origin(Vector2i(0, -scroll_container->get_v_scroll_bar()->get_value())); + DisplayServer::get_singleton()->accessibility_update_set_transform(accessibility_scroll_element, scroll_xform); + DisplayServer::get_singleton()->accessibility_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; + + for (int i = 0; i < items.size(); i++) { + const Item &item = items.write[i]; + + ofs.y += i > 0 ? theme_cache.v_separation : (float)theme_cache.v_separation / 2; + + Point2 item_ofs = ofs; + if (item.accessibility_item_element.is_null()) { + item.accessibility_item_element = DisplayServer::get_singleton()->accessibility_create_sub_element(accessibility_scroll_element, DisplayServer::AccessibilityRole::ROLE_MENU_ITEM); + item.accessibility_item_dirty = true; + } + + item_ofs.x += item.indent * theme_cache.indent; + float h = _get_item_height(i); + + if (item.accessibility_item_dirty || i == prev_mouse_over || i == mouse_over) { + switch (item.checkable_type) { + case Item::CHECKABLE_TYPE_NONE: { + DisplayServer::get_singleton()->accessibility_update_set_role(item.accessibility_item_element, DisplayServer::AccessibilityRole::ROLE_MENU_ITEM); + } break; + case Item::CHECKABLE_TYPE_CHECK_BOX: { + DisplayServer::get_singleton()->accessibility_update_set_role(item.accessibility_item_element, DisplayServer::AccessibilityRole::ROLE_MENU_ITEM_CHECK_BOX); + DisplayServer::get_singleton()->accessibility_update_set_checked(item.accessibility_item_element, item.checked); + } break; + case Item::CHECKABLE_TYPE_RADIO_BUTTON: { + DisplayServer::get_singleton()->accessibility_update_set_role(item.accessibility_item_element, DisplayServer::AccessibilityRole::ROLE_MENU_ITEM_RADIO); + DisplayServer::get_singleton()->accessibility_update_set_checked(item.accessibility_item_element, item.checked); + } break; + } + + DisplayServer::get_singleton()->accessibility_update_add_action(item.accessibility_item_element, DisplayServer::AccessibilityAction::ACTION_CLICK, callable_mp(this, &PopupMenu::_accessibility_action_click).bind(i)); + DisplayServer::get_singleton()->accessibility_update_set_list_item_index(item.accessibility_item_element, i); + DisplayServer::get_singleton()->accessibility_update_set_list_item_level(item.accessibility_item_element, 0); + DisplayServer::get_singleton()->accessibility_update_set_list_item_selected(item.accessibility_item_element, i == mouse_over); + DisplayServer::get_singleton()->accessibility_update_set_name(item.accessibility_item_element, item.xl_text); + DisplayServer::get_singleton()->accessibility_update_set_flag(item.accessibility_item_element, DisplayServer::AccessibilityFlags::FLAG_DISABLED, item.disabled); + DisplayServer::get_singleton()->accessibility_update_set_tooltip(item.accessibility_item_element, item.tooltip); + + DisplayServer::get_singleton()->accessibility_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; + } + prev_mouse_over = -1; + + } break; + case NOTIFICATION_ENTER_TREE: { PopupMenu *pm = Object::cast_to(get_parent()); if (pm) { @@ -1118,12 +1227,6 @@ void PopupMenu::_notification(int p_what) { } } break; - case NOTIFICATION_EXIT_TREE: { - if (system_menu_id != NativeMenu::INVALID_MENU_ID) { - unbind_global_menu(); - } - } break; - case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { panel->add_theme_style_override(SceneStringName(panel), theme_cache.panel_style); @@ -1151,7 +1254,9 @@ void PopupMenu::_notification(int p_what) { if (is_global) { nmenu->set_item_text(global_menu, i, item.xl_text); } + item.accessibility_item_dirty = true; _shape_item(i); + queue_accessibility_update(); } child_controls_changed(); @@ -1166,6 +1271,7 @@ void PopupMenu::_notification(int p_what) { case NOTIFICATION_WM_MOUSE_EXIT: { if (mouse_over >= 0 && (!items[mouse_over].submenu || submenu_over != -1)) { mouse_over = -1; + queue_accessibility_update(); control->queue_redraw(); } } break; @@ -1281,7 +1387,9 @@ void PopupMenu::_notification(int p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { if (!is_visible()) { if (mouse_over >= 0) { + prev_mouse_over = mouse_over; mouse_over = -1; + queue_accessibility_update(); control->queue_redraw(); } @@ -1341,6 +1449,7 @@ void PopupMenu::add_item(const String &p_label, int p_id, Key p_accel) { } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1364,6 +1473,7 @@ void PopupMenu::add_icon_item(const Ref &p_icon, const String &p_labe } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1387,6 +1497,7 @@ void PopupMenu::add_check_item(const String &p_label, int p_id, Key p_accel) { } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1412,6 +1523,7 @@ void PopupMenu::add_icon_check_item(const Ref &p_icon, const String & } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1435,6 +1547,7 @@ void PopupMenu::add_radio_check_item(const String &p_label, int p_id, Key p_acce } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1460,6 +1573,7 @@ void PopupMenu::add_icon_radio_check_item(const Ref &p_icon, const St } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1485,6 +1599,7 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1522,6 +1637,7 @@ void PopupMenu::add_shortcut(const Ref &p_shortcut, int p_id, bool p_g } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1551,6 +1667,7 @@ void PopupMenu::add_icon_shortcut(const Ref &p_icon, const Refqueue_redraw(); child_controls_changed(); @@ -1580,6 +1697,7 @@ void PopupMenu::add_check_shortcut(const Ref &p_shortcut, int p_id, bo } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1611,6 +1729,7 @@ void PopupMenu::add_icon_check_shortcut(const Ref &p_icon, const Ref< } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1640,6 +1759,7 @@ void PopupMenu::add_radio_check_shortcut(const Ref &p_shortcut, int p_ } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1671,6 +1791,7 @@ void PopupMenu::add_icon_radio_check_shortcut(const Ref &p_icon, cons } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1697,6 +1818,7 @@ void PopupMenu::add_submenu_node_item(const String &p_label, PopupMenu *p_submen item.text = p_label; item.xl_text = atr(p_label); item.id = p_id == -1 ? items.size() : p_id; + item.accessibility_item_dirty = true; item.submenu = p_submenu; item.submenu_name = p_submenu->get_name(); items.push_back(item); @@ -1710,6 +1832,7 @@ void PopupMenu::add_submenu_node_item(const String &p_label, PopupMenu *p_submen } _shape_item(items.size() - 1); + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); @@ -1733,13 +1856,16 @@ void PopupMenu::set_item_text(int p_idx, const String &p_text) { items.write[p_idx].text = p_text; items.write[p_idx].xl_text = _atr(p_idx, p_text); items.write[p_idx].dirty = true; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_text(global_menu, p_idx, items[p_idx].xl_text); } - _shape_item(p_idx); + _shape_item(p_idx); + queue_accessibility_update(); control->queue_redraw(); + child_controls_changed(); _menu_changed(); } @@ -1750,9 +1876,14 @@ void PopupMenu::set_item_text_direction(int p_idx, Control::TextDirection p_text } ERR_FAIL_INDEX(p_idx, items.size()); ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (items[p_idx].text_direction != p_text_direction) { items.write[p_idx].text_direction = p_text_direction; items.write[p_idx].dirty = true; + items.write[p_idx].accessibility_item_dirty = true; + + _shape_item(p_idx); + queue_accessibility_update(); control->queue_redraw(); } } @@ -1765,6 +1896,10 @@ void PopupMenu::set_item_language(int p_idx, const String &p_language) { if (items[p_idx].language != p_language) { items.write[p_idx].language = p_language; items.write[p_idx].dirty = true; + items.write[p_idx].accessibility_item_dirty = true; + + _shape_item(p_idx); + queue_accessibility_update(); control->queue_redraw(); } } @@ -1846,11 +1981,13 @@ void PopupMenu::set_item_checked(int p_idx, bool p_checked) { } items.write[p_idx].checked = p_checked; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_checked(global_menu, p_idx, p_checked); } + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1889,11 +2026,13 @@ void PopupMenu::set_item_accelerator(int p_idx, Key p_accel) { items.write[p_idx].accel = p_accel; items.write[p_idx].dirty = true; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_accelerator(global_menu, p_idx, p_accel); } + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1925,11 +2064,13 @@ void PopupMenu::set_item_disabled(int p_idx, bool p_disabled) { } items.write[p_idx].disabled = p_disabled; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_disabled(global_menu, p_idx, p_disabled); } + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -1993,11 +2134,13 @@ void PopupMenu::set_item_submenu_node(int p_idx, PopupMenu *p_submenu) { void PopupMenu::toggle_item_checked(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].checked = !items[p_idx].checked; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_checked(global_menu, p_idx, items[p_idx].checked); } + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); _menu_changed(); @@ -2134,6 +2277,9 @@ void PopupMenu::set_item_as_separator(int p_idx, bool p_separator) { } items.write[p_idx].separator = p_separator; + items.write[p_idx].accessibility_item_dirty = true; + + queue_accessibility_update(); control->queue_redraw(); } @@ -2154,11 +2300,13 @@ void PopupMenu::set_item_as_checkable(int p_idx, bool p_checkable) { } items.write[p_idx].checkable_type = p_checkable ? Item::CHECKABLE_TYPE_CHECK_BOX : Item::CHECKABLE_TYPE_NONE; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_checkable(global_menu, p_idx, p_checkable); } + queue_accessibility_update(); control->queue_redraw(); _menu_changed(); } @@ -2175,11 +2323,13 @@ void PopupMenu::set_item_as_radio_checkable(int p_idx, bool p_radio_checkable) { } items.write[p_idx].checkable_type = p_radio_checkable ? Item::CHECKABLE_TYPE_RADIO_BUTTON : Item::CHECKABLE_TYPE_NONE; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_radio_checkable(global_menu, p_idx, p_radio_checkable); } + queue_accessibility_update(); control->queue_redraw(); _menu_changed(); } @@ -2195,11 +2345,13 @@ void PopupMenu::set_item_tooltip(int p_idx, const String &p_tooltip) { } items.write[p_idx].tooltip = p_tooltip; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_tooltip(global_menu, p_idx, p_tooltip); } + queue_accessibility_update(); control->queue_redraw(); _menu_changed(); } @@ -2299,11 +2451,13 @@ void PopupMenu::set_item_multistate(int p_idx, int p_state) { } items.write[p_idx].state = p_state; + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_state(global_menu, p_idx, p_state); } + queue_accessibility_update(); control->queue_redraw(); _menu_changed(); } @@ -2348,11 +2502,13 @@ void PopupMenu::toggle_item_multistate(int p_idx) { if (items.write[p_idx].max_states <= items[p_idx].state) { items.write[p_idx].state = 0; } + items.write[p_idx].accessibility_item_dirty = true; if (global_menu.is_valid()) { NativeMenu::get_singleton()->set_item_state(global_menu, p_idx, items[p_idx].state); } + queue_accessibility_update(); control->queue_redraw(); _menu_changed(); } @@ -2386,11 +2542,12 @@ void PopupMenu::set_focused_item(int p_idx) { return; } + prev_mouse_over = mouse_over; mouse_over = p_idx; if (mouse_over != -1) { scroll_to_item(mouse_over); } - + queue_accessibility_update(); control->queue_redraw(); } @@ -2412,6 +2569,10 @@ void PopupMenu::set_item_count(int p_count) { if (is_global && prev_size > p_count) { for (int i = prev_size - 1; i >= p_count; i--) { nmenu->remove_item(global_menu, i); + if (items[i].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(items.write[i].accessibility_item_element); + items.write[i].accessibility_item_element = RID(); + } } } @@ -2592,6 +2753,10 @@ void PopupMenu::activate_item(int p_idx) { void PopupMenu::remove_item(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); + if (items[p_idx].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(items.write[p_idx].accessibility_item_element); + items.write[p_idx].accessibility_item_element = RID(); + } if (items[p_idx].shortcut.is_valid()) { _unref_shortcut(items[p_idx].shortcut); } @@ -2611,6 +2776,7 @@ void PopupMenu::add_separator(const String &p_text, int p_id) { Item sep; sep.separator = true; sep.id = p_id; + sep.accessibility_item_dirty = true; if (!p_text.is_empty()) { sep.text = p_text; sep.xl_text = atr(p_text); @@ -2626,7 +2792,11 @@ void PopupMenu::add_separator(const String &p_text, int p_id) { } void PopupMenu::clear(bool p_free_submenus) { - for (const Item &I : items) { + for (Item &I : items) { + if (I.accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(I.accessibility_item_element); + I.accessibility_item_element = RID(); + } if (I.shortcut.is_valid()) { _unref_shortcut(I.shortcut); } @@ -2650,7 +2820,9 @@ void PopupMenu::clear(bool p_free_submenus) { } items.clear(); + prev_mouse_over = -1; mouse_over = -1; + queue_accessibility_update(); control->queue_redraw(); child_controls_changed(); notify_property_list_changed(); @@ -3032,7 +3204,15 @@ void PopupMenu::popup(const Rect2i &p_bounds) { if (native) { _native_popup(p_bounds != Rect2i() ? p_bounds : Rect2i(get_position(), Size2i())); } else { - set_flag(FLAG_NO_FOCUS, !is_embedded()); + if (is_inside_tree()) { + bool ac = get_tree()->is_accessibility_enabled(); + // Note: Native popup menus need keyboard focus to work with screen reader. + set_flag(FLAG_POPUP, !ac); + set_flag(FLAG_NO_FOCUS, !is_embedded() && !ac); + if (ac) { + set_ac_popup(); + } + } moved = Vector2(); popup_time_msec = OS::get_singleton()->get_ticks_msec(); @@ -3065,7 +3245,15 @@ void PopupMenu::set_visible(bool p_visible) { _native_popup(Rect2i(get_position(), get_size())); } } else { - set_flag(FLAG_NO_FOCUS, !is_embedded()); + if (is_inside_tree()) { + bool ac = get_tree()->is_accessibility_enabled(); + // Note: Native popup menus need keyboard focus to work with screen reader. + set_flag(FLAG_POPUP, !ac); + set_flag(FLAG_NO_FOCUS, !is_embedded() && !ac); + if (ac) { + set_ac_popup(); + } + } Popup::set_visible(p_visible); } diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index b014b76fd0e..076eb5638c9 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -44,6 +44,9 @@ class PopupMenu : public Popup { static HashMap system_menus; struct Item { + mutable RID accessibility_item_element; + mutable bool accessibility_item_dirty = true; + Ref icon; int icon_max_width = 0; Color icon_modulate = Color(1, 1, 1, 1); @@ -95,6 +98,7 @@ class PopupMenu : public Popup { Item(bool p_dummy) {} }; + RID accessibility_scroll_element; mutable Rect2i pre_popup_rect; void _update_shadow_offsets() const; @@ -122,6 +126,7 @@ class PopupMenu : public Popup { bool during_grabbed_click = false; bool is_scrolling = false; int mouse_over = -1; + int prev_mouse_over = -1; int submenu_over = -1; String _get_accel_text(const Item &p_item) const; int _get_mouse_over(const Point2 &p_over) const; @@ -134,6 +139,8 @@ class PopupMenu : public Popup { void _shape_item(int p_idx) const; + void _accessibility_action_click(const Variant &p_data, int p_idx); + void _activate_submenu(int p_over, bool p_by_keyboard = false); void _submenu_timeout(); @@ -249,6 +256,8 @@ public: // this value should be updated to reflect the new size. static const int ITEM_PROPERTY_SIZE = 10; + virtual RID get_focused_accessibility_element() const override; + virtual void _parent_focused() override; RID bind_global_menu(); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index baf0c394ac9..0e2e18ce179 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -54,6 +54,14 @@ void ProgressBar::_notification(int p_what) { queue_redraw(); } } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_PROGRESS_INDICATOR); + } break; + case NOTIFICATION_DRAW: { draw_style_box(theme_cache.background_style, Rect2(Point2(), get_size())); diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index d7c8d19b6fc..dfecc1765f4 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -43,12 +43,51 @@ PackedStringArray Range::get_configuration_warnings() const { void Range::_value_changed(double p_value) { GDVIRTUAL_CALL(_value_changed, p_value); } + void Range::_value_changed_notify() { _value_changed(shared->val); emit_signal(SceneStringName(value_changed), shared->val); + queue_accessibility_update(); queue_redraw(); } +void Range::_accessibility_action_inc(const Variant &p_data) { + double step = ((shared->step > 0) ? shared->step : 1); + set_value(shared->val + step); +} + +void Range::_accessibility_action_dec(const Variant &p_data) { + double step = ((shared->step > 0) ? shared->step : 1); + set_value(shared->val - step); +} + +void Range::_accessibility_action_set_value(const Variant &p_data) { + double new_val = p_data; + set_value(new_val); +} + +void Range::_notification(int p_what) { + ERR_MAIN_THREAD_GUARD; + switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SPIN_BUTTON); + DisplayServer::get_singleton()->accessibility_update_set_num_value(ae, shared->val); + DisplayServer::get_singleton()->accessibility_update_set_num_range(ae, shared->min, shared->max); + if (shared->step > 0) { + DisplayServer::get_singleton()->accessibility_update_set_num_step(ae, shared->step); + } else { + DisplayServer::get_singleton()->accessibility_update_set_num_step(ae, 1); + } + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_DECREMENT, callable_mp(this, &Range::_accessibility_action_dec)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_INCREMENT, callable_mp(this, &Range::_accessibility_action_inc)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &Range::_accessibility_action_set_value)); + } break; + } +} + void Range::Shared::emit_value_changed() { for (Range *E : owners) { Range *r = E; @@ -80,6 +119,7 @@ void Range::Shared::redraw_owners() { if (!r->is_inside_tree()) { continue; } + r->queue_accessibility_update(); r->queue_redraw(); } } @@ -91,6 +131,7 @@ void Range::set_value(double p_val) { if (shared->val != prev_val) { shared->emit_value_changed(); } + queue_accessibility_update(); } void Range::_set_value_no_signal(double p_val) { @@ -143,6 +184,8 @@ void Range::set_min(double p_min) { shared->emit_changed("min"); update_configuration_warnings(); + + queue_accessibility_update(); } void Range::set_max(double p_max) { @@ -156,6 +199,8 @@ void Range::set_max(double p_max) { set_value(shared->val); shared->emit_changed("max"); + + queue_accessibility_update(); } void Range::set_step(double p_step) { @@ -165,6 +210,8 @@ void Range::set_step(double p_step) { shared->step = p_step; shared->emit_changed("step"); + + queue_accessibility_update(); } void Range::set_page(double p_page) { @@ -177,6 +224,8 @@ void Range::set_page(double p_page) { set_value(shared->val); shared->emit_changed("page"); + + queue_accessibility_update(); } double Range::get_value() const { @@ -264,6 +313,7 @@ void Range::unshare() { nshared->allow_lesser = shared->allow_lesser; _unref_shared(); _ref_shared(nshared); + queue_accessibility_update(); } void Range::_ref_shared(Shared *p_shared) { diff --git a/scene/gui/range.h b/scene/gui/range.h index 4f3fefeff04..165d6693b91 100644 --- a/scene/gui/range.h +++ b/scene/gui/range.h @@ -64,9 +64,14 @@ class Range : public Control { protected: virtual void _value_changed(double p_value); void _notify_shared_value_changed() { shared->emit_value_changed(); } + void _notification(int p_what); static void _bind_methods(); + void _accessibility_action_inc(const Variant &p_data); + void _accessibility_action_dec(const Variant &p_data); + void _accessibility_action_set_value(const Variant &p_data); + bool _rounded_values = false; GDVIRTUAL1(_value_changed, double) diff --git a/scene/gui/rich_text_label.compat.inc b/scene/gui/rich_text_label.compat.inc index c3a6e114f29..41a944b83c5 100644 --- a/scene/gui/rich_text_label.compat.inc +++ b/scene/gui/rich_text_label.compat.inc @@ -51,7 +51,15 @@ void RichTextLabel::_push_meta_bind_compat_89024(const Variant &p_meta) { } void RichTextLabel::_add_image_bind_compat_80410(const Ref &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region) { - add_image(p_image, p_width, p_height, p_color, p_alignment, p_region, Variant(), false, String(), false); + add_image(p_image, p_width, p_height, p_color, p_alignment, p_region, Variant(), false, String(), false, String()); +} + +void RichTextLabel::_add_image_bind_compat_76829(const Ref &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region, const Variant &p_key, bool p_pad, const String &p_tooltip, bool p_size_in_percent) { + add_image(p_image, p_width, p_height, p_color, p_alignment, p_region, p_key, p_pad, p_tooltip, p_size_in_percent, String()); +} + +void RichTextLabel::_push_table_bind_compat_76829(int p_columns, InlineAlignment p_alignment, int p_align_to_row) { + push_table(p_columns, p_alignment, p_align_to_row, String()); } bool RichTextLabel::_remove_paragraph_bind_compat_91098(int p_paragraph) { @@ -65,6 +73,8 @@ void RichTextLabel::_bind_compatibility_methods() { ClassDB::bind_compatibility_method(D_METHOD("push_meta", "data", "underline_mode"), &RichTextLabel::_push_meta_bind_compat_99481, DEFVAL(META_UNDERLINE_ALWAYS)); ClassDB::bind_compatibility_method(D_METHOD("push_meta", "data"), &RichTextLabel::_push_meta_bind_compat_89024); ClassDB::bind_compatibility_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region"), &RichTextLabel::_add_image_bind_compat_80410, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2())); + ClassDB::bind_compatibility_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region", "key", "pad", "tooltip", "size_in_percent"), &RichTextLabel::_add_image_bind_compat_76829, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(Variant()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); + ClassDB::bind_compatibility_method(D_METHOD("push_table", "columns", "inline_align", "align_to_row"), &RichTextLabel::_push_table_bind_compat_76829, DEFVAL(INLINE_ALIGNMENT_TOP), DEFVAL(-1)); ClassDB::bind_compatibility_method(D_METHOD("remove_paragraph", "paragraph"), &RichTextLabel::_remove_paragraph_bind_compat_91098); } diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 6f3c5961e13..f80c1483cc1 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -59,6 +59,9 @@ RichTextLabel::ItemCustomFX::~ItemCustomFX() { } RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) const { + if (!p_item) { + return nullptr; + } if (p_free) { if (p_item->subitems.size()) { return p_item->subitems.front()->get(); @@ -88,7 +91,7 @@ RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) co return p_item->E->next()->get(); } else { // Go up until something with a next is found. - while (p_item->type != ITEM_FRAME && !p_item->E->next()) { + while (p_item->parent && p_item->type != ITEM_FRAME && !p_item->E->next()) { p_item = p_item->parent; } @@ -102,41 +105,38 @@ RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) co } RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) const { + if (!p_item) { + return nullptr; + } if (p_free) { - if (p_item->subitems.size()) { - return p_item->subitems.back()->get(); - } else if (!p_item->parent) { + if (!p_item->parent) { return nullptr; } else if (p_item->E->prev()) { - return p_item->E->prev()->get(); - } else { - // Go back until something with a prev is found. - while (p_item->parent && !p_item->E->prev()) { - p_item = p_item->parent; + p_item = p_item->E->prev()->get(); + while (p_item->subitems.size()) { + p_item = p_item->subitems.back()->get(); } - + return p_item; + } else { if (p_item->parent) { - return p_item->E->prev()->get(); + return p_item->parent; } else { return nullptr; } } } else { - if (p_item->subitems.size() && p_item->type != ITEM_TABLE) { - return p_item->subitems.back()->get(); - } else if (p_item->type == ITEM_FRAME) { + if (p_item->type == ITEM_FRAME) { return nullptr; } else if (p_item->E->prev()) { - return p_item->E->prev()->get(); - } else { - // Go back until something with a prev is found. - while (p_item->type != ITEM_FRAME && !p_item->E->prev()) { - p_item = p_item->parent; + p_item = p_item->E->prev()->get(); + while (p_item->subitems.size() && p_item->type != ITEM_TABLE) { + p_item = p_item->subitems.back()->get(); } - - if (p_item->type != ITEM_FRAME) { - return p_item->E->prev()->get(); + return p_item; + } else { + if (p_item->parent && p_item->type != ITEM_FRAME) { + return p_item->parent; } else { return nullptr; } @@ -361,6 +361,7 @@ float RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref= (int)p_frame->lines.size(), p_h); Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); l.indent = _find_margin(l.from, p_base_font, p_base_font_size) + l.prefix_width; @@ -430,6 +431,7 @@ float RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), p_h); Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); BitField autowrap_flags = TextServer::BREAK_MANDATORY; @@ -449,6 +451,7 @@ float RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref autowrap_flags = autowrap_flags | autowrap_flags_trim; // Clear cache. + l.dc_item = nullptr; l.text_buf->clear(); l.text_buf->set_break_flags(autowrap_flags); l.text_buf->set_justification_flags(_find_jst_flags(l.from)); @@ -527,8 +530,9 @@ float RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref switch (it->type) { case ITEM_DROPCAP: { // Add dropcap. - const ItemDropcap *dc = static_cast(it); + ItemDropcap *dc = static_cast(it); l.text_buf->set_dropcap(dc->text, dc->font, dc->font_size, dc->dropcap_margins); + l.dc_item = dc; l.dc_color = dc->color; l.dc_ol_size = dc->ol_size; l.dc_ol_color = dc->ol_color; @@ -1879,8 +1883,431 @@ void RichTextLabel::_update_theme_item_cache() { use_selected_font_color = theme_cache.font_selected_color != Color(0, 0, 0, 0); } +PackedStringArray RichTextLabel::get_accessibility_configuration_warnings() const { + PackedStringArray warnings = Control::get_accessibility_configuration_warnings(); + + Item *it = main; + while (it) { + if (it->type == ITEM_IMAGE) { + ItemImage *img = static_cast(it); + if (img && img->alt_text.strip_edges().is_empty()) { + warnings.push_back(RTR("Image alternative text must not be empty.")); + } + } + it = _get_next_item(it, true); + } + + return warnings; +} + +void RichTextLabel::_accessibility_update_line(RID p_id, ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, float p_vsep) { + ERR_FAIL_NULL(p_frame); + ERR_FAIL_COND(p_line < 0 || p_line >= (int)p_frame->lines.size()); + + Line &l = p_frame->lines[p_line]; + + if (l.accessibility_line_element.is_valid()) { + return; + } + l.accessibility_line_element = DisplayServer::get_singleton()->accessibility_create_sub_element(p_id, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + + MutexLock lock(l.text_buf->get_mutex()); + + const RID &line_ae = l.accessibility_line_element; + + Rect2 ae_rect = Rect2(p_ofs, Size2(p_width, l.text_buf->get_size().y + l.text_buf->get_line_count() * theme_cache.line_separation)); + DisplayServer::get_singleton()->accessibility_update_set_bounds(line_ae, ae_rect); + ac_element_bounds_cache[line_ae] = ae_rect; + + Item *it_from = l.from; + if (it_from == nullptr) { + return; + } + + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); + bool lrtl = is_layout_rtl(); + + // Process dropcap. + int dc_lines = l.text_buf->get_dropcap_lines(); + float h_off = l.text_buf->get_dropcap_size().x; + + // Process text. + const RID ¶_rid = l.text_buf->get_rid(); + + String l_text = TS->shaped_get_text(para_rid).replace(String::chr(0xfffc), "").strip_edges(); + if (l.dc_item) { + ItemDropcap *dc = static_cast(l.dc_item); + l_text = dc->text + l_text; + } + if (!l_text.is_empty()) { + Vector2 off; + if (rtl) { + off.x = p_width - l.offset.x - l.text_buf->get_width(); + if (!lrtl && p_frame == main) { // Skip Scrollbar. + off.x -= scroll_w; + } + } else { + off.x = l.offset.x; + if (lrtl && p_frame == main) { // Skip Scrollbar. + off.x += scroll_w; + } + } + + l.accessibility_text_element = DisplayServer::get_singleton()->accessibility_create_sub_element(line_ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT); + DisplayServer::get_singleton()->accessibility_update_set_value(l.accessibility_text_element, l_text); + ae_rect = Rect2(p_ofs + off, l.text_buf->get_size()); + DisplayServer::get_singleton()->accessibility_update_set_bounds(l.accessibility_text_element, ae_rect); + ac_element_bounds_cache[l.accessibility_text_element] = ae_rect; + + DisplayServer::get_singleton()->accessibility_update_add_action(l.accessibility_text_element, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &RichTextLabel::_accessibility_focus_item).bind((uint64_t)l.from, true, true)); + DisplayServer::get_singleton()->accessibility_update_add_action(l.accessibility_text_element, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &RichTextLabel::_accessibility_focus_item).bind((uint64_t)l.from, true, false)); + DisplayServer::get_singleton()->accessibility_update_add_action(l.accessibility_text_element, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &RichTextLabel::_accessibility_scroll_to_item).bind((uint64_t)l.from)); + } + + Vector2 off; + for (int line = 0; line < l.text_buf->get_line_count(); line++) { + if (line > 0) { + off.y += (theme_cache.line_separation + p_vsep); + } + + const Size2 line_size = l.text_buf->get_line_size(line); + + float width = l.text_buf->get_width(); + float length = line_size.x; + + // Process line. + + if (rtl) { + off.x = p_width - l.offset.x - width; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + off.x -= scroll_w; + } + } else { + off.x = l.offset.x; + if (lrtl && p_frame == main) { // Skip Scrollbar. + off.x += scroll_w; + } + } + + // Process text. + switch (l.text_buf->get_alignment()) { + case HORIZONTAL_ALIGNMENT_FILL: + case HORIZONTAL_ALIGNMENT_LEFT: { + if (rtl) { + off.x += width - length; + } + } break; + case HORIZONTAL_ALIGNMENT_CENTER: { + off.x += Math::floor((width - length) / 2.0); + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + if (!rtl) { + off.x += width - length; + } + } break; + } + + if (line <= dc_lines) { + if (rtl) { + off.x -= h_off; + } else { + off.x += h_off; + } + } + + const RID &rid = l.text_buf->get_line_rid(line); + + Array objects = TS->shaped_text_get_objects(rid); + for (int i = 0; i < objects.size(); i++) { + Item *it = reinterpret_cast((uint64_t)objects[i]); + if (it != nullptr) { + Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]); + switch (it->type) { + case ITEM_IMAGE: { + ItemImage *img = static_cast(it); + RID img_ae = DisplayServer::get_singleton()->accessibility_create_sub_element(line_ae, DisplayServer::AccessibilityRole::ROLE_IMAGE); + + DisplayServer::get_singleton()->accessibility_update_set_name(img_ae, img->alt_text); + if (img->pad) { + Size2 pad_size = rect.size.min(img->image->get_size()); + Vector2 pad_off = (rect.size - pad_size) / 2; + ae_rect = Rect2(p_ofs + rect.position + off + pad_off, pad_size); + } else { + ae_rect = Rect2(p_ofs + rect.position + off, rect.size); + } + DisplayServer::get_singleton()->accessibility_update_set_bounds(img_ae, ae_rect); + ac_element_bounds_cache[img_ae] = ae_rect; + + DisplayServer::get_singleton()->accessibility_update_add_action(img_ae, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &RichTextLabel::_accessibility_focus_item).bind((uint64_t)it, false, true)); + DisplayServer::get_singleton()->accessibility_update_add_action(img_ae, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &RichTextLabel::_accessibility_focus_item).bind((uint64_t)it, false, false)); + DisplayServer::get_singleton()->accessibility_update_add_action(img_ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &RichTextLabel::_accessibility_scroll_to_item).bind((uint64_t)it)); + + it->accessibility_item_element = img_ae; + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast(it); + float h_separation = theme_cache.table_h_separation; + float v_separation = theme_cache.table_v_separation; + + RID table_ae = DisplayServer::get_singleton()->accessibility_create_sub_element(line_ae, DisplayServer::AccessibilityRole::ROLE_TABLE); + + int col_count = table->columns.size(); + int row_count = table->rows.size(); + + DisplayServer::get_singleton()->accessibility_update_set_name(table_ae, table->name); + DisplayServer::get_singleton()->accessibility_update_set_role(table_ae, DisplayServer::AccessibilityRole::ROLE_TABLE); + DisplayServer::get_singleton()->accessibility_update_set_table_column_count(table_ae, col_count); + DisplayServer::get_singleton()->accessibility_update_set_table_row_count(table_ae, row_count); + ae_rect = Rect2(p_ofs + rect.position + off + Vector2(0, TS->shaped_text_get_ascent(rid)), rect.size); + DisplayServer::get_singleton()->accessibility_update_set_bounds(table_ae, ae_rect); + ac_element_bounds_cache[table_ae] = ae_rect; + + DisplayServer::get_singleton()->accessibility_update_add_action(table_ae, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &RichTextLabel::_accessibility_focus_item).bind((uint64_t)it, false, true)); + DisplayServer::get_singleton()->accessibility_update_add_action(table_ae, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &RichTextLabel::_accessibility_focus_item).bind((uint64_t)it, false, false)); + DisplayServer::get_singleton()->accessibility_update_add_action(table_ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &RichTextLabel::_accessibility_scroll_to_item).bind((uint64_t)it)); + + Vector row_aes; + Vector2 row_off = Vector2(0, TS->shaped_text_get_ascent(rid)); + for (int j = 0; j < row_count; j++) { + RID row_ae = DisplayServer::get_singleton()->accessibility_create_sub_element(table_ae, DisplayServer::AccessibilityRole::ROLE_ROW); + + DisplayServer::get_singleton()->accessibility_update_set_table_row_index(row_ae, j); + ae_rect = Rect2(p_ofs + rect.position + off + row_off, Size2(rect.size.x, table->rows[j])); + DisplayServer::get_singleton()->accessibility_update_set_bounds(row_ae, ae_rect); + ac_element_bounds_cache[row_ae] = ae_rect; + row_off.y += table->rows[j]; + + row_aes.push_back(row_ae); + } + + int idx = 0; + for (Item *E : table->subitems) { + ItemFrame *frame = static_cast(E); + + int col = idx % col_count; + int row = idx / col_count; + + for (int j = 0; j < (int)frame->lines.size(); j++) { + RID cell_ae = DisplayServer::get_singleton()->accessibility_create_sub_element(row_aes[row], DisplayServer::AccessibilityRole::ROLE_CELL); + + if (frame->lines.size() != 0 && row < row_count) { + Vector2 coff = frame->lines[0].offset; + coff.x -= frame->lines[0].indent; + if (rtl) { + coff.x = rect.size.width - table->columns[col].width - coff.x; + } + ae_rect = Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])); + DisplayServer::get_singleton()->accessibility_update_set_bounds(cell_ae, ae_rect); + ac_element_bounds_cache[cell_ae] = ae_rect; + } + DisplayServer::get_singleton()->accessibility_update_set_table_cell_position(cell_ae, row, col); + + _accessibility_update_line(cell_ae, frame, j, p_ofs + rect.position + off + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_vsep); + } + idx++; + } + + it->accessibility_item_element = table_ae; + } break; + default: + break; + } + } + } + + off.y += TS->shaped_text_get_descent(rid) + TS->shaped_text_get_ascent(rid); + } +} + +void RichTextLabel::_accessibility_action_menu(const Variant &p_data) { + if (context_menu_enabled) { + _update_context_menu(); + menu->set_position(get_screen_position()); + menu->reset_size(); + menu->popup(); + menu->grab_focus(); + } +} + +void RichTextLabel::_accessibility_scroll_down(const Variant &p_data) { + vscroll->set_value(vscroll->get_value() + vscroll->get_page() / 4); +} + +void RichTextLabel::_accessibility_scroll_up(const Variant &p_data) { + vscroll->set_value(vscroll->get_value() - vscroll->get_page() / 4); +} + +void RichTextLabel::_accessibility_scroll_set(const Variant &p_data) { + const Point2 &pos = p_data; + vscroll->set_value(pos.y); +} + +void RichTextLabel::_accessibility_focus_item(const Variant &p_data, uint64_t p_item, bool p_line, bool p_foucs) { + Item *it = reinterpret_cast(p_item); + if (p_foucs) { + ItemFrame *f = nullptr; + _find_frame(it, &f, nullptr); + + if (f && it) { + keyboard_focus_frame = f; + keyboard_focus_line = it->line; + keyboard_focus_item = it; + keyboard_focus_on_text = p_line; + } + } else { + keyboard_focus_frame = nullptr; + keyboard_focus_line = 0; + keyboard_focus_item = nullptr; + keyboard_focus_on_text = true; + } +} + +void RichTextLabel::_accessibility_scroll_to_item(const Variant &p_data, uint64_t p_item) { + Item *it = reinterpret_cast(p_item); + ItemFrame *f = nullptr; + _find_frame(it, &f, nullptr); + + if (f && it) { + vscroll->set_value(f->lines[it->line].offset.y); + } +} + +void RichTextLabel::_invalidate_accessibility() { + if (accessibility_scroll_element.is_null()) { + return; + } + + Item *it = main; + while (it) { + if (it->type == ITEM_FRAME) { + ItemFrame *fr = static_cast(it); + for (size_t i = 0; i < fr->lines.size(); i++) { + if (fr->lines[i].accessibility_line_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(fr->lines[i].accessibility_line_element); + } + fr->lines[i].accessibility_line_element = RID(); + fr->lines[i].accessibility_text_element = RID(); + } + } + it->accessibility_item_element = RID(); + it = _get_next_item(it, true); + } +} + +RID RichTextLabel::get_focused_accessibility_element() const { + if (keyboard_focus_frame && keyboard_focus_item) { + if (keyboard_focus_on_text) { + return keyboard_focus_frame->lines[keyboard_focus_line].accessibility_text_element; + } else { + if (keyboard_focus_item->accessibility_item_element.is_valid()) { + return keyboard_focus_item->accessibility_item_element; + } + } + } else { + if (!main->lines.is_empty()) { + return main->lines[0].accessibility_text_element; + } + } + return get_accessibility_element(); +} + void RichTextLabel::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + accessibility_scroll_element = RID(); + Item *it = main; + while (it) { + if (it->type == ITEM_FRAME) { + ItemFrame *fr = static_cast(it); + for (size_t i = 0; i < fr->lines.size(); i++) { + fr->lines[i].accessibility_line_element = RID(); + fr->lines[i].accessibility_text_element = RID(); + } + } + it->accessibility_item_element = RID(); + it = _get_next_item(it, true); + } + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SHOW_CONTEXT_MENU, callable_mp(this, &RichTextLabel::_accessibility_action_menu)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_DOWN, callable_mp(this, &RichTextLabel::_accessibility_scroll_down)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_UP, callable_mp(this, &RichTextLabel::_accessibility_scroll_up)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_SCROLL_OFFSET, callable_mp(this, &RichTextLabel::_accessibility_scroll_set)); + + if (_validate_line_caches()) { + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_BUSY, false); + } else { + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_BUSY, true); + return; // Do not update internal elements if threaded procesisng is not done. + } + + if (accessibility_scroll_element.is_null()) { + accessibility_scroll_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } + Rect2 text_rect = _get_text_rect(); + + Transform2D scroll_xform; + scroll_xform.set_origin(Vector2i(0, -vscroll->get_value())); + DisplayServer::get_singleton()->accessibility_update_set_transform(accessibility_scroll_element, scroll_xform); + DisplayServer::get_singleton()->accessibility_update_set_bounds(accessibility_scroll_element, text_rect); + + MutexLock data_lock(data_mutex); + + int to_line = main->first_invalid_line.load(); + int from_line = 0; + + int total_height = INT32_MAX; + if (to_line && vertical_alignment != VERTICAL_ALIGNMENT_TOP) { + MutexLock lock(main->lines[to_line - 1].text_buf->get_mutex()); + if (theme_cache.line_separation < 0) { + // Do not apply to the last line to avoid cutting text. + total_height = main->lines[to_line - 1].offset.y + main->lines[to_line - 1].text_buf->get_size().y + (main->lines[to_line - 1].text_buf->get_line_count() - 1) * theme_cache.line_separation; + } else { + total_height = main->lines[to_line - 1].offset.y + main->lines[to_line - 1].text_buf->get_size().y + main->lines[to_line - 1].text_buf->get_line_count() * theme_cache.line_separation; + } + } + float vbegin = 0, vsep = 0; + if (text_rect.size.y > total_height) { + switch (vertical_alignment) { + case VERTICAL_ALIGNMENT_TOP: { + // Nothing. + } break; + case VERTICAL_ALIGNMENT_CENTER: { + vbegin = (text_rect.size.y - total_height) / 2; + } break; + case VERTICAL_ALIGNMENT_BOTTOM: { + vbegin = text_rect.size.y - total_height; + } break; + case VERTICAL_ALIGNMENT_FILL: { + int lines = 0; + for (int l = from_line; l < to_line; l++) { + MutexLock lock(main->lines[l].text_buf->get_mutex()); + lines += main->lines[l].text_buf->get_line_count(); + } + if (lines > 1) { + vsep = (text_rect.size.y - total_height) / (lines - 1); + } + } break; + } + } + + ac_element_bounds_cache.clear(); + Point2 ofs = text_rect.get_position() + Vector2(0, vbegin + main->lines[from_line].offset.y); + while (from_line < to_line) { + MutexLock lock(main->lines[from_line].text_buf->get_mutex()); + + _accessibility_update_line(accessibility_scroll_element, main, from_line, ofs, text_rect.size.x, vsep); + ofs.y += main->lines[from_line].text_buf->get_size().y + main->lines[from_line].text_buf->get_line_count() * (theme_cache.line_separation + vsep); + from_line++; + } + } break; + case NOTIFICATION_MOUSE_EXIT: { if (meta_hovering) { meta_hovering = nullptr; @@ -1893,12 +2320,16 @@ void RichTextLabel::_notification(int p_what) { case NOTIFICATION_RESIZED: { _stop_thread(); main->first_resized_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); + queue_accessibility_update(); queue_redraw(); } break; case NOTIFICATION_THEME_CHANGED: { _stop_thread(); main->first_invalid_font_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); + queue_accessibility_update(); queue_redraw(); } break; @@ -1909,12 +2340,27 @@ void RichTextLabel::_notification(int p_what) { } main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); + queue_accessibility_update(); queue_redraw(); } break; case NOTIFICATION_PREDELETE: case NOTIFICATION_EXIT_TREE: { _stop_thread(); + + accessibility_scroll_element = RID(); + Item *it = main; + while (it) { + if (it->type == ITEM_FRAME) { + ItemFrame *fr = static_cast(it); + for (size_t i = 0; i < fr->lines.size(); i++) { + fr->lines[i].accessibility_line_element = RID(); + fr->lines[i].accessibility_text_element = RID(); + } + } + it = _get_next_item(it, true); + } } break; case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: @@ -2030,6 +2476,25 @@ void RichTextLabel::_notification(int p_what) { ofs.y += main->lines[from_line].text_buf->get_size().y + main->lines[from_line].text_buf->get_line_count() * (theme_cache.line_separation + vsep); from_line++; } + if (has_focus() && get_tree()->is_accessibility_enabled()) { + RID ae; + if (keyboard_focus_frame && keyboard_focus_item) { + if (keyboard_focus_on_text) { + ae = keyboard_focus_frame->lines[keyboard_focus_line].accessibility_text_element; + } else { + if (keyboard_focus_item->accessibility_item_element.is_valid()) { + ae = keyboard_focus_item->accessibility_item_element; + } + } + } else { + if (!main->lines.is_empty()) { + ae = main->lines[0].accessibility_text_element; + } + } + if (ac_element_bounds_cache.has(ae)) { + draw_style_box(theme_cache.focus_style, ac_element_bounds_cache[ae]); + } + } } break; case NOTIFICATION_INTERNAL_PROCESS: { @@ -2148,6 +2613,7 @@ void RichTextLabel::gui_input(const Ref &p_event) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } + queue_accessibility_update(); queue_redraw(); break; } @@ -2230,6 +2696,7 @@ void RichTextLabel::gui_input(const Ref &p_event) { if (pan_gesture.is_valid()) { if (scroll_active) { vscroll->scroll(vscroll->get_page() * pan_gesture->get_delta().y * 0.5 / 8); + queue_accessibility_update(); } return; @@ -2243,28 +2710,111 @@ void RichTextLabel::gui_input(const Ref &p_event) { if (k->is_action("ui_page_up", true) && vscroll->is_visible_in_tree()) { vscroll->scroll(-vscroll->get_page()); + queue_accessibility_update(); handled = true; } if (k->is_action("ui_page_down", true) && vscroll->is_visible_in_tree()) { vscroll->scroll(vscroll->get_page()); + queue_accessibility_update(); handled = true; } if (k->is_action("ui_up", true) && vscroll->is_visible_in_tree()) { vscroll->scroll(-theme_cache.normal_font->get_height(theme_cache.normal_font_size)); + queue_accessibility_update(); handled = true; } if (k->is_action("ui_down", true) && vscroll->is_visible_in_tree()) { vscroll->scroll(theme_cache.normal_font->get_height(theme_cache.normal_font_size)); + queue_accessibility_update(); handled = true; } if (k->is_action("ui_home", true) && vscroll->is_visible_in_tree()) { vscroll->scroll_to(0); + queue_accessibility_update(); handled = true; } if (k->is_action("ui_end", true) && vscroll->is_visible_in_tree()) { vscroll->scroll_to(vscroll->get_max()); + queue_accessibility_update(); handled = true; } + if (get_tree()->is_accessibility_enabled()) { + if (k->is_action("ui_left", true)) { + if (keyboard_focus_frame != nullptr) { + if (!keyboard_focus_on_text && keyboard_focus_line < (int)keyboard_focus_frame->lines.size() && keyboard_focus_frame->lines[keyboard_focus_line].from == keyboard_focus_item) { + keyboard_focus_on_text = true; + } else { + Item *it = keyboard_focus_item; + while (it) { + it = _get_prev_item(it, true); + if (it) { + ItemFrame *f = nullptr; + _find_frame(it, &f, nullptr); + if (it->type == ITEM_IMAGE || it->type == ITEM_TABLE) { + keyboard_focus_frame = f; + keyboard_focus_line = it->line; + keyboard_focus_item = it; + keyboard_focus_on_text = false; + break; + } + if (f && !f->lines.is_empty()) { + if (f->lines[it->line].from == it) { + keyboard_focus_frame = f; + keyboard_focus_line = it->line; + keyboard_focus_item = it; + keyboard_focus_on_text = true; + break; + } + } + } + } + } + } + queue_accessibility_update(); + queue_redraw(); + handled = true; + } + if (k->is_action("ui_right", true)) { + if (keyboard_focus_frame == nullptr) { + keyboard_focus_frame = main; + keyboard_focus_line = 0; + keyboard_focus_item = main->lines.is_empty() ? nullptr : main->lines[0].from; + keyboard_focus_on_text = true; + } else { + if (keyboard_focus_on_text && keyboard_focus_item && (keyboard_focus_item->type == ITEM_IMAGE || keyboard_focus_item->type == ITEM_TABLE)) { + keyboard_focus_on_text = false; + } else { + Item *it = keyboard_focus_item; + while (it) { + it = _get_next_item(it, true); + if (it) { + ItemFrame *f = nullptr; + _find_frame(it, &f, nullptr); + if (f && !f->lines.is_empty()) { + if (f->lines[it->line].from == it) { + keyboard_focus_frame = f; + keyboard_focus_line = it->line; + keyboard_focus_item = it; + keyboard_focus_on_text = true; + break; + } + } + if (it->type == ITEM_IMAGE || it->type == ITEM_TABLE) { + keyboard_focus_frame = f; + keyboard_focus_line = it->line; + keyboard_focus_item = it; + keyboard_focus_on_text = false; + break; + } + } + } + } + } + queue_accessibility_update(); + queue_redraw(); + handled = true; + } + } if (is_shortcut_keys_enabled()) { if (k->is_action("ui_text_select_all", true)) { select_all(); @@ -2355,6 +2905,7 @@ void RichTextLabel::gui_input(const Ref &p_event) { } selection.active = true; + queue_accessibility_update(); queue_redraw(); } @@ -2451,7 +3002,7 @@ RichTextLabel::ItemFont *RichTextLabel::_find_font(Item *p_item) { if (fontitem->type == ITEM_FONT) { ItemFont *fi = static_cast(fontitem); switch (fi->def_font) { - case NORMAL_FONT: { + case RTL_NORMAL_FONT: { if (fi->variation) { Ref fc = fi->font; if (fc.is_valid()) { @@ -2464,7 +3015,7 @@ RichTextLabel::ItemFont *RichTextLabel::_find_font(Item *p_item) { fi->font_size = theme_cache.normal_font_size; } } break; - case BOLD_FONT: { + case RTL_BOLD_FONT: { if (fi->variation) { Ref fc = fi->font; if (fc.is_valid()) { @@ -2477,7 +3028,7 @@ RichTextLabel::ItemFont *RichTextLabel::_find_font(Item *p_item) { fi->font_size = theme_cache.bold_font_size; } } break; - case ITALICS_FONT: { + case RTL_ITALICS_FONT: { if (fi->variation) { Ref fc = fi->font; if (fc.is_valid()) { @@ -2490,7 +3041,7 @@ RichTextLabel::ItemFont *RichTextLabel::_find_font(Item *p_item) { fi->font_size = theme_cache.italics_font_size; } } break; - case BOLD_ITALICS_FONT: { + case RTL_BOLD_ITALICS_FONT: { if (fi->variation) { Ref fc = fi->font; if (fc.is_valid()) { @@ -2503,7 +3054,7 @@ RichTextLabel::ItemFont *RichTextLabel::_find_font(Item *p_item) { fi->font_size = theme_cache.bold_italics_font_size; } } break; - case MONO_FONT: { + case RTL_MONO_FONT: { if (fi->variation) { Ref fc = fi->font; if (fc.is_valid()) { @@ -2952,6 +3503,7 @@ void RichTextLabel::_thread_end() { vscroll->hide(); } if (is_visible_in_tree()) { + queue_accessibility_update(); queue_redraw(); } } @@ -3074,6 +3626,7 @@ bool RichTextLabel::_validate_line_caches() { if (!scroll_visible) { vscroll->hide(); } + queue_accessibility_update(); return true; } @@ -3096,6 +3649,7 @@ bool RichTextLabel::_validate_line_caches() { if (!scroll_visible) { vscroll->hide(); } + queue_accessibility_update(); return true; } validating.store(false); @@ -3106,6 +3660,7 @@ bool RichTextLabel::_validate_line_caches() { task = WorkerThreadPool::get_singleton()->add_template_task(this, &RichTextLabel::_thread_function, nullptr, true, vformat("RichTextLabelShape:%x", (int64_t)get_instance_id())); set_physics_process_internal(true); loading_started = OS::get_singleton()->get_ticks_msec(); + queue_accessibility_update(); return false; } else { updating.store(true); @@ -3113,6 +3668,7 @@ bool RichTextLabel::_validate_line_caches() { if (!scroll_visible) { vscroll->hide(); } + queue_accessibility_update(); queue_redraw(); return true; } @@ -3197,6 +3753,7 @@ void RichTextLabel::_process_line_caches() { void RichTextLabel::_invalidate_current_line(ItemFrame *p_frame) { if ((int)p_frame->lines.size() - 1 <= p_frame->first_invalid_line) { p_frame->first_invalid_line = (int)p_frame->lines.size() - 1; + queue_accessibility_update(); } } @@ -3320,6 +3877,7 @@ void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) if (fit_content) { update_minimum_size(); } + queue_accessibility_update(); queue_redraw(); } @@ -3362,7 +3920,7 @@ Size2 RichTextLabel::_get_image_size(const Ref &p_image, int p_width, return ret; } -void RichTextLabel::add_image(const Ref &p_image, int p_width, int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region, const Variant &p_key, bool p_pad, const String &p_tooltip, bool p_size_in_percent) { +void RichTextLabel::add_image(const Ref &p_image, int p_width, int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region, const Variant &p_key, bool p_pad, const String &p_tooltip, bool p_size_in_percent, const String &p_alt_text) { _stop_thread(); MutexLock data_lock(data_mutex); @@ -3397,10 +3955,12 @@ void RichTextLabel::add_image(const Ref &p_image, int p_width, int p_ item->pad = p_pad; item->key = p_key; item->tooltip = p_tooltip; + item->alt_text = p_alt_text; item->image->connect_changed(callable_mp(this, &RichTextLabel::_texture_changed).bind(item->rid), CONNECT_REFERENCE_COUNTED); _add_item(item, false); + update_configuration_warnings(); } void RichTextLabel::update_image(const Variant &p_key, BitField p_mask, const Ref &p_image, int p_width, int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region, bool p_pad, const String &p_tooltip, bool p_size_in_percent) { @@ -3663,7 +4223,13 @@ bool RichTextLabel::invalidate_paragraph(int p_paragraph) { main->first_invalid_line.store(MIN(main->first_invalid_line.load(), p_paragraph)); main->first_resized_line.store(MIN(main->first_resized_line.load(), p_paragraph)); main->first_invalid_font_line.store(MIN(main->first_invalid_font_line.load(), p_paragraph)); + + _invalidate_accessibility(); + if (is_inside_tree()) { + queue_accessibility_update(); + } queue_redraw(); + update_configuration_warnings(); return true; } @@ -3736,33 +4302,33 @@ void RichTextLabel::push_font(const Ref &p_font, int p_size) { void RichTextLabel::push_normal() { ERR_FAIL_COND(theme_cache.normal_font.is_null()); - _push_def_font(NORMAL_FONT); + _push_def_font(RTL_NORMAL_FONT); } void RichTextLabel::push_bold() { ERR_FAIL_COND(theme_cache.bold_font.is_null()); ItemFont *item_font = _find_font(current); - _push_def_font((item_font && item_font->def_font == ITALICS_FONT) ? BOLD_ITALICS_FONT : BOLD_FONT); + _push_def_font((item_font && item_font->def_font == RTL_ITALICS_FONT) ? RTL_BOLD_ITALICS_FONT : RTL_BOLD_FONT); } void RichTextLabel::push_bold_italics() { ERR_FAIL_COND(theme_cache.bold_italics_font.is_null()); - _push_def_font(BOLD_ITALICS_FONT); + _push_def_font(RTL_BOLD_ITALICS_FONT); } void RichTextLabel::push_italics() { ERR_FAIL_COND(theme_cache.italics_font.is_null()); ItemFont *item_font = _find_font(current); - _push_def_font((item_font && item_font->def_font == BOLD_FONT) ? BOLD_ITALICS_FONT : ITALICS_FONT); + _push_def_font((item_font && item_font->def_font == RTL_BOLD_FONT) ? RTL_BOLD_ITALICS_FONT : RTL_ITALICS_FONT); } void RichTextLabel::push_mono() { ERR_FAIL_COND(theme_cache.mono_font.is_null()); - _push_def_font(MONO_FONT); + _push_def_font(RTL_MONO_FONT); } void RichTextLabel::push_font_size(int p_font_size) { @@ -3924,7 +4490,7 @@ void RichTextLabel::push_hint(const String &p_string) { _add_item(item, true); } -void RichTextLabel::push_table(int p_columns, InlineAlignment p_alignment, int p_align_to_row) { +void RichTextLabel::push_table(int p_columns, InlineAlignment p_alignment, int p_align_to_row, const String &p_alt_text) { _stop_thread(); MutexLock data_lock(data_mutex); @@ -3933,6 +4499,7 @@ void RichTextLabel::push_table(int p_columns, InlineAlignment p_alignment, int p ItemTable *item = memnew(ItemTable); item->owner = get_instance_id(); item->rid = items.make_rid(item); + item->name = p_alt_text; item->columns.resize(p_columns); item->total_width = 0; item->inline_align = p_alignment; @@ -4091,6 +4658,17 @@ void RichTextLabel::set_table_column_expand(int p_column, bool p_expand, int p_r table->columns[p_column].expand_ratio = p_ratio; } +void RichTextLabel::set_table_column_name(int p_column, const String &p_name) { + _stop_thread(); + MutexLock data_lock(data_mutex); + + ERR_FAIL_COND(current->type != ITEM_TABLE); + + ItemTable *table = static_cast(current); + ERR_FAIL_INDEX(p_column, (int)table->columns.size()); + table->columns[p_column].name = p_name; +} + void RichTextLabel::set_cell_row_background_color(const Color &p_odd_row_bg, const Color &p_even_row_bg) { _stop_thread(); MutexLock data_lock(data_mutex); @@ -4153,6 +4731,7 @@ void RichTextLabel::push_cell() { item->lines.resize(1); item->lines[0].from = nullptr; item->first_invalid_line.store(0); // parent frame last line ??? + queue_accessibility_update(); } int RichTextLabel::get_current_table_column() const { @@ -4222,6 +4801,11 @@ void RichTextLabel::clear() { main->lines.clear(); main->lines.resize(1); main->first_invalid_line.store(0); + _invalidate_accessibility(); + + keyboard_focus_frame = nullptr; + keyboard_focus_line = 0; + keyboard_focus_item = nullptr; selection.click_frame = nullptr; selection.click_item = nullptr; @@ -4236,6 +4820,8 @@ void RichTextLabel::clear() { if (fit_content) { update_minimum_size(); } + queue_accessibility_update(); + update_configuration_warnings(); } void RichTextLabel::set_tab_size(int p_spaces) { @@ -4247,6 +4833,8 @@ void RichTextLabel::set_tab_size(int p_spaces) { tab_size = p_spaces; main->first_resized_line.store(0); + _invalidate_accessibility(); + queue_accessibility_update(); queue_redraw(); } @@ -4291,6 +4879,7 @@ bool RichTextLabel::is_hint_underlined() const { void RichTextLabel::set_offset(int p_pixel) { vscroll->set_value(p_pixel); + queue_accessibility_update(); } void RichTextLabel::set_scroll_active(bool p_active) { @@ -4540,9 +5129,9 @@ void RichTextLabel::append_text(const String &p_bbcode) { //use bold font in_bold = true; if (in_italics) { - _push_def_font(BOLD_ITALICS_FONT); + _push_def_font(RTL_BOLD_ITALICS_FONT); } else { - _push_def_font(BOLD_FONT); + _push_def_font(RTL_BOLD_FONT); } pos = brk_end + 1; tag_stack.push_front(tag); @@ -4550,15 +5139,15 @@ void RichTextLabel::append_text(const String &p_bbcode) { //use italics font in_italics = true; if (in_bold) { - _push_def_font(BOLD_ITALICS_FONT); + _push_def_font(RTL_BOLD_ITALICS_FONT); } else { - _push_def_font(ITALICS_FONT); + _push_def_font(RTL_ITALICS_FONT); } pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "code") { //use monospace font - _push_def_font(MONO_FONT); + _push_def_font(RTL_MONO_FONT); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag.begins_with("table=")) { @@ -4604,7 +5193,13 @@ void RichTextLabel::append_text(const String &p_bbcode) { row = subtag[3].to_int(); } - push_table(columns, (InlineAlignment)alignment, row); + OptionMap::Iterator alt_text_option = bbcode_options.find("name"); + String alt_text; + if (alt_text_option) { + alt_text = alt_text_option->value; + } + + push_table(columns, (InlineAlignment)alignment, row, alt_text); pos = brk_end + 1; tag_stack.push_front("table"); } else if (tag == "cell") { @@ -5052,6 +5647,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { } String image = bbcode.substr(brk_end + 1, end - brk_end - 1); + String alt_text; Ref texture = ResourceLoader::load(image, "Texture2D"); if (texture.is_valid()) { @@ -5073,6 +5669,11 @@ void RichTextLabel::append_text(const String &p_bbcode) { color = Color::from_string(color_option->value, color); } + OptionMap::Iterator alt_text_option = bbcode_options.find("alt"); + if (alt_text_option) { + alt_text = alt_text_option->value; + } + int width = 0; int height = 0; bool pad = false; @@ -5146,7 +5747,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { } } - add_image(texture, width, height, color, (InlineAlignment)alignment, region, Variant(), pad, tooltip, size_in_percent); + add_image(texture, width, height, color, (InlineAlignment)alignment, region, Variant(), pad, tooltip, size_in_percent, alt_text); } pos = end; @@ -5179,7 +5780,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { if (subtag.size() > 0) { Ref font = theme_cache.normal_font; - DefaultFont def_font = NORMAL_FONT; + DefaultFont def_font = RTL_NORMAL_FONT; ItemFont *font_it = _find_font(current); if (font_it) { @@ -5206,7 +5807,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { fc->set_base_font(font); fc->set_opentype_features(features); - if (def_font != CUSTOM_FONT) { + if (def_font != RTL_CUSTOM_FONT) { _push_def_font_var(def_font, fc); } else { push_font(fc); @@ -5228,7 +5829,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { } else if (tag.begins_with("font ")) { Ref font = theme_cache.normal_font; - DefaultFont def_font = NORMAL_FONT; + DefaultFont def_font = RTL_NORMAL_FONT; int fnt_size = -1; ItemFont *font_it = _find_font(current); @@ -5251,7 +5852,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { Ref font_data = ResourceLoader::load(fnt, "Font"); if (font_data.is_valid()) { font = font_data; - def_font = CUSTOM_FONT; + def_font = RTL_CUSTOM_FONT; } } OptionMap::Iterator size_option = bbcode_options.find("size"); @@ -5360,7 +5961,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { fc->set_base_font(font); - if (def_font != CUSTOM_FONT) { + if (def_font != RTL_CUSTOM_FONT) { _push_def_font_var(def_font, fc, fnt_size); } else { push_font(fc, fnt_size); @@ -5560,6 +6161,7 @@ void RichTextLabel::scroll_to_selection() { float line_offset = get_selection_line_offset(); if (line_offset != -1.0) { vscroll->set_value(line_offset); + queue_accessibility_update(); } } @@ -5573,6 +6175,7 @@ void RichTextLabel::scroll_to_paragraph(int p_paragraph) { } else { vscroll->set_value(main->lines[p_paragraph].offset.y); } + queue_accessibility_update(); } int RichTextLabel::get_paragraph_count() const { @@ -5591,6 +6194,7 @@ int RichTextLabel::get_visible_paragraph_count() const { void RichTextLabel::scroll_to_line(int p_line) { if (p_line <= 0) { vscroll->set_value(0); + queue_accessibility_update(); return; } _validate_line_caches(); @@ -5605,11 +6209,13 @@ void RichTextLabel::scroll_to_line(int p_line) { line_offset += main->lines[i].text_buf->get_line_ascent(j) + main->lines[i].text_buf->get_line_descent(j) + theme_cache.line_separation; } vscroll->set_value(main->lines[i].offset.y + line_offset); + queue_accessibility_update(); return; } line_count += main->lines[i].text_buf->get_line_count(); } vscroll->set_value(vscroll->get_max()); + queue_accessibility_update(); } float RichTextLabel::get_line_offset(int p_line) { @@ -5692,10 +6298,11 @@ void RichTextLabel::set_selection_enabled(bool p_enabled) { if (selection.active) { deselect(); } - set_focus_mode(FOCUS_NONE); + set_focus_mode(FOCUS_ACCESSIBILITY); } else { set_focus_mode(FOCUS_ALL); } + queue_accessibility_update(); } void RichTextLabel::set_deselect_on_focus_loss_enabled(const bool p_enabled) { @@ -5810,6 +6417,7 @@ bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p selection.to_item = _get_item_at_pos(l.from, it_to, sp + p_string.length()); selection.to_char = sp + p_string.length(); selection.active = true; + queue_accessibility_update(); return true; } @@ -5821,6 +6429,7 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p if (p_string.is_empty()) { selection.active = false; + queue_accessibility_update(); return false; } @@ -6003,6 +6612,7 @@ String RichTextLabel::get_selected_text() const { void RichTextLabel::deselect() { selection.active = false; + queue_accessibility_update(); queue_redraw(); } @@ -6059,6 +6669,7 @@ void RichTextLabel::select_all() { selection.to_char = to_frame->lines[to_line].char_count; selection.to_item = to_item; selection.active = true; + queue_accessibility_update(); queue_redraw(); } @@ -6206,6 +6817,7 @@ void RichTextLabel::set_text_direction(Control::TextDirection p_text_direction) _apply_translation(); } else { main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -6298,6 +6910,7 @@ void RichTextLabel::set_structured_text_bidi_override(TextServer::StructuredText _apply_translation(); } else { main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -6314,6 +6927,7 @@ void RichTextLabel::set_structured_text_bidi_override_options(Array p_args) { st_args = p_args; main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -6332,6 +6946,7 @@ void RichTextLabel::set_language(const String &p_language) { _apply_translation(); } else { main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -6348,6 +6963,7 @@ void RichTextLabel::set_autowrap_mode(TextServer::AutowrapMode p_mode) { autowrap_mode = p_mode; main->first_invalid_line = 0; // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -6389,6 +7005,7 @@ void RichTextLabel::set_visible_ratio(float p_ratio) { if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { main->first_invalid_line.store(0); // Invalidate all lines.. + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -6470,7 +7087,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_parsed_text"), &RichTextLabel::get_parsed_text); ClassDB::bind_method(D_METHOD("add_text", "text"), &RichTextLabel::add_text); ClassDB::bind_method(D_METHOD("set_text", "text"), &RichTextLabel::set_text); - ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region", "key", "pad", "tooltip", "size_in_percent"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(Variant()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align", "region", "key", "pad", "tooltip", "size_in_percent", "alt_text"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(Variant()), DEFVAL(false), DEFVAL(String()), DEFVAL(false), DEFVAL(String())); ClassDB::bind_method(D_METHOD("update_image", "key", "mask", "image", "width", "height", "color", "inline_align", "region", "pad", "tooltip", "size_in_percent"), &RichTextLabel::update_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGNMENT_CENTER), DEFVAL(Rect2()), DEFVAL(false), DEFVAL(String()), DEFVAL(false)); ClassDB::bind_method(D_METHOD("newline"), &RichTextLabel::add_newline); ClassDB::bind_method(D_METHOD("remove_paragraph", "paragraph", "no_invalidate"), &RichTextLabel::remove_paragraph, DEFVAL(false)); @@ -6493,9 +7110,10 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("push_language", "language"), &RichTextLabel::push_language); ClassDB::bind_method(D_METHOD("push_underline"), &RichTextLabel::push_underline); ClassDB::bind_method(D_METHOD("push_strikethrough"), &RichTextLabel::push_strikethrough); - ClassDB::bind_method(D_METHOD("push_table", "columns", "inline_align", "align_to_row"), &RichTextLabel::push_table, DEFVAL(INLINE_ALIGNMENT_TOP), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("push_table", "columns", "inline_align", "align_to_row", "name"), &RichTextLabel::push_table, DEFVAL(INLINE_ALIGNMENT_TOP), DEFVAL(-1), DEFVAL(String())); ClassDB::bind_method(D_METHOD("push_dropcap", "string", "font", "size", "dropcap_margins", "color", "outline_size", "outline_color"), &RichTextLabel::push_dropcap, DEFVAL(Rect2()), DEFVAL(Color(1, 1, 1)), DEFVAL(0), DEFVAL(Color(0, 0, 0, 0))); ClassDB::bind_method(D_METHOD("set_table_column_expand", "column", "expand", "ratio", "shrink"), &RichTextLabel::set_table_column_expand, DEFVAL(1), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("set_table_column_name", "column", "name"), &RichTextLabel::set_table_column_name); ClassDB::bind_method(D_METHOD("set_cell_row_background_color", "odd_row_bg", "even_row_bg"), &RichTextLabel::set_cell_row_background_color); ClassDB::bind_method(D_METHOD("set_cell_border_color", "color"), &RichTextLabel::set_cell_border_color); ClassDB::bind_method(D_METHOD("set_cell_size_override", "min_size", "max_size"), &RichTextLabel::set_cell_size_override); @@ -6759,6 +7377,7 @@ void RichTextLabel::set_visible_characters_behavior(TextServer::VisibleCharacter visible_chars_behavior = p_behavior; main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -6779,6 +7398,7 @@ void RichTextLabel::set_visible_characters(int p_visible) { } if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { main->first_invalid_line.store(0); // Invalidate all lines. + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -7046,6 +7666,7 @@ RichTextLabel::RichTextLabel(const String &p_text) { vscroll->set_step(1); vscroll->hide(); + set_focus_mode(FOCUS_ACCESSIBILITY); set_text(p_text); updating.store(false); validating.store(false); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 8d81a86a7d8..3ff7d2b8f5d 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -106,12 +106,12 @@ public: }; enum DefaultFont { - NORMAL_FONT, - BOLD_FONT, - ITALICS_FONT, - BOLD_ITALICS_FONT, - MONO_FONT, - CUSTOM_FONT, + RTL_NORMAL_FONT, + RTL_BOLD_FONT, + RTL_ITALICS_FONT, + RTL_BOLD_ITALICS_FONT, + RTL_MONO_FONT, + RTL_CUSTOM_FONT, }; enum ImageUpdateMask { @@ -137,8 +137,11 @@ protected: void _push_meta_bind_compat_99481(const Variant &p_meta, MetaUnderline p_underline_mode); void _push_meta_bind_compat_89024(const Variant &p_meta); void _add_image_bind_compat_80410(const Ref &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region); + void _add_image_bind_compat_76829(const Ref &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment, const Rect2 &p_region, const Variant &p_key, bool p_pad, const String &p_tooltip, bool p_size_in_percent); + void _push_table_bind_compat_76829(int p_columns, InlineAlignment p_alignment, int p_align_to_row); bool _remove_paragraph_bind_compat_91098(int p_paragraph); void _set_table_column_expand_bind_compat_101482(int p_column, bool p_expand, int p_ratio); + static void _bind_compatibility_methods(); #endif @@ -151,6 +154,11 @@ private: Ref text_prefix; float prefix_width = 0; Ref text_buf; + + RID accessibility_line_element; + RID accessibility_text_element; + + Item *dc_item = nullptr; Color dc_color; int dc_ol_size = 0; Color dc_ol_color; @@ -160,7 +168,16 @@ private: int char_offset = 0; int char_count = 0; - Line() { text_buf.instantiate(); } + Line() { + text_buf.instantiate(); + } + ~Line() { + if (accessibility_line_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(accessibility_line_element); + accessibility_line_element = RID(); + accessibility_text_element = RID(); + } + } _FORCE_INLINE_ float get_height(float line_separation) const { return offset.y + text_buf->get_size().y + text_buf->get_line_count() * line_separation; @@ -178,6 +195,8 @@ private: int line = 0; RID rid; + RID accessibility_item_element; + void _clear_children() { RichTextLabel *owner_rtl = ObjectDB::get_instance(owner); while (subitems.size()) { @@ -237,6 +256,7 @@ private: struct ItemImage : public Item { Ref image; + String alt_text; InlineAlignment inline_align = INLINE_ALIGNMENT_CENTER; bool pad = false; bool size_in_percent = false; @@ -258,7 +278,7 @@ private: }; struct ItemFont : public Item { - DefaultFont def_font = CUSTOM_FONT; + DefaultFont def_font = RTL_CUSTOM_FONT; Ref font; bool variation = false; bool def_size = false; @@ -341,6 +361,7 @@ private: struct ItemTable : public Item { struct Column { + String name; bool expand = false; bool shrink = true; int expand_ratio = 0; @@ -354,6 +375,7 @@ private: LocalVector rows; LocalVector rows_no_padding; LocalVector rows_baseline; + String name; int align_to_row = -1; int total_width = 0; @@ -503,6 +525,9 @@ private: Array custom_effects; + HashMap ac_element_bounds_cache; + + void _invalidate_accessibility(); void _invalidate_current_line(ItemFrame *p_frame); void _thread_function(void *p_userdata); @@ -552,6 +577,11 @@ private: bool deselect_on_focus_loss_enabled = true; bool drag_and_drop_selection_enabled = true; + ItemFrame *keyboard_focus_frame = nullptr; + int keyboard_focus_line = 0; + Item *keyboard_focus_item = nullptr; + bool keyboard_focus_on_text = true; + bool context_menu_enabled = false; bool shortcut_keys_enabled = true; @@ -580,6 +610,7 @@ private: void _update_line_font(ItemFrame *p_frame, int p_line, const Ref &p_base_font, int p_base_font_size); int _draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, float p_vsep, const Color &p_base_color, int p_outline_size, const Color &p_outline_color, const Color &p_font_shadow_color, int p_shadow_outline_size, const Point2 &p_shadow_ofs, int &r_processed_glyphs); float _find_click_in_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, float p_vsep, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool p_table = false, bool p_meta = false); + void _accessibility_update_line(RID p_id, ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, float p_vsep); String _roman(int p_num, bool p_capitalize) const; String _letters(int p_num, bool p_capitalize) const; @@ -646,6 +677,15 @@ private: bool internal_stack_editing = false; bool stack_externally_modified = false; + void _accessibility_action_menu(const Variant &p_data); + void _accessibility_scroll_down(const Variant &p_data); + void _accessibility_scroll_up(const Variant &p_data); + void _accessibility_scroll_set(const Variant &p_data); + void _accessibility_focus_item(const Variant &p_data, uint64_t p_item, bool p_line, bool p_foucs); + void _accessibility_scroll_to_item(const Variant &p_data, uint64_t p_item); + + RID accessibility_scroll_element; + bool fit_content = false; struct ThemeCache { @@ -692,9 +732,12 @@ private: } theme_cache; public: + virtual RID get_focused_accessibility_element() const override; + PackedStringArray get_accessibility_configuration_warnings() const override; + String get_parsed_text() const; void add_text(const String &p_text); - void add_image(const Ref &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), const Variant &p_key = Variant(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false); + void add_image(const Ref &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), const Variant &p_key = Variant(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false, const String &p_alt_text = String()); void update_image(const Variant &p_key, BitField p_mask, const Ref &p_image, int p_width = 0, int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlignment p_alignment = INLINE_ALIGNMENT_CENTER, const Rect2 &p_region = Rect2(), bool p_pad = false, const String &p_tooltip = String(), bool p_size_in_percent = false); void add_newline(); bool remove_paragraph(int p_paragraph, bool p_no_invalidate = false); @@ -720,7 +763,7 @@ public: void push_list(int p_level, ListType p_list, bool p_capitalize, const String &p_bullet = String::utf8("•")); void push_meta(const Variant &p_meta, MetaUnderline p_underline_mode = META_UNDERLINE_ALWAYS, const String &p_tooltip = String()); void push_hint(const String &p_string); - void push_table(int p_columns, InlineAlignment p_alignment = INLINE_ALIGNMENT_TOP, int p_align_to_row = -1); + void push_table(int p_columns, InlineAlignment p_alignment = INLINE_ALIGNMENT_TOP, int p_align_to_row = -1, const String &p_name = String()); void push_fade(int p_start_index, int p_length); void push_shake(int p_strength, float p_rate, bool p_connected); void push_wave(float p_frequency, float p_amplitude, bool p_connected); @@ -732,6 +775,7 @@ public: void push_customfx(Ref p_custom_effect, Dictionary p_environment); void push_context(); void set_table_column_expand(int p_column, bool p_expand, int p_ratio = 1, bool p_shrink = true); + void set_table_column_name(int p_column, const String &p_name); void set_cell_row_background_color(const Color &p_odd_row_bg, const Color &p_even_row_bg); void set_cell_border_color(const Color &p_color); void set_cell_size_override(const Size2 &p_min_size, const Size2 &p_max_size); diff --git a/scene/gui/scroll_bar.cpp b/scene/gui/scroll_bar.cpp index b81927f1e00..39c86b766b4 100644 --- a/scene/gui/scroll_bar.cpp +++ b/scene/gui/scroll_bar.cpp @@ -224,6 +224,13 @@ void ScrollBar::gui_input(const Ref &p_event) { void ScrollBar::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SCROLL_BAR); + } break; + case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); @@ -654,6 +661,8 @@ ScrollBar::ScrollBar(Orientation p_orientation) { if (focus_by_default) { set_focus_mode(FOCUS_ALL); + } else { + set_focus_mode(FOCUS_ACCESSIBILITY); } set_step(0); } diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index ca7b730e124..00a2e8e01fd 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -353,8 +353,43 @@ void ScrollContainer::_reposition_children() { queue_redraw(); } +void ScrollContainer::_accessibility_action_scroll_set(const Variant &p_data) { + const Point2 &pos = p_data; + h_scroll->set_value(pos.x); + v_scroll->set_value(pos.y); +} + +void ScrollContainer::_accessibility_action_scroll_up(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() / 8); +} + +void ScrollContainer::_accessibility_action_scroll_down(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() / 8); +} + +void ScrollContainer::_accessibility_action_scroll_left(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() / 8); +} + +void ScrollContainer::_accessibility_action_scroll_right(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() / 8); +} + void ScrollContainer::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + 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)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_RIGHT, callable_mp(this, &ScrollContainer::_accessibility_action_scroll_right)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_UP, callable_mp(this, &ScrollContainer::_accessibility_action_scroll_up)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_SCROLL_OFFSET, callable_mp(this, &ScrollContainer::_accessibility_action_scroll_set)); + } break; + case NOTIFICATION_ENTER_TREE: case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: diff --git a/scene/gui/scroll_container.h b/scene/gui/scroll_container.h index 7309c9ea613..7f235270411 100644 --- a/scene/gui/scroll_container.h +++ b/scene/gui/scroll_container.h @@ -99,6 +99,12 @@ protected: void _update_scrollbar_position(); void _scroll_moved(float); + void _accessibility_action_scroll_set(const Variant &p_data); + void _accessibility_action_scroll_up(const Variant &p_data); + void _accessibility_action_scroll_down(const Variant &p_data); + void _accessibility_action_scroll_left(const Variant &p_data); + void _accessibility_action_scroll_right(const Variant &p_data); + public: virtual void gui_input(const Ref &p_gui_input) override; diff --git a/scene/gui/slider.cpp b/scene/gui/slider.cpp index a393ed926af..4d848cd6359 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -237,7 +237,13 @@ void Slider::_notification(int p_what) { } } } + } break; + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SLIDER); } break; case NOTIFICATION_THEME_CHANGED: { diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index 5e8a9f88d62..29aac75ba25 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -34,6 +34,50 @@ #include "core/math/expression.h" #include "scene/theme/theme_db.h" +void SpinBoxLineEdit::_accessibility_action_inc(const Variant &p_data) { + SpinBox *parent_sb = Object::cast_to(get_parent()); + if (parent_sb) { + double step = ((parent_sb->get_step() > 0) ? parent_sb->get_step() : 1); + parent_sb->set_value(parent_sb->get_value() + step); + } +} + +void SpinBoxLineEdit::_accessibility_action_dec(const Variant &p_data) { + SpinBox *parent_sb = Object::cast_to(get_parent()); + if (parent_sb) { + double step = ((parent_sb->get_step() > 0) ? parent_sb->get_step() : 1); + parent_sb->set_value(parent_sb->get_value() - step); + } +} + +void SpinBoxLineEdit::_notification(int p_what) { + ERR_MAIN_THREAD_GUARD; + switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + SpinBox *parent_sb = Object::cast_to(get_parent()); + if (parent_sb) { + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SPIN_BUTTON); + DisplayServer::get_singleton()->accessibility_update_set_name(ae, parent_sb->get_accessibility_name()); + DisplayServer::get_singleton()->accessibility_update_set_description(ae, parent_sb->get_accessibility_description()); + DisplayServer::get_singleton()->accessibility_update_set_live(ae, parent_sb->get_accessibility_live()); + DisplayServer::get_singleton()->accessibility_update_set_num_value(ae, parent_sb->get_value()); + DisplayServer::get_singleton()->accessibility_update_set_num_range(ae, parent_sb->get_min(), parent_sb->get_max()); + if (parent_sb->get_step() > 0) { + DisplayServer::get_singleton()->accessibility_update_set_num_step(ae, parent_sb->get_step()); + } else { + DisplayServer::get_singleton()->accessibility_update_set_num_step(ae, 1); + } + //DisplayServer::get_singleton()->accessibility_update_set_num_jump(ae, ???); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_DECREMENT, callable_mp(this, &SpinBoxLineEdit::_accessibility_action_dec)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_INCREMENT, callable_mp(this, &SpinBoxLineEdit::_accessibility_action_inc)); + } + } break; + } +} + Size2 SpinBox::get_minimum_size() const { Size2 ms = line_edit->get_combined_minimum_size(); ms.width += sizing_cache.buttons_block_width; @@ -650,7 +694,7 @@ void SpinBox::_bind_methods() { } SpinBox::SpinBox() { - line_edit = memnew(LineEdit); + line_edit = memnew(SpinBoxLineEdit); line_edit->set_emoji_menu_enabled(false); add_child(line_edit, false, INTERNAL_MODE_FRONT); diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index e7dbf655c28..977b1f50410 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -34,10 +34,25 @@ #include "scene/gui/range.h" #include "scene/main/timer.h" +class SpinBoxLineEdit : public LineEdit { + GDCLASS(SpinBoxLineEdit, LineEdit); + +protected: + void _notification(int p_what); + + static void _bind_methods() {} + + void _accessibility_action_inc(const Variant &p_data); + void _accessibility_action_dec(const Variant &p_data); + +public: + SpinBoxLineEdit() {} +}; + class SpinBox : public Range { GDCLASS(SpinBox, Range); - LineEdit *line_edit = nullptr; + SpinBoxLineEdit *line_edit = nullptr; bool update_on_text_changed = false; bool accepted = true; diff --git a/scene/gui/split_container.cpp b/scene/gui/split_container.cpp index 78d7fea9bbf..1ab37597226 100644 --- a/scene/gui/split_container.cpp +++ b/scene/gui/split_container.cpp @@ -91,8 +91,59 @@ Control::CursorShape SplitContainerDragger::get_cursor_shape(const Point2 &p_pos return Control::get_cursor_shape(p_pos); } +void SplitContainerDragger::_accessibility_action_inc(const Variant &p_data) { + SplitContainer *sc = Object::cast_to(get_parent()); + + if (sc->collapsed || !sc->_get_sortable_child(0) || !sc->_get_sortable_child(1) || !sc->dragging_enabled) { + return; + } + sc->split_offset -= 10; + sc->_compute_split_offset(true); + sc->queue_sort(); +} + +void SplitContainerDragger::_accessibility_action_dec(const Variant &p_data) { + SplitContainer *sc = Object::cast_to(get_parent()); + + if (sc->collapsed || !sc->_get_sortable_child(0) || !sc->_get_sortable_child(1) || !sc->dragging_enabled) { + return; + } + sc->split_offset += 10; + sc->_compute_split_offset(true); + sc->queue_sort(); +} + +void SplitContainerDragger::_accessibility_action_set_value(const Variant &p_data) { + SplitContainer *sc = Object::cast_to(get_parent()); + + if (sc->collapsed || !sc->_get_sortable_child(0) || !sc->_get_sortable_child(1) || !sc->dragging_enabled) { + return; + } + sc->split_offset = p_data; + sc->_compute_split_offset(true); + sc->queue_sort(); +} + void SplitContainerDragger::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_SPLITTER); + + SplitContainer *sc = Object::cast_to(get_parent()); + if (sc->collapsed || !sc->_get_sortable_child(0) || !sc->_get_sortable_child(1) || !sc->dragging_enabled) { + return; + } + sc->_compute_split_offset(true); + DisplayServer::get_singleton()->accessibility_update_set_num_value(ae, sc->split_offset); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_DECREMENT, callable_mp(this, &SplitContainerDragger::_accessibility_action_dec)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_INCREMENT, callable_mp(this, &SplitContainerDragger::_accessibility_action_inc)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &SplitContainerDragger::_accessibility_action_set_value)); + } break; + case NOTIFICATION_MOUSE_ENTER: { mouse_inside = true; SplitContainer *sc = Object::cast_to(get_parent()); @@ -124,6 +175,10 @@ void SplitContainerDragger::_notification(int p_what) { } } +SplitContainerDragger::SplitContainerDragger() { + set_focus_mode(FOCUS_ACCESSIBILITY); +} + Control *SplitContainer::_get_sortable_child(int p_idx, SortableVisibilityMode p_visibility_mode) const { int idx = 0; for (int i = 0; i < get_child_count(false); i++) { diff --git a/scene/gui/split_container.h b/scene/gui/split_container.h index 51cc331c48b..13961290810 100644 --- a/scene/gui/split_container.h +++ b/scene/gui/split_container.h @@ -41,6 +41,10 @@ protected: void _notification(int p_what); virtual void gui_input(const Ref &p_event) override; + void _accessibility_action_inc(const Variant &p_data); + void _accessibility_action_dec(const Variant &p_data); + void _accessibility_action_set_value(const Variant &p_data); + private: bool dragging = false; int drag_from = 0; @@ -49,6 +53,8 @@ private: public: virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override; + + SplitContainerDragger(); }; class SplitContainer : public Container { diff --git a/scene/gui/tab_bar.cpp b/scene/gui/tab_bar.cpp index a8d9adc453d..36621de4531 100644 --- a/scene/gui/tab_bar.cpp +++ b/scene/gui/tab_bar.cpp @@ -351,6 +351,35 @@ void TabBar::_shape(int p_tab) { tabs.write[p_tab].text_buf->add_string(atr(tabs[p_tab].text), theme_cache.font, theme_cache.font_size, tabs[p_tab].language); } +RID TabBar::get_tab_accessibility_element(int p_tab) const { + RID ae = get_accessibility_element(); + ERR_FAIL_COND_V(ae.is_null(), RID()); + + const Tab &item = tabs[p_tab]; + if (item.accessibility_item_element.is_null()) { + item.accessibility_item_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_TAB); + item.accessibility_item_dirty = true; + } + return item.accessibility_item_element; +} + +RID TabBar::get_focused_accessibility_element() const { + if (current == -1) { + return get_accessibility_element(); + } else { + const Tab &item = tabs[current]; + return item.accessibility_item_element; + } +} + +void TabBar::_accessibility_action_scroll_into_view(const Variant &p_data, int p_index) { + ensure_tab_visible(p_index); +} + +void TabBar::_accessibility_action_focus(const Variant &p_data, int p_index) { + set_current_tab(p_index); +} + void TabBar::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -383,6 +412,46 @@ void TabBar::_notification(int p_what) { } } break; + case NOTIFICATION_EXIT_TREE: + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + for (int i = 0; i < tabs.size(); i++) { + tabs.write[i].accessibility_item_element = RID(); + } + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_TAB_BAR); + DisplayServer::get_singleton()->accessibility_update_set_list_item_count(ae, tabs.size()); + + for (int i = 0; i < tabs.size(); i++) { + const Tab &item = tabs[i]; + + if (item.accessibility_item_element.is_null()) { + item.accessibility_item_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_TAB); + item.accessibility_item_dirty = true; + } + + if (item.accessibility_item_dirty) { + DisplayServer::get_singleton()->accessibility_update_add_action(item.accessibility_item_element, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &TabBar::_accessibility_action_scroll_into_view).bind(i)); + DisplayServer::get_singleton()->accessibility_update_add_action(item.accessibility_item_element, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &TabBar::_accessibility_action_focus).bind(i)); + + DisplayServer::get_singleton()->accessibility_update_set_list_item_index(item.accessibility_item_element, i); + DisplayServer::get_singleton()->accessibility_update_set_name(item.accessibility_item_element, atr(item.text)); + DisplayServer::get_singleton()->accessibility_update_set_list_item_selected(item.accessibility_item_element, i == current); + DisplayServer::get_singleton()->accessibility_update_set_flag(item.accessibility_item_element, DisplayServer::AccessibilityFlags::FLAG_DISABLED, item.disabled); + DisplayServer::get_singleton()->accessibility_update_set_flag(item.accessibility_item_element, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, item.hidden); + DisplayServer::get_singleton()->accessibility_update_set_tooltip(item.accessibility_item_element, item.tooltip); + + DisplayServer::get_singleton()->accessibility_update_set_bounds(item.accessibility_item_element, Rect2(Point2(item.ofs_cache, 0), Size2(item.size_cache, get_size().height))); + + item.accessibility_item_dirty = false; + } + } + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { queue_redraw(); } break; @@ -393,6 +462,7 @@ void TabBar::_notification(int p_what) { _shape(i); } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); @@ -672,6 +742,15 @@ void TabBar::set_tab_count(int p_count) { } ERR_FAIL_COND(p_count < 0); + + if (tabs.size() > p_count) { + for (int i = p_count; i < tabs.size(); i++) { + if (tabs[i].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(tabs.write[i].accessibility_item_element); + tabs.write[i].accessibility_item_element = RID(); + } + } + } tabs.resize(p_count); if (p_count == 0) { @@ -702,6 +781,7 @@ void TabBar::set_tab_count(int p_count) { } } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); notify_property_list_changed(); @@ -737,6 +817,7 @@ void TabBar::set_current_tab(int p_current) { if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); emit_signal(SNAME("tab_changed"), p_current); @@ -785,6 +866,7 @@ void TabBar::set_tab_offset(int p_offset) { ERR_FAIL_INDEX(p_offset, tabs.size()); offset = p_offset; _update_cache(); + queue_accessibility_update(); queue_redraw(); } @@ -811,6 +893,7 @@ void TabBar::set_tab_title(int p_tab, const String &p_title) { if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -823,6 +906,7 @@ String TabBar::get_tab_title(int p_tab) const { void TabBar::set_tab_tooltip(int p_tab, const String &p_tooltip) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].tooltip = p_tooltip; + queue_accessibility_update(); } String TabBar::get_tab_tooltip(int p_tab) const { @@ -836,7 +920,9 @@ void TabBar::set_tab_text_direction(int p_tab, Control::TextDirection p_text_dir if (tabs[p_tab].text_direction != p_text_direction) { tabs.write[p_tab].text_direction = p_text_direction; + _shape(p_tab); + queue_accessibility_update(); queue_redraw(); } } @@ -851,12 +937,14 @@ void TabBar::set_tab_language(int p_tab, const String &p_language) { if (tabs[p_tab].language != p_language) { tabs.write[p_tab].language = p_language; + _shape(p_tab); _update_cache(); _ensure_no_over_offset(); if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -927,6 +1015,7 @@ void TabBar::set_tab_disabled(int p_tab, bool p_disabled) { if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -950,6 +1039,7 @@ void TabBar::set_tab_hidden(int p_tab, bool p_hidden) { if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -1117,6 +1207,8 @@ void TabBar::_update_cache(bool p_update_hover) { max_drawn_tab--; } } + + tabs.write[i].accessibility_item_dirty = true; } missing_right = max_drawn_tab < tabs.size() - 1; @@ -1171,6 +1263,7 @@ void TabBar::add_tab(const String &p_str, const Ref &p_icon) { if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); @@ -1189,12 +1282,19 @@ void TabBar::clear_tabs() { return; } + for (int i = 0; i < tabs.size(); i++) { + if (tabs[i].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(tabs.write[i].accessibility_item_element); + tabs.write[i].accessibility_item_element = RID(); + } + } tabs.clear(); offset = 0; max_drawn_tab = 0; current = -1; previous = -1; + queue_accessibility_update(); queue_redraw(); update_minimum_size(); notify_property_list_changed(); @@ -1202,6 +1302,11 @@ void TabBar::clear_tabs() { void TabBar::remove_tab(int p_idx) { ERR_FAIL_INDEX(p_idx, tabs.size()); + + if (tabs[p_idx].accessibility_item_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(tabs.write[p_idx].accessibility_item_element); + tabs.write[p_idx].accessibility_item_element = RID(); + } tabs.remove_at(p_idx); bool is_tab_changing = current == p_idx; @@ -1251,6 +1356,7 @@ void TabBar::remove_tab(int p_idx) { } } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); notify_property_list_changed(); @@ -1284,7 +1390,7 @@ void TabBar::drop_data(const Point2 &p_point, const Variant &p_data) { } Variant TabBar::_handle_get_drag_data(const String &p_type, const Point2 &p_point) { - int tab_over = get_tab_idx_at_point(p_point); + int tab_over = (p_point == Vector2(INFINITY, INFINITY)) ? current : get_tab_idx_at_point(p_point); if (tab_over < 0) { return Variant(); } @@ -1349,7 +1455,7 @@ void TabBar::_handle_drop_data(const String &p_type, const Point2 &p_point, cons if (String(d["type"]) == p_type) { int tab_from_id = d["tab_index"]; - int hover_now = get_closest_tab_idx_to_point(p_point); + int hover_now = (p_point == Vector2(INFINITY, INFINITY)) ? current : get_closest_tab_idx_to_point(p_point); NodePath from_path = d["from_path"]; NodePath to_path = get_path(); @@ -1407,6 +1513,8 @@ void TabBar::_handle_drop_data(const String &p_type, const Point2 &p_point, cons void TabBar::_move_tab_from(TabBar *p_from_tabbar, int p_from_index, int p_to_index) { Tab moving_tab = p_from_tabbar->tabs[p_from_index]; + moving_tab.accessibility_item_element = RID(); + moving_tab.accessibility_item_dirty = true; p_from_tabbar->remove_tab(p_from_index); tabs.insert(p_to_index, moving_tab); @@ -1426,6 +1534,7 @@ void TabBar::_move_tab_from(TabBar *p_from_tabbar, int p_from_index, int p_to_in queue_redraw(); } + queue_accessibility_update(); update_minimum_size(); } @@ -1515,6 +1624,8 @@ void TabBar::move_tab(int p_from, int p_to) { ERR_FAIL_INDEX(p_to, tabs.size()); Tab tab_from = tabs[p_from]; + tab_from.accessibility_item_dirty = true; + tabs.remove_at(p_from); tabs.insert(p_to, tab_from); @@ -1539,6 +1650,7 @@ void TabBar::move_tab(int p_from, int p_to) { if (scroll_to_selected) { ensure_tab_visible(current); } + queue_accessibility_update(); queue_redraw(); notify_property_list_changed(); } @@ -1957,6 +2069,7 @@ void TabBar::_bind_methods() { } TabBar::TabBar() { + set_focus_mode(FOCUS_ACCESSIBILITY); set_size(Size2(get_size().width, get_minimum_size().height)); set_focus_mode(FOCUS_ALL); connect(SceneStringName(mouse_exited), callable_mp(this, &TabBar::_on_mouse_exited)); diff --git a/scene/gui/tab_bar.h b/scene/gui/tab_bar.h index ead45269c24..a97fdfd5319 100644 --- a/scene/gui/tab_bar.h +++ b/scene/gui/tab_bar.h @@ -54,6 +54,9 @@ public: private: struct Tab { + mutable RID accessibility_item_element; + mutable bool accessibility_item_dirty = true; + String text; String tooltip; @@ -170,6 +173,9 @@ private: void _shape(int p_tab); void _draw_tab(Ref &p_tab_style, Color &p_font_color, int p_index, float p_x, bool p_focus); + void _accessibility_action_scroll_into_view(const Variant &p_data, int p_index); + void _accessibility_action_focus(const Variant &p_data, int p_index); + protected: virtual void gui_input(const Ref &p_event) override; virtual String get_tooltip(const Point2 &p_pos) const override; @@ -188,6 +194,9 @@ protected: void _move_tab_from(TabBar *p_from_tabbar, int p_from_index, int p_to_index); public: + RID get_tab_accessibility_element(int p_tab) const; + virtual RID get_focused_accessibility_element() const override; + Variant _handle_get_drag_data(const String &p_type, const Point2 &p_point); bool _handle_can_drop_data(const String &p_type, const Point2 &p_point, const Variant &p_data) const; void _handle_drop_data(const String &p_type, const Point2 &p_point, const Variant &p_data, const Callable &p_move_tab_callback, const Callable &p_move_tab_from_other_callback); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index 4a77347b1c0..3c8921f7e8a 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -142,6 +142,45 @@ void TabContainer::gui_input(const Ref &p_event) { void TabContainer::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + tab_panels.clear(); + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + int tab_index = 0; + int tab_cur = tab_bar->get_current_tab(); + for (int i = 0; i < get_child_count(); i++) { + Node *child_node = get_child(i); + Window *child_wnd = Object::cast_to(child_node); + if (child_wnd && !child_wnd->is_embedded()) { + continue; + } + if (child_node->is_part_of_edited_scene()) { + continue; + } + Control *control = as_sortable_control(child_node, SortableVisibilityMode::IGNORE); + if (!control || control == tab_bar || children_removing.has(control)) { + DisplayServer::get_singleton()->accessibility_update_add_child(ae, child_node->get_accessibility_element()); + } else { + if (!tab_panels.has(child_node)) { + tab_panels[child_node] = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_TAB_PANEL); + } + RID panel = tab_panels[child_node]; + RID tab = tab_bar->get_tab_accessibility_element(tab_index); + + DisplayServer::get_singleton()->accessibility_update_add_related_controls(tab, panel); + DisplayServer::get_singleton()->accessibility_update_add_related_labeled_by(panel, tab); + DisplayServer::get_singleton()->accessibility_update_set_flag(panel, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, tab_index != tab_cur); + DisplayServer::get_singleton()->accessibility_update_add_child(panel, child_node->get_accessibility_element()); + + tab_index++; + } + } + } break; + case NOTIFICATION_ENTER_TREE: { // If some nodes happen to be renamed outside the tree, the tab names need to be updated manually. if (get_tab_count() > 0) { @@ -556,6 +595,7 @@ void TabContainer::add_child_notify(Node *p_child) { if (get_tab_count() == 1) { queue_redraw(); } + queue_accessibility_update(); p_child->connect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names)); p_child->connect(SceneStringName(visibility_changed), callable_mp(this, &TabContainer::_on_tab_visibility_changed).bind(c)); @@ -579,11 +619,17 @@ void TabContainer::move_child_notify(Node *p_child) { } _refresh_tab_indices(); + queue_accessibility_update(); } void TabContainer::remove_child_notify(Node *p_child) { Container::remove_child_notify(p_child); + if (tab_panels.has(p_child)) { + DisplayServer::get_singleton()->accessibility_free_element(tab_panels[p_child]); + tab_panels.erase(p_child); + } + if (p_child == tab_bar) { return; } @@ -607,6 +653,7 @@ void TabContainer::remove_child_notify(Node *p_child) { if (get_tab_count() == 0) { queue_redraw(); } + queue_accessibility_update(); p_child->remove_meta("_tab_index"); p_child->remove_meta("_tab_name"); diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 3a0e5c40fb7..7a392cb6384 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -97,6 +97,8 @@ private: int tab_font_size; } theme_cache; + HashMap tab_panels; + int _get_tab_height() const; Vector _get_tab_controls() const; void _on_theme_changed(); @@ -129,6 +131,8 @@ protected: static void _bind_methods(); public: + virtual bool accessibility_override_tree_hierarchy() const override { return true; } + TabBar *get_tab_bar() const; int get_tab_idx_at_point(const Point2 &p_point) const; diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 47b0befa29d..b41e613e1cd 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -224,9 +224,35 @@ _FORCE_INLINE_ const String &TextEdit::Text::get_text_with_ime(int p_line) const } } +const Vector TextEdit::Text::get_accessibility_elements(int p_line) { + ERR_FAIL_INDEX_V(p_line, text.size(), Vector()); + + return text[p_line].accessibility_text_root_element; +} + +void TextEdit::Text::update_accessibility(int p_line, RID p_root) { + ERR_FAIL_INDEX(p_line, text.size()); + + Line &l = text.write[p_line]; + if (l.accessibility_text_root_element.is_empty()) { + for (int i = 0; i < l.data_buf->get_line_count(); i++) { + RID rid = DisplayServer::get_singleton()->accessibility_create_sub_text_edit_elements(p_root, l.data_buf->get_line_rid(i), max_line_height, p_line); + l.accessibility_text_root_element.push_back(rid); + } + } +} + void TextEdit::Text::invalidate_cache(int p_line, bool p_text_changed) { ERR_FAIL_INDEX(p_line, text.size()); + Line &l = text.write[p_line]; + for (const RID rid : l.accessibility_text_root_element) { + if (rid.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(rid); + } + } + l.accessibility_text_root_element.clear(); + if (font.is_null()) { return; // Not in tree? } @@ -564,8 +590,173 @@ String TextEdit::Text::get_enabled_word_separators() const { /// TEXT EDIT /// /////////////////////////////////////////////////////////////////////////////// +void TextEdit::_accessibility_action_set_selection(const Variant &p_data) { + Dictionary new_selection = p_data; + RID sel_start = new_selection["start_element"]; + Vector2i sel_start_line = DisplayServer::get_singleton()->accessibility_element_get_meta(sel_start); + int sel_start_pos = new_selection["start_char"]; + + RID sel_end = new_selection["end_element"]; + Vector2i sel_end_line = DisplayServer::get_singleton()->accessibility_element_get_meta(sel_end); + int sel_end_pos = new_selection["end_char"]; + + remove_secondary_carets(); + select(sel_start_line.x, sel_start_pos, sel_end_line.x, sel_end_pos, 0); +} + +void TextEdit::_accessibility_action_replace_selected(const Variant &p_data) { + String new_text = p_data; + insert_text_at_caret(new_text); +} + +void TextEdit::_accessibility_action_set_value(const Variant &p_data) { + String new_text = p_data; + set_text(new_text); +} + +void TextEdit::_accessibility_action_menu(const Variant &p_data) { + if (context_menu_enabled) { + _update_context_menu(); + adjust_viewport_to_caret(); + menu->set_position(get_screen_position() + get_caret_draw_pos()); + menu->reset_size(); + menu->popup(); + menu->grab_focus(); + } +} + +void TextEdit::_accessibility_scroll_down(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() / 4); + queue_accessibility_update(); +} + +void TextEdit::_accessibility_scroll_left(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() / 4); + queue_accessibility_update(); +} + +void TextEdit::_accessibility_scroll_right(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() / 4); + queue_accessibility_update(); +} + +void TextEdit::_accessibility_scroll_up(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() / 4); + queue_accessibility_update(); +} + +void TextEdit::_accessibility_scroll_set(const Variant &p_data) { + const Point2 &pos = p_data; + h_scroll->set_value(pos.x); + v_scroll->set_value(pos.y); + queue_accessibility_update(); +} + +void TextEdit::_accessibility_action_scroll_into_view(const Variant &p_data, int p_line, int p_wrap) { + double delta = get_scroll_pos_for_line(p_line, p_wrap) - get_v_scroll(); + if (delta < 0) { + _scroll_up(-delta, false); + } else { + _scroll_down(delta, false); + } +} + void TextEdit::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_EXIT_TREE: + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + text.clear_accessibility(); + accessibility_text_root_element_nl = RID(); + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_MULTILINE_TEXT_FIELD); + if (text.size() == 1 && text[0].is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_placeholder(ae, atr(placeholder_text)); + } + if (!placeholder_text.is_empty() && get_accessibility_name().is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_name(ae, atr(placeholder_text)); + } + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_READONLY, !editable); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_TEXT_SELECTION, callable_mp(this, &TextEdit::_accessibility_action_set_selection)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_REPLACE_SELECTED_TEXT, callable_mp(this, &TextEdit::_accessibility_action_replace_selected)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &TextEdit::_accessibility_action_set_value)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SHOW_CONTEXT_MENU, callable_mp(this, &TextEdit::_accessibility_action_menu)); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_DOWN, callable_mp(this, &TextEdit::_accessibility_scroll_down)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_LEFT, callable_mp(this, &TextEdit::_accessibility_scroll_left)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_RIGHT, callable_mp(this, &TextEdit::_accessibility_scroll_right)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_UP, callable_mp(this, &TextEdit::_accessibility_scroll_up)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_SCROLL_OFFSET, callable_mp(this, &TextEdit::_accessibility_scroll_set)); + + int first_vis_line = get_first_visible_line(); + int row_height = get_line_height(); + int xmargin_beg = theme_cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding; + Size2 size = get_size(); + bool rtl = is_layout_rtl(); + int lines_drawn = 0; + + RID selection_start; + RID selection_end; + + for (int i = 0; i < text.size(); i++) { + text.update_accessibility(i, ae); + const Ref &ac_buf = text.get_line_data(i); + const Vector &text_aes = text.get_accessibility_elements(i); + for (int j = 0; j < text_aes.size(); j++) { + float text_off_x = 0.0; + float text_off_y = 0.0; + if (!editable) { + text_off_x = theme_cache.style_readonly->get_offset().x / 2; + text_off_x -= theme_cache.style_normal->get_offset().x / 2; + text_off_y = theme_cache.style_readonly->get_offset().y / 2; + } else { + text_off_y = theme_cache.style_normal->get_offset().y / 2; + } + + text_off_y += (lines_drawn + j) * row_height + theme_cache.line_spacing / 2; + text_off_y -= (first_vis_line + first_visible_line_wrap_ofs) * row_height; + text_off_y -= _get_v_scroll_offset() * row_height; + + int char_margin = xmargin_beg - first_visible_col; + if (rtl) { + char_margin = size.width - char_margin - ac_buf->get_line_width(j); + } + + DisplayServer::get_singleton()->accessibility_update_set_flag(text_aes[j], DisplayServer::AccessibilityFlags::FLAG_HIDDEN, _is_line_hidden(i)); + Transform2D tr; + tr.set_origin(Point2(char_margin + text_off_x, text_off_y)); + DisplayServer::get_singleton()->accessibility_update_set_transform(text_aes[j], tr); + DisplayServer::get_singleton()->accessibility_update_set_name(text_aes[j], vformat(RTR("Line %d"), i)); + DisplayServer::get_singleton()->accessibility_element_set_meta(text_aes[j], Vector2i(i, j)); + DisplayServer::get_singleton()->accessibility_update_add_action(text_aes[j], DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &TextEdit::_accessibility_action_scroll_into_view).bind(i, j)); + } + lines_drawn += ac_buf->get_line_count(); + } + if (accessibility_text_root_element_nl.is_null()) { + accessibility_text_root_element_nl = DisplayServer::get_singleton()->accessibility_create_sub_text_edit_elements(ae, RID(), get_line_height()); + } + + // Selection. + if (carets.size() > 0) { + if (carets[0].selection.active) { + int start_wrap = get_line_wrap_index_at_column(carets[0].selection.origin_line, carets[0].selection.origin_column); + RID start_rid = text.get_accessibility_elements(carets[0].selection.origin_line)[start_wrap]; + + int end_wrap = get_line_wrap_index_at_column(carets[0].line, carets[0].column); + RID end_rid = text.get_accessibility_elements(carets[0].line)[end_wrap]; + DisplayServer::get_singleton()->accessibility_update_set_text_selection(ae, start_rid, carets[0].selection.origin_column, end_rid, carets[0].column); + } else { + int caret_wrap = get_line_wrap_index_at_column(carets[0].line, carets[0].column); + RID caret_rid = text.get_accessibility_elements(carets[0].line)[caret_wrap]; + DisplayServer::get_singleton()->accessibility_update_set_text_selection(ae, caret_rid, carets[0].column, caret_rid, carets[0].column); + } + } + } break; + case NOTIFICATION_POSTINITIALIZE: { _update_caches(); } break; @@ -1694,6 +1885,8 @@ void TextEdit::_notification(int p_what) { _update_ime_text(); adjust_viewport_to_caret(0); + + queue_accessibility_update(); queue_redraw(); } } break; @@ -1864,6 +2057,7 @@ void TextEdit::gui_input(const Ref &p_gui_input) { if (mb->get_button_index() == MouseButton::WHEEL_UP && !mb->is_command_or_control_pressed()) { if (mb->is_shift_pressed()) { h_scroll->set_value(h_scroll->get_value() - (100 * mb->get_factor())); + queue_accessibility_update(); } else if (mb->is_alt_pressed()) { // Scroll 5 times as fast as normal (like in Visual Studio Code). _scroll_up(15 * mb->get_factor(), true); @@ -1875,6 +2069,7 @@ void TextEdit::gui_input(const Ref &p_gui_input) { if (mb->get_button_index() == MouseButton::WHEEL_DOWN && !mb->is_command_or_control_pressed()) { if (mb->is_shift_pressed()) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); + queue_accessibility_update(); } else if (mb->is_alt_pressed()) { // Scroll 5 times as fast as normal (like in Visual Studio Code). _scroll_down(15 * mb->get_factor(), true); @@ -1885,9 +2080,11 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } if (mb->get_button_index() == MouseButton::WHEEL_LEFT) { h_scroll->set_value(h_scroll->get_value() - (100 * mb->get_factor())); + queue_accessibility_update(); } if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); + queue_accessibility_update(); } if (mb->get_button_index() == MouseButton::LEFT) { @@ -1961,6 +2158,8 @@ void TextEdit::gui_input(const Ref &p_gui_input) { return; } + queue_accessibility_update(); + last_dblclk = 0; } else if (!mb->is_shift_pressed()) { if (drag_and_drop_selection_enabled && mouse_over_selection_caret >= 0) { @@ -2005,6 +2204,7 @@ void TextEdit::gui_input(const Ref &p_gui_input) { last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } + queue_accessibility_update(); queue_redraw(); } @@ -2086,6 +2286,7 @@ void TextEdit::gui_input(const Ref &p_gui_input) { if (v_scroll->get_value() != prev_v_scroll || h_scroll->get_value() != prev_h_scroll) { accept_event(); // Accept event if scroll changed. } + queue_accessibility_update(); return; } @@ -2417,8 +2618,15 @@ void TextEdit::gui_input(const Ref &p_gui_input) { return; } + // Toggle Tab mode. + if (k->is_action("ui_focus_mode", true)) { + tab_input_mode = !tab_input_mode; + accept_event(); + return; + } + // Handle tab as it has no set unicode value. - if (k->is_action("ui_text_indent", true)) { + if (tab_input_mode && k->is_action("ui_text_indent", true)) { if (editable) { insert_text_at_caret("\t"); } @@ -2996,6 +3204,7 @@ void TextEdit::_update_caches() { syntax_highlighter->set_text_edit(this); } _clear_syntax_highlighting_cache(); + queue_accessibility_update(); } void TextEdit::_close_ime_window() { @@ -3039,6 +3248,7 @@ void TextEdit::_update_ime_text() { } } _clear_syntax_highlighting_cache(); + queue_accessibility_update(); queue_redraw(); } @@ -3111,7 +3321,9 @@ bool TextEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { Control::drop_data(p_point, p_data); - if (p_data.is_string() && is_editable()) { + if (p_point == Vector2(INFINITY, INFINITY)) { + insert_text_at_caret(p_data); + } else if (p_data.is_string() && is_editable()) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); int drop_at_line = pos.y; int drop_at_column = pos.x; @@ -3215,6 +3427,7 @@ String TextEdit::get_tooltip(const Point2 &p_pos) const { void TextEdit::set_tooltip_request_func(const Callable &p_tooltip_callback) { tooltip_callback = p_tooltip_callback; + queue_accessibility_update(); } /* Text */ @@ -3261,7 +3474,7 @@ void TextEdit::set_editable(bool p_editable) { } editable = p_editable; - + queue_accessibility_update(); queue_redraw(); } @@ -3292,6 +3505,7 @@ void TextEdit::set_text_direction(Control::TextDirection p_text_direction) { menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); } + queue_accessibility_update(); queue_redraw(); } } @@ -3312,6 +3526,7 @@ void TextEdit::set_language(const String &p_language) { text.set_direction_and_language(dir, (!language.is_empty()) ? language : TranslationServer::get_singleton()->get_tool_locale()); text.invalidate_all(); _update_placeholder(); + queue_accessibility_update(); queue_redraw(); } } @@ -3326,6 +3541,7 @@ void TextEdit::set_structured_text_bidi_override(TextServer::StructuredTextParse for (int i = 0; i < text.size(); i++) { text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); } + queue_accessibility_update(); queue_redraw(); } } @@ -3343,6 +3559,7 @@ void TextEdit::set_structured_text_bidi_override_options(Array p_args) { for (int i = 0; i < text.size(); i++) { text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); } + queue_accessibility_update(); queue_redraw(); } @@ -3358,6 +3575,7 @@ void TextEdit::set_tab_size(const int p_size) { text.set_tab_size(p_size); text.invalidate_all_lines(); _update_placeholder(); + queue_accessibility_update(); queue_redraw(); } @@ -3379,6 +3597,14 @@ bool TextEdit::is_indent_wrapped_lines() const { return text.is_indent_wrapped_lines(); } +void TextEdit::set_tab_input_mode(bool p_enabled) { + tab_input_mode = p_enabled; +} + +bool TextEdit::get_tab_input_mode() const { + return tab_input_mode; +} + // User controls void TextEdit::set_overtype_mode_enabled(bool p_enabled) { if (overtype_mode == p_enabled) { @@ -3508,7 +3734,7 @@ void TextEdit::set_text(const String &p_text) { set_caret_line(0); set_caret_column(0); - + queue_accessibility_update(); queue_redraw(); setting_text = false; emit_signal(SNAME("text_set")); @@ -3537,6 +3763,7 @@ void TextEdit::set_placeholder(const String &p_text) { placeholder_text = p_text; _update_placeholder(); + queue_accessibility_update(); queue_redraw(); } @@ -3773,6 +4000,9 @@ void TextEdit::remove_line_at(int p_line, bool p_move_carets_down) { _offset_carets_after(next_line, next_column, from_line, from_column); end_multicaret_edit(); end_complex_operation(); + + queue_accessibility_update(); + queue_redraw(); } void TextEdit::insert_text_at_caret(const String &p_text, int p_caret) { @@ -4153,6 +4383,7 @@ void TextEdit::start_action(EditAction p_action) { void TextEdit::end_action() { if (current_action != EditAction::ACTION_NONE) { pending_action_end = true; + queue_accessibility_update(); } } @@ -4172,6 +4403,8 @@ void TextEdit::begin_complex_operation() { void TextEdit::end_complex_operation() { _push_current_op(); + queue_accessibility_update(); + complex_operation_count = MAX(complex_operation_count - 1, 0); if (complex_operation_count > 0) { return; @@ -4264,6 +4497,7 @@ void TextEdit::undo() { _selection_changed(); } adjust_viewport_to_caret(); + queue_accessibility_update(); } void TextEdit::redo() { @@ -4320,6 +4554,7 @@ void TextEdit::redo() { _selection_changed(); } adjust_viewport_to_caret(); + queue_accessibility_update(); } void TextEdit::clear_undo_history() { @@ -4591,7 +4826,7 @@ Rect2i TextEdit::get_rect_at_line_column(int p_line, int p_column) const { ERR_FAIL_COND_V(p_column < 0, Rect2i(-1, -1, 0, 0)); ERR_FAIL_COND_V(p_column > text[p_line].length(), Rect2i(-1, -1, 0, 0)); - if (text.size() == 1 && text[0].length() == 0) { + if (text.size() == 1 && text[0].is_empty()) { // The TextEdit is empty. return Rect2i(); } @@ -4841,6 +5076,7 @@ void TextEdit::remove_secondary_carets() { if (drag_caret_index >= 0) { drag_caret_index = -1; } + queue_accessibility_update(); } int TextEdit::get_caret_count() const { @@ -5262,6 +5498,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ if (caret_moved) { _caret_changed(p_caret); } + queue_accessibility_update(); } int TextEdit::get_caret_line(int p_caret) const { @@ -5296,6 +5533,7 @@ void TextEdit::set_caret_column(int p_column, bool p_adjust_viewport, int p_care if (caret_moved) { _caret_changed(p_caret); } + queue_accessibility_update(); } int TextEdit::get_caret_column(int p_caret) const { @@ -5384,7 +5622,7 @@ void TextEdit::select_all() { return; } - if (text.size() == 1 && text[0].length() == 0) { + if (text.size() == 1 && text[0].is_empty()) { return; } @@ -5401,7 +5639,7 @@ void TextEdit::select_word_under_caret(int p_caret) { return; } - if (text.size() == 1 && text[0].length() == 0) { + if (text.size() == 1 && text[0].is_empty()) { return; } @@ -5446,7 +5684,7 @@ void TextEdit::add_selection_for_next_occurrence() { return; } - if (text.size() == 1 && text[0].length() == 0) { + if (text.size() == 1 && text[0].is_empty()) { return; } @@ -5489,7 +5727,7 @@ void TextEdit::skip_selection_for_next_occurrence() { return; } - if (text.size() == 1 && text[0].length() == 0) { + if (text.size() == 1 && text[0].is_empty()) { return; } @@ -5558,6 +5796,9 @@ void TextEdit::select(int p_origin_line, int p_origin_column, int p_caret_line, if (had_selection != activate) { _selection_changed(p_caret); } + + queue_accessibility_update(); + queue_redraw(); } bool TextEdit::has_selection(int p_caret) const { @@ -5802,6 +6043,9 @@ void TextEdit::deselect(int p_caret) { if (selection_changed) { _selection_changed(p_caret); } + + queue_accessibility_update(); + queue_redraw(); } void TextEdit::delete_selection(int p_caret) { @@ -5843,6 +6087,7 @@ void TextEdit::set_line_wrapping_mode(LineWrappingMode p_wrapping_mode) { if (line_wrapping_mode != p_wrapping_mode) { line_wrapping_mode = p_wrapping_mode; _update_wrap_at_column(true); + queue_accessibility_update(); queue_redraw(); } } @@ -5859,6 +6104,7 @@ void TextEdit::set_autowrap_mode(TextServer::AutowrapMode p_mode) { autowrap_mode = p_mode; if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE) { _update_wrap_at_column(true); + queue_accessibility_update(); queue_redraw(); } } @@ -5964,6 +6210,7 @@ void TextEdit::set_v_scroll(double p_scroll) { if (p_scroll >= max_v_scroll - 1.0) { _scroll_moved(v_scroll->get_value()); } + queue_accessibility_update(); } double TextEdit::get_v_scroll() const { @@ -5975,6 +6222,7 @@ void TextEdit::set_h_scroll(int p_scroll) { p_scroll = 0; } h_scroll->set_value(p_scroll); + queue_accessibility_update(); } int TextEdit::get_h_scroll() const { @@ -6550,6 +6798,7 @@ void TextEdit::set_draw_control_chars(bool p_enabled) { text.set_draw_control_chars(draw_control_chars); text.invalidate_font(); _update_placeholder(); + queue_accessibility_update(); queue_redraw(); } } @@ -6615,6 +6864,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_indent_wrapped_lines", "enabled"), &TextEdit::set_indent_wrapped_lines); ClassDB::bind_method(D_METHOD("is_indent_wrapped_lines"), &TextEdit::is_indent_wrapped_lines); + ClassDB::bind_method(D_METHOD("set_tab_input_mode", "enabled"), &TextEdit::set_tab_input_mode); + ClassDB::bind_method(D_METHOD("get_tab_input_mode"), &TextEdit::get_tab_input_mode); + // User controls ClassDB::bind_method(D_METHOD("set_overtype_mode_enabled", "enabled"), &TextEdit::set_overtype_mode_enabled); ClassDB::bind_method(D_METHOD("is_overtype_mode_enabled"), &TextEdit::is_overtype_mode_enabled); @@ -7037,6 +7289,7 @@ void TextEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "wrap_mode", PROPERTY_HINT_ENUM, "None,Boundary"), "set_line_wrapping_mode", "get_line_wrapping_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Arbitrary:1,Word:2,Word (Smart):3"), "set_autowrap_mode", "get_autowrap_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_wrapped_lines"), "set_indent_wrapped_lines", "is_indent_wrapped_lines"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "tab_input_mode"), "set_tab_input_mode", "get_tab_input_mode"); ADD_GROUP("Scroll", "scroll_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_smooth"), "set_smooth_scroll_enabled", "is_smooth_scroll_enabled"); @@ -7147,6 +7400,7 @@ void TextEdit::_set_hiding_enabled(bool p_enabled) { _unhide_all_lines(); } hiding_enabled = p_enabled; + queue_accessibility_update(); queue_redraw(); } @@ -7164,6 +7418,7 @@ void TextEdit::_unhide_all_lines() { text.set_hidden(i, false); } _update_scrollbars(); + queue_accessibility_update(); queue_redraw(); } @@ -7181,6 +7436,7 @@ void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) { if (_is_hiding_enabled() || !p_hidden) { text.set_hidden(p_line, p_hidden); } + queue_accessibility_update(); queue_redraw(); } @@ -7833,6 +8089,7 @@ void TextEdit::_selection_changed(int p_caret) { } _cancel_drag_and_drop_text(); + queue_accessibility_update(); queue_redraw(); } @@ -8035,6 +8292,7 @@ void TextEdit::_update_wrap_at_column(bool p_force) { first_visible_line_wrap_ofs = 0; } set_line_as_first_visible(first_visible_line, first_visible_line_wrap_ofs); + queue_accessibility_update(); } /* Viewport. */ @@ -8151,6 +8409,7 @@ void TextEdit::_scroll_moved(double p_to_val) { first_visible_line = n_line; first_visible_line_wrap_ofs = wi; } + queue_accessibility_update(); queue_redraw(); } @@ -8185,6 +8444,7 @@ void TextEdit::_scroll_up(real_t p_delta, bool p_animate) { } if (!p_animate || Math::abs(target_v_scroll - v_scroll->get_value()) < 1.0) { v_scroll->set_value(target_v_scroll); + queue_accessibility_update(); } else { scrolling = true; set_physics_process_internal(true); @@ -8213,6 +8473,7 @@ void TextEdit::_scroll_down(real_t p_delta, bool p_animate) { } if (!p_animate || Math::abs(target_v_scroll - v_scroll->get_value()) < 1.0) { v_scroll->set_value(target_v_scroll); + queue_accessibility_update(); } else { scrolling = true; set_physics_process_internal(true); @@ -8326,6 +8587,8 @@ void TextEdit::_adjust_viewport_to_caret_horizontally(int p_caret, bool p_maximi } h_scroll->set_value(first_visible_col); + + queue_accessibility_update(); queue_redraw(); } @@ -8418,6 +8681,7 @@ void TextEdit::_update_gutter_width() { if (get_viewport()) { hovered_gutter = _get_hovered_gutter(get_local_mouse_position()); } + queue_accessibility_update(); queue_redraw(); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index e4948decdf0..df8d73f66cb 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -144,12 +144,15 @@ private: Color color = Color(1, 1, 1); }; + mutable int64_t next_item_id = 0; + struct Line { Vector gutters; String data; Array bidi_override; Ref data_buf; + Vector accessibility_text_root_element; String ime_data; Array ime_bidi_override; @@ -227,6 +230,14 @@ private: BitField get_brk_flags() const; int get_line_wrap_amount(int p_line) const; + const Vector get_accessibility_elements(int p_line); + void update_accessibility(int p_line, RID p_root); + void clear_accessibility() { + for (int i = 0; i < text.size(); i++) { + text.write[i].accessibility_text_root_element.clear(); + } + } + Vector get_line_wrap_ranges(int p_line) const; const Ref get_line_data(int p_line) const; float get_indent_offset(int p_line, bool p_rtl) const; @@ -275,13 +286,14 @@ private: /* Text */ Text text; - bool setting_text = false; bool alt_start = false; bool alt_start_no_hold = false; uint32_t alt_code = 0; + bool tab_input_mode = true; + // Text properties. String ime_text = ""; Point2 ime_selection; @@ -628,6 +640,8 @@ private: bool draw_tabs = false; bool draw_spaces = false; + RID accessibility_text_root_element_nl; + /*** Super internal Core API. Everything builds on it. ***/ bool text_changed_dirty = false; void _text_changed(); @@ -718,6 +732,17 @@ protected: virtual void _paste_internal(int p_caret); virtual void _paste_primary_clipboard_internal(int p_caret); + void _accessibility_action_set_selection(const Variant &p_data); + void _accessibility_action_replace_selected(const Variant &p_data); + void _accessibility_action_set_value(const Variant &p_data); + void _accessibility_action_menu(const Variant &p_data); + void _accessibility_scroll_down(const Variant &p_data); + void _accessibility_scroll_left(const Variant &p_data); + void _accessibility_scroll_right(const Variant &p_data); + void _accessibility_scroll_up(const Variant &p_data); + void _accessibility_scroll_set(const Variant &p_data); + void _accessibility_action_scroll_into_view(const Variant &p_data, int p_line, int p_wrap); + GDVIRTUAL2(_handle_unicode_input, int, int) GDVIRTUAL1(_backspace, int) GDVIRTUAL1(_cut, int) @@ -765,6 +790,9 @@ public: void set_indent_wrapped_lines(bool p_enabled); bool is_indent_wrapped_lines() const; + void set_tab_input_mode(bool p_enabled); + bool get_tab_input_mode() const; + // User controls void set_overtype_mode_enabled(bool p_enabled); bool is_overtype_mode_enabled() const; diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index 3ed7bf2f511..dd754de720b 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -429,6 +429,13 @@ void TextureProgressBar::draw_nine_patch_stretched(const Ref &p_textu void TextureProgressBar::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_PROGRESS_INDICATOR); + } break; + case NOTIFICATION_DRAW: { if (under.is_valid()) { if (nine_patch_stretch) { diff --git a/scene/gui/tree.compat.inc b/scene/gui/tree.compat.inc new file mode 100644 index 00000000000..f4355764643 --- /dev/null +++ b/scene/gui/tree.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* tree.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +void TreeItem::_add_button_bind_compat_76829(int p_column, const Ref &p_button, int p_id, bool p_disabled, const String &p_tooltip) { + add_button(p_column, p_button, p_id, p_disabled, p_tooltip, String()); +} + +void TreeItem::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip_text"), &TreeItem::_add_button_bind_compat_76829, DEFVAL(-1), DEFVAL(false), DEFVAL("")); +} + +#endif // DISABLE_DEPRECATED diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index d14300482e3..9ea1130e4d9 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "tree.h" +#include "tree.compat.inc" #include "core/config/project_settings.h" #include "core/input/input.h" @@ -102,6 +103,7 @@ void TreeItem::_change_tree(Tree *p_tree) { if (p_tree == tree) { return; } + accessibility_row_dirty = true; TreeItem *c = first_child; while (c) { @@ -144,13 +146,31 @@ void TreeItem::_change_tree(Tree *p_tree) { tree->edited_item = nullptr; tree->pressing_for_editor = false; } - + tree->queue_accessibility_update(); tree->queue_redraw(); } tree = p_tree; + if (accessibility_row_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(accessibility_row_element); + accessibility_row_element = RID(); + } + for (Cell &cell : cells) { + if (cell.accessibility_cell_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(cell.accessibility_cell_element); + cell.accessibility_cell_element = RID(); + } + for (Cell::Button &btn : cell.buttons) { + if (btn.accessibility_button_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(btn.accessibility_button_element); + btn.accessibility_button_element = RID(); + } + } + } + if (tree) { + tree->queue_accessibility_update(); tree->queue_redraw(); cells.resize(tree->columns.size()); } @@ -366,6 +386,9 @@ void TreeItem::set_text(int p_column, String p_text) { cells.write[p_column].cached_minimum_size_dirty = true; _changed_notify(p_column); + if (get_tree()) { + get_tree()->update_configuration_warnings(); + } } String TreeItem::get_text(int p_column) const { @@ -373,6 +396,26 @@ String TreeItem::get_text(int p_column) const { return cells[p_column].text; } +void TreeItem::set_alt_text(int p_column, String p_text) { + ERR_FAIL_INDEX(p_column, cells.size()); + + if (cells[p_column].alt_text == p_text) { + return; + } + + cells.write[p_column].alt_text = p_text; + + _changed_notify(p_column); + if (get_tree()) { + get_tree()->update_configuration_warnings(); + } +} + +String TreeItem::get_alt_text(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), ""); + return cells[p_column].alt_text; +} + void TreeItem::set_text_direction(int p_column, Control::TextDirection p_text_direction) { ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); @@ -696,6 +739,7 @@ void TreeItem::set_collapsed(bool p_collapsed) { select(tree->selected_col); } + tree->queue_accessibility_update(); tree->queue_redraw(); } } @@ -765,6 +809,7 @@ void TreeItem::set_visible(bool p_visible) { } visible = p_visible; if (tree) { + tree->queue_accessibility_update(); tree->queue_redraw(); _changed_notify(); } @@ -823,6 +868,7 @@ TreeItem *TreeItem::create_child(int p_index) { TreeItem *ti = memnew(TreeItem(tree)); if (tree) { ti->cells.resize(tree->columns.size()); + tree->queue_accessibility_update(); tree->queue_redraw(); } @@ -1160,6 +1206,7 @@ void TreeItem::move_before(TreeItem *p_item) { p_item->prev = this; if (tree && old_tree == tree) { + tree->queue_accessibility_update(); tree->queue_redraw(); } @@ -1205,6 +1252,7 @@ void TreeItem::move_after(TreeItem *p_item) { } if (tree && old_tree == tree) { + tree->queue_accessibility_update(); tree->queue_redraw(); } validate_cache(); @@ -1238,6 +1286,8 @@ void TreeItem::set_as_cursor(int p_column) { } tree->selected_item = this; tree->selected_col = p_column; + tree->selected_button = -1; + tree->queue_accessibility_update(); tree->queue_redraw(); } @@ -1255,6 +1305,11 @@ void TreeItem::clear_buttons() { int i = 0; for (Cell &cell : cells) { if (!cell.buttons.is_empty()) { + for (Cell::Button &btn : cell.buttons) { + if (btn.accessibility_button_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(btn.accessibility_button_element); + } + } cell.buttons.clear(); cell.cached_minimum_size_dirty = true; _changed_notify(i); @@ -1263,7 +1318,7 @@ void TreeItem::clear_buttons() { } } -void TreeItem::add_button(int p_column, const Ref &p_button, int p_id, bool p_disabled, const String &p_tooltip) { +void TreeItem::add_button(int p_column, const Ref &p_button, int p_id, bool p_disabled, const String &p_tooltip, const String &p_alt_text) { ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_COND(p_button.is_null()); TreeItem::Cell::Button button; @@ -1274,10 +1329,14 @@ void TreeItem::add_button(int p_column, const Ref &p_button, int p_id button.id = p_id; button.disabled = p_disabled; button.tooltip = p_tooltip; + button.alt_text = p_alt_text; cells.write[p_column].buttons.push_back(button); cells.write[p_column].cached_minimum_size_dirty = true; _changed_notify(p_column); + if (get_tree()) { + get_tree()->update_configuration_warnings(); + } } int TreeItem::get_button_count(int p_column) const { @@ -1306,8 +1365,14 @@ int TreeItem::get_button_id(int p_column, int p_index) const { void TreeItem::erase_button(int p_column, int p_index) { ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_INDEX(p_index, cells[p_column].buttons.size()); + if (cells[p_column].buttons[p_index].accessibility_button_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(cells.write[p_column].buttons.write[p_index].accessibility_button_element); + } cells.write[p_column].buttons.remove_at(p_index); _changed_notify(p_column); + if (get_tree()) { + get_tree()->update_configuration_warnings(); + } } int TreeItem::get_button_by_id(int p_column, int p_id) const { @@ -1331,6 +1396,8 @@ void TreeItem::set_button_tooltip_text(int p_column, int p_index, const String & ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_INDEX(p_index, cells[p_column].buttons.size()); cells.write[p_column].buttons.write[p_index].tooltip = p_tooltip; + + _changed_notify(p_column); } void TreeItem::set_button(int p_column, int p_index, const Ref &p_button) { @@ -1348,6 +1415,21 @@ void TreeItem::set_button(int p_column, int p_index, const Ref &p_but _changed_notify(p_column); } +void TreeItem::set_button_alt_text(int p_column, int p_index, const String &p_alt_text) { + ERR_FAIL_INDEX(p_column, cells.size()); + ERR_FAIL_INDEX(p_index, cells[p_column].buttons.size()); + + if (cells[p_column].buttons[p_index].alt_text == p_alt_text) { + return; + } + + cells.write[p_column].buttons.write[p_index].alt_text = p_alt_text; + _changed_notify(p_column); + if (get_tree()) { + get_tree()->update_configuration_warnings(); + } +} + void TreeItem::set_button_color(int p_column, int p_index, const Color &p_color) { ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_INDEX(p_index, cells[p_column].buttons.size()); @@ -1681,6 +1763,9 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "column", "text"), &TreeItem::set_text); ClassDB::bind_method(D_METHOD("get_text", "column"), &TreeItem::get_text); + ClassDB::bind_method(D_METHOD("set_alt_text", "column", "text"), &TreeItem::set_alt_text); + ClassDB::bind_method(D_METHOD("get_alt_text", "column"), &TreeItem::get_alt_text); + ClassDB::bind_method(D_METHOD("set_text_direction", "column", "direction"), &TreeItem::set_text_direction); ClassDB::bind_method(D_METHOD("get_text_direction", "column"), &TreeItem::get_text_direction); @@ -1774,7 +1859,7 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("is_custom_set_as_button", "column"), &TreeItem::is_custom_set_as_button); ClassDB::bind_method(D_METHOD("clear_buttons"), &TreeItem::clear_buttons); - ClassDB::bind_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip_text"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL("")); + ClassDB::bind_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip_text", "alt_text"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL(""), DEFVAL("")); ClassDB::bind_method(D_METHOD("get_button_count", "column"), &TreeItem::get_button_count); ClassDB::bind_method(D_METHOD("get_button_tooltip_text", "column", "button_index"), &TreeItem::get_button_tooltip_text); ClassDB::bind_method(D_METHOD("get_button_id", "column", "button_index"), &TreeItem::get_button_id); @@ -1784,6 +1869,7 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_button_tooltip_text", "column", "button_index", "tooltip"), &TreeItem::set_button_tooltip_text); ClassDB::bind_method(D_METHOD("set_button", "column", "button_index", "button"), &TreeItem::set_button); ClassDB::bind_method(D_METHOD("erase_button", "column", "button_index"), &TreeItem::erase_button); + ClassDB::bind_method(D_METHOD("set_button_alt_text", "column", "button_index", "alt_text"), &TreeItem::set_button_alt_text); ClassDB::bind_method(D_METHOD("set_button_disabled", "column", "button_index", "disabled"), &TreeItem::set_button_disabled); ClassDB::bind_method(D_METHOD("set_button_color", "column", "button_index", "color"), &TreeItem::set_button_color); ClassDB::bind_method(D_METHOD("is_button_disabled", "column", "button_index"), &TreeItem::is_button_disabled); @@ -2544,6 +2630,13 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 theme_cache.button_hover->draw(get_canvas_item(), Rect2(od.x, od.y, button_size.width, MAX(button_size.height, label_h))); } } + if (selected_item == p_item && selected_col == i && selected_button == j) { + Point2 od = button_ofs; + if (rtl) { + od.x = get_size().width - od.x - button_size.x; + } + theme_cache.button_hover->draw(get_canvas_item(), Rect2(od.x, od.y, button_size.width, MAX(button_size.height, label_h))); + } button_ofs.y += (label_h - button_size.height) / 2; button_ofs += theme_cache.button_pressed->get_offset(); @@ -2831,6 +2924,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c selected_item = p_selected; selected_col = i; + selected_button = -1; emit_signal(SNAME("cell_selected")); if (select_mode == SELECT_MULTI) { @@ -2842,6 +2936,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c } else if (select_mode == SELECT_MULTI && (selected_item != p_selected || selected_col != i)) { selected_item = p_selected; selected_col = i; + selected_button = -1; emit_signal(SNAME("cell_selected")); } } else { @@ -2873,6 +2968,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c } c = c->next; } + queue_accessibility_update(); } Rect2 Tree::search_item_rect(TreeItem *p_from, TreeItem *p_item) { @@ -3051,6 +3147,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int emit_signal(SNAME("item_mouse_selected"), get_local_mouse_position(), p_button); } } + queue_accessibility_update(); queue_redraw(); } } @@ -3356,7 +3453,10 @@ void Tree::popup_select(int p_option) { } void Tree::_go_left() { - if (selected_col == 0) { + if (get_tree()->is_accessibility_enabled() && selected_button >= 0) { + selected_button--; + } else if (selected_col == 0) { + selected_button = -1; if (selected_item->get_first_child() != nullptr && !selected_item->is_collapsed()) { selected_item->set_collapsed(true); } else { @@ -3371,6 +3471,7 @@ void Tree::_go_left() { } } } else { + selected_button = -1; if (select_mode == SELECT_MULTI) { selected_col--; emit_signal(SNAME("cell_selected")); @@ -3378,13 +3479,18 @@ void Tree::_go_left() { selected_item->select(selected_col - 1); } } + queue_accessibility_update(); queue_redraw(); accept_event(); ensure_cursor_is_visible(); } void Tree::_go_right() { - if (selected_col == (columns.size() - 1)) { + int buttons = (selected_item && selected_col >= 0 && selected_col < columns.size()) ? selected_item->cells[selected_col].buttons.size() : 0; + if (get_tree()->is_accessibility_enabled() && selected_button < buttons - 1) { + selected_button++; + } else if (selected_col == (columns.size() - 1)) { + selected_button = -1; if (selected_item->get_first_child() != nullptr && selected_item->is_collapsed()) { selected_item->set_collapsed(false); } else if (selected_item->get_next_visible()) { @@ -3392,6 +3498,7 @@ void Tree::_go_right() { _go_down(); } } else { + selected_button = -1; if (select_mode == SELECT_MULTI) { selected_col++; emit_signal(SNAME("cell_selected")); @@ -3399,6 +3506,7 @@ void Tree::_go_right() { selected_item->select(selected_col + 1); } } + queue_accessibility_update(); queue_redraw(); ensure_cursor_is_visible(); accept_event(); @@ -3409,6 +3517,7 @@ void Tree::_go_up() { if (!selected_item) { prev = get_last_item(); selected_col = 0; + selected_button = -1; } else { prev = selected_item->get_prev_visible(); } @@ -3433,6 +3542,7 @@ void Tree::_go_up() { prev->select(col); } + queue_accessibility_update(); ensure_cursor_is_visible(); accept_event(); } @@ -3467,6 +3577,7 @@ void Tree::_go_down() { next->select(col); } + queue_accessibility_update(); ensure_cursor_is_visible(); accept_event(); } @@ -3575,7 +3686,12 @@ void Tree::gui_input(const Ref &p_event) { } _go_down(); + } else if (p_event->is_action("ui_menu") && p_event->is_pressed()) { + if (allow_rmb_select && selected_item) { + emit_signal(SNAME("item_mouse_selected"), get_item_rect(selected_item).position, MouseButton::RIGHT); + } + accept_event(); } else if (p_event->is_action("ui_page_down") && p_event->is_pressed()) { if (!cursor_can_exit_tree) { accept_event(); @@ -3602,6 +3718,7 @@ void Tree::gui_input(const Ref &p_event) { if (select_mode == SELECT_MULTI) { selected_item = next; emit_signal(SNAME("cell_selected")); + queue_accessibility_update(); queue_redraw(); } else { while (next && !next->cells[selected_col].selectable) { @@ -3640,6 +3757,7 @@ void Tree::gui_input(const Ref &p_event) { if (select_mode == SELECT_MULTI) { selected_item = prev; emit_signal(SNAME("cell_selected")); + queue_accessibility_update(); queue_redraw(); } else { while (prev && !prev->cells[selected_col].selectable) { @@ -3656,19 +3774,28 @@ void Tree::gui_input(const Ref &p_event) { if (!selected_item) { return; } - if (selected_item->is_selected(selected_col)) { + if (selected_item && selected_col != -1 && selected_button != -1) { + const TreeItem::Cell &c = selected_item->cells[selected_col]; + emit_signal("button_clicked", selected_item, selected_col, c.buttons[selected_button].id, MouseButton::LEFT); + } else if (selected_item->is_selected(selected_col)) { selected_item->deselect(selected_col); emit_signal(SNAME("multi_selected"), selected_item, selected_col, false); } else if (selected_item->is_selectable(selected_col)) { selected_item->select(selected_col); emit_signal(SNAME("multi_selected"), selected_item, selected_col, true); } + } else if (selected_item && selected_col != -1 && selected_button != -1) { + const TreeItem::Cell &c = selected_item->cells[selected_col]; + emit_signal("button_clicked", selected_item, selected_col, c.buttons[selected_button].id, MouseButton::LEFT); } accept_event(); } else if (p_event->is_action("ui_accept") && p_event->is_pressed()) { if (selected_item) { // Bring up editor if possible. - if (!edit_selected()) { + if (selected_item && selected_col != -1 && selected_button != -1) { + const TreeItem::Cell &c = selected_item->cells[selected_col]; + emit_signal("button_clicked", selected_item, selected_col, c.buttons[selected_button].id, MouseButton::LEFT); + } else if (!edit_selected()) { emit_signal(SNAME("item_activated")); incr_search.clear(); } @@ -4312,8 +4439,360 @@ int Tree::_get_title_button_height() const { return h; } +void Tree::_check_item_accessibility(TreeItem *p_item, PackedStringArray &r_warnings, int &r_row) const { + for (int i = 0; i < p_item->cells.size(); i++) { + const TreeItem::Cell &cell = p_item->cells[i]; + if (cell.alt_text.strip_edges().is_empty() && cell.text.strip_edges().is_empty()) { + r_warnings.push_back(vformat(RTR("Cell %d x %d: either text or alternative text must not be empty."), r_row, i)); + } + for (int j = 0; j < cell.buttons.size(); j++) { + if (cell.buttons[j].alt_text.strip_edges().is_empty()) { + r_warnings.push_back(vformat(RTR("Button %d in %d x %d: alternative text must not be empty."), j, r_row, i)); + } + } + } + r_row++; + + // Children. + if (!p_item->collapsed) { + TreeItem *c = p_item->first_child; + while (c) { + _check_item_accessibility(c, r_warnings, r_row); + c = c->next; + } + } +} + +PackedStringArray Tree::get_accessibility_configuration_warnings() const { + PackedStringArray warnings = Control::get_accessibility_configuration_warnings(); + + if (root) { + int row = 1; + _check_item_accessibility(root, warnings, row); + } + + return warnings; +} + +void Tree::_accessibility_action_scroll_down(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() / 4); +} + +void Tree::_accessibility_action_scroll_left(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() / 4); +} + +void Tree::_accessibility_action_scroll_right(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() / 4); +} + +void Tree::_accessibility_action_scroll_up(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() / 4); +} + +void Tree::_accessibility_action_scroll_set(const Variant &p_data) { + const Point2 &pos = p_data; + h_scroll->set_value(pos.x); + v_scroll->set_value(pos.y); +} + +void Tree::_accessibility_action_scroll_into_view(const Variant &p_data, TreeItem *p_item, int p_col) { + scroll_to_item(p_item); +} + +void Tree::_accessibility_action_focus(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->select(p_col); +} + +void Tree::_accessibility_action_blur(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->deselect(p_col); +} + +void Tree::_accessibility_action_collapse(const Variant &p_data, TreeItem *p_item) { + p_item->set_collapsed(true); +} + +void Tree::_accessibility_action_expand(const Variant &p_data, TreeItem *p_item) { + p_item->set_collapsed(false); +} + +void Tree::_accessibility_action_set_text_value(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->set_text(p_col, p_data); +} + +void Tree::_accessibility_action_set_num_value(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->set_range(p_col, p_data); +} + +void Tree::_accessibility_action_set_bool_value(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->set_checked(p_col, !p_item->cells[p_col].checked); +} + +void Tree::_accessibility_action_edit_custom(const Variant &p_data, TreeItem *p_item, int p_col) { + float popup_scale = popup_editor->is_embedded() ? 1.0 : popup_editor->get_parent_visible_window()->get_content_scale_factor(); + Rect2 rect; + if (select_mode == SELECT_ROW) { + rect = p_item->get_meta("__focus_col_" + itos(p_col)); + } else { + rect = p_item->get_meta("__focus_rect"); + } + rect.position *= popup_scale; + + edited_item = p_item; + edited_col = p_col; + custom_popup_rect = Rect2i(get_global_position() + rect.position, rect.size); + emit_signal(SNAME("custom_popup_edited"), false); + item_edited(p_col, p_item); +} + +void Tree::_accessibility_action_set_inc(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->set_range(p_col, p_item->cells[p_col].val + p_item->cells[p_col].step); +} + +void Tree::_accessibility_action_set_dec(const Variant &p_data, TreeItem *p_item, int p_col) { + p_item->set_range(p_col, p_item->cells[p_col].val - p_item->cells[p_col].step); +} + +void Tree::_accessibility_action_button_press(const Variant &p_data, TreeItem *p_item, int p_col, int p_btn) { + emit_signal("button_clicked", p_item, p_col, p_btn, MouseButton::LEFT); +} + +RID Tree::get_focused_accessibility_element() const { + if (selected_item) { + if (selected_col >= 0) { + if (selected_button >= 0) { + return selected_item->cells[selected_col].buttons[selected_button].accessibility_button_element; + } else { + return selected_item->cells[selected_col].accessibility_cell_element; + } + } else { + return selected_item->accessibility_row_element; + } + } else { + return get_accessibility_element(); + } +} + +void Tree::_accessibility_clean_info(TreeItem *p_item) { + p_item->accessibility_row_element = RID(); + for (TreeItem::Cell &cell : p_item->cells) { + cell.accessibility_cell_element = RID(); + for (TreeItem::Cell::Button &btn : cell.buttons) { + btn.accessibility_button_element = RID(); + } + } + + // Children. + TreeItem *c = p_item->first_child; + while (c) { + _accessibility_clean_info(c); + c = c->next; + } +} + +void Tree::_accessibility_update_item(Point2 &r_ofs, TreeItem *p_item, int &r_row, int p_level) { + // Row. + if ((p_item != root || !hide_root) && p_item->is_visible()) { + if (p_item->accessibility_row_element.is_null()) { + p_item->accessibility_row_element = DisplayServer::get_singleton()->accessibility_create_sub_element(accessibility_scroll_element, DisplayServer::AccessibilityRole::ROLE_TREE_ITEM); + p_item->accessibility_row_dirty = true; + } + + DisplayServer::get_singleton()->accessibility_update_set_table_row_index(p_item->accessibility_row_element, r_row); + DisplayServer::get_singleton()->accessibility_update_set_list_item_level(p_item->accessibility_row_element, p_level); + DisplayServer::get_singleton()->accessibility_update_set_list_item_expanded(p_item->accessibility_row_element, !p_item->collapsed); + DisplayServer::get_singleton()->accessibility_update_set_flag(p_item->accessibility_row_element, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, !(p_item->visible && !p_item->parent_visible_in_tree)); + DisplayServer::get_singleton()->accessibility_update_add_action(p_item->accessibility_row_element, DisplayServer::AccessibilityAction::ACTION_COLLAPSE, callable_mp(this, &Tree::_accessibility_action_collapse).bind(p_item)); + DisplayServer::get_singleton()->accessibility_update_add_action(p_item->accessibility_row_element, DisplayServer::AccessibilityAction::ACTION_EXPAND, callable_mp(this, &Tree::_accessibility_action_expand).bind(p_item)); + + DisplayServer::get_singleton()->accessibility_update_set_list_item_selected(p_item->accessibility_row_element, selected_item == p_item); + if (p_item == root && is_root_hidden()) { + DisplayServer::get_singleton()->accessibility_update_set_flag(p_item->accessibility_row_element, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, true); + } + Transform2D row_xform; + row_xform.set_origin(r_ofs); + DisplayServer::get_singleton()->accessibility_update_set_transform(p_item->accessibility_row_element, row_xform); + + Size2 item_size = Size2(get_size().width, compute_item_height(p_item)); + DisplayServer::get_singleton()->accessibility_update_set_bounds(p_item->accessibility_row_element, Rect2(Vector2(), item_size)); + + if (p_item->accessibility_row_dirty) { + // Cells. + int col_offset = 0; + for (int i = 0; i < p_item->cells.size(); i++) { + TreeItem::Cell &cell = p_item->cells.write[i]; + + if (cell.accessibility_cell_element.is_null()) { + cell.accessibility_cell_element = DisplayServer::get_singleton()->accessibility_create_sub_element(p_item->accessibility_row_element, DisplayServer::AccessibilityRole::ROLE_CELL); + } + + float cw = get_column_width(i); + + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &Tree::_accessibility_action_scroll_into_view).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &Tree::_accessibility_action_focus).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &Tree::_accessibility_action_blur).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_COLLAPSE, callable_mp(this, &Tree::_accessibility_action_collapse).bind(p_item)); + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_EXPAND, callable_mp(this, &Tree::_accessibility_action_expand).bind(p_item)); + + DisplayServer::get_singleton()->accessibility_update_set_table_cell_position(cell.accessibility_cell_element, r_row, i); + DisplayServer::get_singleton()->accessibility_update_set_list_item_selected(cell.accessibility_cell_element, cell.selected); + if (cell.alt_text.is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_name(cell.accessibility_cell_element, cell.xl_text); + } else { + DisplayServer::get_singleton()->accessibility_update_set_name(cell.accessibility_cell_element, cell.alt_text); + } + + DisplayServer::get_singleton()->accessibility_update_set_text_align(cell.accessibility_cell_element, cell.text_alignment); + DisplayServer::get_singleton()->accessibility_update_set_flag(cell.accessibility_cell_element, DisplayServer::AccessibilityFlags::FLAG_HIDDEN, !(p_item->visible && !p_item->parent_visible_in_tree)); + DisplayServer::get_singleton()->accessibility_update_set_flag(cell.accessibility_cell_element, DisplayServer::AccessibilityFlags::FLAG_READONLY, !cell.editable); + DisplayServer::get_singleton()->accessibility_update_set_tooltip(cell.accessibility_cell_element, cell.tooltip); + switch (cell.mode) { + case TreeItem::CELL_MODE_STRING: { + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &Tree::_accessibility_action_set_text_value).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_set_value(cell.accessibility_cell_element, cell.xl_text); + } break; + case TreeItem::CELL_MODE_CHECK: { + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_CLICK, callable_mp(this, &Tree::_accessibility_action_set_bool_value).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_set_checked(cell.accessibility_cell_element, cell.checked); + } break; + case TreeItem::CELL_MODE_RANGE: { + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_DECREMENT, callable_mp(this, &Tree::_accessibility_action_set_dec).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_INCREMENT, callable_mp(this, &Tree::_accessibility_action_set_inc).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_SET_VALUE, callable_mp(this, &Tree::_accessibility_action_set_num_value).bind(p_item, i)); + DisplayServer::get_singleton()->accessibility_update_set_num_value(cell.accessibility_cell_element, cell.val); + DisplayServer::get_singleton()->accessibility_update_set_num_range(cell.accessibility_cell_element, cell.min, cell.max); + if (cell.step > 0) { + DisplayServer::get_singleton()->accessibility_update_set_num_step(cell.accessibility_cell_element, cell.step); + } else { + DisplayServer::get_singleton()->accessibility_update_set_num_step(cell.accessibility_cell_element, 1); + } + } break; + case TreeItem::CELL_MODE_ICON: { + // NOP + } break; + case TreeItem::CELL_MODE_CUSTOM: { + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_CLICK, callable_mp(this, &Tree::_accessibility_action_edit_custom).bind(p_item, i)); + } break; + } + DisplayServer::get_singleton()->accessibility_update_set_background_color(cell.accessibility_cell_element, cell.color); + DisplayServer::get_singleton()->accessibility_update_set_foreground_color(cell.accessibility_cell_element, cell.bg_color); + + DisplayServer::get_singleton()->accessibility_update_set_bounds(cell.accessibility_cell_element, Rect2(Point2(col_offset, 0), Size2(cw, item_size.y))); + + Vector2 ofst = Vector2(col_offset + cw, 0); + for (int j = cell.buttons.size() - 1; j >= 0; j--) { + if (cell.buttons[j].accessibility_button_element.is_null()) { + cell.buttons[j].accessibility_button_element = DisplayServer::get_singleton()->accessibility_create_sub_element(cell.accessibility_cell_element, DisplayServer::AccessibilityRole::ROLE_BUTTON); + } + + DisplayServer::get_singleton()->accessibility_update_add_action(cell.buttons[j].accessibility_button_element, DisplayServer::AccessibilityAction::ACTION_CLICK, callable_mp(this, &Tree::_accessibility_action_button_press).bind(p_item, i, j)); + DisplayServer::get_singleton()->accessibility_update_set_flag(cell.buttons[j].accessibility_button_element, DisplayServer::AccessibilityFlags::FLAG_DISABLED, cell.buttons[j].disabled); + DisplayServer::get_singleton()->accessibility_update_set_tooltip(cell.buttons[j].accessibility_button_element, cell.buttons[j].tooltip); + if (cell.buttons[j].alt_text.is_empty()) { + DisplayServer::get_singleton()->accessibility_update_set_name(cell.buttons[j].accessibility_button_element, cell.buttons[j].tooltip); + } else { + DisplayServer::get_singleton()->accessibility_update_set_name(cell.buttons[j].accessibility_button_element, cell.buttons[j].alt_text); + } + + Ref b = cell.buttons[j].texture; + Size2 b_size = b->get_size() + theme_cache.button_pressed->get_minimum_size(); + ofst.x -= b_size.x; + + DisplayServer::get_singleton()->accessibility_update_set_bounds(cell.buttons[j].accessibility_button_element, Rect2(ofst, b_size)); + } + col_offset += cw; + } + } + + r_ofs.y += item_size.y; + r_ofs.y += theme_cache.v_separation; + + p_item->accessibility_row_dirty = false; + r_row++; + } + + // Children. + if (!p_item->collapsed) { + TreeItem *c = p_item->first_child; + while (c) { + _accessibility_update_item(r_ofs, c, r_row, p_level + 1); + c = c->next; + } + } +} + void Tree::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_EXIT_TREE: + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + if (root) { + _accessibility_clean_info(root); + } + for (ColumnInfo &col : columns) { + col.accessibility_col_element = RID(); + } + accessibility_scroll_element = RID(); + } break; + + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_TREE); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_DOWN, callable_mp(this, &Tree::_accessibility_action_scroll_down)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_LEFT, callable_mp(this, &Tree::_accessibility_action_scroll_left)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_RIGHT, callable_mp(this, &Tree::_accessibility_action_scroll_right)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_UP, callable_mp(this, &Tree::_accessibility_action_scroll_up)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SET_SCROLL_OFFSET, callable_mp(this, &Tree::_accessibility_action_scroll_set)); + + Ref bg = theme_cache.panel_style; + int tbh = _get_title_button_height(); + + // Columns. + int ofs = theme_cache.panel_style->get_margin(SIDE_LEFT); + int cs = columns.size(); + for (int i = 0; i < cs; i++) { + ColumnInfo &column = columns.write[i]; + + if (column.accessibility_col_element.is_null()) { + column.accessibility_col_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_COLUMN_HEADER); + } + + DisplayServer::get_singleton()->accessibility_update_set_table_column_index(column.accessibility_col_element, i); + DisplayServer::get_singleton()->accessibility_update_set_name(column.accessibility_col_element, column.xl_title); + DisplayServer::get_singleton()->accessibility_update_set_text_align(column.accessibility_col_element, column.title_alignment); + + Rect2 tbrect = Rect2(ofs - theme_cache.offset.x, bg->get_margin(SIDE_TOP), get_column_width(i), tbh); + if (cache.rtl) { + tbrect.position.x = get_size().width - tbrect.size.x - tbrect.position.x; + } + ofs += tbrect.size.width; + DisplayServer::get_singleton()->accessibility_update_set_bounds(column.accessibility_col_element, Rect2(tbrect.position, tbrect.size)); + } + + DisplayServer::get_singleton()->accessibility_update_set_table_column_count(ae, cs); + + // Scroll container. + if (accessibility_scroll_element.is_null()) { + accessibility_scroll_element = DisplayServer::get_singleton()->accessibility_create_sub_element(ae, DisplayServer::AccessibilityRole::ROLE_CONTAINER); + } + + Transform2D scroll_xform; + scroll_xform.set_origin(Vector2i(-h_scroll->get_value(), -v_scroll->get_value())); + DisplayServer::get_singleton()->accessibility_update_set_transform(accessibility_scroll_element, scroll_xform); + DisplayServer::get_singleton()->accessibility_update_set_bounds(accessibility_scroll_element, Rect2(0, 0, h_scroll->get_max(), v_scroll->get_max())); + + // Rows (and cells). + Point2 origin = Point2(theme_cache.panel_style->get_margin(SIDE_LEFT) - theme_cache.offset.x, bg->get_margin(SIDE_TOP) + tbh); + int rows = 0; + if (root) { + _accessibility_update_item(origin, root, rows, 0); + } + DisplayServer::get_singleton()->accessibility_update_set_table_row_count(ae, rows); + + } break; + case NOTIFICATION_FOCUS_ENTER: { if (get_viewport()) { focus_in_id = get_viewport()->get_processed_events_count(); @@ -4565,6 +5044,7 @@ TreeItem *Tree::create_item(TreeItem *p_parent, int p_index) { _determine_hovered_item(); + queue_accessibility_update(); return ti; } @@ -4592,6 +5072,7 @@ void Tree::item_edited(int p_column, TreeItem *p_item, MouseButton p_custom_mous if (p_custom_mouse_index != MouseButton::NONE) { emit_signal(SNAME("custom_item_clicked"), p_custom_mouse_index); } + queue_accessibility_update(); } void Tree::item_changed(int p_column, TreeItem *p_item) { @@ -4605,7 +5086,9 @@ void Tree::item_changed(int p_column, TreeItem *p_item) { columns.write[i].cached_minimum_width_dirty = true; } } + p_item->accessibility_row_dirty = true; } + queue_accessibility_update(); queue_redraw(); } @@ -4620,9 +5103,12 @@ void Tree::item_selected(int p_column, TreeItem *p_item) { selected_col = p_column; selected_item = p_item; + selected_button = -1; } else { select_single_item(p_item, root, p_column); } + p_item->accessibility_row_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -4641,6 +5127,7 @@ void Tree::item_deselected(int p_column, TreeItem *p_item) { } } } + selected_button = -1; if (select_mode == SELECT_MULTI || select_mode == SELECT_SINGLE) { p_item->cells.write[p_column].selected = false; @@ -4649,6 +5136,8 @@ void Tree::item_deselected(int p_column, TreeItem *p_item) { p_item->cells.write[i].selected = false; } } + p_item->accessibility_row_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -4679,7 +5168,8 @@ void Tree::deselect_all() { selected_item = nullptr; selected_col = -1; - + selected_button = -1; + queue_accessibility_update(); queue_redraw(); } @@ -4711,6 +5201,7 @@ void Tree::clear() { _determine_hovered_item(); + queue_accessibility_update(); queue_redraw(); } @@ -4720,6 +5211,7 @@ void Tree::set_hide_root(bool p_enabled) { } hide_root = p_enabled; + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -4740,6 +5232,7 @@ void Tree::set_column_custom_minimum_width(int p_column, int p_min_width) { } columns.write[p_column].custom_min_width = p_min_width; columns.write[p_column].cached_minimum_width_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -4752,6 +5245,7 @@ void Tree::set_column_expand(int p_column, bool p_expand) { columns.write[p_column].expand = p_expand; columns.write[p_column].cached_minimum_width_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -4764,6 +5258,7 @@ void Tree::set_column_expand_ratio(int p_column, int p_ratio) { columns.write[p_column].expand_ratio = p_ratio; columns.write[p_column].cached_minimum_width_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -4776,6 +5271,7 @@ void Tree::set_column_clip_content(int p_column, bool p_fit) { columns.write[p_column].clip_content = p_fit; columns.write[p_column].cached_minimum_width_dirty = true; + queue_accessibility_update(); queue_redraw(); } @@ -4936,6 +5432,19 @@ int Tree::get_column_width(int p_column) const { void Tree::propagate_set_columns(TreeItem *p_item) { p_item->cells.resize(columns.size()); + p_item->accessibility_row_dirty = true; + for (TreeItem::Cell &cell : p_item->cells) { + if (cell.accessibility_cell_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(cell.accessibility_cell_element); + cell.accessibility_cell_element = RID(); + } + for (TreeItem::Cell::Button &btn : cell.buttons) { + if (btn.accessibility_button_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(btn.accessibility_button_element); + btn.accessibility_button_element = RID(); + } + } + } TreeItem *c = p_item->get_first_child(); while (c) { @@ -4947,6 +5456,16 @@ void Tree::propagate_set_columns(TreeItem *p_item) { void Tree::set_columns(int p_columns) { ERR_FAIL_COND(p_columns < 1); ERR_FAIL_COND(blocked > 0); + + if (columns.size() > p_columns) { + for (int i = p_columns; i < columns.size(); i++) { + if (columns[i].accessibility_col_element.is_valid()) { + DisplayServer::get_singleton()->accessibility_free_element(columns[i].accessibility_col_element); + columns.write[i].accessibility_col_element = RID(); + } + } + } + columns.resize(p_columns); if (root) { @@ -4954,7 +5473,9 @@ void Tree::set_columns(int p_columns) { } if (selected_col >= p_columns) { selected_col = p_columns - 1; + selected_button = -1; } + queue_accessibility_update(); queue_redraw(); } @@ -5053,6 +5574,8 @@ void Tree::ensure_cursor_is_visible() { h_scroll->set_value(x_offset); } } + + queue_accessibility_update(); } int Tree::get_pressed_button() const { @@ -5139,6 +5662,7 @@ void Tree::set_column_titles_visible(bool p_show) { } show_column_titles = p_show; + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -5157,6 +5681,7 @@ void Tree::set_column_title(int p_column, const String &p_title) { columns.write[p_column].title = p_title; columns.write[p_column].xl_title = atr(p_title); update_column(p_column); + queue_accessibility_update(); queue_redraw(); } @@ -5178,6 +5703,7 @@ void Tree::set_column_title_alignment(int p_column, HorizontalAlignment p_alignm columns.write[p_column].title_alignment = p_alignment; update_column(p_column); + queue_accessibility_update(); queue_redraw(); } @@ -5192,6 +5718,7 @@ void Tree::set_column_title_direction(int p_column, Control::TextDirection p_tex if (columns[p_column].text_direction != p_text_direction) { columns.write[p_column].text_direction = p_text_direction; update_column(p_column); + queue_accessibility_update(); queue_redraw(); } } @@ -5206,6 +5733,7 @@ void Tree::set_column_title_language(int p_column, const String &p_language) { if (columns[p_column].language != p_language) { columns.write[p_column].language = p_language; update_column(p_column); + queue_accessibility_update(); queue_redraw(); } } @@ -5256,6 +5784,7 @@ void Tree::scroll_to_item(TreeItem *p_item, bool p_center_on_item) { } } } + queue_accessibility_update(); } void Tree::set_h_scroll_enabled(bool p_enable) { @@ -5694,18 +6223,29 @@ TreeItem *Tree::get_item_at_position(const Point2 &p_pos) const { } int Tree::get_button_id_at_position(const Point2 &p_pos) const { - if (!root || !Rect2(Vector2(), get_size()).has_point(p_pos)) { + if (!root) { return -1; } - TreeItem *it; - int col, index, section; - _find_button_at_pos(p_pos, it, col, index, section); + if (p_pos == Vector2(INFINITY, INFINITY)) { + if (selected_item && selected_button >= 0) { + return selected_item->cells[selected_col].buttons[selected_button].id; + } + } else { + if (!Rect2(Vector2(), get_size()).has_point(p_pos)) { + return -1; + } - if (index == -1) { - return -1; + TreeItem *it; + int col, index, section; + _find_button_at_pos(p_pos, it, col, index, section); + + if (index == -1) { + return -1; + } + return it->cells[col].buttons[index].id; } - return it->cells[col].buttons[index].id; + return -1; } String Tree::get_tooltip(const Point2 &p_pos) const { diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 784c264663b..979b7ac2da3 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -56,6 +56,7 @@ private: friend class Tree; struct Cell { + mutable RID accessibility_cell_element; TreeCellMode mode = TreeItem::CELL_MODE_STRING; Ref icon; @@ -64,6 +65,7 @@ private: String text; String xl_text; Node::AutoTranslateMode auto_translate_mode = Node::AUTO_TRANSLATE_MODE_INHERIT; + String alt_text; bool edit_multiline = false; String suffix; Ref text_buf; @@ -104,11 +106,13 @@ private: Callable custom_draw_callback; struct Button { + mutable RID accessibility_button_element; int id = 0; bool disabled = false; Ref texture; Color color = Color(1, 1, 1, 1); String tooltip; + String alt_text; }; Vector