diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml index 0420a02b1dd9e0..95d71d77f87f2c 100644 --- a/.github/workflows/linux_builds.yml +++ b/.github/workflows/linux_builds.yml @@ -6,7 +6,7 @@ on: env: # Used for the cache key. Add version suffix to force clean build. GODOT_BASE_BRANCH: master - SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes + SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes "accesskit_sdk_path=${{github.workspace}}/accesskit_c-v0.9.0/" DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true TSAN_OPTIONS: suppressions=misc/error_suppressions/tsan.txt @@ -118,6 +118,17 @@ jobs: - name: Setup python and scons uses: ./.github/actions/godot-deps + - name: Download pre-built AccessKit + uses: dsaltares/fetch-gh-release-asset@1.0.0 + with: + repo: AccessKit/accesskit + version: tags/accesskit_c-v0.9.0 + file: accesskit_c-v0.9.0.zip + target: accesskit_c-v0.9.0/accesskit_c.zip + + - name: Extract pre-built AccessKit + run: unzip -o accesskit_c-v0.9.0/accesskit_c.zip + - name: Setup GCC problem matcher uses: ammaraskar/gcc-problem-matcher@master diff --git a/.github/workflows/macos_builds.yml b/.github/workflows/macos_builds.yml index 70031ec4c34185..2d51fbb46d5c5d 100644 --- a/.github/workflows/macos_builds.yml +++ b/.github/workflows/macos_builds.yml @@ -6,7 +6,7 @@ on: env: # Used for the cache key. Add version suffix to force clean build. GODOT_BASE_BRANCH: master - SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes + SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes "accesskit_sdk_path=${{github.workspace}}/accesskit_c-v0.9.0/" concurrency: group: ci-${{github.actor}}-${{github.head_ref || github.run_number}}-${{github.ref}}-macos @@ -46,6 +46,17 @@ jobs: - name: Setup python and scons uses: ./.github/actions/godot-deps + - name: Download pre-built AccessKit + uses: dsaltares/fetch-gh-release-asset@1.0.0 + with: + repo: AccessKit/accesskit + version: tags/accesskit_c-v0.9.0 + file: accesskit_c-v0.9.0.zip + target: accesskit_c-v0.9.0/accesskit_c.zip + + - name: Extract pre-built AccessKit + run: unzip -o accesskit_c-v0.9.0/accesskit_c.zip + - name: Setup Vulkan SDK run: | sh misc/scripts/install_vulkan_sdk_macos.sh diff --git a/.github/workflows/windows_builds.yml b/.github/workflows/windows_builds.yml index 5443ba20ab5cc6..ae05088136c04d 100644 --- a/.github/workflows/windows_builds.yml +++ b/.github/workflows/windows_builds.yml @@ -7,7 +7,7 @@ on: env: # Used for the cache key. Add version suffix to force clean build. GODOT_BASE_BRANCH: master - SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes d3d12=yes + SCONSFLAGS: verbose=yes warnings=extra werror=yes module_text_server_fb_enabled=yes d3d12=yes "accesskit_sdk_path=${{github.workspace}}/accesskit_c-v0.9.0/" SCONS_CACHE_MSVC_CONFIG: true concurrency: @@ -54,6 +54,17 @@ jobs: - name: Download Direct3D 12 SDK components run: python ./misc/scripts/install_d3d12_sdk_windows.py + - name: Download pre-built AccessKit + uses: dsaltares/fetch-gh-release-asset@1.0.0 + with: + repo: AccessKit/accesskit + version: tags/accesskit_c-v0.9.0 + file: accesskit_c-v0.9.0.zip + target: accesskit_c-v0.9.0/accesskit_c.zip + + - name: Extract pre-built AccessKit + run: Expand-Archive -Force accesskit_c-v0.9.0/accesskit_c.zip ${{github.workspace}}/ + - name: Setup MSVC problem matcher uses: ammaraskar/msvc-problem-matcher@master diff --git a/SConstruct b/SConstruct index 81ce4bca52db4a..f89d432eead617 100644 --- a/SConstruct +++ b/SConstruct @@ -216,6 +216,7 @@ opts.Add(BoolVariable("use_volk", "Use the volk library to load the Vulkan loade opts.Add(BoolVariable("disable_exceptions", "Force disabling exception handling code", True)) opts.Add("custom_modules", "A list of comma-separated directory paths containing custom modules to build.", "") opts.Add(BoolVariable("custom_modules_recursive", "Detect custom modules recursively for each specified path.", True)) +opts.Add(("accesskit_sdk_path", "Path to the AccessKit C SDK", "")) # Advanced options opts.Add(BoolVariable("dev_mode", "Alias for dev options: verbose=yes warnings=extra werror=yes tests=yes", False)) diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index ee20aea35d3d3f..3c5ebd0bae4c29 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1448,6 +1448,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/accessibility/accessibility_support", PROPERTY_HINT_ENUM, "Auto (When Screen Reader is Running),Always Active,Disabled"), 0); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "accessibility/accessibility/updates_per_second", PROPERTY_HINT_RANGE, "1,100,1"), 15); + // 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/string/ustring.cpp b/core/string/ustring.cpp index 2b62b72a51ad20..72c3d25175be9a 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -2024,34 +2024,43 @@ Error String::parse_utf8(const char *p_utf8, int p_len, bool p_skip_cr) { } } -CharString String::utf8() const { +CharString String::utf8(Vector *r_ch_length_map) const { int l = length(); if (!l) { return CharString(); } + if (r_ch_length_map) { + r_ch_length_map->resize(l); + } + 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 (r_ch_length_map) { + r_ch_length_map->write[i] = ch_w; + } } CharString utf8s; diff --git a/core/string/ustring.h b/core/string/ustring.h index 693df6dcbabd85..1c3eedfa4691cb 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -388,7 +388,7 @@ class String { char32_t unicode_at(int p_idx) const; CharString ascii(bool p_allow_extended = false) const; - CharString utf8() const; + CharString utf8(Vector *r_ch_length_map = nullptr) const; Error parse_utf8(const char *p_utf8, int p_len = -1, bool p_skip_cr = false); static String utf8(const char *p_utf8, int p_len = -1); diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index a0c76a3ad6cb35..0b8499b07f4795 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -1146,6 +1146,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]. + Sent when the node changes size. Use [member size] to get the new size. diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index a0540482667344..b306698218145a 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -10,6 +10,610 @@ + + + + + + Creates a new, empty accessibility element resource for the [Node]. + Node: An accessibility element for each [Node] is created and freed automatically. 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 object which are not [Node]s, such as list items, table cells or menu item. Sub-elements are freed automatically when the parent element is freed, or can be free early using [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 free early using [method accessibility_free_element] method. + + + + + + + Returns metadata of the accessibility element. + + + + + + + + Sets 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 a main accessibility element of the OS native window. + + + + + + + Returns [code]true[/code] if [param id] is valid accessibility element. + + + + + + Returns [code]1[/code] if 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 (X11), 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 app and will not affect this value. To test your app with these tools, set [member ProjectSettings.accessibility/accessibility/accessibility_support] to [code]1[/code]. + + + + + + Returns [code]1[/code] if 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, 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 mark this action as support. Action callback receive one [Variant] argument, which value depends on action type. + + + + + + + + Adds child accessibility element. + [b]Note:[/b] [Node] children are added to the child list automatically. + [b]Note:[/b] Sub-elements are added to the child list automatically. + + + + + + + + + Adds support for the custom accessibility action, [param action_id] is passed as an argument to the callback of [constant ACTION_CUSTOM] action. + + + + + + + + Adds element that is controlled by this element. + + + + + + + + Adds element that describes this element. + + + + + + + + Adds element that details this element. + + + + + + + + Adds element that this element flow into. + + + + + + + + Adds element that labels this element. + + + + + + + + Adds 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 an [param related_id]. + + + + + + + + Adds element that is active descendent 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 verb used to describe default action. + + + + + + + + Sets element accessibility description. + + + + + + + + Sets element which contains error message for this element. + + + + + + + + + Sets element flag, see [enum DisplayServer.AccessibilityFlags]. + + + + + + + Sets currently foucsed 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 position of the element in the list. + + + + + + + + Sets hierarchical level of the element in the list. + + + + + + + + Sets list/tree item selected status. + + + + + + + + Sets orientation of the list elements. + + + + + + + + Sets priority of the live region updates. + + + + + + + + Sets element to as a member of 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 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. + + @@ -1853,6 +2457,294 @@ Display server supports spawning dialogs for selecting files or directories using the operating system's native look-and-feel. See [method file_dialog_show] and [method file_dialog_with_options_show]. [b]Windows, macOS, Linux (X11/Wayland)[/b] + + Display server supports interaction with screen reader or Braille display. + + + 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 ny screen reader. + + + 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 view element. + + + List view item element. + + + Tab bar element. + + + Tab bar item 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 hovered list/tree item. + + + Element is hidden for accessibility tools. + + + + + Element is support multiple item selection. + + + + + + + Element content is not ready (e.g. loading). + + + Element is modal window. + + + + + Element is text field with selectable but read-only text. + + + Element is disabled. + + + Element clips children. + + + "Click" verb. + + + "Focus" verb. + + + "Check" verb. + + + "Uncheck" verb. + + + "Click Ancestor" verb. + + + "Jump" verb. + + + "Open" verb. + + + "Press" verb. + + + "Select" verb. + + + Default 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 (e.g. 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/EditorSettings.xml b/doc/classes/EditorSettings.xml index 54c20bc2ff2c7f..a78aefc3c5fec0 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -217,6 +217,9 @@ The tint intensity to use for the subresources background in the Inspector dock. The tint is used to distinguish between different subresources in the inspector. Higher values result in a more noticeable background color difference. + + If [code]true[/code], accessibility related warnings are displayed alongside other configuration warnings. + If [code]true[/code], the scene tree dock will automatically unfold nodes when a node that has folded parents is selected. diff --git a/doc/classes/Label.xml b/doc/classes/Label.xml index 8acd05cbd14f33..0873e37c0fcd8c 100644 --- a/doc/classes/Label.xml +++ b/doc/classes/Label.xml @@ -55,6 +55,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. @@ -144,6 +145,9 @@ Font size of the [Label]'s text. + + [StyleBox] used when the [Label] is focused (when using with assistive apps). + Background [StyleBox] for the [Label]. diff --git a/doc/classes/LinkButton.xml b/doc/classes/LinkButton.xml index bcdffcd1eeafcc..853a63fccb83f5 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 9e4287331c61b5..50928e6a47534b 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 16b3772fa90d20..a1e0b34ffc4a1b 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 3342e99ab6415c..4ac28f44b5b8d3 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -36,6 +36,13 @@ 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. + + @@ -56,6 +63,12 @@ [/codeblock] + + + + Called during accessibility information updates to determine currently focused sub-element, should return sub-element RID or value returned by [method get_accessibility_element]. + + @@ -295,6 +308,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]). + + @@ -765,6 +785,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. + + @@ -975,6 +1001,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]. @@ -1249,6 +1296,12 @@ Notification received when the [TextServer] is changed. + + Notification received when 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 e8d7dfb91374d7..7ae2ce78797127 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -238,6 +238,16 @@ + + Accessibility support mode: + If [code]0[/code], accessibility support is fully disabled. + If [code]1[/code], accessibility support is enabled, and accessibility information updates are processed regardless of current assistive apps' status. + If [code]2[/code], accessibility support is enabled, but accessibility information updates are processed only is assistive app (e.g. screen reader or Braille display) is active (default). + [b]Note:[/b] Accessibility debugging tools, such as Accessibility Insights for Windows, macOS Accessibility Inspector or AT-SPI Browser do not count as assistive app. 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. diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml index f4e3c1209fb3f3..42a5c36e66956e 100644 --- a/doc/classes/RichTextLabel.xml +++ b/doc/classes/RichTextLabel.xml @@ -28,6 +28,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. @@ -35,6 +36,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. @@ -491,8 +493,10 @@ + Adds a [code skip-lint][table=columns,inline_align][/code] tag to the tag stack. + [param name] is used as the table name for assistive apps. @@ -579,6 +583,14 @@ If [param expand] is [code]false[/code], the column will not contribute to the total ratio. + + + + + + Sets table column name for assistive apps. + + @@ -622,6 +634,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 bae5fe12054be2..8445a59f7e2a95 100644 --- a/doc/classes/SceneTree.xml +++ b/doc/classes/SceneTree.xml @@ -151,6 +151,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 activity processed. + + + + + + Returns [code]true[/code] if accessibility features are supported by OS and enabled in project settings. + + diff --git a/doc/classes/TextLine.xml b/doc/classes/TextLine.xml index 2e4a3f7e4c82ed..47efabbd0688eb 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 text writing direction inferred by BiDi algorithm. + + diff --git a/doc/classes/TextParagraph.xml b/doc/classes/TextParagraph.xml index 541078ed223897..db469334228d86 100644 --- a/doc/classes/TextParagraph.xml +++ b/doc/classes/TextParagraph.xml @@ -122,6 +122,12 @@ Returns drop cap bounding box size. + + + + Returns text writing direction inferred by BiDi algorithm. + + @@ -205,6 +211,12 @@ Returns the size of the bounding box of the paragraph, without line breaks. + + + + Returns character range of the paragraph. + + diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index 7579a3938f7ab6..69609085b2dabe 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -1157,6 +1157,69 @@ [b]Note:[/b] This function is used by during project export, to include TextServer database. + + + + + Returns number of uniform text runs in the buffer. + + + + + + + + Returns direction of the [param index] text run (in visual order). + + + + + + + + Returns font RID of the [param index] text run (in visual order). + + + + + + + + Returns font size of the [param index] text run (in visual order). + + + + + + + + Returns language of the [param index] text run (in visual order). + + + + + + + + Returns embedded object of the [param index] text run (in visual order). + + + + + + + + Returns source text range of the [param index] text run (in visual order). + + + + + + + + Returns source text of the [param index] text run (in visual order). + + @@ -1172,6 +1235,29 @@ Returns text span metadata. + + + + + + Returns text span embedded object key. + + + + + + + + Returns text span source text. + + + + + + + Returns text buffer source text, including object replacement characters. + + diff --git a/doc/classes/TextServerExtension.xml b/doc/classes/TextServerExtension.xml index c148cdad528d9e..4363027e92f51c 100644 --- a/doc/classes/TextServerExtension.xml +++ b/doc/classes/TextServerExtension.xml @@ -1274,6 +1274,61 @@ Saves optional TextServer database (e.g. ICU break iterators and dictionaries) to the file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1291,6 +1346,26 @@ Returns text span metadata. + + + + + + + + + + + + + + + + + + + + diff --git a/doc/classes/Tree.xml b/doc/classes/Tree.xml index 43cd1a8aaa5e08..a81b8857bf50f8 100644 --- a/doc/classes/Tree.xml +++ b/doc/classes/Tree.xml @@ -645,6 +645,9 @@ The updown arrow icon to display for the [constant TreeItem.CELL_MODE_RANGE] mode cell. + + [StyleBox] used when a button is selected (when using with assistive apps). + [StyleBox] used when a button in the tree is pressed. diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml index c679838ec55c25..3442d420853eb6 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -18,8 +18,9 @@ + - Adds a button with [Texture2D] [param button] 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] 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. @@ -73,6 +74,13 @@ Removes the button at index [param button_index] in column [param column]. + + + + + Returns the given column's alternative text. + + @@ -483,6 +491,14 @@ Selects the given [param column]. + + + + + + Sets the given column's alternative (description) text for assistive apps. + + @@ -500,6 +516,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/Window.xml b/doc/classes/Window.xml index 9c0e8011dc286b..1a97e1517bda42 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 foused window. + + diff --git a/drivers/SCsub b/drivers/SCsub index e77b96cc87d817..add8f071aea19b 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -19,6 +19,10 @@ if env["platform"] == "windows": if env["xaudio2"]: SConscript("xaudio2/SCsub") +# Accessibility +if env["platform"] in ["macos", "windows", "linuxbsd"]: + SConscript("accesskit/SCsub") + # Midi drivers SConscript("alsamidi/SCsub") SConscript("coremidi/SCsub") diff --git a/drivers/accesskit/SCsub b/drivers/accesskit/SCsub new file mode 100644 index 00000000000000..bcd3caf681fea0 --- /dev/null +++ b/drivers/accesskit/SCsub @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +Import("env") + +# Driver source files +if env["accesskit_sdk_path"] != "": + env.add_source_files(env.drivers_sources, "accessibility_driver_accesskit.cpp") diff --git a/drivers/accesskit/accessibility_driver_accesskit.cpp b/drivers/accesskit/accessibility_driver_accesskit.cpp new file mode 100644 index 00000000000000..c9be8d8c29e91d --- /dev/null +++ b/drivers/accesskit/accessibility_driver_accesskit.cpp @@ -0,0 +1,1588 @@ +/**************************************************************************/ +/* accessibility_driver_accesskit.cpp */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#ifdef ACCESSKIT_ENABLED + +#include "accessibility_driver_accesskit.h" + +#include "core/config/project_settings.h" +#include "core/version.h" + +#include "servers/text_server.h" + +AccessibilityDriverAccessKit *AccessibilityDriverAccessKit::singleton = nullptr; + +_FORCE_INLINE_ accesskit_role AccessibilityDriverAccessKit::_accessibility_role(DisplayServer::AccessibilityRole p_role) const { + if (role_map.has(p_role)) { + return role_map[p_role]; + } + return ACCESSKIT_ROLE_UNKNOWN; +} + +_FORCE_INLINE_ accesskit_default_action_verb AccessibilityDriverAccessKit::_accessibility_def_action(DisplayServer::AccessibilityActionVerb p_action_verb) const { + if (action_verb_map.has(p_action_verb)) { + return action_verb_map[p_action_verb]; + } + return ACCESSKIT_DEFAULT_ACTION_VERB_CLICK; +} + +_FORCE_INLINE_ accesskit_action AccessibilityDriverAccessKit::_accessibility_action(DisplayServer::AccessibilityAction p_action) const { + if (action_map.has(p_action)) { + return action_map[p_action]; + } + return ACCESSKIT_ACTION_DEFAULT; +} + +bool AccessibilityDriverAccessKit::window_create(DisplayServer::WindowID p_window_id, void *p_handle) { + ERR_FAIL_COND_V(windows.has(p_window_id), false); + + WindowData &wd = windows[p_window_id]; + + AccessibilityElement *ae = memnew(AccessibilityElement); + ae->role = ACCESSKIT_ROLE_WINDOW; + ae->window_id = p_window_id; + wd.root_id = rid_owner.make_rid(ae); + +#ifdef WINDOWS_ENABLED + wd.adapter = accesskit_windows_subclassing_adapter_new(static_cast(p_handle), &_accessibility_initial_tree_update_callback, (void *)(size_t)p_window_id, &_accessibility_action_callback, (void *)(size_t)p_window_id); +#endif +#ifdef MACOS_ENABLED + wd.adapter = accesskit_macos_subclassing_adapter_new(p_handle, &_accessibility_initial_tree_update_callback, (void *)(size_t)p_window_id, &_accessibility_action_callback, (void *)(size_t)p_window_id); +#endif +#ifdef LINUXBSD_ENABLED + wd.adapter = accesskit_unix_adapter_new(&_accessibility_initial_tree_update_callback, (void *)(size_t)p_window_id, &_accessibility_action_callback, (void *)(size_t)p_window_id, &_accessibility_deactivation_callback, (void *)(size_t)p_window_id); +#endif + + if (wd.adapter == nullptr) { + memdelete(ae); + rid_owner.free(wd.root_id); + windows.erase(p_window_id); + + return false; + } else { + return true; + } +} + +void AccessibilityDriverAccessKit::window_destroy(DisplayServer::WindowID p_window_id) { + WindowData *wd = windows.getptr(p_window_id); + ERR_FAIL_NULL(wd); + +#ifdef WINDOWS_ENABLED + accesskit_windows_subclassing_adapter_free(wd->adapter); +#endif +#ifdef MACOS_ENABLED + accesskit_macos_subclassing_adapter_free(wd->adapter); +#endif +#ifdef LINUXBSD_ENABLED + accesskit_unix_adapter_free(wd->adapter); +#endif + accessibility_free_element(wd->root_id); + + windows.erase(p_window_id); +} + +void AccessibilityDriverAccessKit::_accessibility_deactivation_callback(void *p_user_data) { + // NOP +} + +void AccessibilityDriverAccessKit::_accessibility_action_callback(const accesskit_action_request *p_request, void *p_user_data) { + DisplayServer::WindowID window_id = (DisplayServer::WindowID)(size_t)p_user_data; + ERR_FAIL_COND(!singleton->windows.has(window_id)); + + RID rid = RID::from_uint64(p_request->target); + AccessibilityElement *ae = singleton->rid_owner.get_or_null(rid); + ERR_FAIL_NULL(ae); + + Variant rq_data; + if (!ae->actions.has(p_request->action) && ae->role == ACCESSKIT_ROLE_INLINE_TEXT_BOX && p_request->action == ACCESSKIT_ACTION_SCROLL_INTO_VIEW) { + AccessibilityElement *root_ae = singleton->rid_owner.get_or_null(ae->parent); + ERR_FAIL_NULL(root_ae); + ae = root_ae; + rq_data = ae->run; + } + + if (ae->actions.has(p_request->action)) { + Callable &cb = ae->actions[p_request->action]; + if (cb.is_valid()) { + if (p_request->data.has_value) { + switch (p_request->data.value.tag) { + case ACCESSKIT_ACTION_DATA_CUSTOM_ACTION: { + rq_data = p_request->data.value.custom_action; + } break; + case ACCESSKIT_ACTION_DATA_VALUE: { + rq_data = String::utf8(p_request->data.value.value); + } break; + case ACCESSKIT_ACTION_DATA_NUMERIC_VALUE: { + rq_data = p_request->data.value.numeric_value; + } break; + case ACCESSKIT_ACTION_DATA_SCROLL_TARGET_RECT: { + rq_data = Rect2(p_request->data.value.scroll_target_rect.x0, p_request->data.value.scroll_target_rect.y0, p_request->data.value.scroll_target_rect.x1 - p_request->data.value.scroll_target_rect.x0, p_request->data.value.scroll_target_rect.y1 - p_request->data.value.scroll_target_rect.y0); + } break; + case ACCESSKIT_ACTION_DATA_SCROLL_TO_POINT: { + rq_data = Point2(p_request->data.value.scroll_to_point.x, p_request->data.value.scroll_to_point.y); + } break; + case ACCESSKIT_ACTION_DATA_SET_SCROLL_OFFSET: { + rq_data = Point2(p_request->data.value.set_scroll_offset.x, p_request->data.value.set_scroll_offset.y); + } break; + case ACCESSKIT_ACTION_DATA_SET_TEXT_SELECTION: { + Dictionary sel; + + RID start_rid = RID::from_uint64(p_request->data.value.set_text_selection.anchor.node); + AccessibilityElement *start_ae = singleton->rid_owner.get_or_null(start_rid); + ERR_FAIL_NULL(start_ae); + + RID end_rid = RID::from_uint64(p_request->data.value.set_text_selection.focus.node); + AccessibilityElement *end_ae = singleton->rid_owner.get_or_null(end_rid); + ERR_FAIL_NULL(end_ae); + + sel["start_element"] = start_ae->parent; + sel["start_char"] = (int64_t)p_request->data.value.set_text_selection.anchor.character_index + start_ae->run.x; + sel["end_element"] = end_ae->parent; + sel["end_char"] = (int64_t)p_request->data.value.set_text_selection.focus.character_index + end_ae->run.x; + rq_data = sel; + } break; + } + } + + cb.call_deferred(rq_data); + } + } +} + +accesskit_tree_update *AccessibilityDriverAccessKit::_accessibility_initial_tree_update_callback(void *p_user_data) { + DisplayServer::WindowID window_id = (DisplayServer::WindowID)(size_t)p_user_data; + WindowData *wd = singleton->windows.getptr(window_id); + ERR_FAIL_NULL_V(wd, nullptr); + + accesskit_node_builder *win_bld = accesskit_node_builder_new(ACCESSKIT_ROLE_WINDOW); + accesskit_node_builder_set_name(win_bld, "Godot Engine"); + accesskit_node_builder_set_busy(win_bld); + + accesskit_node *win_node = accesskit_node_builder_build(win_bld); + accesskit_node_id win_id = (accesskit_node_id)wd->root_id.get_id(); + + accesskit_tree_update *tree_update = accesskit_tree_update_with_capacity_and_focus(1, win_id); + + accesskit_tree_update_set_tree(tree_update, accesskit_tree_new(win_id)); + accesskit_tree_update_push_node(tree_update, win_id, win_node); + + return tree_update; +} + +RID AccessibilityDriverAccessKit::accessibility_create_element(DisplayServer::WindowID p_window_id, DisplayServer::AccessibilityRole p_role) { + AccessibilityElement *ae = memnew(AccessibilityElement); + ae->role = _accessibility_role(p_role); + ae->window_id = p_window_id; + RID rid = rid_owner.make_rid(ae); + + return rid; +} + +RID AccessibilityDriverAccessKit::accessibility_create_sub_element(const RID &p_parent_rid, DisplayServer::AccessibilityRole p_role, int p_insert_pos) { + AccessibilityElement *parent_ae = rid_owner.get_or_null(p_parent_rid); + if (!parent_ae) { + CRASH_NOW(); + } + ERR_FAIL_NULL_V(parent_ae, RID()); + + WindowData *wd = windows.getptr(parent_ae->window_id); + ERR_FAIL_NULL_V(wd, RID()); + + AccessibilityElement *ae = memnew(AccessibilityElement); + ae->role = _accessibility_role(p_role); + ae->window_id = parent_ae->window_id; + ae->parent = p_parent_rid; + ae->builder = accesskit_node_builder_new(ae->role); + RID rid = rid_owner.make_rid(ae); + if (p_insert_pos == -1) { + parent_ae->children.push_back(rid); + } else { + parent_ae->children.insert(p_insert_pos, rid); + } + wd->update.insert(rid); + + return rid; +} + +RID AccessibilityDriverAccessKit::accessibility_create_sub_text_edit_elements(const RID &p_parent_rid, const RID &p_shaped_text, float p_min_height, int p_insert_pos) { + AccessibilityElement *parent_ae = rid_owner.get_or_null(p_parent_rid); + ERR_FAIL_NULL_V(parent_ae, RID()); + + WindowData *wd = windows.getptr(parent_ae->window_id); + ERR_FAIL_NULL_V(wd, RID()); + + AccessibilityElement *root_ae = memnew(AccessibilityElement); + root_ae->role = ACCESSKIT_ROLE_GENERIC_CONTAINER; + root_ae->window_id = parent_ae->window_id; + root_ae->parent = p_parent_rid; + root_ae->builder = accesskit_node_builder_new(root_ae->role); + RID root_rid = rid_owner.make_rid(root_ae); + if (p_insert_pos == -1) { + parent_ae->children.push_back(root_rid); + } else { + parent_ae->children.insert(p_insert_pos, root_rid); + } + wd->update.insert(root_rid); + + float text_width = 0; + float text_height = p_min_height; + Vector words; + int64_t run_count = 0; // Note: runs in visual order. + const Glyph *gl = nullptr; + int64_t gl_count = 0; + int64_t gl_index = 0; + float run_off_x = 0.0; + Vector2i full_range; + + if (p_shaped_text.is_valid()) { + text_width = TS->shaped_text_get_size(p_shaped_text).x; + text_height = MAX(text_height, TS->shaped_text_get_size(p_shaped_text).y); + words = TS->shaped_text_get_word_breaks(p_shaped_text); + run_count = TS->shaped_get_run_count(p_shaped_text); + gl = TS->shaped_text_get_glyphs(p_shaped_text); + gl_count = TS->shaped_text_get_glyph_count(p_shaped_text); + full_range = TS->shaped_text_get_range(p_shaped_text); + } + + accesskit_rect root_rect; + root_rect.x0 = 0; + root_rect.y0 = 0; + root_rect.x1 = text_width; + root_rect.y1 = MAX(p_min_height, text_height); + accesskit_node_builder_set_bounds(root_ae->builder, root_rect); + + // Create text element for each run. + Vector text_elements; + for (int64_t i = 0; i < run_count; i++) { + const Vector2i range = TS->shaped_get_run_range(p_shaped_text, i); + String t = TS->shaped_get_run_text(p_shaped_text, i); + + if (t.is_empty()) { + continue; + } + + AccessibilityElement *ae = memnew(AccessibilityElement); + ae->role = ACCESSKIT_ROLE_INLINE_TEXT_BOX; + ae->window_id = parent_ae->window_id; + ae->parent = root_rid; + ae->run = Vector3i(range.x, range.y, i); + ae->builder = accesskit_node_builder_new(ae->role); + + text_elements.push_back(ae); + + // UTF-8 text and char lengths. + Vector char_lengths; + CharString text = t.utf8(&char_lengths); + + accesskit_node_builder_set_value(ae->builder, text.ptr()); + accesskit_node_builder_set_character_lengths(ae->builder, char_lengths.size(), char_lengths.ptr()); + + // Word sizes. + Vector word_lengths; + + int32_t prev = ae->run.x; + int32_t total = 0; + for (int j = 0; j < words.size(); j += 2) { + if (words[j] < ae->run.x) { + continue; + } + if (words[j] >= ae->run.y) { + break; + } + int32_t wlen = words[j] - prev; + while (wlen > 255) { + word_lengths.push_back(255); + wlen -= 255; + total += 255; + } + if (wlen > 0) { + word_lengths.push_back(wlen); + total += wlen; + } + prev = words[j]; + } + if (total < t.length()) { + word_lengths.push_back(t.length() - total); + } + accesskit_node_builder_set_word_lengths(ae->builder, word_lengths.size(), word_lengths.ptr()); + + // Char widths and positions. + Vector char_positions; + Vector char_widths; + + char_positions.resize_zeroed(t.length()); + float *positions_ptr = char_positions.ptrw(); + + char_widths.resize_zeroed(t.length()); + float *widths_ptr = char_widths.ptrw(); + + float size_x = 0.0; + for (int j = gl_index; j < gl_count; j += gl[j].count) { + if (gl[j].start >= ae->run.y) { + gl_index = j; + break; + } + + float advance = 0.0; // Graphame advance. + for (int k = 0; k < gl[j].count; k++) { + advance += gl[j + k].advance; + } + int chars = gl[j].end - gl[j].start; + float adv_per_char = advance / (float)chars; + + for (int k = 0; k < chars; k++) { + int index = gl[j].start + k - ae->run.x; + ERR_CONTINUE(index < 0 || index >= t.length()); + positions_ptr[index] = size_x + adv_per_char * k; + widths_ptr[index] = adv_per_char; + } + size_x += advance * gl[j].repeat; + } + positions_ptr[t.length() - 1] = size_x; + widths_ptr[t.length() - 1] = 1.0; + + accesskit_node_builder_set_character_positions(ae->builder, char_positions.size(), char_positions.ptr()); + accesskit_node_builder_set_character_widths(ae->builder, char_widths.size(), char_widths.ptr()); + + RID font_rid = TS->shaped_get_run_font_rid(p_shaped_text, i); + if (font_rid != RID()) { + CharString font_name = TS->font_get_name(font_rid).utf8(); + if (font_name.length() > 0) { + accesskit_node_builder_set_font_family(ae->builder, font_name.ptr()); + } + if (TS->font_get_style(font_rid).has_flag(TextServer::FONT_BOLD)) { + accesskit_node_builder_set_bold(ae->builder); + } + if (TS->font_get_style(font_rid).has_flag(TextServer::FONT_ITALIC)) { + accesskit_node_builder_set_italic(ae->builder); + } + accesskit_node_builder_set_font_weight(ae->builder, TS->font_get_weight(font_rid)); + } + accesskit_node_builder_set_font_size(ae->builder, TS->shaped_get_run_font_size(p_shaped_text, i)); + CharString language = TS->shaped_get_run_language(p_shaped_text, i).utf8(); + if (language.length() > 0) { + accesskit_node_builder_set_language(ae->builder, language.ptr()); + } + accesskit_node_builder_set_text_direction(ae->builder, ACCESSKIT_TEXT_DIRECTION_LEFT_TO_RIGHT); + + accesskit_rect rect; + rect.x0 = run_off_x; + rect.y0 = 0; + rect.x1 = run_off_x + size_x; + rect.y1 = text_height; + accesskit_node_builder_set_bounds(ae->builder, rect); + accesskit_node_builder_add_action(ae->builder, ACCESSKIT_ACTION_SCROLL_INTO_VIEW); + + run_off_x += size_x; + } + { + // Add "\n" at the end. + AccessibilityElement *ae = memnew(AccessibilityElement); + ae->role = ACCESSKIT_ROLE_INLINE_TEXT_BOX; + ae->window_id = parent_ae->window_id; + ae->parent = root_rid; + ae->run = Vector3i(full_range.y, full_range.y, run_count); + ae->builder = accesskit_node_builder_new(ae->role); + + text_elements.push_back(ae); + + Vector char_lengths; + char_lengths.push_back(1); + accesskit_node_builder_set_value(ae->builder, "\n"); + accesskit_node_builder_set_character_lengths(ae->builder, char_lengths.size(), char_lengths.ptr()); + + Vector char_positions; + Vector char_widths; + char_positions.push_back(0.0); + char_widths.push_back(1.0); + + accesskit_node_builder_set_character_positions(ae->builder, char_positions.size(), char_positions.ptr()); + accesskit_node_builder_set_character_widths(ae->builder, char_widths.size(), char_widths.ptr()); + accesskit_node_builder_set_text_direction(ae->builder, ACCESSKIT_TEXT_DIRECTION_LEFT_TO_RIGHT); + + accesskit_rect rect; + rect.x0 = run_off_x; + rect.y0 = 0; + rect.x1 = run_off_x + 1; + rect.y1 = text_height; + accesskit_node_builder_set_bounds(ae->builder, rect); + } + + // Sort runs in logical order. + struct RunCompare { + _FORCE_INLINE_ bool operator()(const AccessibilityElement *l, const AccessibilityElement *r) const { + return l->run.x < r->run.x; + } + }; + text_elements.sort_custom(); + for (int64_t i = 0; i < text_elements.size(); i++) { + RID rid = rid_owner.make_rid(text_elements[i]); + root_ae->children.push_back(rid); + wd->update.insert(rid); + } + + return root_rid; +} + +bool AccessibilityDriverAccessKit::accessibility_has_element(const RID &p_id) const { + return rid_owner.owns(p_id); +} + +void AccessibilityDriverAccessKit::_free_recursive(WindowData *p_wd, const RID &p_id) { + if (p_wd && p_wd->update.has(p_id)) { + p_wd->update.erase(p_id); + } + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + for (const RID &rid : ae->children) { + _free_recursive(p_wd, rid); + } + if (ae->builder) { + accesskit_node_builder_free(ae->builder); + } + memdelete(ae); + rid_owner.free(p_id); +} + +void AccessibilityDriverAccessKit::accessibility_free_element(const RID &p_id) { + ERR_FAIL_COND_MSG(in_accessibility_update, "Element can't be removed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + if (ae) { + WindowData *wd = windows.getptr(ae->window_id); + AccessibilityElement *parent_ae = rid_owner.get_or_null(ae->parent); + if (parent_ae) { + parent_ae->children.erase(p_id); + } + _free_recursive(wd, p_id); + } +} + +void AccessibilityDriverAccessKit::accessibility_element_set_meta(const RID &p_id, const Variant &p_meta) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + ae->meta = p_meta; +} + +Variant AccessibilityDriverAccessKit::accessibility_element_get_meta(const RID &p_id) const { + const AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL_V(ae, Variant()); + return ae->meta; +} + +void AccessibilityDriverAccessKit::accessibility_update_set_focus(const RID &p_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + if (p_id.is_valid() && rid_owner.owns(p_id)) { + focus = p_id; + } else { + focus = RID(); + } +} + +RID AccessibilityDriverAccessKit::accessibility_get_window_root(DisplayServer::WindowID p_window_id) const { + const WindowData *wd = windows.getptr(p_window_id); + ERR_FAIL_NULL_V(wd, RID()); + + return wd->root_id; +} + +accesskit_tree_update *AccessibilityDriverAccessKit::_accessibility_build_tree_update(void *p_user_data) { + DisplayServer::WindowID window_id = (DisplayServer::WindowID)(size_t)p_user_data; + + ERR_FAIL_COND_V(!singleton->windows.has(window_id), nullptr); + WindowData &wd = singleton->windows[window_id]; + + singleton->in_accessibility_update = true; + if (singleton->update_cb.is_valid()) { + singleton->update_cb.call(window_id); + } + singleton->in_accessibility_update = false; + + AccessibilityElement *focus_ae = singleton->rid_owner.get_or_null(singleton->focus); + uint32_t update_size = wd.update.size(); + + accesskit_node_id ac_focus = (accesskit_node_id)wd.root_id.get_id(); + if (focus_ae && focus_ae->window_id == window_id) { + ac_focus = (accesskit_node_id)singleton->focus.get_id(); + } + + accesskit_tree_update *tree_update = accesskit_tree_update_with_capacity_and_focus(update_size, ac_focus); + for (const RID &rid : wd.update) { + AccessibilityElement *ae = singleton->rid_owner.get_or_null(rid); + if (ae && ae->builder) { + for (const RID &child_rid : ae->children) { + accesskit_node_builder_push_child(ae->builder, (accesskit_node_id)child_rid.get_id()); + } + + accesskit_node *ac_node = accesskit_node_builder_build(ae->builder); + accesskit_node_id ac_id = (accesskit_node_id)rid.get_id(); + ae->builder = nullptr; + + accesskit_tree_update_push_node(tree_update, ac_id, ac_node); + } + } + wd.update.clear(); + + return tree_update; +} + +void AccessibilityDriverAccessKit::accessibility_update_if_active(const Callable &p_callable) { + ERR_FAIL_COND(p_callable.is_null()); + update_cb = p_callable; + for (KeyValue &window : windows) { +#ifdef WINDOWS_ENABLED + accesskit_windows_queued_events *events = accesskit_windows_subclassing_adapter_update_if_active(window.value.adapter, _accessibility_build_tree_update, (void *)(size_t)window.key); + if (events) { + accesskit_windows_queued_events_raise(events); + } +#endif +#ifdef MACOS_ENABLED + accesskit_macos_queued_events *events = accesskit_macos_subclassing_adapter_update_if_active(window.value.adapter, _accessibility_build_tree_update, (void *)(size_t)window.key); + if (events) { + accesskit_macos_queued_events_raise(events); + } +#endif +#ifdef LINUXBSD_ENABLED + accesskit_unix_adapter_update_if_active(window.value.adapter, _accessibility_build_tree_update, (void *)(size_t)window.key); +#endif + } + update_cb = Callable(); +} + +_FORCE_INLINE_ void AccessibilityDriverAccessKit::_ensure_builder(const RID &p_id, AccessibilityElement *p_ae) { + if (unlikely(!p_ae->builder)) { + WindowData *wd = windows.getptr(p_ae->window_id); + ERR_FAIL_NULL(wd); + + wd->update.insert(p_id); + p_ae->builder = accesskit_node_builder_new(p_ae->role); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_role(const RID &p_id, DisplayServer::AccessibilityRole p_role) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + if (ae->role == _accessibility_role(p_role)) { + return; + } + ae->role = _accessibility_role(p_role); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_role(ae->builder, ae->role); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_name(const RID &p_id, const String &p_name) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_name.is_empty()) { + accesskit_node_builder_set_name(ae->builder, p_name.utf8().ptr()); + } else { + accesskit_node_builder_set_name(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_description(const RID &p_id, const String &p_description) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_description.is_empty()) { + accesskit_node_builder_set_description(ae->builder, p_description.utf8().ptr()); + } else { + accesskit_node_builder_set_description(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_value(const RID &p_id, const String &p_value) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_value.is_empty()) { + Vector ch_length; + accesskit_node_builder_set_value(ae->builder, p_value.utf8(&ch_length).ptr()); + accesskit_node_builder_set_character_lengths(ae->builder, ch_length.size(), ch_length.ptr()); + } else { + accesskit_node_builder_set_value(ae->builder, ""); + accesskit_node_builder_set_character_lengths(ae->builder, 0, nullptr); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_tooltip(const RID &p_id, const String &p_tooltip) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_tooltip.is_empty()) { + accesskit_node_builder_set_tooltip(ae->builder, p_tooltip.utf8().ptr()); + } else { + accesskit_node_builder_set_tooltip(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_bounds(const RID &p_id, const Rect2 &p_rect) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_rect rect; + rect.x0 = p_rect.position.x; + rect.y0 = p_rect.position.y; + rect.x1 = p_rect.position.x + p_rect.size.x; + rect.y1 = p_rect.position.y + p_rect.size.y; + accesskit_node_builder_set_bounds(ae->builder, rect); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_transform(const RID &p_id, const Transform2D &p_transform) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_affine transform = { p_transform.columns[0][0], p_transform.columns[0][1], p_transform.columns[1][0], p_transform.columns[1][1], p_transform.columns[2][0], p_transform.columns[2][1] }; + accesskit_node_builder_set_transform(ae->builder, transform); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_child(const RID &p_id, const RID &p_child_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_child_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_child(ae->builder, (accesskit_node_id)p_child_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_related_controls(const RID &p_id, const RID &p_related_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_related_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_controlled(ae->builder, (accesskit_node_id)p_related_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_related_details(const RID &p_id, const RID &p_related_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_related_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_detail(ae->builder, (accesskit_node_id)p_related_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_related_described_by(const RID &p_id, const RID &p_related_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_related_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_described_by(ae->builder, (accesskit_node_id)p_related_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_related_flow_to(const RID &p_id, const RID &p_related_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_related_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_flow_to(ae->builder, (accesskit_node_id)p_related_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_related_labeled_by(const RID &p_id, const RID &p_related_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_related_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_labelled_by(ae->builder, (accesskit_node_id)p_related_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_related_radio_group(const RID &p_id, const RID &p_related_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_related_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_push_to_radio_group(ae->builder, (accesskit_node_id)p_related_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_active_descendant(const RID &p_id, const RID &p_other_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_other_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_active_descendant(ae->builder, (accesskit_node_id)p_other_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_next_on_line(const RID &p_id, const RID &p_other_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_other_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_next_on_line(ae->builder, (accesskit_node_id)p_other_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_previous_on_line(const RID &p_id, const RID &p_other_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_other_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_previous_on_line(ae->builder, (accesskit_node_id)p_other_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_member_of(const RID &p_id, const RID &p_other_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_other_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_member_of(ae->builder, (accesskit_node_id)p_other_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_in_page_link_target(const RID &p_id, const RID &p_other_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_other_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_in_page_link_target(ae->builder, (accesskit_node_id)p_other_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_error_message(const RID &p_id, const RID &p_other_id) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *other_ae = rid_owner.get_or_null(p_other_id); + ERR_FAIL_NULL(other_ae); + ERR_FAIL_COND(other_ae->window_id != ae->window_id); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_error_message(ae->builder, (accesskit_node_id)p_other_id.get_id()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_live(const RID &p_id, DisplayServer::AccessibilityLiveMode p_live) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + switch (p_live) { + case DisplayServer::AccessibilityLiveMode::LIVE_OFF: { + accesskit_node_builder_set_live(ae->builder, ACCESSKIT_LIVE_OFF); + } break; + case DisplayServer::AccessibilityLiveMode::LIVE_POLITE: { + accesskit_node_builder_set_live(ae->builder, ACCESSKIT_LIVE_POLITE); + } break; + case DisplayServer::AccessibilityLiveMode::LIVE_ASSERTIVE: { + accesskit_node_builder_set_live(ae->builder, ACCESSKIT_LIVE_ASSERTIVE); + } break; + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_default_action_verb(const RID &p_id, DisplayServer::AccessibilityActionVerb p_action) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_default_action_verb(ae->builder, _accessibility_def_action(p_action)); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_action(const RID &p_id, DisplayServer::AccessibilityAction p_action, const Callable &p_callable) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + ae->actions[_accessibility_action(p_action)] = p_callable; + + accesskit_node_builder_add_action(ae->builder, _accessibility_action(p_action)); +} + +void AccessibilityDriverAccessKit::accessibility_update_add_custom_action(const RID &p_id, int p_action_id, const String &p_action_description) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_action_description.is_empty()) { + accesskit_custom_action ca = accesskit_custom_action_new(p_action_id, p_action_description.utf8().ptr()); + accesskit_node_builder_push_custom_action(ae->builder, ca); + } else { + String cs_name = vformat("Custom Action %d", p_action_id); + accesskit_custom_action ca = accesskit_custom_action_new(p_action_id, cs_name.utf8().ptr()); + accesskit_node_builder_push_custom_action(ae->builder, ca); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_table_row_count(const RID &p_id, int p_count) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_row_count(ae->builder, p_count); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_table_column_count(const RID &p_id, int p_count) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_column_count(ae->builder, p_count); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_table_row_index(const RID &p_id, int p_index) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_row_index(ae->builder, p_index); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_table_column_index(const RID &p_id, int p_index) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_column_index(ae->builder, p_index); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_table_cell_position(const RID &p_id, int p_row_index, int p_column_index) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_row_index(ae->builder, p_row_index); + accesskit_node_builder_set_column_index(ae->builder, p_column_index); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_table_cell_span(const RID &p_id, int p_row_span, int p_column_span) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_row_span(ae->builder, p_row_span); + accesskit_node_builder_set_column_span(ae->builder, p_column_span); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_list_item_count(const RID &p_id, int p_size) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_size_of_set(ae->builder, p_size); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_list_item_index(const RID &p_id, int p_index) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_position_in_set(ae->builder, p_index); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_list_item_level(const RID &p_id, int p_level) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_level(ae->builder, p_level); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_list_item_selected(const RID &p_id, bool p_selected) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_selected(ae->builder, p_selected); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_list_item_expanded(const RID &p_id, bool p_expanded) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_expanded(ae->builder, p_expanded); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_popup_type(const RID &p_id, DisplayServer::AccessibilityPopupType p_popup) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + switch (p_popup) { + case DisplayServer::AccessibilityPopupType::POPUP_UNKNOWN: { + accesskit_node_builder_set_has_popup(ae->builder, ACCESSKIT_HAS_POPUP_TRUE); + } break; + case DisplayServer::AccessibilityPopupType::POPUP_MENU: { + accesskit_node_builder_set_has_popup(ae->builder, ACCESSKIT_HAS_POPUP_MENU); + } break; + case DisplayServer::AccessibilityPopupType::POPUP_LIST: { + accesskit_node_builder_set_has_popup(ae->builder, ACCESSKIT_HAS_POPUP_LISTBOX); + } break; + case DisplayServer::AccessibilityPopupType::POPUP_TREE: { + accesskit_node_builder_set_has_popup(ae->builder, ACCESSKIT_HAS_POPUP_TREE); + } break; + case DisplayServer::AccessibilityPopupType::POPUP_DIALOG: { + accesskit_node_builder_set_has_popup(ae->builder, ACCESSKIT_HAS_POPUP_DIALOG); + } break; + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_checked(const RID &p_id, bool p_checekd) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (p_checekd) { + accesskit_node_builder_set_toggled(ae->builder, ACCESSKIT_TOGGLED_TRUE); + } else { + accesskit_node_builder_set_toggled(ae->builder, ACCESSKIT_TOGGLED_FALSE); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_num_value(const RID &p_id, double p_position) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_numeric_value(ae->builder, p_position); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_num_range(const RID &p_id, double p_min, double p_max) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_min_numeric_value(ae->builder, p_min); + accesskit_node_builder_set_max_numeric_value(ae->builder, p_max); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_num_step(const RID &p_id, double p_step) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_numeric_value_step(ae->builder, p_step); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_num_jump(const RID &p_id, double p_jump) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_numeric_value_jump(ae->builder, p_jump); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_scroll_x(const RID &p_id, double p_position) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_scroll_x(ae->builder, p_position); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_scroll_x_range(const RID &p_id, double p_min, double p_max) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_scroll_x_min(ae->builder, p_min); + accesskit_node_builder_set_scroll_x_max(ae->builder, p_max); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_scroll_y(const RID &p_id, double p_position) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_scroll_y(ae->builder, p_position); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_scroll_y_range(const RID &p_id, double p_min, double p_max) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_scroll_y_min(ae->builder, p_min); + accesskit_node_builder_set_scroll_y_max(ae->builder, p_max); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_text_decorations(const RID &p_id, bool p_underline, bool p_strikethrough, bool p_overline) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (p_underline) { + accesskit_node_builder_set_underline(ae->builder, ACCESSKIT_TEXT_DECORATION_SOLID); + } else { + accesskit_node_builder_clear_underline(ae->builder); + } + if (p_overline) { + accesskit_node_builder_set_overline(ae->builder, ACCESSKIT_TEXT_DECORATION_SOLID); + } else { + accesskit_node_builder_clear_overline(ae->builder); + } + if (p_strikethrough) { + accesskit_node_builder_set_strikethrough(ae->builder, ACCESSKIT_TEXT_DECORATION_SOLID); + } else { + accesskit_node_builder_clear_strikethrough(ae->builder); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_text_align(const RID &p_id, HorizontalAlignment p_align) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + switch (p_align) { + case HORIZONTAL_ALIGNMENT_LEFT: { + accesskit_node_builder_set_text_align(ae->builder, ACCESSKIT_TEXT_ALIGN_LEFT); + } break; + case HORIZONTAL_ALIGNMENT_CENTER: { + accesskit_node_builder_set_text_align(ae->builder, ACCESSKIT_TEXT_ALIGN_RIGHT); + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + accesskit_node_builder_set_text_align(ae->builder, ACCESSKIT_TEXT_ALIGN_CENTER); + } break; + case HORIZONTAL_ALIGNMENT_FILL: { + accesskit_node_builder_set_text_align(ae->builder, ACCESSKIT_TEXT_ALIGN_JUSTIFY); + } break; + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_text_selection(const RID &p_id, const RID &p_text_start_id, int p_start_char, const RID &p_text_end_id, int p_end_char) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + AccessibilityElement *start_ae = rid_owner.get_or_null(p_text_start_id); + ERR_FAIL_NULL(start_ae); + ERR_FAIL_COND(start_ae->window_id != ae->window_id); + AccessibilityElement *end_ae = rid_owner.get_or_null(p_text_end_id); + ERR_FAIL_NULL(end_ae); + ERR_FAIL_COND(end_ae->window_id != ae->window_id); + + int start_pos = p_start_char; + int end_pos = p_end_char; + RID start_rid; + RID end_rid; + for (const RID &rid : start_ae->children) { + const AccessibilityElement *child_ae = rid_owner.get_or_null(rid); + if (child_ae && child_ae->role == ACCESSKIT_ROLE_INLINE_TEXT_BOX) { + if (p_start_char >= child_ae->run.x && p_start_char <= child_ae->run.y) { + start_rid = rid; + start_pos = p_start_char - child_ae->run.x; + break; + } + } + } + for (const RID &rid : end_ae->children) { + const AccessibilityElement *child_ae = rid_owner.get_or_null(rid); + if (child_ae && child_ae->role == ACCESSKIT_ROLE_INLINE_TEXT_BOX) { + if (p_end_char >= child_ae->run.x && p_end_char <= child_ae->run.y) { + end_rid = rid; + end_pos = p_end_char - child_ae->run.x; + break; + } + } + } + ERR_FAIL_COND(start_rid.is_null() && end_rid.is_null()); + _ensure_builder(p_id, ae); + + accesskit_text_selection sel; + sel.anchor.node = (accesskit_node_id)start_rid.get_id(); + sel.anchor.character_index = start_pos; + sel.focus.node = (accesskit_node_id)end_rid.get_id(); + sel.focus.character_index = end_pos; + accesskit_builder_set_text_selection(ae->builder, sel); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_flag(const RID &p_id, DisplayServer::AccessibilityFlags p_flag, bool p_value) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + switch (p_flag) { + case DisplayServer::AccessibilityFlags::FLAG_HOVERED: { + if (p_value) { + accesskit_node_builder_set_hovered(ae->builder); + } else { + accesskit_node_builder_clear_hovered(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_HIDDEN: { + if (p_value) { + accesskit_node_builder_set_hidden(ae->builder); + } else { + accesskit_node_builder_clear_hidden(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_LINKED: { + if (p_value) { + accesskit_node_builder_set_linked(ae->builder); + } else { + accesskit_node_builder_clear_linked(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_MULTISELECTABLE: { + if (p_value) { + accesskit_node_builder_set_multiselectable(ae->builder); + } else { + accesskit_node_builder_clear_multiselectable(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_REQUIRED: { + if (p_value) { + accesskit_node_builder_set_required(ae->builder); + } else { + accesskit_node_builder_clear_required(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_VISITED: { + if (p_value) { + accesskit_node_builder_set_visited(ae->builder); + } else { + accesskit_node_builder_clear_visited(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_BUSY: { + if (p_value) { + accesskit_node_builder_set_busy(ae->builder); + } else { + accesskit_node_builder_clear_busy(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_MODAL: { + if (p_value) { + accesskit_node_builder_set_modal(ae->builder); + } else { + accesskit_node_builder_clear_modal(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_TOUCH_PASSTHROUGH: { + if (p_value) { + accesskit_node_builder_set_touch_transparent(ae->builder); + } else { + accesskit_node_builder_clear_touch_transparent(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_READONLY: { + if (p_value) { + accesskit_node_builder_set_read_only(ae->builder); + } else { + accesskit_node_builder_clear_read_only(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_DISABLED: { + if (p_value) { + accesskit_node_builder_set_disabled(ae->builder); + } else { + accesskit_node_builder_clear_disabled(ae->builder); + } + } break; + case DisplayServer::AccessibilityFlags::FLAG_CLIPS_CHILDREN: { + if (p_value) { + accesskit_node_builder_set_clips_children(ae->builder); + } else { + accesskit_node_builder_clear_clips_children(ae->builder); + } + } break; + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_classname(const RID &p_id, const String &p_classname) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_classname.is_empty()) { + accesskit_node_builder_set_class_name(ae->builder, p_classname.utf8().ptr()); + } else { + accesskit_node_builder_set_class_name(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_placeholder(const RID &p_id, const String &p_placeholder) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_placeholder.is_empty()) { + accesskit_node_builder_set_placeholder(ae->builder, p_placeholder.utf8().ptr()); + } else { + accesskit_node_builder_clear_placeholder(ae->builder); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_language(const RID &p_id, const String &p_language) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_language(ae->builder, p_language.utf8().ptr()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_text_orientation(const RID &p_id, bool p_vertical) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (p_vertical) { + accesskit_node_builder_set_text_direction(ae->builder, ACCESSKIT_TEXT_DIRECTION_TOP_TO_BOTTOM); + } else { + accesskit_node_builder_set_text_direction(ae->builder, ACCESSKIT_TEXT_DIRECTION_LEFT_TO_RIGHT); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_list_orientation(const RID &p_id, bool p_vertical) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (p_vertical) { + accesskit_node_builder_set_orientation(ae->builder, ACCESSKIT_ORIENTATION_VERTICAL); + } else { + accesskit_node_builder_set_orientation(ae->builder, ACCESSKIT_ORIENTATION_HORIZONTAL); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_shortcut(const RID &p_id, const String &p_shortcut) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_shortcut.is_empty()) { + accesskit_node_builder_set_keyboard_shortcut(ae->builder, p_shortcut.utf8().ptr()); + } else { + accesskit_node_builder_clear_keyboard_shortcut(ae->builder); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_url(const RID &p_id, const String &p_url) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_url.is_empty()) { + accesskit_node_builder_set_url(ae->builder, p_url.utf8().ptr()); + } else { + accesskit_node_builder_set_url(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_role_description(const RID &p_id, const String &p_description) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_description.is_empty()) { + accesskit_node_builder_set_role_description(ae->builder, p_description.utf8().ptr()); + } else { + accesskit_node_builder_set_role_description(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_state_description(const RID &p_id, const String &p_description) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + if (!p_description.is_empty()) { + accesskit_node_builder_set_state_description(ae->builder, p_description.utf8().ptr()); + } else { + accesskit_node_builder_set_state_description(ae->builder, ""); + } +} + +void AccessibilityDriverAccessKit::accessibility_update_set_color_value(const RID &p_id, const Color &p_color) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_color_value(ae->builder, p_color.to_rgba32()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_background_color(const RID &p_id, const Color &p_color) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_background_color(ae->builder, p_color.to_rgba32()); +} + +void AccessibilityDriverAccessKit::accessibility_update_set_foreground_color(const RID &p_id, const Color &p_color) { + ERR_FAIL_COND_MSG(!in_accessibility_update, "Accessiblinity update is only allowed inside NOTIFICATION_ACCESSIBILITY_UPDATE notification."); + + AccessibilityElement *ae = rid_owner.get_or_null(p_id); + ERR_FAIL_NULL(ae); + _ensure_builder(p_id, ae); + + accesskit_node_builder_set_foreground_color(ae->builder, p_color.to_rgba32()); +} + +Error AccessibilityDriverAccessKit::init() { + return OK; +} + +AccessibilityDriverAccessKit::AccessibilityDriverAccessKit() { + singleton = this; + + role_map[DisplayServer::AccessibilityRole::ROLE_UNKNOWN] = ACCESSKIT_ROLE_UNKNOWN; + role_map[DisplayServer::AccessibilityRole::ROLE_DEFAULT_BUTTON] = ACCESSKIT_ROLE_DEFAULT_BUTTON; + role_map[DisplayServer::AccessibilityRole::ROLE_AUDIO] = ACCESSKIT_ROLE_AUDIO; + role_map[DisplayServer::AccessibilityRole::ROLE_VIDEO] = ACCESSKIT_ROLE_VIDEO; + role_map[DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT] = ACCESSKIT_ROLE_STATIC_TEXT; + role_map[DisplayServer::AccessibilityRole::ROLE_CONTAINER] = ACCESSKIT_ROLE_GENERIC_CONTAINER; + role_map[DisplayServer::AccessibilityRole::ROLE_PANEL] = ACCESSKIT_ROLE_PANE; + role_map[DisplayServer::AccessibilityRole::ROLE_BUTTON] = ACCESSKIT_ROLE_BUTTON; + role_map[DisplayServer::AccessibilityRole::ROLE_LINK] = ACCESSKIT_ROLE_LINK; + role_map[DisplayServer::AccessibilityRole::ROLE_CHECK_BOX] = ACCESSKIT_ROLE_CHECK_BOX; + role_map[DisplayServer::AccessibilityRole::ROLE_RADIO_BUTTON] = ACCESSKIT_ROLE_RADIO_BUTTON; + role_map[DisplayServer::AccessibilityRole::ROLE_CHECK_BUTTON] = ACCESSKIT_ROLE_SWITCH; + role_map[DisplayServer::AccessibilityRole::ROLE_SCROLL_BAR] = ACCESSKIT_ROLE_SCROLL_BAR; + role_map[DisplayServer::AccessibilityRole::ROLE_SCROLL_VIEW] = ACCESSKIT_ROLE_SCROLL_VIEW; + role_map[DisplayServer::AccessibilityRole::ROLE_SPLITTER] = ACCESSKIT_ROLE_SPLITTER; + role_map[DisplayServer::AccessibilityRole::ROLE_SLIDER] = ACCESSKIT_ROLE_SLIDER; + role_map[DisplayServer::AccessibilityRole::ROLE_SPIN_BUTTON] = ACCESSKIT_ROLE_SPIN_BUTTON; + role_map[DisplayServer::AccessibilityRole::ROLE_PROGRESS_INDICATOR] = ACCESSKIT_ROLE_PROGRESS_INDICATOR; + role_map[DisplayServer::AccessibilityRole::ROLE_TEXT_FIELD] = ACCESSKIT_ROLE_TEXT_INPUT; + role_map[DisplayServer::AccessibilityRole::ROLE_MULTILINE_TEXT_FIELD] = ACCESSKIT_ROLE_MULTILINE_TEXT_INPUT; + role_map[DisplayServer::AccessibilityRole::ROLE_COLOR_PICKER] = ACCESSKIT_ROLE_COLOR_WELL; + role_map[DisplayServer::AccessibilityRole::ROLE_TABLE] = ACCESSKIT_ROLE_TABLE; + role_map[DisplayServer::AccessibilityRole::ROLE_CELL] = ACCESSKIT_ROLE_CELL; + role_map[DisplayServer::AccessibilityRole::ROLE_ROW] = ACCESSKIT_ROLE_ROW; + role_map[DisplayServer::AccessibilityRole::ROLE_ROW_GROUP] = ACCESSKIT_ROLE_ROW_GROUP; + role_map[DisplayServer::AccessibilityRole::ROLE_ROW_HEADER] = ACCESSKIT_ROLE_ROW_HEADER; + role_map[DisplayServer::AccessibilityRole::ROLE_COLUMN_HEADER] = ACCESSKIT_ROLE_COLUMN_HEADER; + role_map[DisplayServer::AccessibilityRole::ROLE_TREE] = ACCESSKIT_ROLE_TREE; + role_map[DisplayServer::AccessibilityRole::ROLE_TREE_ITEM] = ACCESSKIT_ROLE_TREE_ITEM; + role_map[DisplayServer::AccessibilityRole::ROLE_LIST] = ACCESSKIT_ROLE_LIST; + role_map[DisplayServer::AccessibilityRole::ROLE_LIST_ITEM] = ACCESSKIT_ROLE_LIST_ITEM; + role_map[DisplayServer::AccessibilityRole::ROLE_TAB_BAR] = ACCESSKIT_ROLE_TAB_LIST; + role_map[DisplayServer::AccessibilityRole::ROLE_TAB] = ACCESSKIT_ROLE_TAB; + role_map[DisplayServer::AccessibilityRole::ROLE_MENU_BAR] = ACCESSKIT_ROLE_MENU_BAR; + role_map[DisplayServer::AccessibilityRole::ROLE_MENU] = ACCESSKIT_ROLE_MENU; + role_map[DisplayServer::AccessibilityRole::ROLE_MENU_ITEM] = ACCESSKIT_ROLE_MENU_ITEM; + role_map[DisplayServer::AccessibilityRole::ROLE_MENU_ITEM_CHECK_BOX] = ACCESSKIT_ROLE_MENU_ITEM_CHECK_BOX; + role_map[DisplayServer::AccessibilityRole::ROLE_MENU_ITEM_RADIO] = ACCESSKIT_ROLE_MENU_ITEM_RADIO; + role_map[DisplayServer::AccessibilityRole::ROLE_IMAGE] = ACCESSKIT_ROLE_IMAGE; + role_map[DisplayServer::AccessibilityRole::ROLE_WINDOW] = ACCESSKIT_ROLE_WINDOW; + role_map[DisplayServer::AccessibilityRole::ROLE_TITLE_BAR] = ACCESSKIT_ROLE_TITLE_BAR; + role_map[DisplayServer::AccessibilityRole::ROLE_DIALOG] = ACCESSKIT_ROLE_DIALOG; + role_map[DisplayServer::AccessibilityRole::ROLE_TOOLTIP] = ACCESSKIT_ROLE_TOOLTIP; + + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_CLICK] = ACCESSKIT_DEFAULT_ACTION_VERB_CLICK; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_FOCUS] = ACCESSKIT_DEFAULT_ACTION_VERB_FOCUS; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_CHECK] = ACCESSKIT_DEFAULT_ACTION_VERB_CHECK; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_UNCHECK] = ACCESSKIT_DEFAULT_ACTION_VERB_UNCHECK; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_CLICK_ANCESTOR] = ACCESSKIT_DEFAULT_ACTION_VERB_CLICK_ANCESTOR; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_JUMP] = ACCESSKIT_DEFAULT_ACTION_VERB_JUMP; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_OPEN] = ACCESSKIT_DEFAULT_ACTION_VERB_OPEN; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_PRESS] = ACCESSKIT_DEFAULT_ACTION_VERB_PRESS; + action_verb_map[DisplayServer::AccessibilityActionVerb::ACTION_VERB_SELECT] = ACCESSKIT_DEFAULT_ACTION_VERB_SELECT; + + action_map[DisplayServer::AccessibilityAction::ACTION_DEFAULT] = ACCESSKIT_ACTION_DEFAULT; + action_map[DisplayServer::AccessibilityAction::ACTION_FOCUS] = ACCESSKIT_ACTION_FOCUS; + action_map[DisplayServer::AccessibilityAction::ACTION_BLUR] = ACCESSKIT_ACTION_BLUR; + action_map[DisplayServer::AccessibilityAction::ACTION_COLLAPSE] = ACCESSKIT_ACTION_COLLAPSE; + action_map[DisplayServer::AccessibilityAction::ACTION_EXPAND] = ACCESSKIT_ACTION_EXPAND; + action_map[DisplayServer::AccessibilityAction::ACTION_DECREMENT] = ACCESSKIT_ACTION_DECREMENT; + action_map[DisplayServer::AccessibilityAction::ACTION_INCREMENT] = ACCESSKIT_ACTION_INCREMENT; + action_map[DisplayServer::AccessibilityAction::ACTION_HIDE_TOOLTIP] = ACCESSKIT_ACTION_HIDE_TOOLTIP; + action_map[DisplayServer::AccessibilityAction::ACTION_SHOW_TOOLTIP] = ACCESSKIT_ACTION_SHOW_TOOLTIP; + //action_map[DisplayServer::AccessibilityAction::ACTION_INVALIDATE_TREE] = ACCESSKIT_ACTION_INVALIDATE_TREE; + //action_map[DisplayServer::AccessibilityAction::ACTION_LOAD_INLINE_TEXT_BOXES] = ACCESSKIT_ACTION_LOAD_INLINE_TEXT_BOXES; + action_map[DisplayServer::AccessibilityAction::ACTION_SET_TEXT_SELECTION] = ACCESSKIT_ACTION_SET_TEXT_SELECTION; + action_map[DisplayServer::AccessibilityAction::ACTION_REPLACE_SELECTED_TEXT] = ACCESSKIT_ACTION_REPLACE_SELECTED_TEXT; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_BACKWARD] = ACCESSKIT_ACTION_SCROLL_BACKWARD; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_DOWN] = ACCESSKIT_ACTION_SCROLL_DOWN; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_FORWARD] = ACCESSKIT_ACTION_SCROLL_FORWARD; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_LEFT] = ACCESSKIT_ACTION_SCROLL_LEFT; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_RIGHT] = ACCESSKIT_ACTION_SCROLL_RIGHT; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_UP] = ACCESSKIT_ACTION_SCROLL_UP; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW] = ACCESSKIT_ACTION_SCROLL_INTO_VIEW; + action_map[DisplayServer::AccessibilityAction::ACTION_SCROLL_TO_POINT] = ACCESSKIT_ACTION_SCROLL_TO_POINT; + action_map[DisplayServer::AccessibilityAction::ACTION_SET_SCROLL_OFFSET] = ACCESSKIT_ACTION_SET_SCROLL_OFFSET; + //action_map[DisplayServer::AccessibilityAction::ACTION_SET_SEQUENTIAL_FOCUS_NAVIGATION_STARTING_POINT] = ACCESSKIT_ACTION_SET_SEQUENTIAL_FOCUS_NAVIGATION_STARTING_POINT; + action_map[DisplayServer::AccessibilityAction::ACTION_SET_VALUE] = ACCESSKIT_ACTION_SET_VALUE; + action_map[DisplayServer::AccessibilityAction::ACTION_SHOW_CONTEXT_MENU] = ACCESSKIT_ACTION_SHOW_CONTEXT_MENU; + action_map[DisplayServer::AccessibilityAction::ACTION_CUSTOM] = ACCESSKIT_ACTION_CUSTOM_ACTION; +} + +AccessibilityDriverAccessKit::~AccessibilityDriverAccessKit() { + singleton = nullptr; +} + +#endif // ACCESSKIT_ENABLED diff --git a/drivers/accesskit/accessibility_driver_accesskit.h b/drivers/accesskit/accessibility_driver_accesskit.h new file mode 100644 index 00000000000000..d07f2ec8d42df1 --- /dev/null +++ b/drivers/accesskit/accessibility_driver_accesskit.h @@ -0,0 +1,185 @@ +/**************************************************************************/ +/* accessibility_driver_accesskit.h */ +/**************************************************************************/ +/* 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 ACCESSIBILITY_DRIVER_ACCESSKIT_H +#define ACCESSIBILITY_DRIVER_ACCESSKIT_H + +#ifdef ACCESSKIT_ENABLED + +#include "core/templates/rid_owner.h" +#include "servers/display_server.h" + +#include + +class AccessibilityDriverAccessKit : public AccessibilityDriver { + static AccessibilityDriverAccessKit *singleton; + + struct AccessibilityElement { + HashMap actions; + + DisplayServer::WindowID window_id = DisplayServer::INVALID_WINDOW_ID; + RID parent; + LocalVector children; + Vector3i run; + Variant meta; + + accesskit_role role = ACCESSKIT_ROLE_UNKNOWN; + accesskit_node_builder *builder = nullptr; + }; + mutable RID_PtrOwner rid_owner; + + struct WindowData { + // Adapter. +#ifdef WINDOWS_ENABLED + accesskit_windows_subclassing_adapter *adapter = nullptr; +#endif +#ifdef MACOS_ENABLED + accesskit_macos_subclassing_adapter *adapter = nullptr; +#endif +#ifdef LINUXBSD_ENABLED + accesskit_unix_adapter *adapter = nullptr; +#endif + + RID root_id; + HashSet update; + }; + + RID focus; + + HashMap windows; + + HashMap role_map; + HashMap action_verb_map; + HashMap action_map; + + _FORCE_INLINE_ accesskit_role _accessibility_role(DisplayServer::AccessibilityRole p_role) const; + _FORCE_INLINE_ accesskit_default_action_verb _accessibility_def_action(DisplayServer::AccessibilityActionVerb p_action_verb) const; + _FORCE_INLINE_ accesskit_action _accessibility_action(DisplayServer::AccessibilityAction p_action) const; + + void _free_recursive(WindowData *p_wd, const RID &p_id); + _FORCE_INLINE_ void _ensure_builder(const RID &p_id, AccessibilityElement *p_ae); + + static void _accessibility_action_callback(const accesskit_action_request *p_request, void *p_user_data); + static accesskit_tree_update *_accessibility_initial_tree_update_callback(void *p_user_data); + static void _accessibility_deactivation_callback(void *p_user_data); + static accesskit_tree_update *_accessibility_build_tree_update(void *p_user_data); + + bool in_accessibility_update = false; + Callable update_cb; + +public: + Error init() override; + + bool window_create(DisplayServer::WindowID p_window_id, void *p_handle) override; + void window_destroy(DisplayServer::WindowID p_window_id) override; + + RID accessibility_create_element(DisplayServer::WindowID p_window_id, DisplayServer::AccessibilityRole p_role) override; + RID accessibility_create_sub_element(const RID &p_parent_rid, DisplayServer::AccessibilityRole p_role, int p_insert_pos = -1) override; + virtual RID accessibility_create_sub_text_edit_elements(const RID &p_parent_rid, const RID &p_shaped_text, float p_min_height, int p_insert_pos = -1) override; + bool accessibility_has_element(const RID &p_id) const override; + void accessibility_free_element(const RID &p_id) override; + + void accessibility_element_set_meta(const RID &p_id, const Variant &p_meta) override; + Variant accessibility_element_get_meta(const RID &p_id) const override; + + void accessibility_update_if_active(const Callable &p_callable) override; + + void accessibility_update_set_focus(const RID &p_id) override; + RID accessibility_get_window_root(DisplayServer::WindowID p_window_id) const override; + + void accessibility_update_set_role(const RID &p_id, DisplayServer::AccessibilityRole p_role) override; + void accessibility_update_set_name(const RID &p_id, const String &p_name) override; + void accessibility_update_set_description(const RID &p_id, const String &p_description) override; + void accessibility_update_set_value(const RID &p_id, const String &p_value) override; + void accessibility_update_set_tooltip(const RID &p_id, const String &p_tooltip) override; + void accessibility_update_set_bounds(const RID &p_id, const Rect2 &p_rect) override; + void accessibility_update_set_transform(const RID &p_id, const Transform2D &p_transform) override; + void accessibility_update_add_child(const RID &p_id, const RID &p_child_id) override; + void accessibility_update_add_related_controls(const RID &p_id, const RID &p_related_id) override; + void accessibility_update_add_related_details(const RID &p_id, const RID &p_related_id) override; + void accessibility_update_add_related_described_by(const RID &p_id, const RID &p_related_id) override; + void accessibility_update_add_related_flow_to(const RID &p_id, const RID &p_related_id) override; + void accessibility_update_add_related_labeled_by(const RID &p_id, const RID &p_related_id) override; + void accessibility_update_add_related_radio_group(const RID &p_id, const RID &p_related_id) override; + void accessibility_update_set_active_descendant(const RID &p_id, const RID &p_other_id) override; + void accessibility_update_set_next_on_line(const RID &p_id, const RID &p_other_id) override; + void accessibility_update_set_previous_on_line(const RID &p_id, const RID &p_other_id) override; + void accessibility_update_set_member_of(const RID &p_id, const RID &p_other_id) override; + void accessibility_update_set_in_page_link_target(const RID &p_id, const RID &p_other_id) override; + void accessibility_update_set_error_message(const RID &p_id, const RID &p_other_id) override; + void accessibility_update_set_live(const RID &p_id, DisplayServer::AccessibilityLiveMode p_live) override; + void accessibility_update_set_default_action_verb(const RID &p_id, DisplayServer::AccessibilityActionVerb p_action) override; + void accessibility_update_add_action(const RID &p_id, DisplayServer::AccessibilityAction p_action, const Callable &p_callable) override; + void accessibility_update_add_custom_action(const RID &p_id, int p_action_id, const String &p_action_description) override; + void accessibility_update_set_table_row_count(const RID &p_id, int p_count) override; + void accessibility_update_set_table_column_count(const RID &p_id, int p_count) override; + void accessibility_update_set_table_row_index(const RID &p_id, int p_index) override; + void accessibility_update_set_table_column_index(const RID &p_id, int p_index) override; + void accessibility_update_set_table_cell_position(const RID &p_id, int p_row_index, int p_column_index) override; + void accessibility_update_set_table_cell_span(const RID &p_id, int p_row_span, int p_column_span) override; + void accessibility_update_set_list_item_count(const RID &p_id, int p_size) override; + void accessibility_update_set_list_item_index(const RID &p_id, int p_index) override; + void accessibility_update_set_list_item_level(const RID &p_id, int p_level) override; + void accessibility_update_set_list_item_selected(const RID &p_id, bool p_selected) override; + void accessibility_update_set_list_item_expanded(const RID &p_id, bool p_expanded) override; + void accessibility_update_set_popup_type(const RID &p_id, DisplayServer::AccessibilityPopupType p_popup) override; + void accessibility_update_set_checked(const RID &p_id, bool p_checekd) override; + void accessibility_update_set_num_value(const RID &p_id, double p_position) override; + void accessibility_update_set_num_range(const RID &p_id, double p_min, double p_max) override; + void accessibility_update_set_num_step(const RID &p_id, double p_step) override; + void accessibility_update_set_num_jump(const RID &p_id, double p_jump) override; + void accessibility_update_set_scroll_x(const RID &p_id, double p_position) override; + void accessibility_update_set_scroll_x_range(const RID &p_id, double p_min, double p_max) override; + void accessibility_update_set_scroll_y(const RID &p_id, double p_position) override; + void accessibility_update_set_scroll_y_range(const RID &p_id, double p_min, double p_max) override; + void accessibility_update_set_text_decorations(const RID &p_id, bool p_underline, bool p_strikethrough, bool p_overline) override; + void accessibility_update_set_text_align(const RID &p_id, HorizontalAlignment p_align) override; + void accessibility_update_set_text_selection(const RID &p_id, const RID &p_text_start_id, int p_start_char, const RID &p_text_end_id, int p_end_char) override; + void accessibility_update_set_flag(const RID &p_id, DisplayServer::AccessibilityFlags p_flag, bool p_value) override; + void accessibility_update_set_classname(const RID &p_id, const String &p_classname) override; + void accessibility_update_set_placeholder(const RID &p_id, const String &p_placeholder) override; + void accessibility_update_set_language(const RID &p_id, const String &p_language) override; + void accessibility_update_set_text_orientation(const RID &p_id, bool p_vertical) override; + void accessibility_update_set_list_orientation(const RID &p_id, bool p_vertical) override; + void accessibility_update_set_shortcut(const RID &p_id, const String &p_shortcut) override; + void accessibility_update_set_url(const RID &p_id, const String &p_url) override; + void accessibility_update_set_role_description(const RID &p_id, const String &p_description) override; + void accessibility_update_set_state_description(const RID &p_id, const String &p_description) override; + void accessibility_update_set_color_value(const RID &p_id, const Color &p_color) override; + void accessibility_update_set_background_color(const RID &p_id, const Color &p_color) override; + void accessibility_update_set_foreground_color(const RID &p_id, const Color &p_color) override; + + AccessibilityDriverAccessKit(); + ~AccessibilityDriverAccessKit(); +}; + +#endif // ACCESSKIT_ENABLED + +#endif // ACCESSIBILITY_DRIVER_ACCESSKIT_H diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index cec50ac30f0a10..8ff3a5b5d25efa 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -580,6 +580,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { _initial_set("docks/scene_tree/start_create_dialog_fully_expanded", false); _initial_set("docks/scene_tree/auto_expand_to_selected", true); _initial_set("docks/scene_tree/center_node_on_reparent", false); + _initial_set("docks/scene_tree/accessibility_warnings", false); // FileSystem EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "docks/filesystem/thumbnail_size", 64, "32,128,16") diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 361ae2a9459ca1..708252199ee19d 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -129,8 +129,10 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i } undo_redo->commit_action(); } else if (p_id == BUTTON_WARNING) { - const PackedStringArray warnings = n->get_configuration_warnings(); - + PackedStringArray warnings = n->get_configuration_warnings(); + if (accessibility_warnings) { + warnings.append_array(n->get_accessibility_configuration_warnings()); + } if (warnings.is_empty()) { return; } @@ -287,8 +289,10 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { } if (can_rename) { //should be can edit.. - - const PackedStringArray warnings = p_node->get_configuration_warnings(); + PackedStringArray warnings = p_node->get_configuration_warnings(); + if (accessibility_warnings) { + warnings.append_array(p_node->get_accessibility_configuration_warnings()); + } const int num_warnings = warnings.size(); if (num_warnings > 0) { String warning_icon; @@ -1495,6 +1499,13 @@ void SceneTreeEditor::set_auto_expand_selected(bool p_auto, bool p_update_settin auto_expand_selected = p_auto; } +void SceneTreeEditor::set_accessibility_warnings(bool p_enable, bool p_update_settings) { + if (p_update_settings) { + EditorSettings::get_singleton()->set("docks/scene_tree/accessibility_warnings", p_enable); + } + accessibility_warnings = p_enable; +} + void SceneTreeEditor::set_connect_to_script_mode(bool p_enable) { connect_to_script_mode = p_enable; update_tree(); diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h index 9ae1e99a271c1d..c11fd67ca73607 100644 --- a/editor/gui/scene_tree_editor.h +++ b/editor/gui/scene_tree_editor.h @@ -69,6 +69,7 @@ class SceneTreeEditor : public Control { AcceptDialog *warning = nullptr; bool auto_expand_selected = true; + bool accessibility_warnings = false; bool connect_to_script_mode = false; bool connecting_signal = false; @@ -172,6 +173,7 @@ class SceneTreeEditor : public Control { void update_tree() { _update_tree(); } void set_auto_expand_selected(bool p_auto, bool p_update_settings); + void set_accessibility_warnings(bool p_enable, bool p_update_settings); void set_connect_to_script_mode(bool p_enable); void set_connecting_signal(bool p_enable); diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 4fe91d1cc51112..cd4f6192d85024 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -1152,6 +1152,8 @@ ProjectManager::ProjectManager() { title_bar_logo = memnew(Button); title_bar_logo->set_flat(true); + title_bar_logo->set_tooltip_text(TTR("About Godot")); + title_bar_logo->set_accessibility_name(TTR("About Godot")); left_hbox->add_child(title_bar_logo); title_bar_logo->connect("pressed", callable_mp(this, &ProjectManager::_show_about)); @@ -1238,6 +1240,7 @@ ProjectManager::ProjectManager() { search_box = memnew(LineEdit); search_box->set_placeholder(TTR("Filter Projects")); + search_box->set_accessibility_name(TTR("Filter Projects")); search_box->set_tooltip_text(TTR("This field filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character.")); search_box->set_clear_button_enabled(true); search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed)); diff --git a/editor/project_manager/project_list.cpp b/editor/project_manager/project_list.cpp index d125754dd72e96..aa5befc3eabfb7 100644 --- a/editor/project_manager/project_list.cpp +++ b/editor/project_manager/project_list.cpp @@ -74,11 +74,45 @@ void ProjectListItemControl::_notification(int p_what) { case NOTIFICATION_MOUSE_ENTER: { is_hovering = true; queue_redraw(); + queue_accessibility_update(); } break; case NOTIFICATION_MOUSE_EXIT: { is_hovering = false; queue_redraw(); + queue_accessibility_update(); + } 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_LIST_ITEM); + DisplayServer::get_singleton()->accessibility_update_set_name(ae, TTR("Project")); + DisplayServer::get_singleton()->accessibility_update_set_value(ae, project_title->get_text()); + + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_SCROLL_INTO_VIEW, callable_mp(this, &ProjectListItemControl::_accessibility_action_scroll_into_view)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_FOCUS, callable_mp(this, &ProjectListItemControl::_accessibility_action_focus)); + DisplayServer::get_singleton()->accessibility_update_add_action(ae, DisplayServer::AccessibilityAction::ACTION_BLUR, callable_mp(this, &ProjectListItemControl::_accessibility_action_blur)); + + ProjectList *pl = get_list(); + if (pl) { + DisplayServer::get_singleton()->accessibility_update_set_list_item_index(ae, pl->get_index(this)); + } + DisplayServer::get_singleton()->accessibility_update_set_list_item_level(ae, 0); + DisplayServer::get_singleton()->accessibility_update_set_list_item_selected(ae, is_selected); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_HOVERED, is_hovering); + } break; + + case NOTIFICATION_FOCUS_ENTER: { + ProjectList *pl = get_list(); + if (pl) { + int idx = pl->get_index(this); + if (idx >= 0) { + pl->ensure_project_visible(idx); + pl->select_project(idx); + } + } } break; case NOTIFICATION_DRAW: { @@ -94,6 +128,46 @@ void ProjectListItemControl::_notification(int p_what) { } } +ProjectList *ProjectListItemControl::get_list() const { + if (!is_inside_tree()) { + return nullptr; + } + ProjectList *pl = Object::cast_to(get_parent()->get_parent()); + return pl; +} + +void ProjectListItemControl::_accessibility_action_scroll_into_view(const Variant &p_data) { + ProjectList *pl = get_list(); + if (pl) { + int idx = pl->get_index(this); + if (idx >= 0) { + pl->ensure_project_visible(idx); + } + } +} + +void ProjectListItemControl::_accessibility_action_focus(const Variant &p_data) { + ProjectList *pl = get_list(); + if (pl) { + int idx = pl->get_index(this); + if (idx >= 0) { + pl->ensure_project_visible(idx); + pl->select_project(idx); + } + } +} + +void ProjectListItemControl::_accessibility_action_blur(const Variant &p_data) { + ProjectList *pl = get_list(); + if (pl) { + int idx = pl->get_index(this); + if (idx >= 0) { + pl->ensure_project_visible(idx); + pl->deselect_project(idx); + } + } +} + void ProjectListItemControl::_favorite_button_pressed() { emit_signal(SNAME("favorite_pressed")); } @@ -104,10 +178,14 @@ void ProjectListItemControl::_explore_button_pressed() { void ProjectListItemControl::set_project_title(const String &p_title) { project_title->set_text(p_title); + project_title->set_accessibility_name(TTR("Project name")); + queue_accessibility_update(); } void ProjectListItemControl::set_project_path(const String &p_path) { project_path->set_text(p_path); + project_path->set_accessibility_name(TTR("Project path")); + queue_accessibility_update(); } void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) { @@ -144,6 +222,8 @@ void ProjectListItemControl::set_unsupported_features(PackedStringArray p_featur String unsupported_features_str = String(", ").join(p_features); tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str; } + project_unsupported_features->set_focus_mode(FOCUS_ACCESSIBILITY); + project_unsupported_features->set_accessibility_name(tooltip_text); project_unsupported_features->set_tooltip_text(tooltip_text); project_unsupported_features->show(); } else { @@ -158,6 +238,7 @@ bool ProjectListItemControl::should_load_project_icon() const { void ProjectListItemControl::set_selected(bool p_selected) { is_selected = p_selected; queue_redraw(); + queue_accessibility_update(); } void ProjectListItemControl::set_is_favorite(bool p_favorite) { @@ -213,6 +294,8 @@ ProjectListItemControl::ProjectListItemControl() { favorite_button = memnew(TextureButton); favorite_button->set_name("FavoriteButton"); + favorite_button->set_tooltip_text(TTR("Add to favorites")); + favorite_button->set_accessibility_name(TTR("Add to favorites")); // This makes the project's "hover" style display correctly when hovering the favorite icon. favorite_button->set_mouse_filter(MOUSE_FILTER_PASS); favorite_box->add_child(favorite_button); @@ -260,6 +343,8 @@ ProjectListItemControl::ProjectListItemControl() { explore_button = memnew(Button); explore_button->set_name("ExploreButton"); + explore_button->set_tooltip_text(TTR("Open in file manager")); + explore_button->set_accessibility_name(TTR("Open in file manager")); explore_button->set_flat(true); path_hb->add_child(explore_button); explore_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_explore_button_pressed)); @@ -335,6 +420,15 @@ void ProjectList::_notification(int p_what) { set_process(false); } } 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_LIST); + DisplayServer::get_singleton()->accessibility_update_set_list_item_count(ae, _projects.size()); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_MULTISELECTABLE, false); + } } } @@ -495,6 +589,7 @@ void ProjectList::update_project_list() { set_v_scroll(0); emit_signal(SNAME(SIGNAL_LIST_CHANGED)); + queue_accessibility_update(); } void ProjectList::sort_projects() { @@ -559,6 +654,7 @@ void ProjectList::sort_projects() { // Rewind the coroutine because order of projects changed _update_icons_async(); update_dock_menu(); + queue_accessibility_update(); } int ProjectList::get_project_count() const { @@ -627,6 +723,7 @@ void ProjectList::add_project(const String &dir_path, bool favorite) { if (!_config.has_section(dir_path)) { _config.set_value(dir_path, "favorite", favorite); } + queue_accessibility_update(); } void ProjectList::set_project_version(const String &p_project_path, int p_version) { @@ -686,6 +783,15 @@ int ProjectList::refresh_project(const String &dir_path) { return index; } +int ProjectList::get_index(const ProjectListItemControl *p_control) const { + for (int i = 0; i < _projects.size(); ++i) { + if (_projects[i].control == p_control) { + return i; + } + } + return -1; +} + void ProjectList::ensure_project_visible(int p_index) { const Item &item = _projects[p_index]; ensure_control_visible(item.control); @@ -751,6 +857,7 @@ void ProjectList::_remove_project(int p_index, bool p_update_config) { // Not actually saving the file, in case you are doing more changes to settings } + queue_accessibility_update(); update_dock_menu(); } @@ -832,18 +939,21 @@ void ProjectList::_clear_project_selection() { for (int i = 0; i < previous_selected_items.size(); ++i) { previous_selected_items[i].control->set_selected(false); } + queue_accessibility_update(); } void ProjectList::_select_project_nocheck(int p_index) { Item &item = _projects.write[p_index]; _selected_project_paths.insert(item.path); item.control->set_selected(true); + queue_accessibility_update(); } void ProjectList::_deselect_project_nocheck(int p_index) { Item &item = _projects.write[p_index]; _selected_project_paths.erase(item.path); item.control->set_selected(false); + queue_accessibility_update(); } inline void _sort_project_range(int &a, int &b) { @@ -869,6 +979,10 @@ void ProjectList::select_project(int p_index) { _select_project_nocheck(p_index); } +void ProjectList::deselect_project(int p_index) { + _deselect_project_nocheck(p_index); +} + void ProjectList::select_first_visible_project() { _clear_project_selection(); diff --git a/editor/project_manager/project_list.h b/editor/project_manager/project_list.h index 981df0f3a00deb..ad27046c6c81c5 100644 --- a/editor/project_manager/project_list.h +++ b/editor/project_manager/project_list.h @@ -62,6 +62,12 @@ class ProjectListItemControl : public HBoxContainer { void _favorite_button_pressed(); void _explore_button_pressed(); + ProjectList *get_list() const; + + void _accessibility_action_scroll_into_view(const Variant &p_data); + void _accessibility_action_focus(const Variant &p_data); + void _accessibility_action_blur(const Variant &p_data); + protected: void _notification(int p_what); static void _bind_methods(); @@ -234,10 +240,12 @@ class ProjectList : public ScrollContainer { void set_project_version(const String &p_project_path, int version); int refresh_project(const String &dir_path); void ensure_project_visible(int p_index); + int get_index(const ProjectListItemControl *p_control) const; // Project list selection. void select_project(int p_index); + void deselect_project(int p_index); void select_first_visible_project(); Vector get_selected_projects() const; const HashSet &get_selected_project_keys() const; diff --git a/editor/project_manager/project_tag.cpp b/editor/project_manager/project_tag.cpp index 8fb05b549e2218..401939b5735b28 100644 --- a/editor/project_manager/project_tag.cpp +++ b/editor/project_manager/project_tag.cpp @@ -68,7 +68,8 @@ ProjectTag::ProjectTag(const String &p_text, bool p_display_close) { add_child(button); button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); button->set_text(p_text.capitalize()); - button->set_focus_mode(FOCUS_NONE); + button->set_focus_mode(FOCUS_ACCESSIBILITY); + button->set_accessibility_name(vformat(TTR("Project tag: %s"), p_text)); button->set_icon_alignment(HORIZONTAL_ALIGNMENT_RIGHT); button->set_theme_type_variation(SNAME("ProjectTag")); } diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 67c269aa80b745..36da84f230f7e6 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -1110,6 +1110,9 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { case TOOL_CENTER_PARENT: { EditorSettings::get_singleton()->set("docks/scene_tree/center_node_on_reparent", !EDITOR_GET("docks/scene_tree/center_node_on_reparent")); } break; + case TOOL_ACCESSIBILITY_WARNINGS: { + scene_tree->set_accessibility_warnings(!EDITOR_GET("docks/scene_tree/accessibility_warnings"), true); + } break; case TOOL_SCENE_EDITABLE_CHILDREN: { if (!profile_allow_editing) { break; @@ -1526,6 +1529,7 @@ void SceneTreeDock::_notification(int p_what) { case NOTIFICATION_ENTER_TREE: { clear_inherit_confirm->connect("confirmed", callable_mp(this, &SceneTreeDock::_tool_selected).bind(TOOL_SCENE_CLEAR_INHERITANCE_CONFIRM, false)); scene_tree->set_auto_expand_selected(EDITOR_GET("docks/scene_tree/auto_expand_to_selected"), false); + scene_tree->set_accessibility_warnings(EDITOR_GET("docks/scene_tree/accessibility_warnings"), false); } break; case NOTIFICATION_EXIT_TREE: { @@ -1535,6 +1539,7 @@ void SceneTreeDock::_notification(int p_what) { case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { if (EditorSettings::get_singleton()->check_changed_settings_in_group("docks/scene_tree")) { scene_tree->set_auto_expand_selected(EDITOR_GET("docks/scene_tree/auto_expand_to_selected"), false); + scene_tree->set_accessibility_warnings(EDITOR_GET("docks/scene_tree/accessibility_warnings"), false); } } break; @@ -3576,6 +3581,10 @@ void SceneTreeDock::_update_tree_menu() { tree_menu->set_item_checked(tree_menu->get_item_index(TOOL_CENTER_PARENT), EDITOR_GET("docks/scene_tree/center_node_on_reparent")); tree_menu->set_item_tooltip(tree_menu->get_item_index(TOOL_CENTER_PARENT), TTR("If enabled, Reparent to New Node will create the new node in the center of the selected nodes, if possible.")); + tree_menu->add_separator(); + tree_menu->add_check_item(TTR("Show Accessibility Warnings"), TOOL_ACCESSIBILITY_WARNINGS); + tree_menu->set_item_checked(tree_menu->get_item_index(TOOL_ACCESSIBILITY_WARNINGS), EDITOR_GET("docks/scene_tree/accessibility_warnings")); + PopupMenu *resource_list = memnew(PopupMenu); resource_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); resource_list->connect("about_to_popup", callable_mp(this, &SceneTreeDock::_list_all_subresources).bind(resource_list)); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index 21e1b00f931d21..54439a2da0893e 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -95,7 +95,7 @@ class SceneTreeDock : public VBoxContainer { TOOL_CREATE_USER_INTERFACE, TOOL_CREATE_FAVORITE, TOOL_CENTER_PARENT, - + TOOL_ACCESSIBILITY_WARNINGS, }; enum { diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index ee008e56367133..a6eb71133895ff 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1446,6 +1446,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref &p_the // Label. p_theme->set_stylebox("normal", "Label", p_config.base_empty_style); + p_theme->set_stylebox("focus", "Label", p_config.button_style_focus); p_theme->set_color("font_color", "Label", p_config.font_color); p_theme->set_color("font_shadow_color", "Label", Color(0, 0, 0, 0)); diff --git a/main/main.cpp b/main/main.cpp index 90c84114f223dd..327892492cb8b8 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -174,6 +174,8 @@ static int audio_driver_idx = -1; // Engine config/tools +static DisplayServer::AccessibilityMode accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_AUTO; +static bool accessibility_node_set = false; static bool single_window = false; static bool editor = false; static bool project_manager = false; @@ -584,6 +586,7 @@ void Main::print_help(const char *p_binary) { print_help_option("--screen ", "Request window screen.\n"); print_help_option("--single-window", "Use a single window (no separate subwindows).\n"); print_help_option("--xr-mode ", "Select XR (Extended Reality) mode [\"default\", \"off\", \"on\"].\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"); @@ -1223,6 +1226,22 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } else if (I->get() == "--single-window") { // force single window single_window = true; + } else if (I->get() == "--accessibility") { + if (I->next()) { + if (I->next()->get() == "auto") { + accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_AUTO; + } else if (I->next()->get() == "always") { + accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_ALWAYS; + } else if (I->next()->get() == "disabled") { + accessibility_mode = DisplayServer::AccessibilityMode::ACCESSIBILITY_DISABLED; + } + accessibility_node_set = true; + + N = I->next()->next(); + } else { + OS::get_singleton()->print("Missing accessibility mode argument, aborting.\n"); + goto error; + } } else if (I->get() == "-t" || I->get() == "--always-on-top") { // force always-on-top window init_always_on_top = true; @@ -2683,6 +2702,11 @@ Error Main::setup2() { Color boot_bg_color = GLOBAL_DEF_BASIC("application/boot_splash/bg_color", boot_splash_bg_color); DisplayServer::set_early_window_clear_color_override(true, boot_bg_color); + if (!accessibility_node_set) { + accessibility_mode = (DisplayServer::AccessibilityMode)GLOBAL_GET("accessibility/accessibility/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, err); diff --git a/misc/extension_api_validation/4.1-stable_4.2-stable.expected b/misc/extension_api_validation/4.1-stable_4.2-stable.expected index 11cf8531c6392f..59ebe1811947c8 100644 --- a/misc/extension_api_validation/4.1-stable_4.2-stable.expected +++ b/misc/extension_api_validation/4.1-stable_4.2-stable.expected @@ -192,7 +192,7 @@ Validate extension JSON: JSON file: Field was added in a way that breaks compati GH-80410 -------- -Validate extension JSON: Error: Field 'classes/RichTextLabel/methods/add_image/arguments': size changed value in new API, from 6 to 10. +Validate extension JSON: Error: Field 'classes/RichTextLabel/methods/add_image/arguments': size changed value in new API, from 6 to 11. Added optional argument. Compatibility method registered. diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index 55a4e0b18c327b..963b95e459e816 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -343,4 +343,13 @@ GH-91143 -------- Validate extension JSON: Error: Field 'classes/Input/methods/vibrate_handheld/arguments': size changed value in new API, from 1 to 2. -Added optional argument. Compatibility method registered. \ No newline at end of file +Added optional argument. Compatibility method registered. + + +GH-76829 +-------- +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 09a037fd28dade..e5d6b185c450f8 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -4029,6 +4029,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++) { @@ -4274,19 +4276,239 @@ TextServer::Orientation TextServerAdvanced::_shaped_text_get_orientation(const R int64_t TextServerAdvanced::_shaped_get_span_count(const RID &p_shaped) const { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, 0); - return sd->spans.size(); + ShapedTextDataAdvanced *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, 0); + } + return span_sd->spans.size(); } Variant TextServerAdvanced::_shaped_get_span_meta(const RID &p_shaped, int64_t p_index) const { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, Variant()); - ERR_FAIL_INDEX_V(p_index, sd->spans.size(), Variant()); - return sd->spans[p_index].meta; + 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].meta; +} + +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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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); + if (sd->parent != RID()) { + full_copy(sd); + } ERR_FAIL_INDEX(p_index, sd->spans.size()); ShapedTextDataAdvanced::Span &span = sd->spans.ptrw()[p_index]; @@ -4364,6 +4586,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); @@ -4556,6 +4785,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); @@ -6253,6 +6484,8 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { if (sd->bidi_override.is_empty()) { sd->bidi_override.push_back(Vector3i(sd->start, sd->end, DIRECTION_INHERITED)); } + sd->runs.clear(); + sd->runs_dirty = true; for (int ov = 0; ov < sd->bidi_override.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 7e29f984c1668d..9ec5a0a881d5db 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -446,6 +446,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; @@ -475,6 +483,9 @@ class TextServerAdvanced : public TextServerExtension { }; Vector spans; + Vector runs; + bool runs_dirty = true; + struct EmbeddedObject { int start = -1; int end = -1; @@ -648,6 +659,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; @@ -932,11 +944,23 @@ class TextServerAdvanced : public TextServerExtension { 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(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 c62f30881875e7..d85d97f5c1454f 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -2931,6 +2931,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) { @@ -3129,18 +3131,238 @@ int64_t TextServerFallback::_shaped_get_span_count(const RID &p_shaped) const { ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, 0); return sd->spans.size(); + ShapedTextDataFallback *span_sd = sd; + if (sd->parent.is_valid()) { + span_sd = shaped_owner.get_or_null(sd->parent); + ERR_FAIL_NULL_V(span_sd, 0); + } + return span_sd->spans.size(); } Variant TextServerFallback::_shaped_get_span_meta(const RID &p_shaped, int64_t p_index) const { ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, Variant()); - ERR_FAIL_INDEX_V(p_index, sd->spans.size(), Variant()); - return sd->spans[p_index].meta; + 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].meta; +} + +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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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); + if (sd->parent != RID()) { + full_copy(sd); + } ERR_FAIL_INDEX(p_index, sd->spans.size()); ShapedTextDataFallback::Span &span = sd->spans.ptrw()[p_index]; @@ -3246,6 +3468,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); @@ -4143,6 +4372,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 = true; diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 31370c7da74780..68e7a376d2abd6 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -392,6 +392,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; @@ -421,6 +428,9 @@ class TextServerFallback : public TextServerExtension { }; Vector spans; + Vector runs; + bool runs_dirty = true; + struct EmbeddedObject { int start = -1; int end = -1; @@ -563,6 +573,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); @@ -800,11 +811,23 @@ class TextServerFallback : public TextServerExtension { 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(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/platform/linuxbsd/SCsub b/platform/linuxbsd/SCsub index 0802b528f4b029..a9b2402315f423 100644 --- a/platform/linuxbsd/SCsub +++ b/platform/linuxbsd/SCsub @@ -10,6 +10,7 @@ common_linuxbsd = [ "joypad_linux.cpp", "freedesktop_portal_desktop.cpp", "freedesktop_screensaver.cpp", + "freedesktop_at_spi_monitor.cpp", ] if env["use_sowrap"]: diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index afc9d25a80dbc0..0ca532c6a7e052 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -474,6 +474,19 @@ def configure(env: "SConsEnvironment"): env.Append(CPPDEFINES=["WAYLAND_ENABLED"]) env.Append(LIBS=["rt"]) # Needed by glibc, used by _allocate_shm_file + if env["accesskit_sdk_path"] != "": + env.Prepend(CPPPATH=[env["accesskit_sdk_path"] + "/include"]) + if env["arch"] == "arm64": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/linux/arm64/static/"]) + elif env["arch"] == "rv64": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/linux/riscv64gc/static/"]) + elif env["arch"] == "x86_64": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/linux/x86_64/static/"]) + elif env["arch"] == "x86_32": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/linux/x86/static/"]) + env.Append(CPPDEFINES=["ACCESSKIT_ENABLED"]) + env.Append(LIBS=["accesskit"]) + if env["vulkan"]: env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) if not env["use_volk"]: diff --git a/platform/linuxbsd/freedesktop_at_spi_monitor.cpp b/platform/linuxbsd/freedesktop_at_spi_monitor.cpp new file mode 100644 index 00000000000000..20a5d27b8b5704 --- /dev/null +++ b/platform/linuxbsd/freedesktop_at_spi_monitor.cpp @@ -0,0 +1,161 @@ +/**************************************************************************/ +/* freedesktop_at_spi_monitor.cpp */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#include "freedesktop_at_spi_monitor.h" + +#ifdef DBUS_ENABLED + +#include "core/error/error_macros.h" +#include "core/os/os.h" +#include "core/string/ustring.h" + +#ifdef SOWRAP_ENABLED +#include "dbus-so_wrap.h" +#else +#include +#endif + +#include + +#include "core/variant/variant.h" + +#define BUS_OBJECT_NAME "org.a11y.Bus" +#define BUS_OBJECT_PATH "/org/a11y/bus" + +#define BUS_INTERFACE_PROPERTIES "org.freedesktop.DBus.Properties" + +void FreeDesktopAtSPIMonitor::monitor_thread_func(void *p_userdata) { + FreeDesktopAtSPIMonitor *mon = (FreeDesktopAtSPIMonitor *)p_userdata; + + DBusError error; + dbus_error_init(&error); + + DBusConnection *bus = dbus_bus_get(DBUS_BUS_SESSION, &error); + if (dbus_error_is_set(&error)) { + dbus_error_free(&error); + mon->supported.clear(); + return; + } + + static const char *iface = "org.a11y.Status"; + static const char *member = "IsEnabled"; + + while (!mon->exit_thread.is_set()) { + DBusMessage *message = dbus_message_new_method_call(BUS_OBJECT_NAME, BUS_OBJECT_PATH, BUS_INTERFACE_PROPERTIES, "Get"); + + dbus_message_append_args( + message, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &member, + DBUS_TYPE_INVALID); + + DBusMessage *reply = dbus_connection_send_with_reply_and_block(bus, message, 50, &error); + dbus_message_unref(message); + + if (!dbus_error_is_set(&error)) { + DBusMessageIter iter, iter_variant, iter_struct; + dbus_bool_t result; + dbus_message_iter_init(reply, &iter); + dbus_message_iter_recurse(&iter, &iter_variant); + switch (dbus_message_iter_get_arg_type(&iter_variant)) { + case DBUS_TYPE_STRUCT: { + dbus_message_iter_recurse(&iter_variant, &iter_struct); + if (dbus_message_iter_get_arg_type(&iter_struct) == DBUS_TYPE_BOOLEAN) { + dbus_message_iter_get_basic(&iter_struct, &result); + if (result) { + mon->sr_enabled.set(); + } else { + mon->sr_enabled.clear(); + } + } + } break; + case DBUS_TYPE_BOOLEAN: { + dbus_message_iter_get_basic(&iter_variant, &result); + if (result) { + mon->sr_enabled.set(); + } else { + mon->sr_enabled.clear(); + } + } break; + default: + break; + } + dbus_message_unref(reply); + } else { + dbus_error_free(&error); + } + + usleep(50000); + } + + dbus_connection_unref(bus); +} + +FreeDesktopAtSPIMonitor::FreeDesktopAtSPIMonitor() { +#ifdef SOWRAP_ENABLED +#ifdef DEBUG_ENABLED + int dylibloader_verbose = 1; +#else + int dylibloader_verbose = 0; +#endif + if (initialize_dbus(dylibloader_verbose) != 0) { + print_verbose("AT-SPI2: Failed to load DBus library!"); + supported.clear(); + return; + } +#endif + bool ver_ok = false; + int version_major = 0; + int version_minor = 0; + int version_rev = 0; + dbus_get_version(&version_major, &version_minor, &version_rev); + ver_ok = (version_major == 1 && version_minor >= 10) || (version_major > 1); // 1.10.0 + print_verbose(vformat("AT-SPI2: DBus %d.%d.%d detected.", version_major, version_minor, version_rev)); + if (!ver_ok) { + print_verbose("AT-SPI2: Unsupported DBus library version!"); + supported.clear(); + return; + } + + supported.set(); + sr_enabled.clear(); + exit_thread.clear(); + + thread.start(FreeDesktopAtSPIMonitor::monitor_thread_func, this); +} + +FreeDesktopAtSPIMonitor::~FreeDesktopAtSPIMonitor() { + exit_thread.set(); + if (thread.is_started()) { + thread.wait_to_finish(); + } +} + +#endif // DBUS_ENABLED diff --git a/platform/linuxbsd/freedesktop_at_spi_monitor.h b/platform/linuxbsd/freedesktop_at_spi_monitor.h new file mode 100644 index 00000000000000..77e39b7061280c --- /dev/null +++ b/platform/linuxbsd/freedesktop_at_spi_monitor.h @@ -0,0 +1,59 @@ +/**************************************************************************/ +/* freedesktop_at_spi_monitor.h */ +/**************************************************************************/ +/* 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 FREEDESKTOP_AT_SPI_MONITOR_H +#define FREEDESKTOP_AT_SPI_MONITOR_H + +#ifdef DBUS_ENABLED + +#include "core/os/thread.h" +#include "core/os/thread_safe.h" + +class FreeDesktopAtSPIMonitor { +private: + Thread thread; + + SafeFlag exit_thread; + SafeFlag sr_enabled; + SafeFlag supported; + + static void monitor_thread_func(void *p_userdata); + +public: + FreeDesktopAtSPIMonitor(); + ~FreeDesktopAtSPIMonitor(); + + bool is_supported() { return supported.is_set(); } + bool is_active() { return sr_enabled.is_set(); } +}; + +#endif // DBUS_ENABLED + +#endif // FREEDESKTOP_AT_SPI_MONITOR_H diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index e65404a53138c4..0b2e25a06afcf9 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -130,6 +130,18 @@ uint32_t FreeDesktopPortalDesktop::get_appearance_color_scheme() { return value; } +uint32_t FreeDesktopPortalDesktop::get_high_contrast() { + if (unsupported) { + return -1; + } + + dbus_bool_t value = false; + if (read_setting("org.gnome.desktop.a11y.interface", "high-contrast", DBUS_TYPE_BOOLEAN, &value)) { + return value; + } + return -1; +} + static const char *cs_empty = ""; void FreeDesktopPortalDesktop::append_dbus_string(DBusMessageIter *p_iter, const String &p_string) { diff --git a/platform/linuxbsd/freedesktop_portal_desktop.h b/platform/linuxbsd/freedesktop_portal_desktop.h index 96c38de2c28288..87b31d90497873 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/platform/linuxbsd/freedesktop_portal_desktop.h @@ -104,6 +104,12 @@ class FreeDesktopPortalDesktop : public Object { void set_system_theme_change_callback(const Callable &p_system_theme_changed) { system_theme_changed = p_system_theme_changed; } + + // Retrieve high-contrast setting. + // -1: Unknown. + // 0: Disabled. + // 1: Enabled. + uint32_t get_high_contrast(); }; #endif // DBUS_ENABLED diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 1c3cae0435ef74..09fbab26672e88 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -50,6 +50,10 @@ #include "wayland/egl_manager_wayland_gles.h" #endif +#ifdef ACCESSKIT_ENABLED +#include "drivers/accesskit/accessibility_driver_accesskit.h" +#endif + String DisplayServerWayland::_get_app_id_from_context(Context p_context) { String app_id; @@ -137,6 +141,18 @@ void DisplayServerWayland::_show_window() { wayland_thread.window_set_app_id(MAIN_WINDOW_ID, _get_app_id_from_context(context)); wayland_thread.window_set_borderless(MAIN_WINDOW_ID, window_get_flag(WINDOW_FLAG_BORDERLESS)); +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + if (!accessibility_driver->window_create(wd.id, nullptr)) { + if (OS::get_singleton()->is_stdout_verbose()) { + ERR_PRINT("Can't create an accessibility adapter for window, accessibility support disabled!"); + } + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif + // NOTE: The XDG shell protocol is built in a way that causes the window to // be immediately shown as soon as a valid buffer is assigned to it. Hence, // the only acceptable way of implementing window showing is to move the @@ -226,6 +242,12 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const { } break; #endif +#ifdef ACCESSKIT_ENABLED + case FEATURE_ACCESSIBILITY_SCREEN_READER: { + return (accessibility_driver != nullptr); + } break; +#endif + default: { return false; } @@ -1257,6 +1279,16 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win tts = memnew(TTS_Linux); #endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_get_mode() != DisplayServer::AccessibilityMode::ACCESSIBILITY_DISABLED) { + accessibility_driver = memnew(AccessibilityDriverAccessKit); + if (accessibility_driver->init() != OK) { + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif + rendering_driver = p_rendering_driver; #ifdef RD_ENABLED @@ -1428,6 +1460,12 @@ DisplayServerWayland::~DisplayServerWayland() { } #endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + accessibility_driver->window_destroy(MAIN_WINDOW_ID); + } +#endif + wayland_thread.destroy(); // Destroy all drivers. @@ -1447,6 +1485,12 @@ DisplayServerWayland::~DisplayServerWayland() { } #endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + memdelete(accessibility_driver); + } +#endif + #ifdef DBUS_ENABLED if (portal_desktop) { memdelete(portal_desktop); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index b76cbc126fa54b..5c498a95e3954f 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -39,6 +39,7 @@ #include "core/math/math_funcs.h" #include "core/string/print_string.h" #include "core/string/ustring.h" +#include "core/version.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" @@ -50,6 +51,10 @@ #include "drivers/gles3/rasterizer_gles3.h" #endif +#ifdef ACCESSKIT_ENABLED +#include "drivers/accesskit/accessibility_driver_accesskit.h" +#endif + #include #include #include @@ -141,6 +146,11 @@ bool DisplayServerX11::has_feature(Feature p_feature) const { case FEATURE_TEXT_TO_SPEECH: case FEATURE_SCREEN_CAPTURE: return true; +#ifdef ACCESSKIT_ENABLED + case FEATURE_ACCESSIBILITY_SCREEN_READER: { + return (accessibility_driver != nullptr); + } break; +#endif default: { } } @@ -1750,6 +1760,12 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { } #endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + accessibility_driver->window_destroy(p_id); + } +#endif + if (wd.xic) { XDestroyIC(wd.xic); wd.xic = nullptr; @@ -3015,6 +3031,22 @@ void DisplayServerX11::window_set_ime_position(const Point2i &p_pos, WindowID p_ } } +int DisplayServerX11::accessibility_should_increase_contrast() const { +#ifdef DBUS_ENABLED + return portal_desktop->get_high_contrast(); +#endif + return -1; +} + +int DisplayServerX11::accessibility_screen_reader_active() const { +#ifdef DBUS_ENABLED + if (atspi_monitor->is_supported()) { + return atspi_monitor->is_active(); + } +#endif + return -1; +}; + Point2i DisplayServerX11::ime_get_selection() const { return im_selection; } @@ -5470,6 +5502,17 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, V if (dead_tbl && xkb_loaded_v05p) { wd.xkb_state = xkb_compose_state_new(dead_tbl, XKB_COMPOSE_STATE_NO_FLAGS); } +#endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + if (!accessibility_driver->window_create(id, nullptr)) { + if (OS::get_singleton()->is_stdout_verbose()) { + ERR_PRINT("Can't create an accessibility adapter for window, accessibility support disabled!"); + } + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } #endif // Enable receiving notification when the window is initialized (MapNotify) // so the focus can be set at the right time. @@ -6044,6 +6087,16 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } #endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_get_mode() != DisplayServer::AccessibilityMode::ACCESSIBILITY_DISABLED) { + accessibility_driver = memnew(AccessibilityDriverAccessKit); + if (accessibility_driver->init() != OK) { + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif + //!!!!!!!!!!!!!!!!!!!!!!!!!! //TODO - do Vulkan and OpenGL support checks, driver selection and fallback rendering_driver = p_rendering_driver; @@ -6332,6 +6385,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode screen_set_keep_on(GLOBAL_GET("display/window/energy_saving/keep_screen_on")); portal_desktop = memnew(FreeDesktopPortalDesktop); + atspi_monitor = memnew(FreeDesktopAtSPIMonitor); #endif XSetErrorHandler(&default_window_error_handler); @@ -6372,6 +6426,12 @@ DisplayServerX11::~DisplayServerX11() { } #endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + accessibility_driver->window_destroy(E.key); + } +#endif + WindowData &wd = E.value; if (wd.xic) { XDestroyIC(wd.xic); @@ -6446,7 +6506,11 @@ DisplayServerX11::~DisplayServerX11() { if (xmbstring) { memfree(xmbstring); } - +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + memdelete(accessibility_driver); + } +#endif #ifdef SPEECHD_ENABLED if (tts) { memdelete(tts); @@ -6456,6 +6520,7 @@ DisplayServerX11::~DisplayServerX11() { #ifdef DBUS_ENABLED memdelete(screensaver); memdelete(portal_desktop); + memdelete(atspi_monitor); #endif } diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index 8a7062857c4c18..cb477e9c0bea6b 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -66,6 +66,7 @@ #endif #if defined(DBUS_ENABLED) +#include "freedesktop_at_spi_monitor.h" #include "freedesktop_portal_desktop.h" #include "freedesktop_screensaver.h" #endif @@ -160,6 +161,7 @@ class DisplayServerX11 : public DisplayServer { #if defined(DBUS_ENABLED) FreeDesktopPortalDesktop *portal_desktop = nullptr; + FreeDesktopAtSPIMonitor *atspi_monitor = nullptr; #endif struct WindowData { @@ -505,6 +507,9 @@ class DisplayServerX11 : public DisplayServer { virtual void window_set_ime_active(const bool p_active, WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_set_ime_position(const Point2i &p_pos, WindowID p_window = MAIN_WINDOW_ID) override; + virtual int accessibility_should_increase_contrast() const override; + virtual int accessibility_screen_reader_active() const override; + virtual Point2i ime_get_selection() const override; virtual String ime_get_text() const override; diff --git a/platform/macos/detect.py b/platform/macos/detect.py index a5ef29e34f3db9..7fffad9ee366cb 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -186,6 +186,15 @@ def configure(env: "SConsEnvironment"): ## Dependencies + if env["accesskit_sdk_path"] != "": + env.Prepend(CPPPATH=[env["accesskit_sdk_path"] + "/include"]) + if env["arch"] == "arm64" or env["arch"] == "universal": + env.Append(LINKFLAGS=["-L" + env["accesskit_sdk_path"] + "/lib/macos/arm64/static/"]) + if env["arch"] == "x86_64" or env["arch"] == "universal": + env.Append(LINKFLAGS=["-L" + env["accesskit_sdk_path"] + "/lib/macos/x86_64/static/"]) + env.Append(CPPDEFINES=["ACCESSKIT_ENABLED"]) + env.Append(LINKFLAGS=["-laccesskit"]) + if env["builtin_libtheora"] and env["arch"] == "x86_64": env["x86_libtheora_opt_gcc"] = True diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index db76b7d78a6865..6d86ba182c6a6f 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -85,7 +85,6 @@ class DisplayServerMacOS : public DisplayServer { id window_object; id window_view; id window_button_view; - Vector mpath; Point2i mouse_pos; @@ -404,6 +403,11 @@ class DisplayServerMacOS : public DisplayServer { virtual void window_set_window_buttons_offset(const Vector2i &p_offset, WindowID p_window = MAIN_WINDOW_ID) override; virtual Vector3i window_get_safe_title_margins(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual int accessibility_should_increase_contrast() const override; + virtual int accessibility_should_reduce_animation() const override; + virtual int accessibility_should_reduce_transparency() const override; + virtual int accessibility_screen_reader_active() const override; + virtual Point2i ime_get_selection() const override; virtual String ime_get_text() const override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 0041848c786d2f..083c1403afb9e0 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -30,6 +30,7 @@ #include "display_server_macos.h" +#include "godot_application_delegate.h" #include "godot_button_view.h" #include "godot_content_view.h" #include "godot_menu_delegate.h" @@ -58,6 +59,10 @@ #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" #endif +#if defined(ACCESSKIT_ENABLED) +#include "drivers/accesskit/accessibility_driver_accesskit.h" +#endif + #import #import #import @@ -66,14 +71,15 @@ #import DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect) { - WindowID id; const float scale = screen_get_max_scale(); + + WindowID id = window_id_counter; { - WindowData wd; + WindowData &wd = windows[id]; wd.window_delegate = [[GodotWindowDelegate alloc] init]; ERR_FAIL_NULL_V_MSG(wd.window_delegate, INVALID_WINDOW_ID, "Can't create a window delegate"); - [wd.window_delegate setWindowID:window_id_counter]; + [wd.window_delegate setWindowID:id]; int rq_screen = get_screen_from_rect(p_rect); if (rq_screen < 0) { @@ -98,12 +104,15 @@ backing:NSBackingStoreBuffered defer:NO]; ERR_FAIL_NULL_V_MSG(wd.window_object, INVALID_WINDOW_ID, "Can't create a window"); - [wd.window_object setWindowID:window_id_counter]; + [wd.window_object setWindowID:id]; [wd.window_object setReleasedWhenClosed:NO]; wd.window_view = [[GodotContentView alloc] init]; - ERR_FAIL_NULL_V_MSG(wd.window_view, INVALID_WINDOW_ID, "Can't create a window view"); - [wd.window_view setWindowID:window_id_counter]; + if (wd.window_view == nil) { + windows.erase(id); + ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Can't create a window view"); + } + [wd.window_view setWindowID:id]; [wd.window_view setWantsLayer:TRUE]; [wd.window_object setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; @@ -113,6 +122,18 @@ [wd.window_object setRestorable:NO]; [wd.window_object setColorSpace:[NSColorSpace sRGBColorSpace]]; +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + if (!accessibility_driver->window_create(id, (__bridge void *)wd.window_view)) { + if (OS::get_singleton()->is_stdout_verbose()) { + ERR_PRINT("Can't create an accessibility adapter for window, accessibility support disabled!"); + } + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif + if ([wd.window_object respondsToSelector:@selector(setTabbingMode:)]) { [wd.window_object setTabbingMode:NSWindowTabbingModeDisallowed]; } @@ -146,23 +167,43 @@ } #endif Error err = rendering_context->window_create(window_id_counter, &wpd); +#ifdef ACCESSKIT_ENABLED + if (err != OK && accessibility_driver) { + accessibility_driver->window_destroy(id); + } +#endif ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, vformat("Can't create a %s context", rendering_driver)); rendering_context->window_set_size(window_id_counter, p_rect.size.width, p_rect.size.height); rendering_context->window_set_vsync_mode(window_id_counter, p_vsync_mode); } #endif + #if defined(GLES3_ENABLED) + bool gl_failed = false; if (gl_manager_legacy) { Error err = gl_manager_legacy->window_create(window_id_counter, wd.window_view, p_rect.size.width, p_rect.size.height); - ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Can't create an OpenGL context."); + if (err != OK) { + gl_failed = true; + } } if (gl_manager_angle) { CALayer *layer = [(NSView *)wd.window_view layer]; Error err = gl_manager_angle->window_create(window_id_counter, nullptr, (__bridge void *)layer, p_rect.size.width, p_rect.size.height); - ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Can't create an OpenGL context."); + if (err != OK) { + gl_failed = true; + } + } + if (gl_failed) { +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + accessibility_driver->window_destroy(id); + } +#endif + windows.erase(id); + ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Can't create an OpenGL context"); } - window_set_vsync_mode(p_vsync_mode, window_id_counter); + window_set_vsync_mode(p_vsync_mode, id); #endif [wd.window_view updateLayerDelegate]; @@ -174,10 +215,8 @@ offset.y = (nsrect.origin.y + nsrect.size.height); offset.y -= (windowRect.origin.y + windowRect.size.height); [wd.window_object setFrameTopLeftPoint:NSMakePoint(wpos.x - offset.x, wpos.y - offset.y)]; - - id = window_id_counter++; - windows[id] = wd; } + window_id_counter++; WindowData &wd = windows[id]; window_set_mode(p_mode, id); @@ -709,6 +748,8 @@ } void DisplayServerMacOS::window_destroy(WindowID p_window) { + ERR_FAIL_COND(!windows.has(p_window)); + #if defined(GLES3_ENABLED) if (gl_manager_legacy) { gl_manager_legacy->window_destroy(p_window); @@ -722,6 +763,11 @@ if (rendering_context) { rendering_context->window_destroy(p_window); } +#endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + accessibility_driver->window_destroy(p_window); + } #endif windows.erase(p_window); update_presentation_mode(); @@ -773,6 +819,11 @@ case FEATURE_STATUS_INDICATOR: case FEATURE_NATIVE_HELP: return true; +#ifdef ACCESSKIT_ENABLED + case FEATURE_ACCESSIBILITY_SCREEN_READER: { + return (accessibility_driver != nullptr); + } break; +#endif default: { } } @@ -2716,6 +2767,22 @@ return DisplayServer::VSYNC_ENABLED; } +int DisplayServerMacOS::accessibility_should_increase_contrast() const { + return [(GodotApplicationDelegate *)[[NSApplication sharedApplication] delegate] getHighContrast]; +} + +int DisplayServerMacOS::accessibility_should_reduce_animation() const { + return [(GodotApplicationDelegate *)[[NSApplication sharedApplication] delegate] getReduceMotion]; +} + +int DisplayServerMacOS::accessibility_should_reduce_transparency() const { + return [(GodotApplicationDelegate *)[[NSApplication sharedApplication] delegate] getReduceTransparency]; +} + +int DisplayServerMacOS::accessibility_screen_reader_active() const { + return [(GodotApplicationDelegate *)[[NSApplication sharedApplication] delegate] getVoiceOver]; +} + Point2i DisplayServerMacOS::ime_get_selection() const { return im_selection; } @@ -3536,6 +3603,16 @@ native_menu = memnew(NativeMenuMacOS); +#ifdef ACCESSKIT_ENABLED + if (accessibility_get_mode() != DisplayServer::AccessibilityMode::ACCESSIBILITY_DISABLED) { + accessibility_driver = memnew(AccessibilityDriverAccessKit); + if (accessibility_driver->init() != OK) { + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif + NSMenuItem *menu_item; NSString *title; @@ -3763,7 +3840,11 @@ rendering_context = nullptr; } #endif - +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + memdelete(accessibility_driver); + } +#endif CFNotificationCenterRemoveObserver(CFNotificationCenterGetDistributedCenter(), nullptr, kTISNotifySelectedKeyboardInputSourceChanged, nullptr); CGDisplayRemoveReconfigurationCallback(_displays_arrangement_changed, nullptr); diff --git a/platform/macos/godot_application_delegate.h b/platform/macos/godot_application_delegate.h index 45bd85c45cf402..229f36477834a5 100644 --- a/platform/macos/godot_application_delegate.h +++ b/platform/macos/godot_application_delegate.h @@ -36,11 +36,23 @@ #import #import -@interface GodotApplicationDelegate : NSObject +@interface GodotApplicationDelegate : NSObject { + bool high_contrast; + bool reduce_motion; + bool reduce_transparency; + bool voice_over; +} + - (void)forceUnbundledWindowActivationHackStep1; - (void)forceUnbundledWindowActivationHackStep2; - (void)forceUnbundledWindowActivationHackStep3; - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent; +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context; +- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification; +- (bool)getHighContrast; +- (bool)getReduceMotion; +- (bool)getReduceTransparency; +- (bool)getVoiceOver; @end #endif // GODOT_APPLICATION_DELEGATE_H diff --git a/platform/macos/godot_application_delegate.mm b/platform/macos/godot_application_delegate.mm index 02466bab974b30..0f75591b0b55a9 100644 --- a/platform/macos/godot_application_delegate.mm +++ b/platform/macos/godot_application_delegate.mm @@ -137,6 +137,8 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notice { [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleColorPreferencesChangedNotification" object:nil]; } +static const char *godot_ac_ctx = "gd_accessibility_observer_ctx"; + - (id)init { self = [super init]; @@ -144,12 +146,50 @@ - (id)init { [aem setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; [aem setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEOpenDocuments]; + [[NSWorkspace sharedWorkspace] addObserver:self forKeyPath:@"voiceOverEnabled" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:(void *)godot_ac_ctx]; + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(accessibilityDisplayOptionsChange:) name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification object:nil]; + high_contrast = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast]; + reduce_motion = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion]; + reduce_transparency = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency]; + voice_over = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; + return self; } - (void)dealloc { [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"AppleInterfaceThemeChangedNotification" object:nil]; [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"AppleColorPreferencesChangedNotification" object:nil]; + [[NSWorkspace sharedWorkspace] removeObserver:self forKeyPath:@"voiceOverEnabled" context:(void *)godot_ac_ctx]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (context == (void *)godot_ac_ctx) { + voice_over = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification { + high_contrast = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast]; + reduce_motion = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion]; + reduce_transparency = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency]; +} + +- (bool)getHighContrast { + return high_contrast; +} + +- (bool)getReduceMotion { + return reduce_motion; +} + +- (bool)getReduceTransparency { + return reduce_transparency; +} + +- (bool)getVoiceOver { + return voice_over; } - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 435c501956c4c8..e086e7f10cf0d6 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -53,6 +53,12 @@ res_obj = env.RES(res_target, res_file) env.add_source_files(sources, common_win) sources += res_obj +if env["accesskit_sdk_path"] != "" and not env.msvc: + def_file = "uiautomationcore." + env["arch"] + ".def" + def_target = "libuiautomationcore." + env["arch"] + ".a" + def_obj = env.DEF(def_target, def_file) + sources += def_obj + prog = env.add_program("#bin/godot", sources, PROGSUFFIX=env["PROGSUFFIX"]) arrange_program_clean(prog) diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 93eb34001e929d..cec9f6d10bd808 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -289,6 +289,42 @@ def build_res_file(target, source, env: "SConsEnvironment"): return 0 +def build_def_file(target, source, env: "SConsEnvironment"): + arch_aliases = { + "x86_32": "i386", + "x86_64": "i386:x86-64", + "arm32": "arm", + "arm64": "arm64", + } + + cmdbase = "dlltool --no-leading-underscore -m " + arch_aliases[env["arch"]] + + mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) + + for x in range(len(source)): + ok = True + # Try prefixed executable (MinGW on Linux). + cmd = mingw_bin_prefix + cmdbase + " -d " + str(source[x]) + " -l " + str(target[x]) + try: + out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() + if len(out[1]): + ok = False + except Exception: + ok = False + + # Try generic executable (MSYS2). + if not ok: + cmd = cmdbase + " -d " + str(source[x]) + " -l " + str(target[x]) + try: + out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() + if len(out[1]): + return -1 + except Exception: + return -1 + + return 0 + + def setup_msvc_manual(env: "SConsEnvironment"): """Running from VCVARS environment""" @@ -498,6 +534,17 @@ def spawn_capture(sh, escape, cmd, args, env): if env.debug_features: LIBS += ["psapi", "dbghelp"] + if env["accesskit_sdk_path"] != "": + env.Prepend(CPPPATH=[env["accesskit_sdk_path"] + "/include"]) + if env["arch"] == "arm64": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/windows/arm64/msvc/static"]) + elif env["arch"] == "x86_64": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/windows/x86_64/msvc/static"]) + elif env["arch"] == "x86_32": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/windows/x86/msvc/static"]) + env.Append(CPPDEFINES=["ACCESSKIT_ENABLED"]) + LIBS += ["accesskit", "uiautomationcore", "oleaut32", "user32", "userenv", "ntdll"] + if env["vulkan"]: env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) if not env["use_volk"]: @@ -727,6 +774,16 @@ def configure_mingw(env: "SConsEnvironment"): ] ) + if env["accesskit_sdk_path"] != "": + env.Prepend(CPPPATH=[env["accesskit_sdk_path"] + "/include"]) + if env["arch"] == "x86_64": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/windows/x86_64/mingw/static/"]) + elif env["arch"] == "x86_32": + env.Append(LIBPATH=[env["accesskit_sdk_path"] + "/lib/windows/x86/mingw/static/"]) + env.Append(LIBPATH=["#platform/windows"]) + env.Append(CPPDEFINES=["ACCESSKIT_ENABLED"]) + env.Append(LIBS=["accesskit", "uiautomationcore." + env["arch"], "oleaut32", "user32", "userenv", "ntdll"]) + if env.debug_features: env.Append(LIBS=["psapi", "dbghelp"]) @@ -782,6 +839,8 @@ def configure_mingw(env: "SConsEnvironment"): # resrc env.Append(BUILDERS={"RES": env.Builder(action=build_res_file, suffix=".o", src_suffix=".rc")}) + # dlltool + env.Append(BUILDERS={"DEF": env.Builder(action=build_def_file, suffix=".a", src_suffix=".def")}) def configure(env: "SConsEnvironment"): diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index f101d85d5832a6..c09055d8193c73 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -49,6 +49,10 @@ #include "drivers/gles3/rasterizer_gles3.h" #endif +#if defined(ACCESSKIT_ENABLED) +#include "drivers/accesskit/accessibility_driver_accesskit.h" +#endif + #include #include #include @@ -122,6 +126,11 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_SCREEN_CAPTURE: case FEATURE_STATUS_INDICATOR: return true; +#ifdef ACCESSKIT_ENABLED + case FEATURE_ACCESSIBILITY_SCREEN_READER: { + return (accessibility_driver != nullptr); + } break; +#endif default: return false; } @@ -2253,6 +2262,46 @@ bool DisplayServerWindows::can_any_window_draw() const { return false; } +int DisplayServerWindows::accessibility_should_increase_contrast() const { + HIGHCONTRASTA hc; + hc.cbSize = sizeof(HIGHCONTRAST); + if (!SystemParametersInfoA(SPI_GETHIGHCONTRAST, sizeof(HIGHCONTRAST), &hc, 0)) { + return -1; + } + return (hc.dwFlags & HCF_HIGHCONTRASTON); +} + +int DisplayServerWindows::accessibility_should_reduce_animation() const { + bool anim_enabled = false; + if (!SystemParametersInfoA(SPI_GETCLIENTAREAANIMATION, 0, &anim_enabled, 0)) { + return -1; + } + return (!anim_enabled); +} + +int DisplayServerWindows::accessibility_should_reduce_transparency() const { + bool tr_enabled = false; + if (!SystemParametersInfoA(SPI_GETDISABLEOVERLAPPEDCONTENT, 0, &tr_enabled, 0)) { + return -1; + } + return tr_enabled; +} + +int DisplayServerWindows::accessibility_screen_reader_active() const { + bool sr_enabled = false; + if (SystemParametersInfoA(SPI_GETSCREENREADER, 0, &sr_enabled, 0) && sr_enabled) { + return true; + } + + static const WCHAR *narrator_mutex_name = L"NarratorRunning"; + HANDLE narrator_mutex = OpenMutexW(MUTEX_ALL_ACCESS, false, narrator_mutex_name); + if (narrator_mutex) { + CloseHandle(narrator_mutex); + return true; + } + return false; +} + Vector2i DisplayServerWindows::ime_get_selection() const { _THREAD_SAFE_METHOD_ @@ -3658,6 +3707,18 @@ LRESULT DisplayServerWindows::_handle_early_window_message(HWND hWnd, UINT uMsg, // Fix this up so we can recognize the remaining messages. pWindowData->hWnd = hWnd; + +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + if (!accessibility_driver->window_create(pWindowData->id, (void *)hWnd)) { + if (OS::get_singleton()->is_stdout_verbose()) { + ERR_PRINT("Can't create an accessibility adapter for window, accessibility support disabled!"); + } + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif } break; default: { // Additional messages during window creation should happen after we fixed @@ -4801,6 +4862,11 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA joypad->probe_joypads(); } break; case WM_DESTROY: { +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + accessibility_driver->window_destroy(window_id); + } +#endif Input::get_singleton()->flush_buffered_events(); if (window_mouseover_id == window_id) { window_mouseover_id = INVALID_WINDOW_ID; @@ -5136,6 +5202,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, { WindowData &wd = windows[id]; + wd.id = id; wd.hWnd = CreateWindowExW( dwExStyle, L"Engine", L"", @@ -5515,6 +5582,19 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } native_menu = memnew(NativeMenuWindows); +#ifdef ACCESSKIT_ENABLED + if (accessibility_get_mode() != DisplayServer::AccessibilityMode::ACCESSIBILITY_DISABLED) { + accessibility_driver = memnew(AccessibilityDriverAccessKit); + if (accessibility_driver->init() != OK) { + if (OS::get_singleton()->is_stdout_verbose()) { + ERR_PRINT("Can't create an accessibility driver, accessibility support disabled!"); + } + memdelete(accessibility_driver); + accessibility_driver = nullptr; + } + } +#endif + // Enforce default keep screen on value. screen_set_keep_on(GLOBAL_GET("display/window/energy_saving/keep_screen_on")); @@ -5967,6 +6047,11 @@ DisplayServerWindows::~DisplayServerWindows() { memdelete(gl_manager_native); gl_manager_native = nullptr; } +#endif +#ifdef ACCESSKIT_ENABLED + if (accessibility_driver) { + memdelete(accessibility_driver); + } #endif if (tts) { memdelete(tts); diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 80f6061348a4f6..f8e0466b6319ab 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -376,6 +376,7 @@ class DisplayServerWindows : public DisplayServer { struct WindowData { HWND hWnd; + WindowID id; Vector mpath; @@ -645,6 +646,11 @@ class DisplayServerWindows : public DisplayServer { virtual void window_set_ime_active(const bool p_active, WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_set_ime_position(const Point2i &p_pos, WindowID p_window = MAIN_WINDOW_ID) override; + virtual int accessibility_should_increase_contrast() const override; + virtual int accessibility_should_reduce_animation() const override; + virtual int accessibility_should_reduce_transparency() const override; + virtual int accessibility_screen_reader_active() const override; + virtual Point2i ime_get_selection() const override; virtual String ime_get_text() const override; diff --git a/platform/windows/uiautomationcore.arm32.def b/platform/windows/uiautomationcore.arm32.def new file mode 100644 index 00000000000000..a83dd7cc7aa5bb --- /dev/null +++ b/platform/windows/uiautomationcore.arm32.def @@ -0,0 +1,115 @@ +; +; Definition file of UIAutomationCore.DLL +; Automatic generated by gendef +; written by Kai Tietz 2008 +; +LIBRARY "UIAutomationCore.DLL" +EXPORTS +DllGetActivationFactory +DllCanUnloadNow +DllGetClassObject +DockPattern_SetDockPosition +ExpandCollapsePattern_Collapse +ExpandCollapsePattern_Expand +GridPattern_GetItem +IgnoreLeaksInCurrentlyTrackedMemory +InitializeChannelBasedConnectionForProviderProxy +InvokePattern_Invoke +IsIgnoringLeaks +ItemContainerPattern_FindItemByProperty +LegacyIAccessiblePattern_DoDefaultAction +LegacyIAccessiblePattern_GetIAccessible +LegacyIAccessiblePattern_Select +LegacyIAccessiblePattern_SetValue +MultipleViewPattern_GetViewName +MultipleViewPattern_SetCurrentView +PostTestCheckForLeaks +RangeValuePattern_SetValue +ScrollItemPattern_ScrollIntoView +ScrollPattern_Scroll +ScrollPattern_SetScrollPercent +SelectionItemPattern_AddToSelection +SelectionItemPattern_RemoveFromSelection +SelectionItemPattern_Select +StartIgnoringLeaks +StopIgnoringLeaks +SynchronizedInputPattern_Cancel +SynchronizedInputPattern_StartListening +TextPattern_GetSelection +TextPattern_GetVisibleRanges +TextPattern_RangeFromChild +TextPattern_RangeFromPoint +TextPattern_get_DocumentRange +TextPattern_get_SupportedTextSelection +TextRange_AddToSelection +TextRange_Clone +TextRange_Compare +TextRange_CompareEndpoints +TextRange_ExpandToEnclosingUnit +TextRange_FindAttribute +TextRange_FindText +TextRange_GetAttributeValue +TextRange_GetBoundingRectangles +TextRange_GetChildren +TextRange_GetEnclosingElement +TextRange_GetText +TextRange_Move +TextRange_MoveEndpointByRange +TextRange_MoveEndpointByUnit +TextRange_RemoveFromSelection +TextRange_ScrollIntoView +TextRange_Select +TogglePattern_Toggle +TransformPattern_Move +TransformPattern_Resize +TransformPattern_Rotate +UiaAddEvent +UiaClientsAreListening +UiaDisconnectAllProviders +UiaDisconnectProvider +UiaEventAddWindow +UiaEventRemoveWindow +UiaFind +UiaGetErrorDescription +UiaGetPatternProvider +UiaGetPropertyValue +UiaGetReservedMixedAttributeValue +UiaGetReservedNotSupportedValue +UiaGetRootNode +UiaGetRuntimeId +UiaGetUpdatedCache +UiaHPatternObjectFromVariant +UiaHTextRangeFromVariant +UiaHUiaNodeFromVariant +UiaHasServerSideProvider +UiaHostProviderFromHwnd +UiaIAccessibleFromProvider +UiaLookupId +UiaNavigate +UiaNodeFromFocus +UiaNodeFromHandle +UiaNodeFromPoint +UiaNodeFromProvider +UiaNodeRelease +UiaPatternRelease +UiaProviderForNonClient +UiaProviderFromIAccessible +UiaRaiseActiveTextPositionChangedEvent +UiaRaiseAsyncContentLoadedEvent +UiaRaiseAutomationEvent +UiaRaiseAutomationPropertyChangedEvent +UiaRaiseChangesEvent +UiaRaiseNotificationEvent +UiaRaiseStructureChangedEvent +UiaRaiseTextEditTextChangedEvent +UiaRegisterProviderCallback +UiaRemoveEvent +UiaReturnRawElementProvider +UiaSetFocus +UiaTextRangeRelease +UpdateErrorLoggingCallback +ValuePattern_SetValue +VirtualizedItemPattern_Realize +WindowPattern_Close +WindowPattern_SetWindowVisualState +WindowPattern_WaitForInputIdle diff --git a/platform/windows/uiautomationcore.arm64.def b/platform/windows/uiautomationcore.arm64.def new file mode 100644 index 00000000000000..a83dd7cc7aa5bb --- /dev/null +++ b/platform/windows/uiautomationcore.arm64.def @@ -0,0 +1,115 @@ +; +; Definition file of UIAutomationCore.DLL +; Automatic generated by gendef +; written by Kai Tietz 2008 +; +LIBRARY "UIAutomationCore.DLL" +EXPORTS +DllGetActivationFactory +DllCanUnloadNow +DllGetClassObject +DockPattern_SetDockPosition +ExpandCollapsePattern_Collapse +ExpandCollapsePattern_Expand +GridPattern_GetItem +IgnoreLeaksInCurrentlyTrackedMemory +InitializeChannelBasedConnectionForProviderProxy +InvokePattern_Invoke +IsIgnoringLeaks +ItemContainerPattern_FindItemByProperty +LegacyIAccessiblePattern_DoDefaultAction +LegacyIAccessiblePattern_GetIAccessible +LegacyIAccessiblePattern_Select +LegacyIAccessiblePattern_SetValue +MultipleViewPattern_GetViewName +MultipleViewPattern_SetCurrentView +PostTestCheckForLeaks +RangeValuePattern_SetValue +ScrollItemPattern_ScrollIntoView +ScrollPattern_Scroll +ScrollPattern_SetScrollPercent +SelectionItemPattern_AddToSelection +SelectionItemPattern_RemoveFromSelection +SelectionItemPattern_Select +StartIgnoringLeaks +StopIgnoringLeaks +SynchronizedInputPattern_Cancel +SynchronizedInputPattern_StartListening +TextPattern_GetSelection +TextPattern_GetVisibleRanges +TextPattern_RangeFromChild +TextPattern_RangeFromPoint +TextPattern_get_DocumentRange +TextPattern_get_SupportedTextSelection +TextRange_AddToSelection +TextRange_Clone +TextRange_Compare +TextRange_CompareEndpoints +TextRange_ExpandToEnclosingUnit +TextRange_FindAttribute +TextRange_FindText +TextRange_GetAttributeValue +TextRange_GetBoundingRectangles +TextRange_GetChildren +TextRange_GetEnclosingElement +TextRange_GetText +TextRange_Move +TextRange_MoveEndpointByRange +TextRange_MoveEndpointByUnit +TextRange_RemoveFromSelection +TextRange_ScrollIntoView +TextRange_Select +TogglePattern_Toggle +TransformPattern_Move +TransformPattern_Resize +TransformPattern_Rotate +UiaAddEvent +UiaClientsAreListening +UiaDisconnectAllProviders +UiaDisconnectProvider +UiaEventAddWindow +UiaEventRemoveWindow +UiaFind +UiaGetErrorDescription +UiaGetPatternProvider +UiaGetPropertyValue +UiaGetReservedMixedAttributeValue +UiaGetReservedNotSupportedValue +UiaGetRootNode +UiaGetRuntimeId +UiaGetUpdatedCache +UiaHPatternObjectFromVariant +UiaHTextRangeFromVariant +UiaHUiaNodeFromVariant +UiaHasServerSideProvider +UiaHostProviderFromHwnd +UiaIAccessibleFromProvider +UiaLookupId +UiaNavigate +UiaNodeFromFocus +UiaNodeFromHandle +UiaNodeFromPoint +UiaNodeFromProvider +UiaNodeRelease +UiaPatternRelease +UiaProviderForNonClient +UiaProviderFromIAccessible +UiaRaiseActiveTextPositionChangedEvent +UiaRaiseAsyncContentLoadedEvent +UiaRaiseAutomationEvent +UiaRaiseAutomationPropertyChangedEvent +UiaRaiseChangesEvent +UiaRaiseNotificationEvent +UiaRaiseStructureChangedEvent +UiaRaiseTextEditTextChangedEvent +UiaRegisterProviderCallback +UiaRemoveEvent +UiaReturnRawElementProvider +UiaSetFocus +UiaTextRangeRelease +UpdateErrorLoggingCallback +ValuePattern_SetValue +VirtualizedItemPattern_Realize +WindowPattern_Close +WindowPattern_SetWindowVisualState +WindowPattern_WaitForInputIdle diff --git a/platform/windows/uiautomationcore.x86_32.def b/platform/windows/uiautomationcore.x86_32.def new file mode 100644 index 00000000000000..fce8c03fb1e48b --- /dev/null +++ b/platform/windows/uiautomationcore.x86_32.def @@ -0,0 +1,115 @@ +; +; Definition file of UIAutomationCore.DLL +; Automatic generated by gendef +; written by Kai Tietz 2008 +; +LIBRARY "UIAutomationCore.DLL" +EXPORTS +DllGetActivationFactory@8 +DllCanUnloadNow +DllGetClassObject@12 +DockPattern_SetDockPosition@8 +ExpandCollapsePattern_Collapse@4 +ExpandCollapsePattern_Expand@4 +GridPattern_GetItem@16 +IgnoreLeaksInCurrentlyTrackedMemory +InitializeChannelBasedConnectionForProviderProxy@12 +InvokePattern_Invoke@4 +IsIgnoringLeaks +ItemContainerPattern_FindItemByProperty@32 +LegacyIAccessiblePattern_DoDefaultAction@4 +LegacyIAccessiblePattern_GetIAccessible@8 +LegacyIAccessiblePattern_Select@8 +LegacyIAccessiblePattern_SetValue@8 +MultipleViewPattern_GetViewName@12 +MultipleViewPattern_SetCurrentView@8 +PostTestCheckForLeaks@8 +RangeValuePattern_SetValue@12 +ScrollItemPattern_ScrollIntoView@4 +ScrollPattern_Scroll@12 +ScrollPattern_SetScrollPercent@20 +SelectionItemPattern_AddToSelection@4 +SelectionItemPattern_RemoveFromSelection@4 +SelectionItemPattern_Select@4 +StartIgnoringLeaks@4 +StopIgnoringLeaks +SynchronizedInputPattern_Cancel@4 +SynchronizedInputPattern_StartListening@8 +TextPattern_GetSelection@8 +TextPattern_GetVisibleRanges@8 +TextPattern_RangeFromChild@12 +TextPattern_RangeFromPoint@24 +TextPattern_get_DocumentRange@8 +TextPattern_get_SupportedTextSelection@8 +TextRange_AddToSelection@4 +TextRange_Clone@8 +TextRange_Compare@12 +TextRange_CompareEndpoints@20 +TextRange_ExpandToEnclosingUnit@8 +TextRange_FindAttribute@32 +TextRange_FindText@20 +TextRange_GetAttributeValue@12 +TextRange_GetBoundingRectangles@8 +TextRange_GetChildren@8 +TextRange_GetEnclosingElement@8 +TextRange_GetText@12 +TextRange_Move@16 +TextRange_MoveEndpointByRange@16 +TextRange_MoveEndpointByUnit@20 +TextRange_RemoveFromSelection@4 +TextRange_ScrollIntoView@8 +TextRange_Select@4 +TogglePattern_Toggle@4 +TransformPattern_Move@20 +TransformPattern_Resize@20 +TransformPattern_Rotate@12 +UiaAddEvent@32 +UiaClientsAreListening +UiaDisconnectAllProviders +UiaDisconnectProvider@4 +UiaEventAddWindow@8 +UiaEventRemoveWindow@8 +UiaFind@24 +UiaGetErrorDescription@4 +UiaGetPatternProvider@12 +UiaGetPropertyValue@12 +UiaGetReservedMixedAttributeValue@4 +UiaGetReservedNotSupportedValue@4 +UiaGetRootNode@4 +UiaGetRuntimeId@8 +UiaGetUpdatedCache@24 +UiaHPatternObjectFromVariant@8 +UiaHTextRangeFromVariant@8 +UiaHUiaNodeFromVariant@8 +UiaHasServerSideProvider@4 +UiaHostProviderFromHwnd@8 +UiaIAccessibleFromProvider@16 +UiaLookupId@8 +UiaNavigate@24 +UiaNodeFromFocus@12 +UiaNodeFromHandle@8 +UiaNodeFromPoint@28 +UiaNodeFromProvider@8 +UiaNodeRelease@4 +UiaPatternRelease@4 +UiaProviderForNonClient@16 +UiaProviderFromIAccessible@16 +UiaRaiseActiveTextPositionChangedEvent@8 +UiaRaiseAsyncContentLoadedEvent@16 +UiaRaiseAutomationEvent@8 +UiaRaiseAutomationPropertyChangedEvent@40 +UiaRaiseChangesEvent@12 +UiaRaiseNotificationEvent@20 +UiaRaiseStructureChangedEvent@16 +UiaRaiseTextEditTextChangedEvent@12 +UiaRegisterProviderCallback@4 +UiaRemoveEvent@4 +UiaReturnRawElementProvider@16 +UiaSetFocus@4 +UiaTextRangeRelease@4 +UpdateErrorLoggingCallback@4 +ValuePattern_SetValue@8 +VirtualizedItemPattern_Realize@4 +WindowPattern_Close@4 +WindowPattern_SetWindowVisualState@8 +WindowPattern_WaitForInputIdle@12 diff --git a/platform/windows/uiautomationcore.x86_64.def b/platform/windows/uiautomationcore.x86_64.def new file mode 100644 index 00000000000000..a83dd7cc7aa5bb --- /dev/null +++ b/platform/windows/uiautomationcore.x86_64.def @@ -0,0 +1,115 @@ +; +; Definition file of UIAutomationCore.DLL +; Automatic generated by gendef +; written by Kai Tietz 2008 +; +LIBRARY "UIAutomationCore.DLL" +EXPORTS +DllGetActivationFactory +DllCanUnloadNow +DllGetClassObject +DockPattern_SetDockPosition +ExpandCollapsePattern_Collapse +ExpandCollapsePattern_Expand +GridPattern_GetItem +IgnoreLeaksInCurrentlyTrackedMemory +InitializeChannelBasedConnectionForProviderProxy +InvokePattern_Invoke +IsIgnoringLeaks +ItemContainerPattern_FindItemByProperty +LegacyIAccessiblePattern_DoDefaultAction +LegacyIAccessiblePattern_GetIAccessible +LegacyIAccessiblePattern_Select +LegacyIAccessiblePattern_SetValue +MultipleViewPattern_GetViewName +MultipleViewPattern_SetCurrentView +PostTestCheckForLeaks +RangeValuePattern_SetValue +ScrollItemPattern_ScrollIntoView +ScrollPattern_Scroll +ScrollPattern_SetScrollPercent +SelectionItemPattern_AddToSelection +SelectionItemPattern_RemoveFromSelection +SelectionItemPattern_Select +StartIgnoringLeaks +StopIgnoringLeaks +SynchronizedInputPattern_Cancel +SynchronizedInputPattern_StartListening +TextPattern_GetSelection +TextPattern_GetVisibleRanges +TextPattern_RangeFromChild +TextPattern_RangeFromPoint +TextPattern_get_DocumentRange +TextPattern_get_SupportedTextSelection +TextRange_AddToSelection +TextRange_Clone +TextRange_Compare +TextRange_CompareEndpoints +TextRange_ExpandToEnclosingUnit +TextRange_FindAttribute +TextRange_FindText +TextRange_GetAttributeValue +TextRange_GetBoundingRectangles +TextRange_GetChildren +TextRange_GetEnclosingElement +TextRange_GetText +TextRange_Move +TextRange_MoveEndpointByRange +TextRange_MoveEndpointByUnit +TextRange_RemoveFromSelection +TextRange_ScrollIntoView +TextRange_Select +TogglePattern_Toggle +TransformPattern_Move +TransformPattern_Resize +TransformPattern_Rotate +UiaAddEvent +UiaClientsAreListening +UiaDisconnectAllProviders +UiaDisconnectProvider +UiaEventAddWindow +UiaEventRemoveWindow +UiaFind +UiaGetErrorDescription +UiaGetPatternProvider +UiaGetPropertyValue +UiaGetReservedMixedAttributeValue +UiaGetReservedNotSupportedValue +UiaGetRootNode +UiaGetRuntimeId +UiaGetUpdatedCache +UiaHPatternObjectFromVariant +UiaHTextRangeFromVariant +UiaHUiaNodeFromVariant +UiaHasServerSideProvider +UiaHostProviderFromHwnd +UiaIAccessibleFromProvider +UiaLookupId +UiaNavigate +UiaNodeFromFocus +UiaNodeFromHandle +UiaNodeFromPoint +UiaNodeFromProvider +UiaNodeRelease +UiaPatternRelease +UiaProviderForNonClient +UiaProviderFromIAccessible +UiaRaiseActiveTextPositionChangedEvent +UiaRaiseAsyncContentLoadedEvent +UiaRaiseAutomationEvent +UiaRaiseAutomationPropertyChangedEvent +UiaRaiseChangesEvent +UiaRaiseNotificationEvent +UiaRaiseStructureChangedEvent +UiaRaiseTextEditTextChangedEvent +UiaRegisterProviderCallback +UiaRemoveEvent +UiaReturnRawElementProvider +UiaSetFocus +UiaTextRangeRelease +UpdateErrorLoggingCallback +ValuePattern_SetValue +VirtualizedItemPattern_Realize +WindowPattern_Close +WindowPattern_SetWindowVisualState +WindowPattern_WaitForInputIdle diff --git a/scene/2d/animated_sprite_2d.cpp b/scene/2d/animated_sprite_2d.cpp index 6eaf31d701e3a3..3577039c15f54a 100644 --- a/scene/2d/animated_sprite_2d.cpp +++ b/scene/2d/animated_sprite_2d.cpp @@ -166,6 +166,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_null() && frames->has_animation(autoplay)) { play(autoplay); diff --git a/scene/2d/node_2d.cpp b/scene/2d/node_2d.cpp index aae7eff7bd0b1f..48d57acbbec27b 100644 --- a/scene/2d/node_2d.cpp +++ b/scene/2d/node_2d.cpp @@ -427,6 +427,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/sprite_2d.cpp b/scene/2d/sprite_2d.cpp index 5745a592972184..534830fa563d1a 100644 --- a/scene/2d/sprite_2d.cpp +++ b/scene/2d/sprite_2d.cpp @@ -114,6 +114,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/2d/touch_screen_button.cpp b/scene/2d/touch_screen_button.cpp index 5ed7fadb2a91b6..ed630c16639331 100644 --- a/scene/2d/touch_screen_button.cpp +++ b/scene/2d/touch_screen_button.cpp @@ -113,8 +113,28 @@ 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_DEFAULT, callable_mp(this, &TouchScreenButton::_accessibility_action_click)); + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(ae, DisplayServer::AccessibilityActionVerb::ACTION_VERB_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/touch_screen_button.h b/scene/2d/touch_screen_button.h index 4467604e2b2a20..4ed033d9413146 100644 --- a/scene/2d/touch_screen_button.h +++ b/scene/2d/touch_screen_button.h @@ -75,6 +75,8 @@ class TouchScreenButton : public Node2D { bool _set(const StringName &p_name, const Variant &p_value); #endif // DISABLE_DEPRECATED + void _accessibility_action_click(const Variant &p_data); + public: #ifdef TOOLS_ENABLED virtual Rect2 _edit_get_rect() const override; diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp index 98a5134283a6be..931ac691746b84 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/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp index 0c2bd64e845f71..55de34484259e1 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 66b14dc9671808..d6f88f22968837 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -42,6 +42,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) { @@ -84,15 +85,63 @@ 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(SNAME("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_DEFAULT, callable_mp(this, &BaseButton::_accessibility_action_click)); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_DISABLED, status.disabled); + DisplayServer::get_singleton()->accessibility_update_set_flag(ae, DisplayServer::AccessibilityFlags::FLAG_HOVERED, status.hovering); + if (toggle_mode) { + DisplayServer::get_singleton()->accessibility_update_set_checked(ae, status.pressed); + if (status.pressed) { + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(ae, DisplayServer::AccessibilityActionVerb::ACTION_VERB_UNCHECK); + } else { + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(ae, DisplayServer::AccessibilityActionVerb::ACTION_VERB_CHECK); + } + } else { + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(ae, DisplayServer::AccessibilityActionVerb::ACTION_VERB_CLICK); + } + if (button_group.is_valid()) { + for (const BaseButton *btn : button_group->buttons) { + DisplayServer::get_singleton()->accessibility_update_add_related_radio_group(ae, btn->get_accessibility_element()); + } + } + } 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; @@ -166,6 +215,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)) { @@ -179,6 +229,7 @@ void BaseButton::on_action_event(Ref p_event) { if (mouse_button.is_valid()) { if (!has_point(mouse_button->get_position())) { status.hovering = false; + queue_accessibility_update(); } } status.press_attempt = false; @@ -208,6 +259,7 @@ void BaseButton::set_disabled(bool p_disabled) { status.press_attempt = false; status.pressing_inside = false; } + queue_accessibility_update(); queue_redraw(); } @@ -240,7 +292,7 @@ void BaseButton::set_pressed_no_signal(bool p_pressed) { return; } status.pressed = p_pressed; - + queue_accessibility_update(); queue_redraw(); } @@ -296,6 +348,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(); @@ -373,7 +426,7 @@ void BaseButton::shortcut_input(const Ref &p_event) { _toggled(status.pressed); _pressed(); - + queue_accessibility_update(); } else { _pressed(); } @@ -418,6 +471,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 a8d5cee44cade0..14f813600c0e53 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -87,6 +87,7 @@ class BaseButton : public Control { 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 ad3f6076618f8e..b871e47e17c8e2 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "button.h" +#include "dialogs.h" #include "core/string/translation.h" #include "scene/theme/theme_db.h" @@ -105,6 +106,19 @@ 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); + } + 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; @@ -495,6 +509,7 @@ void Button::_shape(Ref p_paragraph, String p_text) { } p_paragraph->add_string(p_text, font, font_size, language); p_paragraph->set_text_overrun_behavior(overrun_behavior); + queue_accessibility_update(); } void Button::set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior) { @@ -520,7 +535,6 @@ void Button::set_text(const String &p_text) { text = p_text; xl_text = atr(text); _shape(); - queue_redraw(); update_minimum_size(); } @@ -637,6 +651,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 af6696834ecb8f..1299be42020104 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -82,6 +82,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: { @@ -135,7 +146,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 87261816311c88..fa12aaf6fbc9f9 100644 --- a/scene/gui/check_box.h +++ b/scene/gui/check_box.h @@ -58,7 +58,7 @@ class CheckBox : public Button { 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 ab3b74a3c37a8b..16af8b09918639 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -86,6 +86,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 848a598ebb82ce..5ce6be5c9e0a66 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -50,6 +50,14 @@ List ColorPicker::recent_preset_cache; 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; @@ -256,8 +264,11 @@ 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")); slider_theme_modified = modes[current_mode]->apply_theme(); @@ -640,6 +651,7 @@ void ColorPicker::_update_color(bool p_update_sliders) { wheel->queue_redraw(); wheel_uv->queue_redraw(); updating = false; + queue_accessibility_update(); } void ColorPicker::_update_presets() { @@ -1833,10 +1845,12 @@ ColorPicker::ColorPicker() { btn_pick = memnew(Button); sample_hbc->add_child(btn_pick); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_SCREEN_CAPTURE)) { + btn_pick->set_accessibility_name(ETR("Pick a color from the screen.")); btn_pick->set_tooltip_text(ETR("Pick a color from the screen.")); btn_pick->connect(SNAME("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 a color from the application window.")); btn_pick->set_tooltip_text(ETR("Pick a color from the application window.")); btn_pick->connect(SNAME("pressed"), callable_mp(this, &ColorPicker::_pick_button_pressed_legacy)); } @@ -1851,6 +1865,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("Select a picker shape.")); btn_shape->set_tooltip_text(ETR("Select a picker shape.")); current_shape = SHAPE_HSV_RECTANGLE; @@ -1890,6 +1905,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("Select a picker mode.")); btn_mode->set_tooltip_text(ETR("Select a picker mode.")); current_mode = MODE_RGB; @@ -1932,7 +1948,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("Switch between hexadecimal and code values.")); + text_type->set_tooltip_text(ETR("Switch between hexadecimal and code values.")); if (Engine::get_singleton()->is_editor_hint()) { text_type->connect("pressed", callable_mp(this, &ColorPicker::_text_type_toggled)); } else { @@ -1944,6 +1961,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 named color")); 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("text_submitted", callable_mp(this, &ColorPicker::_html_submitted)); @@ -2054,6 +2072,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); } @@ -2093,6 +2112,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); @@ -2126,7 +2154,7 @@ void ColorPickerButton::set_pick_color(const Color &p_color) { if (picker) { picker->set_pick_color(p_color); } - + queue_accessibility_update(); queue_redraw(); } @@ -2206,6 +2234,13 @@ 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_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(); @@ -2257,6 +2292,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 fc334325b6746d..141581c22d6dcb 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 c6e66c95c69515..9a2b56f12311b4 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -171,6 +171,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_ENTER_TREE: { pending_sort = false; queue_sort(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index d78077316a7778..66665db9b60e1a 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -40,6 +40,7 @@ #include "core/string/translation.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" #include "scene/scene_string_names.h" @@ -244,6 +245,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; @@ -1529,6 +1551,7 @@ void Control::set_scale(const Vector2 &p_scale) { } queue_redraw(); _notify_transform(); + queue_accessibility_update(); } Vector2 Control::get_scale() const { @@ -1545,6 +1568,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) { @@ -1571,6 +1595,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 { @@ -1741,6 +1766,8 @@ void Control::_size_changed() { if (pos_changed) { _notify_transform(); } + + queue_accessibility_update(); } } @@ -1987,7 +2014,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(); @@ -2065,6 +2092,7 @@ static Control *_next_control(Control *p_from) { Control *Control::find_next_valid_focus() const { ERR_READ_THREAD_GUARD_V(nullptr); Control *from = const_cast(this); + bool ac_enabled = get_tree() && get_tree()->is_accessibility_enabled(); while (true) { // If the focus property is manually overwritten, attempt to use it. @@ -2114,10 +2142,10 @@ Control *Control::find_next_valid_focus() const { } if (next_child == from || next_child == this) { // No next control. - return (get_focus_mode() == FOCUS_ALL) ? next_child : nullptr; + return ((get_focus_mode() == FOCUS_ALL) || (ac_enabled && get_focus_mode() == FOCUS_ACCESSIBILITY)) ? next_child : nullptr; } if (next_child) { - if (next_child->get_focus_mode() == FOCUS_ALL) { + if ((next_child->get_focus_mode() == FOCUS_ALL) || (ac_enabled && next_child->get_focus_mode() == FOCUS_ACCESSIBILITY)) { return next_child; } from = next_child; @@ -2152,6 +2180,7 @@ static Control *_prev_control(Control *p_from) { Control *Control::find_prev_valid_focus() const { ERR_READ_THREAD_GUARD_V(nullptr); Control *from = const_cast(this); + bool ac_enabled = get_tree() && get_tree()->is_accessibility_enabled(); while (true) { // If the focus property is manually overwritten, attempt to use it. @@ -2195,10 +2224,10 @@ Control *Control::find_prev_valid_focus() const { } if (prev_child == from || prev_child == this) { // No prev control. - return (get_focus_mode() == FOCUS_ALL) ? prev_child : nullptr; + return ((get_focus_mode() == FOCUS_ALL) || (ac_enabled && get_focus_mode() == FOCUS_ACCESSIBILITY)) ? prev_child : nullptr; } - if (prev_child->get_focus_mode() == FOCUS_ALL) { + if ((prev_child->get_focus_mode() == FOCUS_ALL) || (ac_enabled && prev_child->get_focus_mode() == FOCUS_ACCESSIBILITY)) { return prev_child; } @@ -2329,8 +2358,9 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons } Control *c = Object::cast_to(p_at); + bool ac_enabled = get_tree() && get_tree()->is_accessibility_enabled(); - if (c && c != this && c->get_focus_mode() == FOCUS_ALL && c->is_visible_in_tree()) { + if (c && c != this && ((c->get_focus_mode() == FOCUS_ALL) || (ac_enabled && c->get_focus_mode() == FOCUS_ACCESSIBILITY)) && c->is_visible_in_tree()) { Point2 points[4]; Transform2D xform = c->get_global_transform(); @@ -3154,6 +3184,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) { @@ -3165,6 +3224,24 @@ void Control::_notification(int p_notification) { saving = false; } break; #endif + + 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)); + } break; + case NOTIFICATION_POSTINITIALIZE: { data.initialized = true; @@ -3604,6 +3681,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_CONSTANT(NOTIFICATION_RESIZED); BIND_CONSTANT(NOTIFICATION_MOUSE_ENTER); diff --git a/scene/gui/control.h b/scene/gui/control.h index c784d4330dac23..6baf57a8d725f9 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -66,7 +66,8 @@ class Control : public CanvasItem { enum FocusMode { FOCUS_NONE, FOCUS_CLICK, - FOCUS_ALL + FOCUS_ALL, + FOCUS_ACCESSIBILITY, }; enum SizeFlags { @@ -352,6 +353,12 @@ class Control : public CanvasItem { static void _bind_compatibility_methods(); #endif + 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) @@ -415,6 +422,7 @@ class Control : public CanvasItem { 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 diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index 4d2080dda2b488..56ae12442a1583 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -54,6 +54,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 97a2917dc1f419..1de420b4df56f7 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -1398,11 +1398,14 @@ FileDialog::FileDialog() { dir_prev = memnew(Button); dir_prev->set_theme_type_variation("FlatButton"); + dir_prev->set_accessibility_name(ETR("Go to previous folder.")); dir_prev->set_tooltip_text(ETR("Go to previous folder.")); dir_next = memnew(Button); + dir_next->set_accessibility_name(ETR("Go to next folder.")); dir_next->set_theme_type_variation("FlatButton"); dir_next->set_tooltip_text(ETR("Go to next folder.")); dir_up = memnew(Button); + dir_up->set_accessibility_name(ETR("Go to parent folder.")); dir_up->set_theme_type_variation("FlatButton"); dir_up->set_tooltip_text(ETR("Go to parent folder.")); hbc->add_child(dir_prev); @@ -1422,12 +1425,14 @@ FileDialog::FileDialog() { hbc->add_child(drives); dir = memnew(LineEdit); + dir->set_accessibility_name(ETR("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("FlatButton"); + refresh->set_accessibility_name(ETR("Refresh files.")); refresh->set_tooltip_text(ETR("Refresh files.")); refresh->connect("pressed", callable_mp(this, &FileDialog::update_file_list)); hbc->add_child(refresh); @@ -1436,6 +1441,7 @@ FileDialog::FileDialog() { show_hidden->set_theme_type_variation("FlatButton"); show_hidden->set_toggle_mode(true); show_hidden->set_pressed(is_showing_hidden_files()); + show_hidden->set_accessibility_name(ETR("Toggle the visibility of hidden files.")); show_hidden->set_tooltip_text(ETR("Toggle the visibility of hidden files.")); show_hidden->connect("toggled", callable_mp(this, &FileDialog::set_show_hidden_files)); hbc->add_child(show_hidden); @@ -1451,6 +1457,7 @@ FileDialog::FileDialog() { vbox->add_child(hbc); tree = memnew(Tree); + tree->set_accessibility_name(ETR("Directories & Files:")); tree->set_hide_root(true); vbox->add_margin_child(ETR("Directories & Files:"), tree, true); @@ -1464,6 +1471,7 @@ FileDialog::FileDialog() { file_box = memnew(HBoxContainer); file_box->add_child(memnew(Label(ETR("File:")))); file = memnew(LineEdit); + file->set_accessibility_name(ETR("File:")); 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 646e45b27aecb2..e1b3011596d5cb 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -259,6 +259,14 @@ PackedStringArray GraphEdit::get_configuration_warnings() const { return warnings; } +PackedStringArray GraphEdit::get_accessibility_configuration_warnings() const { + PackedStringArray warnings = Control::get_accessibility_configuration_warnings(); + + warnings.push_back(RTR("GraphEdit and GraphNode currently are not accessible.")); + + return warnings; +} + Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) { return OK; diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index eeda9ae20026b7..d42d5b3e471d24 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -391,6 +391,7 @@ class GraphEdit : public Control { virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override; PackedStringArray get_configuration_warnings() const override; + PackedStringArray get_accessibility_configuration_warnings() const override; // This method has to be public (for undo redo). // TODO: Find a better way to do this. diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index 8376ef48b67a99..7dd85970d5f9b7 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -64,7 +64,7 @@ int ItemList::add_item(const String &p_item, const Ref &p_texture, bo int item_id = items.size() - 1; _shape_text(items.size() - 1); - + queue_accessibility_update(); queue_redraw(); shape_changed = true; notify_property_list_changed(); @@ -78,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(); @@ -97,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_text); _shape_text(p_idx); + queue_accessibility_update(); queue_redraw(); shape_changed = true; } @@ -115,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(); } } @@ -132,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(); } } @@ -146,7 +150,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 { @@ -165,6 +173,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; } @@ -331,6 +340,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 { @@ -349,6 +360,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(); } @@ -386,7 +399,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; @@ -394,8 +410,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(); } @@ -408,6 +426,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(); } @@ -417,9 +437,13 @@ void ItemList::deselect_all() { } for (int i = 0; i < items.size(); i++) { - items.write[i].selected = false; + if (items.write[i].selected != false) { + items.write[i].selected = false; + items.write[i].accessibility_item_dirty = true; + } } current = -1; + queue_accessibility_update(); queue_redraw(); } @@ -440,6 +464,7 @@ void ItemList::set_current(int p_current) { select(p_current, true); } else { current = p_current; + queue_accessibility_update(); queue_redraw(); } } @@ -460,6 +485,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(); @@ -472,7 +498,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(); @@ -485,10 +521,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; @@ -496,9 +537,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; @@ -548,6 +596,7 @@ void ItemList::set_max_text_lines(int p_lines) { } } shape_changed = true; + queue_accessibility_update(); queue_redraw(); } } @@ -578,6 +627,7 @@ void ItemList::set_select_mode(SelectMode p_mode) { } select_mode = p_mode; + queue_accessibility_update(); queue_redraw(); } @@ -665,7 +715,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(); } } @@ -995,8 +1047,114 @@ 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->set_value(pos.y); +} + +void ItemList::_accessibility_action_scroll_up(const Variant &p_data) { + scroll_bar->set_value(scroll_bar->get_value() - scroll_bar->get_page() / 4); +} + +void ItemList::_accessibility_action_scroll_down(const Variant &p_data) { + scroll_bar->set_value(scroll_bar->get_value() + scroll_bar->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 = scroll_bar->get_value(); + int to = from + scroll_bar->get_page(); + + if (r.position.y < from) { + scroll_bar->set_value(r.position.y); + } else if (r.position.y + r.size.y > to) { + scroll_bar->set_value(r.position.y + r.size.y - (to - from)); + } +} + +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); + 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_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(0, -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_bar->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_ITEM); + 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.text); + DisplayServer::get_singleton()->accessibility_update_set_flag(item.accessibility_item_element, DisplayServer::AccessibilityFlags::FLAG_HOVERED, i == hovered); + 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(); @@ -1008,6 +1166,7 @@ void ItemList::_notification(int p_what) { _shape_text(i); } shape_changed = true; + queue_accessibility_update(); queue_redraw(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { @@ -1397,6 +1556,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 - scroll_bar_minwidth; @@ -1486,7 +1647,9 @@ void ItemList::_scroll_changed(double) { void ItemList::_mouse_exited() { if (hovered > -1) { + prev_hovered = hovered; hovered = -1; + queue_accessibility_update(); queue_redraw(); } } @@ -1562,6 +1725,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; @@ -1666,6 +1830,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 4c035ee4e64e9f..1886112c4fea14 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -52,6 +52,9 @@ class ItemList : public Control { private: struct Item { + mutable RID accessibility_item_element; + mutable bool accessibility_item_dirty = true; + Ref icon; bool icon_transposed = false; Rect2i icon_region; @@ -86,12 +89,14 @@ class ItemList : public Control { 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; @@ -167,7 +172,16 @@ class ItemList : public Control { 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_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 42b4e56b484bcb..abaada8e5fbb51 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -136,6 +136,7 @@ void Label::_shape() { dirty = false; font_dirty = false; lines_dirty = true; + queue_accessibility_update(); } if (lines_dirty) { @@ -380,6 +381,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) { @@ -438,7 +448,11 @@ void Label::_notification(int p_what) { bool rtl = (TS->shaped_text_get_inferred_direction(text_rid) == TextServer::DIRECTION_RTL); 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())); + } float total_h = 0.0; int lines_visible = 0; @@ -839,7 +853,7 @@ void Label::set_horizontal_alignment(HorizontalAlignment p_alignment) { lines_dirty = true; // Reshape lines. } horizontal_alignment = p_alignment; - + queue_accessibility_update(); queue_redraw(); } @@ -1187,6 +1201,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_FONT, Label, font); @@ -1203,6 +1218,7 @@ void Label::_bind_methods() { Label::Label(const String &p_text) { text_rid = TS->create_shaped_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 e0ebca944ab31a..519946499c3b63 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -79,6 +79,7 @@ class Label : public Control { 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 ddfe202c13cc24..59c9c4a739452a 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -321,6 +321,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(); @@ -335,6 +336,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; } } @@ -767,6 +769,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 @@ -781,6 +810,95 @@ 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, 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(); @@ -940,6 +1058,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); @@ -1683,6 +1802,8 @@ void LineEdit::set_caret_column(int p_column) { caret_column = p_column; + queue_accessibility_update(); + // Fit to window. if (!is_inside_tree()) { @@ -1844,6 +1965,7 @@ void LineEdit::deselect() { selection.enabled = false; selection.creating = false; selection.double_click = false; + queue_accessibility_update(); queue_redraw(); } @@ -1902,6 +2024,7 @@ void LineEdit::selection_fill_at_caret() { } selection.enabled = (selection.begin != selection.end); + queue_accessibility_update(); } void LineEdit::select_all() { @@ -1916,6 +2039,7 @@ void LineEdit::select_all() { selection.begin = 0; selection.end = text.length(); selection.enabled = true; + queue_accessibility_update(); queue_redraw(); } @@ -1928,6 +2052,7 @@ void LineEdit::set_editable(bool p_editable) { _validate_caret_can_draw(); update_minimum_size(); + queue_accessibility_update(); queue_redraw(); } @@ -1997,6 +2122,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(); } @@ -2354,6 +2480,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 993bc727e481d1..71028aaba4a858 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -102,6 +102,7 @@ class LineEdit : public Control { Point2 ime_selection; RID text_rid; + RID accessibility_text_root_element; float full_width = 0.0; bool selecting_enabled = true; @@ -256,6 +257,11 @@ class LineEdit : public Control { 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 set_horizontal_alignment(HorizontalAlignment p_alignment); HorizontalAlignment get_horizontal_alignment() const; diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index 1142ba37f5648e..285efb3171fca2 100644 --- a/scene/gui/link_button.cpp +++ b/scene/gui/link_button.cpp @@ -148,6 +148,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(); @@ -289,7 +300,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 2ce12794a7888c..9ced2accf91cf9 100644 --- a/scene/gui/menu_bar.cpp +++ b/scene/gui/menu_bar.cpp @@ -86,6 +86,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; @@ -273,6 +282,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(); @@ -405,6 +420,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; } @@ -921,6 +940,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 e83d9c7c1bdb4b..348c0689f5759f 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -127,6 +127,15 @@ 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); + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(ae, DisplayServer::AccessibilityActionVerb::ACTION_VERB_OPEN); + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { popup->set_layout_direction((Window::LayoutDirection)get_layout_direction()); } break; @@ -209,7 +218,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 509c6aca99af4e..cf58a6520a52e5 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -75,6 +75,15 @@ 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); + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(ae, DisplayServer::AccessibilityActionVerb::ACTION_VERB_OPEN); + } 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 8096191933dbd5..9f175ea34fee4c 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_menu.cpp b/scene/gui/popup_menu.cpp index 9b991972be85e4..25d2166cdfd3db 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -473,9 +473,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"), i); scroll_to_item(i); + queue_accessibility_update(); control->queue_redraw(); set_input_as_handled(); match_found = true; @@ -487,9 +489,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"), i); scroll_to_item(i); + queue_accessibility_update(); control->queue_redraw(); set_input_as_handled(); break; @@ -511,9 +515,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"), i); scroll_to_item(i); + queue_accessibility_update(); control->queue_redraw(); set_input_as_handled(); match_found = true; @@ -525,9 +531,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"), i); scroll_to_item(i); + queue_accessibility_update(); control->queue_redraw(); set_input_as_handled(); break; @@ -690,9 +698,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"), i); scroll_to_item(i); + queue_accessibility_update(); control->queue_redraw(); set_input_as_handled(); break; @@ -707,6 +717,7 @@ void PopupMenu::_mouse_over_update(const Point2 &p_over) { if (id < 0) { mouse_over = -1; + queue_accessibility_update(); control->queue_redraw(); return; } @@ -718,6 +729,7 @@ void PopupMenu::_mouse_over_update(const Point2 &p_over) { if (over != mouse_over) { mouse_over = over; + queue_accessibility_update(); control->queue_redraw(); } } @@ -1005,8 +1017,101 @@ void PopupMenu::remove_child_notify(Node *p_child) { _menu_changed(); } +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_LIST_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_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_flag(item.accessibility_item_element, DisplayServer::AccessibilityFlags::FLAG_HOVERED, 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) { @@ -1022,12 +1127,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 NOTIFICATION_THEME_CHANGED: { scroll_container->add_theme_style_override("panel", theme_cache.panel_style); @@ -1047,7 +1146,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(); @@ -1062,6 +1163,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; @@ -1165,7 +1267,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(); } @@ -1210,6 +1314,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(); @@ -1233,6 +1338,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(); @@ -1256,6 +1362,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(); @@ -1281,6 +1388,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(); @@ -1304,6 +1412,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(); @@ -1329,6 +1438,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(); @@ -1354,6 +1464,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(); @@ -1391,6 +1502,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(); @@ -1420,6 +1532,7 @@ void PopupMenu::add_icon_shortcut(const Ref &p_icon, const Refqueue_redraw(); child_controls_changed(); @@ -1449,6 +1562,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(); @@ -1480,6 +1594,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(); @@ -1509,6 +1624,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(); @@ -1540,6 +1656,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(); @@ -1566,6 +1683,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); @@ -1579,6 +1697,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(); @@ -1602,13 +1721,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_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(); } @@ -1619,9 +1741,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(); } } @@ -1634,6 +1761,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(); } } @@ -1701,11 +1832,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(); @@ -1744,11 +1877,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(); @@ -1780,11 +1915,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(); @@ -1848,11 +1985,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(); @@ -1984,6 +2123,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(); } @@ -2004,11 +2146,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(); } @@ -2025,11 +2169,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(); } @@ -2045,11 +2191,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(); } @@ -2149,11 +2297,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(); } @@ -2198,11 +2348,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(); } @@ -2236,11 +2388,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(); } @@ -2262,6 +2415,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(); + } } } @@ -2432,6 +2589,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); } @@ -2451,6 +2612,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); @@ -2466,7 +2628,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); } @@ -2490,7 +2656,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(); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index 832c1bcc8be53d..0c405da415efe0 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -43,6 +43,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); @@ -93,6 +96,7 @@ class PopupMenu : public Popup { Item(bool p_dummy) {} }; + RID accessibility_scroll_element; static inline PropertyListHelper base_property_helper; PropertyListHelper property_helper; @@ -117,6 +121,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; @@ -239,6 +244,8 @@ class PopupMenu : public Popup { // 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 90ce01e38377f1..18d73861c41a21 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 236dfcc864a132..641520fed94ac3 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(SNAME("value_changed"), shared->val); 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_set_num_jump(ae, ???); + 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; @@ -91,6 +130,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 +183,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 +198,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 +209,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 +223,8 @@ void Range::set_page(double p_page) { set_value(shared->val); shared->emit_changed("page"); + + queue_accessibility_update(); } double Range::get_value() const { diff --git a/scene/gui/range.h b/scene/gui/range.h index b1c2446deda36a..67405a1ae4849b 100644 --- a/scene/gui/range.h +++ b/scene/gui/range.h @@ -65,9 +65,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 97739c4b7900f0..6765ac0fe365b6 100644 --- a/scene/gui/rich_text_label.compat.inc +++ b/scene/gui/rich_text_label.compat.inc @@ -35,7 +35,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_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) { @@ -45,6 +53,8 @@ bool RichTextLabel::_remove_paragraph_bind_compat_91098(int p_paragraph) { void RichTextLabel::_bind_compatibility_methods() { 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_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 19b02f33c6af6f..f11aa857fcc6f1 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -61,6 +61,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(); @@ -90,7 +93,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; } @@ -104,41 +107,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; } @@ -286,6 +286,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.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); @@ -475,6 +476,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; @@ -494,6 +496,7 @@ float RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref autowrap_flags = autowrap_flags | TextServer::BREAK_TRIM_EDGE_SPACES; // 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)); @@ -526,8 +529,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; @@ -1779,8 +1783,372 @@ 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) { + 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; + + DisplayServer::get_singleton()->accessibility_update_set_bounds(line_ae, Rect2(p_ofs, Size2(p_width, l.text_buf->get_size().y + l.text_buf->get_line_count() * theme_cache.line_separation))); + + 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); + DisplayServer::get_singleton()->accessibility_update_set_bounds(l.accessibility_text_element, Rect2(p_ofs + off, l.text_buf->get_size())); + + 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; + } + + 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); + DisplayServer::get_singleton()->accessibility_update_set_bounds(img_ae, Rect2(p_ofs + rect.position + off + Vector2(0, TS->shaped_text_get_ascent(rid)), rect.size)); + + 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); + int h_separation = theme_cache.table_h_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); + DisplayServer::get_singleton()->accessibility_update_set_bounds(table_ae, Rect2(p_ofs + rect.position + off + Vector2(0, TS->shaped_text_get_ascent(rid)), rect.size)); + + 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); + DisplayServer::get_singleton()->accessibility_update_set_bounds(row_ae, Rect2(p_ofs + rect.position + off + row_off, Size2(rect.size.x, table->rows[j]))); + 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++) { + Vector2 coff = frame->lines[j].offset; + if (rtl) { + coff.x = rect.size.width - table->columns[col].width - coff.x; + } + + RID cell_ae = DisplayServer::get_singleton()->accessibility_create_sub_element(row_aes[row], DisplayServer::AccessibilityRole::ROLE_CELL); + + DisplayServer::get_singleton()->accessibility_update_set_table_cell_position(cell_ae, row, col); + DisplayServer::get_singleton()->accessibility_update_set_bounds(cell_ae, Rect2(p_ofs + rect.position + off + coff - frame->padding.position + Vector2(0, TS->shaped_text_get_ascent(rid)), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row]))); + + _accessibility_update_line(cell_ae, frame, j, p_ofs + rect.position + off + Vector2(0, frame->lines[j].offset.y), rect.size.x); + } + 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); + + int to_line = main->first_invalid_line.load(); + int from_line = 0; + + Point2 ofs = text_rect.get_position(); + 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); + ofs.y += main->lines[from_line].text_buf->get_size().y + main->lines[from_line].text_buf->get_line_count() * theme_cache.line_separation; + from_line++; + } + } break; + case NOTIFICATION_MOUSE_EXIT: { if (meta_hovering) { meta_hovering = nullptr; @@ -1793,12 +2161,16 @@ void RichTextLabel::_notification(int p_what) { case NOTIFICATION_RESIZED: { _stop_thread(); main->first_resized_line.store(0); //invalidate ALL + _invalidate_accessibility(); + queue_accessibility_update(); queue_redraw(); } break; case NOTIFICATION_THEME_CHANGED: { _stop_thread(); main->first_invalid_font_line.store(0); //invalidate ALL + _invalidate_accessibility(); + queue_accessibility_update(); queue_redraw(); } break; @@ -1809,12 +2181,27 @@ void RichTextLabel::_notification(int p_what) { } main->first_invalid_line.store(0); //invalidate ALL + _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: @@ -2010,6 +2397,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; } @@ -2074,6 +2462,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; @@ -2087,28 +2476,109 @@ 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(vscroll->get_value() + 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(); + 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(); + handled = true; + } + } if (is_shortcut_keys_enabled()) { if (k->is_action("ui_text_select_all", true)) { select_all(); @@ -2176,6 +2646,7 @@ void RichTextLabel::gui_input(const Ref &p_event) { } selection.active = true; + queue_accessibility_update(); queue_redraw(); } @@ -2269,7 +2740,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()) { @@ -2282,7 +2753,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()) { @@ -2295,7 +2766,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()) { @@ -2308,7 +2779,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()) { @@ -2321,7 +2792,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()) { @@ -2762,6 +3233,7 @@ void RichTextLabel::_thread_end() { vscroll->hide(); } if (is_visible_in_tree()) { + queue_accessibility_update(); queue_redraw(); } } @@ -2880,6 +3352,7 @@ bool RichTextLabel::_validate_line_caches() { if (!scroll_visible) { vscroll->hide(); } + queue_accessibility_update(); return true; } @@ -2902,6 +3375,7 @@ bool RichTextLabel::_validate_line_caches() { if (!scroll_visible) { vscroll->hide(); } + queue_accessibility_update(); return true; } validating.store(false); @@ -2912,6 +3386,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); @@ -2920,6 +3395,7 @@ bool RichTextLabel::_validate_line_caches() { if (!scroll_visible) { vscroll->hide(); } + queue_accessibility_update(); queue_redraw(); return true; } @@ -2999,6 +3475,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(); } } @@ -3113,6 +3590,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(); } @@ -3155,7 +3633,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); @@ -3190,10 +3668,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) { @@ -3432,7 +3912,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; } @@ -3505,33 +3991,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) { @@ -3692,7 +4178,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); @@ -3701,6 +4187,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; @@ -3856,6 +4343,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); @@ -3918,6 +4416,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 { @@ -3974,6 +4473,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; @@ -3988,6 +4492,8 @@ void RichTextLabel::clear() { if (fit_content) { update_minimum_size(); } + queue_accessibility_update(); + update_configuration_warnings(); } void RichTextLabel::set_tab_size(int p_spaces) { @@ -3999,6 +4505,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(); } @@ -4043,6 +4551,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) { @@ -4226,9 +4735,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); @@ -4236,15 +4745,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=")) { @@ -4290,7 +4799,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") { @@ -4692,6 +5207,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { } String image = p_bbcode.substr(brk_end + 1, end - brk_end - 1); + String alt_text; Ref texture = ResourceLoader::load(image, "Texture2D"); if (texture.is_valid()) { @@ -4713,6 +5229,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; @@ -4754,7 +5275,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; @@ -4787,7 +5308,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) { @@ -4814,7 +5335,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); @@ -4839,7 +5360,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { _normalize_subtags(subtag); 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) { @@ -4863,7 +5384,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; } } else if (subtag_a[0] == "size" || subtag_a[0] == "s") { fnt_size = subtag_a[1].to_int(); @@ -4923,7 +5444,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); @@ -5143,6 +5664,7 @@ void RichTextLabel::scroll_to_selection() { it = it->parent_frame; } vscroll->set_value(line_offset); + queue_accessibility_update(); } } @@ -5156,6 +5678,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 { @@ -5174,6 +5697,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(); @@ -5188,11 +5712,13 @@ void RichTextLabel::scroll_to_line(int p_line) { line_offset += main->lines[i].text_buf->get_line_size(j).y + 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) { @@ -5255,10 +5781,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) { @@ -5372,6 +5899,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; } @@ -5383,6 +5911,7 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p if (p_string.size() == 0) { selection.active = false; + queue_accessibility_update(); return false; } @@ -5560,6 +6089,7 @@ String RichTextLabel::get_selected_text() const { void RichTextLabel::deselect() { selection.active = false; + queue_accessibility_update(); queue_redraw(); } @@ -5616,6 +6146,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(); } @@ -5721,6 +6252,7 @@ void RichTextLabel::set_text_direction(Control::TextDirection p_text_direction) if (text_direction != p_text_direction) { text_direction = p_text_direction; main->first_invalid_line.store(0); //invalidate ALL + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -5732,6 +6264,7 @@ void RichTextLabel::set_structured_text_bidi_override(TextServer::StructuredText st_parser = p_parser; main->first_invalid_line.store(0); //invalidate ALL + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -5747,6 +6280,7 @@ void RichTextLabel::set_structured_text_bidi_override_options(Array p_args) { st_args = p_args; main->first_invalid_line.store(0); //invalidate ALL + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -5766,6 +6300,7 @@ void RichTextLabel::set_language(const String &p_language) { language = p_language; main->first_invalid_line.store(0); //invalidate ALL + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -5780,7 +6315,8 @@ void RichTextLabel::set_autowrap_mode(TextServer::AutowrapMode p_mode) { _stop_thread(); autowrap_mode = p_mode; - main->first_invalid_line = 0; //invalidate ALL + main->first_invalid_line.store(0); //invalidate ALL + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -5807,6 +6343,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. + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -5884,7 +6421,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)); @@ -5907,9 +6444,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"), &RichTextLabel::set_table_column_expand, DEFVAL(1)); + 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); @@ -6154,6 +6692,7 @@ void RichTextLabel::set_visible_characters_behavior(TextServer::VisibleCharacter visible_chars_behavior = p_behavior; main->first_invalid_line.store(0); //invalidate ALL + _invalidate_accessibility(); _validate_line_caches(); queue_redraw(); } @@ -6174,6 +6713,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 + _invalidate_accessibility(); _validate_line_caches(); } queue_redraw(); @@ -6441,6 +6981,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 189ee1da6e6b11..504550f21b664d 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -106,12 +106,12 @@ class RichTextLabel : public Control { }; 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 { @@ -134,7 +134,10 @@ class RichTextLabel : public Control { #ifndef DISABLE_DEPRECATED 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_76829(int p_columns, InlineAlignment p_alignment, int p_align_to_row); bool _remove_paragraph_bind_compat_91098(int p_paragraph); + static void _bind_compatibility_methods(); #endif @@ -145,6 +148,11 @@ class RichTextLabel : public Control { Item *from = nullptr; 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; @@ -153,7 +161,16 @@ class RichTextLabel : public Control { 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; @@ -171,6 +188,8 @@ class RichTextLabel : public Control { int line = 0; RID rid; + RID accessibility_item_element; + void _clear_children() { RichTextLabel *owner_rtl = Object::cast_to(ObjectDB::get_instance(owner)); while (subitems.size()) { @@ -229,6 +248,7 @@ class RichTextLabel : public Control { struct ItemImage : public Item { Ref image; + String alt_text; InlineAlignment inline_align = INLINE_ALIGNMENT_CENTER; bool pad = false; bool size_in_percent = false; @@ -250,7 +270,7 @@ class RichTextLabel : public Control { }; struct ItemFont : public Item { - DefaultFont def_font = CUSTOM_FONT; + DefaultFont def_font = RTL_CUSTOM_FONT; Ref font; bool variation = false; bool def_size = false; @@ -331,6 +351,7 @@ class RichTextLabel : public Control { struct ItemTable : public Item { struct Column { + String name; bool expand = false; int expand_ratio = 0; int min_width = 0; @@ -341,6 +362,7 @@ class RichTextLabel : public Control { LocalVector columns; LocalVector rows; LocalVector rows_baseline; + String name; int align_to_row = -1; int total_width = 0; @@ -485,6 +507,7 @@ class RichTextLabel : public Control { Array custom_effects; + void _invalidate_accessibility(); void _invalidate_current_line(ItemFrame *p_frame); void _thread_function(void *p_userdata); @@ -531,6 +554,11 @@ class RichTextLabel : public Control { 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; @@ -557,6 +585,7 @@ class RichTextLabel : public Control { 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, 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, 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); String _roman(int p_num, bool p_capitalize) const; String _letters(int p_num, bool p_capitalize) const; @@ -614,6 +643,15 @@ class RichTextLabel : public Control { String text; void _apply_translation(); + 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 { @@ -660,9 +698,12 @@ class RichTextLabel : public Control { } 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); @@ -688,7 +729,7 @@ class RichTextLabel : public Control { 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); 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); @@ -700,6 +741,7 @@ class RichTextLabel : public Control { 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); + 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 b35c4e9308462e..80058e86d098ba 100644 --- a/scene/gui/scroll_bar.cpp +++ b/scene/gui/scroll_bar.cpp @@ -227,6 +227,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(); diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index 4f49f60d700329..b4eb56333e4d3a 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -338,8 +338,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 02146618cddd1d..80c0c3116edc80 100644 --- a/scene/gui/scroll_container.h +++ b/scene/gui/scroll_container.h @@ -88,6 +88,12 @@ class ScrollContainer : public Container { 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 f984d781d3a417..6cba87a68814db 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -219,7 +219,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 70acaf7adffa09..46b628bd852d39 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 += last_w; @@ -401,7 +445,7 @@ void SpinBox::_bind_methods() { } SpinBox::SpinBox() { - line_edit = memnew(LineEdit); + line_edit = memnew(SpinBoxLineEdit); add_child(line_edit, false, INTERNAL_MODE_FRONT); line_edit->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index 4d49626d71d86b..979d95bc013295 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -35,10 +35,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; int last_w = 0; bool update_on_text_changed = false; diff --git a/scene/gui/split_container.cpp b/scene/gui/split_container.cpp index 5f4586a6d532c7..e6fa60528cb718 100644 --- a/scene/gui/split_container.cpp +++ b/scene/gui/split_container.cpp @@ -92,8 +92,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_containable_child(0) || !sc->get_containable_child(1) || sc->dragger_visibility != SplitContainer::DRAGGER_VISIBLE) { + return; + } + sc->split_offset -= 10; + sc->_compute_middle_sep(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_containable_child(0) || !sc->get_containable_child(1) || sc->dragger_visibility != SplitContainer::DRAGGER_VISIBLE) { + return; + } + sc->split_offset += 10; + sc->_compute_middle_sep(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_containable_child(0) || !sc->get_containable_child(1) || sc->dragger_visibility != SplitContainer::DRAGGER_VISIBLE) { + return; + } + sc->split_offset = p_data; + sc->_compute_middle_sep(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_containable_child(0) || !sc->get_containable_child(1) || sc->dragger_visibility != SplitContainer::DRAGGER_VISIBLE) { + return; + } + sc->_compute_middle_sep(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()); @@ -122,6 +173,10 @@ void SplitContainerDragger::_notification(int p_what) { } } +SplitContainerDragger::SplitContainerDragger() { + set_focus_mode(FOCUS_ACCESSIBILITY); +} + Control *SplitContainer::get_containable_child(int p_idx) const { int idx = 0; diff --git a/scene/gui/split_container.h b/scene/gui/split_container.h index 0f45ef166d08a3..1acb45047cb305 100644 --- a/scene/gui/split_container.h +++ b/scene/gui/split_container.h @@ -40,6 +40,10 @@ class SplitContainerDragger : public Control { 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; @@ -48,6 +52,8 @@ class SplitContainerDragger : public Control { 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 0e130d60af7c1d..7b1c8e2d257ff1 100644 --- a/scene/gui/tab_bar.cpp +++ b/scene/gui/tab_bar.cpp @@ -111,6 +111,24 @@ Size2 TabBar::get_minimum_size() const { void TabBar::gui_input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); + Ref k = p_event; + + if (get_tree()->is_accessibility_enabled() && k.is_valid()) { + if (k->is_pressed()) { + if (k->is_action("ui_left", true)) { + if (current > 0) { + set_current_tab(current - 1); + accept_event(); + } + } else if (k->is_action("ui_right", true)) { + if (current < get_tab_count() - 1) { + set_current_tab(current + 1); + accept_event(); + } + } + } + } + Ref mm = p_event; if (mm.is_valid()) { @@ -378,6 +396,42 @@ 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_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_name(item.accessibility_item_element, item.text); + 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_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; @@ -629,6 +683,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) { @@ -659,6 +722,7 @@ void TabBar::set_tab_count(int p_count) { initialized = true; } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); notify_property_list_changed(); @@ -694,6 +758,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); @@ -761,6 +826,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(); } @@ -786,7 +852,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(); } } @@ -801,12 +869,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(); } @@ -877,6 +947,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(); } @@ -900,6 +971,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(); } @@ -1061,6 +1133,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; @@ -1115,6 +1189,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(); @@ -1133,12 +1208,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(); @@ -1146,6 +1228,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; @@ -1195,6 +1282,7 @@ void TabBar::remove_tab(int p_idx) { } } + queue_accessibility_update(); queue_redraw(); update_minimum_size(); notify_property_list_changed(); @@ -1369,6 +1457,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(); } @@ -1464,6 +1553,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(); } @@ -1875,6 +1965,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("mouse_exited", callable_mp(this, &TabBar::_on_mouse_exited)); diff --git a/scene/gui/tab_bar.h b/scene/gui/tab_bar.h index 52f1da5ec87116..065af7e0f1d7e0 100644 --- a/scene/gui/tab_bar.h +++ b/scene/gui/tab_bar.h @@ -55,6 +55,9 @@ class TabBar : public Control { private: struct Tab { + mutable RID accessibility_item_element; + mutable bool accessibility_item_dirty = true; + String text; String tooltip; diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 49cfa8a0300fe6..e5ff07ca6b9fba 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -187,15 +187,41 @@ void TextEdit::Text::_calculate_max_line_width() { max_width = line_width; } +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), line_height, p_line); + l.accessibility_text_root_element.push_back(rid); + } + } +} + void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_changed, const String &p_ime_text, const Array &p_bidi_override) { ERR_FAIL_INDEX(p_line, text.size()); + Line &l = text.write[p_line]; + for (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? } if (p_text_changed) { - text.write[p_line].data_buf->clear(); + l.data_buf->clear(); } BitField flags = brk_flags; @@ -203,28 +229,28 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan flags.set_flag(TextServer::BREAK_TRIM_INDENT); } - text.write[p_line].data_buf->set_width(width); - text.write[p_line].data_buf->set_direction((TextServer::Direction)direction); - text.write[p_line].data_buf->set_break_flags(flags); - text.write[p_line].data_buf->set_preserve_control(draw_control_chars); + l.data_buf->set_width(width); + l.data_buf->set_direction((TextServer::Direction)direction); + l.data_buf->set_break_flags(flags); + l.data_buf->set_preserve_control(draw_control_chars); if (p_ime_text.length() > 0) { if (p_text_changed) { - text.write[p_line].data_buf->add_string(p_ime_text, font, font_size, language); + l.data_buf->add_string(p_ime_text, font, font_size, language); } if (!p_bidi_override.is_empty()) { - TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), p_bidi_override); + TS->shaped_text_set_bidi_override(l.data_buf->get_rid(), p_bidi_override); } } else { if (p_text_changed) { - text.write[p_line].data_buf->add_string(text[p_line].data, font, font_size, language); + l.data_buf->add_string(l.data, font, font_size, language); } - if (!text[p_line].bidi_override.is_empty()) { - TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), text[p_line].bidi_override); + if (!l.bidi_override.is_empty()) { + TS->shaped_text_set_bidi_override(l.data_buf->get_rid(), l.bidi_override); } } if (!p_text_changed) { - RID r = text.write[p_line].data_buf->get_rid(); + RID r = l.data_buf->get_rid(); int spans = TS->shaped_get_span_count(r); for (int i = 0; i < spans; i++) { TS->shaped_set_span_update_font(r, i, font->get_rids(), font_size, font->get_opentype_features()); @@ -235,17 +261,17 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan if (tab_size > 0) { Vector tabs; tabs.push_back(font->get_char_size(' ', font_size).width * tab_size); - text.write[p_line].data_buf->tab_align(tabs); + l.data_buf->tab_align(tabs); } // Update height. - const int old_height = text.write[p_line].height; + const int old_height = l.height; const int wrap_amount = get_line_wrap_amount(p_line); int height = font_height; for (int i = 0; i <= wrap_amount; i++) { - height = MAX(height, text[p_line].data_buf->get_line_size(i).y); + height = MAX(height, l.data_buf->get_line_size(i).y); } - text.write[p_line].height = height; + l.height = height; // If this line has shrunk, this may no longer the tallest line. if (old_height == line_height && height < line_height) { @@ -255,9 +281,9 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan } // Update width. - const int old_width = text.write[p_line].width; + const int old_width = l.width; int line_width = get_line_width(p_line); - text.write[p_line].width = line_width; + l.width = line_width; // If this line has shrunk, this may no longer the longest line. if (old_width == max_width && line_width < max_width) { @@ -269,20 +295,21 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan void TextEdit::Text::invalidate_all_lines() { for (int i = 0; i < text.size(); i++) { + Line &l = text.write[i]; BitField flags = brk_flags; if (indent_wrapped_lines) { flags.set_flag(TextServer::BREAK_TRIM_INDENT); } - text.write[i].data_buf->set_width(width); - text.write[i].data_buf->set_break_flags(flags); + l.data_buf->set_width(width); + l.data_buf->set_break_flags(flags); if (tab_size_dirty) { if (tab_size > 0) { Vector tabs; tabs.push_back(font->get_char_size(' ', font_size).width * tab_size); - text.write[i].data_buf->tab_align(tabs); + l.data_buf->tab_align(tabs); } } - text.write[i].width = get_line_width(i); + l.width = get_line_width(i); } tab_size_dirty = false; @@ -440,8 +467,165 @@ void TextEdit::Text::move_gutters(int p_from_line, int p_to_line) { /// 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); +} + +void TextEdit::_accessibility_scroll_left(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() / 4); +} + +void TextEdit::_accessibility_scroll_right(const Variant &p_data) { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() / 4); +} + +void TextEdit::_accessibility_scroll_up(const Variant &p_data) { + v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() / 4); +} + +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); +} + +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].length() == 0) { + DisplayServer::get_singleton()->accessibility_update_set_placeholder(ae, 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; @@ -1577,6 +1761,8 @@ void TextEdit::_notification(int p_what) { _update_ime_text(); adjust_viewport_to_caret(0); + + queue_accessibility_update(); queue_redraw(); } } break; @@ -1716,9 +1902,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) { @@ -1792,6 +1980,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) { @@ -1915,6 +2105,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; } @@ -2834,6 +3025,7 @@ void TextEdit::_update_caches() { if (syntax_highlighter.is_valid()) { syntax_highlighter->set_text_edit(this); } + queue_accessibility_update(); } void TextEdit::_close_ime_window() { @@ -3019,6 +3211,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 */ @@ -3053,7 +3246,7 @@ void TextEdit::set_editable(const bool p_editable) { } editable = p_editable; - + queue_accessibility_update(); queue_redraw(); } @@ -3084,6 +3277,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(); } } @@ -3104,6 +3298,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(); } } @@ -3118,6 +3313,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(); } } @@ -3135,6 +3331,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(); } @@ -3150,6 +3347,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(); } @@ -3276,7 +3474,6 @@ void TextEdit::set_text(const String &p_text) { set_caret_line(0); set_caret_column(0); - queue_redraw(); setting_text = false; emit_signal(SNAME("text_set")); @@ -3305,6 +3502,7 @@ void TextEdit::set_placeholder(const String &p_text) { placeholder_text = p_text; _update_placeholder(); + queue_accessibility_update(); queue_redraw(); } @@ -3508,6 +3706,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) { @@ -3885,6 +4086,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(); } } @@ -3904,6 +4106,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; @@ -3994,6 +4198,7 @@ void TextEdit::undo() { _selection_changed(); } adjust_viewport_to_caret(); + queue_accessibility_update(); } void TextEdit::redo() { @@ -4050,6 +4255,7 @@ void TextEdit::redo() { _selection_changed(); } adjust_viewport_to_caret(); + queue_accessibility_update(); } void TextEdit::clear_undo_history() { @@ -4560,6 +4766,7 @@ void TextEdit::remove_secondary_carets() { if (drag_caret_index >= 0) { drag_caret_index = -1; } + queue_accessibility_update(); } int TextEdit::get_caret_count() const { @@ -4979,6 +5186,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 { @@ -5013,6 +5221,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 { @@ -5267,6 +5476,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 { @@ -5511,6 +5723,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) { @@ -5673,6 +5888,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 { @@ -5684,6 +5900,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 { @@ -5880,7 +6097,7 @@ void TextEdit::adjust_viewport_to_caret(int p_caret) { first_visible_col = MIN(caret_pos.x, caret_pos.y); } h_scroll->set_value(first_visible_col); - + queue_accessibility_update(); queue_redraw(); } @@ -5935,7 +6152,7 @@ void TextEdit::center_viewport_to_caret(int p_caret) { first_visible_col = 0; } h_scroll->set_value(first_visible_col); - + queue_accessibility_update(); queue_redraw(); } @@ -7710,6 +7927,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. */ @@ -7827,6 +8045,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(); } @@ -8027,6 +8246,7 @@ void TextEdit::_update_gutter_width() { if (gutters_width > 0) { gutter_padding = 2; } + queue_accessibility_update(); queue_redraw(); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index efade398767385..64011a01b6d2c1 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -144,12 +144,15 @@ class TextEdit : public Control { 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; Color background_color = Color(0, 0, 0, 0); bool hidden = false; @@ -207,6 +210,14 @@ class TextEdit : public Control { 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; @@ -262,7 +273,6 @@ class TextEdit : public Control { /* Text */ Text text; - bool setting_text = false; bool alt_start = false; @@ -604,6 +614,8 @@ class TextEdit : public Control { 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(); @@ -691,6 +703,17 @@ class TextEdit : public Control { 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) diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index bbe5ddf1c3e033..cc156654f56395 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -428,6 +428,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 (nine_patch_stretch && (mode == FILL_LEFT_TO_RIGHT || mode == FILL_RIGHT_TO_LEFT || mode == FILL_TOP_TO_BOTTOM || mode == FILL_BOTTOM_TO_TOP || mode == FILL_BILINEAR_LEFT_AND_RIGHT || mode == FILL_BILINEAR_TOP_AND_BOTTOM)) { if (under.is_valid()) { diff --git a/scene/gui/tree.compat.inc b/scene/gui/tree.compat.inc new file mode 100644 index 00000000000000..7796caf5bffe4f --- /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_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_76829, DEFVAL(-1), DEFVAL(false), DEFVAL("")); +} + +#endif // DISABLE_DEPRECATED diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 376ace2fe25271..8b91ec6274b306 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" @@ -95,6 +96,7 @@ void TreeItem::_change_tree(Tree *p_tree) { if (p_tree == tree) { return; } + accessibility_row_dirty = true; TreeItem *c = first_child; while (c) { @@ -137,13 +139,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()); } @@ -325,6 +345,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 { @@ -332,6 +355,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); @@ -1105,6 +1148,7 @@ void TreeItem::move_before(TreeItem *p_item) { p_item->prev = this; if (tree && old_tree == tree) { + tree->queue_accessibility_update(); tree->queue_redraw(); } @@ -1149,6 +1193,7 @@ void TreeItem::move_after(TreeItem *p_item) { } if (tree && old_tree == tree) { + tree->queue_accessibility_update(); tree->queue_redraw(); } validate_cache(); @@ -1182,6 +1227,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(); } @@ -1195,7 +1242,7 @@ void TreeItem::deselect(int p_column) { _cell_deselected(p_column); } -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_valid()); TreeItem::Cell::Button button; @@ -1206,10 +1253,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 { @@ -1238,8 +1289,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.write[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 { @@ -1280,6 +1337,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()); @@ -1607,6 +1679,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); @@ -1696,7 +1771,7 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_custom_as_button", "column", "enable"), &TreeItem::set_custom_as_button); ClassDB::bind_method(D_METHOD("is_custom_set_as_button", "column"), &TreeItem::is_custom_set_as_button); - 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); @@ -1706,6 +1781,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); @@ -2451,6 +2527,13 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } theme_cache.button_pressed->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(); @@ -2712,6 +2795,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) { @@ -2723,6 +2807,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 { @@ -2753,6 +2838,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c select_single_item(p_selected, c, p_col, p_prev, r_in_range, p_current->is_collapsed() || p_force_deselect); c = c->next; } + queue_accessibility_update(); } Rect2 Tree::search_item_rect(TreeItem *p_from, TreeItem *p_item) { @@ -3277,7 +3363,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 { @@ -3292,6 +3381,7 @@ void Tree::_go_left() { } } } else { + selected_button = -1; if (select_mode == SELECT_MULTI) { selected_col--; emit_signal(SNAME("cell_selected")); @@ -3299,13 +3389,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()) { @@ -3313,6 +3408,7 @@ void Tree::_go_right() { _go_down(); } } else { + selected_button = -1; if (select_mode == SELECT_MULTI) { selected_col++; emit_signal(SNAME("cell_selected")); @@ -3320,6 +3416,7 @@ void Tree::_go_right() { selected_item->select(selected_col + 1); } } + queue_accessibility_update(); queue_redraw(); ensure_cursor_is_visible(); accept_event(); @@ -3330,6 +3427,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(); } @@ -3352,6 +3450,7 @@ void Tree::_go_up() { prev->select(col); } + queue_accessibility_update(); ensure_cursor_is_visible(); accept_event(); } @@ -3385,6 +3484,7 @@ void Tree::_go_down() { next->select(col); } + queue_accessibility_update(); ensure_cursor_is_visible(); accept_event(); } @@ -4196,8 +4296,341 @@ 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++) { + TreeItem::Cell &cell = p_item->cells.write[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_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); + 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.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_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.text); + } break; + case TreeItem::CELL_MODE_CHECK: { + DisplayServer::get_singleton()->accessibility_update_add_action(cell.accessibility_cell_element, DisplayServer::AccessibilityAction::ACTION_DEFAULT, callable_mp(this, &Tree::_accessibility_action_set_bool_value).bind(p_item, i)); + if (!cell.checked) { + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(cell.accessibility_cell_element, DisplayServer::AccessibilityActionVerb::ACTION_VERB_UNCHECK); + } else { + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(cell.accessibility_cell_element, DisplayServer::AccessibilityActionVerb::ACTION_VERB_CHECK); + } + 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: + case TreeItem::CELL_MODE_CUSTOM: { + } 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_DEFAULT, callable_mp(this, &Tree::_accessibility_action_button_press).bind(p_item, i, j)); + DisplayServer::get_singleton()->accessibility_update_set_default_action_verb(cell.buttons[j].accessibility_button_element, DisplayServer::AccessibilityActionVerb::ACTION_VERB_CLICK); + 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); + 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.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(); @@ -4436,6 +4869,7 @@ TreeItem *Tree::create_item(TreeItem *p_parent, int p_index) { } } + queue_accessibility_update(); return ti; } @@ -4469,13 +4903,16 @@ 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) { if (p_item != nullptr && p_column >= 0 && p_column < p_item->cells.size()) { p_item->cells.write[p_column].dirty = true; - columns.write[p_column].cached_minimum_width_dirty = true; + p_item->cells.write[p_column].cached_minimum_size_dirty = true; + p_item->accessibility_row_dirty = true; } + queue_accessibility_update(); queue_redraw(); } @@ -4490,9 +4927,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(); } @@ -4511,6 +4951,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; @@ -4519,6 +4960,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(); } @@ -4549,7 +4992,8 @@ void Tree::deselect_all() { selected_item = nullptr; selected_col = -1; - + selected_button = -1; + queue_accessibility_update(); queue_redraw(); } @@ -4578,7 +5022,7 @@ void Tree::clear() { edited_item = nullptr; popup_edited_item = nullptr; popup_pressing_edited_item = nullptr; - + queue_accessibility_update(); queue_redraw(); }; @@ -4588,6 +5032,7 @@ void Tree::set_hide_root(bool p_enabled) { } hide_root = p_enabled; + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -4608,6 +5053,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(); } @@ -4620,6 +5066,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(); } @@ -4632,6 +5079,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(); } @@ -4644,6 +5092,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(); } @@ -4804,6 +5253,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) { @@ -4815,6 +5277,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.write[i].accessibility_col_element); + columns.write[i].accessibility_col_element = RID(); + } + } + } + columns.resize(p_columns); if (root) { @@ -4822,7 +5294,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(); } @@ -4920,6 +5394,8 @@ void Tree::ensure_cursor_is_visible() { h_scroll->set_value(x_offset); } } + + queue_accessibility_update(); } int Tree::get_pressed_button() const { @@ -4977,6 +5453,7 @@ void Tree::set_column_titles_visible(bool p_show) { } show_column_titles = p_show; + queue_accessibility_update(); queue_redraw(); update_minimum_size(); } @@ -4995,6 +5472,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(); } @@ -5016,6 +5494,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(); } @@ -5030,6 +5509,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(); } } @@ -5044,6 +5524,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(); } } @@ -5092,6 +5573,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) { @@ -5690,6 +6172,7 @@ void Tree::_bind_methods() { BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, cursor); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, Tree, cursor_unfocus, "cursor_unfocused"); BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, button_pressed); + BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Tree, button_hover); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Tree, checked); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Tree, unchecked); diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 311055a2f8b9e4..d4540283109396 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -57,12 +57,14 @@ class TreeItem : public Object { friend class Tree; struct Cell { + mutable RID accessibility_cell_element; TreeCellMode mode = TreeItem::CELL_MODE_STRING; Ref icon; Rect2i icon_region; String text; String xl_text; + String alt_text; bool edit_multiline = false; String suffix; Ref text_buf; @@ -103,11 +105,13 @@ class TreeItem : public Object { 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