zaniah 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +18 -0
- data/README.md +14 -4
- data/assets/icons/check.svg +1 -0
- data/assets/icons/close.svg +1 -0
- data/assets/icons/info.svg +1 -0
- data/assets/icons/menu.svg +1 -0
- data/assets/icons/search.svg +1 -0
- data/assets/icons/warning.svg +1 -0
- data/docs/accessibility.md +61 -0
- data/docs/adr/004-style-resolution.md +17 -0
- data/docs/adr/005-instance-layout.md +43 -0
- data/docs/adr/006-component-model.md +17 -0
- data/docs/adr/007-text-buffer.md +17 -0
- data/docs/adr/008-accessibility-bridge.md +19 -0
- data/docs/adr/009-grid-scope.md +17 -0
- data/docs/animation.md +46 -0
- data/docs/components.md +109 -0
- data/docs/gallery/command-palette-dark.png +0 -0
- data/docs/gallery/command-palette-high_contrast.png +0 -0
- data/docs/gallery/command-palette-light.png +0 -0
- data/docs/gallery/components-dark.png +0 -0
- data/docs/gallery/components-high_contrast.png +0 -0
- data/docs/gallery/components-light.png +0 -0
- data/docs/gallery/context-menu-dark.png +0 -0
- data/docs/gallery/context-menu-high_contrast.png +0 -0
- data/docs/gallery/context-menu-light.png +0 -0
- data/docs/gallery/dialog-dark.png +0 -0
- data/docs/gallery/dialog-high_contrast.png +0 -0
- data/docs/gallery/dialog-light.png +0 -0
- data/docs/gallery/drawer-left-dark.png +0 -0
- data/docs/gallery/drawer-left-high_contrast.png +0 -0
- data/docs/gallery/drawer-left-light.png +0 -0
- data/docs/gallery/drawer-right-dark.png +0 -0
- data/docs/gallery/drawer-right-high_contrast.png +0 -0
- data/docs/gallery/drawer-right-light.png +0 -0
- data/docs/gallery/menu-dark.png +0 -0
- data/docs/gallery/menu-high_contrast.png +0 -0
- data/docs/gallery/menu-light.png +0 -0
- data/docs/gallery/modal-dark.png +0 -0
- data/docs/gallery/modal-high_contrast.png +0 -0
- data/docs/gallery/modal-light.png +0 -0
- data/docs/gallery/popover-bottom-dark.png +0 -0
- data/docs/gallery/popover-bottom-high_contrast.png +0 -0
- data/docs/gallery/popover-bottom-light.png +0 -0
- data/docs/gallery/popover-left-dark.png +0 -0
- data/docs/gallery/popover-left-high_contrast.png +0 -0
- data/docs/gallery/popover-left-light.png +0 -0
- data/docs/gallery/popover-right-dark.png +0 -0
- data/docs/gallery/popover-right-high_contrast.png +0 -0
- data/docs/gallery/popover-right-light.png +0 -0
- data/docs/gallery/popover-top-dark.png +0 -0
- data/docs/gallery/popover-top-high_contrast.png +0 -0
- data/docs/gallery/popover-top-light.png +0 -0
- data/docs/gallery/toast-danger-dark.png +0 -0
- data/docs/gallery/toast-danger-high_contrast.png +0 -0
- data/docs/gallery/toast-danger-light.png +0 -0
- data/docs/gallery/toast-info-dark.png +0 -0
- data/docs/gallery/toast-info-high_contrast.png +0 -0
- data/docs/gallery/toast-info-light.png +0 -0
- data/docs/gallery/toast-success-dark.png +0 -0
- data/docs/gallery/toast-success-high_contrast.png +0 -0
- data/docs/gallery/toast-success-light.png +0 -0
- data/docs/gallery/toast-warning-dark.png +0 -0
- data/docs/gallery/toast-warning-high_contrast.png +0 -0
- data/docs/gallery/toast-warning-light.png +0 -0
- data/docs/gallery/tooltip-bottom-dark.png +0 -0
- data/docs/gallery/tooltip-bottom-high_contrast.png +0 -0
- data/docs/gallery/tooltip-bottom-light.png +0 -0
- data/docs/gallery/tooltip-left-dark.png +0 -0
- data/docs/gallery/tooltip-left-high_contrast.png +0 -0
- data/docs/gallery/tooltip-left-light.png +0 -0
- data/docs/gallery/tooltip-right-dark.png +0 -0
- data/docs/gallery/tooltip-right-high_contrast.png +0 -0
- data/docs/gallery/tooltip-right-light.png +0 -0
- data/docs/gallery/tooltip-top-dark.png +0 -0
- data/docs/gallery/tooltip-top-high_contrast.png +0 -0
- data/docs/gallery/tooltip-top-light.png +0 -0
- data/docs/index.html +74 -0
- data/docs/layout.md +31 -0
- data/docs/native.md +7 -2
- data/docs/site.css +32 -0
- data/docs/text.md +35 -0
- data/docs/theme.md +50 -0
- data/docs/tui.md +36 -0
- data/examples/gallery.rb +155 -0
- data/examples/native_ime.rb +13 -6
- data/lib/zaniah/accessibility/linux/service.rb +361 -0
- data/lib/zaniah/accessibility/linux.rb +23 -0
- data/lib/zaniah/accessibility/mac.rb +136 -0
- data/lib/zaniah/accessibility/native_tree.rb +53 -0
- data/lib/zaniah/accessibility/node.rb +12 -0
- data/lib/zaniah/accessibility/tree.rb +61 -0
- data/lib/zaniah/accessibility/windows/provider.rb +273 -0
- data/lib/zaniah/accessibility/windows.rb +38 -0
- data/lib/zaniah/accessibility.rb +63 -0
- data/lib/zaniah/animation/animator.rb +95 -0
- data/lib/zaniah/animation/easing.rb +45 -0
- data/lib/zaniah/animation/spring.rb +36 -0
- data/lib/zaniah/animation/transition.rb +5 -0
- data/lib/zaniah/animation.rb +6 -0
- data/lib/zaniah/app.rb +22 -2
- data/lib/zaniah/color.rb +16 -0
- data/lib/zaniah/devtools/hot_reload.rb +40 -0
- data/lib/zaniah/devtools/inspector.rb +111 -0
- data/lib/zaniah/devtools/stats_overlay.rb +16 -0
- data/lib/zaniah/devtools.rb +13 -0
- data/lib/zaniah/element.rb +210 -15
- data/lib/zaniah/frame_context.rb +4 -1
- data/lib/zaniah/gpu/frame_encoder.rb +10 -1
- data/lib/zaniah/gpu/instance_packing.rb +33 -5
- data/lib/zaniah/gpu/metal.rb +26 -10
- data/lib/zaniah/gpu/open_gl.rb +35 -14
- data/lib/zaniah/gpu/software.rb +116 -14
- data/lib/zaniah/gpu/vulkan/scene.frag +51 -0
- data/lib/zaniah/gpu/vulkan/scene.frag.spv +0 -0
- data/lib/zaniah/gpu/vulkan/scene.vert +50 -0
- data/lib/zaniah/gpu/vulkan/scene.vert.spv +0 -0
- data/lib/zaniah/gpu/vulkan.rb +282 -36
- data/lib/zaniah/gpu.rb +8 -3
- data/lib/zaniah/input/dispatcher.rb +96 -12
- data/lib/zaniah/input/focus_handle.rb +13 -4
- data/lib/zaniah/input/focus_tree.rb +77 -0
- data/lib/zaniah/input/keymap.rb +77 -0
- data/lib/zaniah/input.rb +1 -0
- data/lib/zaniah/interactivity.rb +30 -0
- data/lib/zaniah/layout/engine.rb +173 -36
- data/lib/zaniah/layout/grid.rb +134 -0
- data/lib/zaniah/layout/style.rb +16 -1
- data/lib/zaniah/layout.rb +1 -0
- data/lib/zaniah/length.rb +3 -0
- data/lib/zaniah/length_units.rb +7 -0
- data/lib/zaniah/list.rb +27 -15
- data/lib/zaniah/platform/appearance.rb +11 -0
- data/lib/zaniah/platform/headless/window.rb +101 -9
- data/lib/zaniah/platform/linux/appearance_aware.rb +8 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +3 -1
- data/lib/zaniah/platform/linux/window.rb +4 -2
- data/lib/zaniah/platform/mac/app.rb +1 -1
- data/lib/zaniah/platform/mac/core_text.rb +3 -0
- data/lib/zaniah/platform/mac/window.rb +9 -1
- data/lib/zaniah/platform/tui/window.rb +1 -1
- data/lib/zaniah/platform/windows/window.rb +12 -2
- data/lib/zaniah/platform.rb +1 -0
- data/lib/zaniah/scene.rb +137 -28
- data/lib/zaniah/scroll_state.rb +117 -0
- data/lib/zaniah/scroll_view.rb +94 -0
- data/lib/zaniah/style/gradient.rb +28 -0
- data/lib/zaniah/style/ring.rb +13 -0
- data/lib/zaniah/style/shadow.rb +13 -0
- data/lib/zaniah/style/style_set.rb +43 -0
- data/lib/zaniah/style/transform.rb +40 -0
- data/lib/zaniah/svg.rb +19 -0
- data/lib/zaniah/text.rb +261 -6
- data/lib/zaniah/text_buffer.rb +119 -0
- data/lib/zaniah/text_selection.rb +18 -0
- data/lib/zaniah/text_system/atlas_cache.rb +1 -1
- data/lib/zaniah/text_system/fallback.rb +24 -0
- data/lib/zaniah/text_system/font_db.rb +4 -1
- data/lib/zaniah/text_system/kinsoku.rb +28 -0
- data/lib/zaniah/text_system/line_breaker.rb +54 -0
- data/lib/zaniah/text_system/paragraph.rb +99 -0
- data/lib/zaniah/text_system/renderer.rb +13 -2
- data/lib/zaniah/text_system/typesetter.rb +4 -0
- data/lib/zaniah/text_system.rb +3 -0
- data/lib/zaniah/theme.rb +39 -0
- data/lib/zaniah/ui/charts.rb +165 -0
- data/lib/zaniah/ui/choice_fields.rb +134 -0
- data/lib/zaniah/ui/component.rb +73 -0
- data/lib/zaniah/ui/controls.rb +439 -0
- data/lib/zaniah/ui/data.rb +364 -0
- data/lib/zaniah/ui/editors.rb +67 -0
- data/lib/zaniah/ui/forms.rb +131 -0
- data/lib/zaniah/ui/layout_components.rb +205 -0
- data/lib/zaniah/ui/modal.rb +129 -0
- data/lib/zaniah/ui/overlays.rb +282 -0
- data/lib/zaniah/ui/pickers.rb +169 -0
- data/lib/zaniah/ui/primitives.rb +199 -0
- data/lib/zaniah/ui/scrollbar.rb +104 -0
- data/lib/zaniah/ui/structure.rb +224 -0
- data/lib/zaniah/ui/text_inputs.rb +169 -0
- data/lib/zaniah/ui/variants.rb +23 -0
- data/lib/zaniah/ui.rb +18 -0
- data/lib/zaniah/unicode.rb +42 -0
- data/lib/zaniah/uniform_list.rb +27 -6
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +14 -0
- data/sig/accessibility.rbs +34 -0
- data/sig/animation.rbs +54 -0
- data/sig/app.rbs +2 -1
- data/sig/devtools.rbs +37 -0
- data/sig/elements.rbs +88 -6
- data/sig/gpu.rbs +9 -2
- data/sig/input.rbs +31 -5
- data/sig/layout.rbs +2 -1
- data/sig/platform.rbs +24 -4
- data/sig/scene.rbs +15 -2
- data/sig/style.rbs +82 -0
- data/sig/text.rbs +35 -0
- data/sig/theme.rbs +64 -0
- data/sig/ui.rbs +390 -0
- data/sig/zaniah.rbs +56 -3
- data/tools/generate_component_gallery.rb +48 -0
- metadata +147 -2
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "open3"
|
|
5
|
+
require_relative "../../ffi/library"
|
|
6
|
+
|
|
7
|
+
module Zaniah
|
|
8
|
+
module Accessibility
|
|
9
|
+
module Linux
|
|
10
|
+
class Service
|
|
11
|
+
attr_reader :unique_name
|
|
12
|
+
P = Fiddle::TYPE_VOIDP
|
|
13
|
+
I = Fiddle::TYPE_INT
|
|
14
|
+
U = Fiddle::TYPE_UINT
|
|
15
|
+
ROOT_PATH = "/org/a11y/atspi/accessible/root"
|
|
16
|
+
NULL_PATH = "/org/a11y/atspi/null"
|
|
17
|
+
ACCESSIBLE = "org.a11y.atspi.Accessible"
|
|
18
|
+
COMPONENT = "org.a11y.atspi.Component"
|
|
19
|
+
ACTION = "org.a11y.atspi.Action"
|
|
20
|
+
APPLICATION = "org.a11y.atspi.Application"
|
|
21
|
+
|
|
22
|
+
ROLES = {
|
|
23
|
+
application: 75, button: 43, checkbox: 7, radio: 44, switch: 62, textbox: 79,
|
|
24
|
+
searchbox: 79, combobox: 11, listbox: 98, dialog: 16, table: 55, row: 90,
|
|
25
|
+
cell: 56, columnheader: 57, tree: 65, treeitem: 91, list: 31, listitem: 32,
|
|
26
|
+
slider: 51, progressbar: 42, meter: 103, link: 88, image: 27, heading: 83,
|
|
27
|
+
text: 61, separator: 50, toolbar: 63, menubar: 34, menu: 33, menuitem: 35,
|
|
28
|
+
tab: 37, tabpanel: 38, tooltip: 64, status: 54, navigation: 110,
|
|
29
|
+
radiogroup: 99, form: 87, group: 99
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
XML = <<~XML.freeze
|
|
33
|
+
<node>
|
|
34
|
+
<interface name="#{ACCESSIBLE}">
|
|
35
|
+
<property name="Name" type="s" access="read"/><property name="Description" type="s" access="read"/>
|
|
36
|
+
<property name="Parent" type="(so)" access="read"/><property name="ChildCount" type="i" access="read"/>
|
|
37
|
+
<property name="Locale" type="s" access="read"/><property name="AccessibleId" type="s" access="read"/>
|
|
38
|
+
<property name="HelpText" type="s" access="read"/>
|
|
39
|
+
<method name="GetChildAtIndex"><arg type="i" direction="in"/><arg type="(so)" direction="out"/></method>
|
|
40
|
+
<method name="GetChildren"><arg type="a(so)" direction="out"/></method>
|
|
41
|
+
<method name="GetIndexInParent"><arg type="i" direction="out"/></method>
|
|
42
|
+
<method name="GetRelationSet"><arg type="a(ua(so))" direction="out"/></method>
|
|
43
|
+
<method name="GetRole"><arg type="u" direction="out"/></method>
|
|
44
|
+
<method name="GetRoleName"><arg type="s" direction="out"/></method>
|
|
45
|
+
<method name="GetLocalizedRoleName"><arg type="s" direction="out"/></method>
|
|
46
|
+
<method name="GetState"><arg type="au" direction="out"/></method>
|
|
47
|
+
<method name="GetAttributes"><arg type="a{ss}" direction="out"/></method>
|
|
48
|
+
<method name="GetApplication"><arg type="(so)" direction="out"/></method>
|
|
49
|
+
<method name="GetInterfaces"><arg type="as" direction="out"/></method>
|
|
50
|
+
</interface>
|
|
51
|
+
<interface name="#{COMPONENT}">
|
|
52
|
+
<method name="Contains"><arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="u" direction="in"/><arg type="b" direction="out"/></method>
|
|
53
|
+
<method name="GetAccessibleAtPoint"><arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="u" direction="in"/><arg type="(so)" direction="out"/></method>
|
|
54
|
+
<method name="GetExtents"><arg type="u" direction="in"/><arg type="(iiii)" direction="out"/></method>
|
|
55
|
+
<method name="GetPosition"><arg type="u" direction="in"/><arg type="i" direction="out"/><arg type="i" direction="out"/></method>
|
|
56
|
+
<method name="GetSize"><arg type="i" direction="out"/><arg type="i" direction="out"/></method>
|
|
57
|
+
<method name="GetLayer"><arg type="u" direction="out"/></method><method name="GetMDIZOrder"><arg type="n" direction="out"/></method>
|
|
58
|
+
<method name="GrabFocus"><arg type="b" direction="out"/></method><method name="GetAlpha"><arg type="d" direction="out"/></method>
|
|
59
|
+
</interface>
|
|
60
|
+
<interface name="#{ACTION}">
|
|
61
|
+
<property name="NActions" type="i" access="read"/>
|
|
62
|
+
<method name="GetDescription"><arg type="i" direction="in"/><arg type="s" direction="out"/></method>
|
|
63
|
+
<method name="GetName"><arg type="i" direction="in"/><arg type="s" direction="out"/></method>
|
|
64
|
+
<method name="GetLocalizedName"><arg type="i" direction="in"/><arg type="s" direction="out"/></method>
|
|
65
|
+
<method name="GetKeyBinding"><arg type="i" direction="in"/><arg type="s" direction="out"/></method>
|
|
66
|
+
<method name="DoAction"><arg type="i" direction="in"/><arg type="b" direction="out"/></method>
|
|
67
|
+
</interface>
|
|
68
|
+
<interface name="#{APPLICATION}">
|
|
69
|
+
<property name="ToolkitName" type="s" access="read"/><property name="Version" type="s" access="read"/>
|
|
70
|
+
<property name="ToolkitVersion" type="s" access="read"/><property name="AtspiVersion" type="s" access="read"/>
|
|
71
|
+
<property name="InterfaceVersion" type="u" access="read"/><property name="Id" type="i" access="readwrite"/>
|
|
72
|
+
<method name="GetLocale"><arg type="u" direction="in"/><arg type="s" direction="out"/></method>
|
|
73
|
+
<method name="GetApplicationBusAddress"><arg type="s" direction="out"/></method>
|
|
74
|
+
</interface>
|
|
75
|
+
</node>
|
|
76
|
+
XML
|
|
77
|
+
|
|
78
|
+
INSTANCES = {}
|
|
79
|
+
|
|
80
|
+
def initialize(window)
|
|
81
|
+
@window, @registrations, @application_id = window, {}, -1
|
|
82
|
+
connect
|
|
83
|
+
install_callbacks
|
|
84
|
+
parse_interfaces
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update(root)
|
|
88
|
+
application = Accessibility.node(role: :application, label: window_title,
|
|
89
|
+
bounds: root.bounds, children: [root])
|
|
90
|
+
@tree = NativeTree.new(application)
|
|
91
|
+
@entries = @tree.to_h { |entry| [path(entry), entry] }
|
|
92
|
+
(@registrations.keys - @entries.keys).each { |object_path| unregister(object_path) }
|
|
93
|
+
(@entries.keys - @registrations.keys).each { |object_path| register(object_path) }
|
|
94
|
+
embed unless @embedded
|
|
95
|
+
emit_change
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def poll = glib.fn(:g_main_context_iteration, [P, I], I).call(0, 0)
|
|
100
|
+
|
|
101
|
+
def close
|
|
102
|
+
@registrations.keys.each { |object_path| unregister(object_path) }
|
|
103
|
+
object.fn(:g_object_unref, [P], Fiddle::TYPE_VOID).call(@connection) if @connection&.to_i&.positive?
|
|
104
|
+
gio.fn(:g_dbus_node_info_unref, [P], Fiddle::TYPE_VOID).call(@node_info) if @node_info&.to_i&.positive?
|
|
105
|
+
INSTANCES.delete(object_id)
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def connect
|
|
112
|
+
address = ENV["AT_SPI_BUS_ADDRESS"] || accessibility_bus_address
|
|
113
|
+
raise Error, "AT-SPI accessibility bus is unavailable" if address.to_s.empty?
|
|
114
|
+
error = [0].pack("J")
|
|
115
|
+
@connection = gio.fn(:g_dbus_connection_new_for_address_sync, [P, I, P, P, P], P)
|
|
116
|
+
.call(address, 9, 0, 0, error)
|
|
117
|
+
raise Error, "cannot connect to AT-SPI accessibility bus" if @connection.to_i.zero?
|
|
118
|
+
gio.fn(:g_dbus_connection_set_exit_on_close, [P, I], Fiddle::TYPE_VOID).call(@connection, 0)
|
|
119
|
+
@unique_name = pointer_text(gio.fn(:g_dbus_connection_get_unique_name, [P], P).call(@connection))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def accessibility_bus_address
|
|
123
|
+
output, _error, status = Open3.capture3("gdbus", "call", "--session", "--dest", "org.a11y.Bus",
|
|
124
|
+
"--object-path", "/org/a11y/bus", "--method", "org.a11y.Bus.GetAddress")
|
|
125
|
+
status.success? ? output[/\('([^']+)'/, 1] : nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def install_callbacks
|
|
129
|
+
INSTANCES[object_id] = self
|
|
130
|
+
@method_callback = Fiddle::Closure::BlockCaller.new(Fiddle::TYPE_VOID, [P] * 8) do |_connection, _sender, object_path, interface, method, parameters, invocation, data|
|
|
131
|
+
INSTANCES[data.to_i]&.__send__(:method_call, pointer_text(object_path), pointer_text(interface), pointer_text(method), parameters, invocation)
|
|
132
|
+
rescue StandardError => error
|
|
133
|
+
INSTANCES[data.to_i]&.__send__(:return_error, invocation, error)
|
|
134
|
+
end
|
|
135
|
+
@get_callback = Fiddle::Closure::BlockCaller.new(P, [P] * 7) do |_connection, _sender, object_path, interface, property, _error, data|
|
|
136
|
+
INSTANCES[data.to_i]&.__send__(:property, pointer_text(object_path), pointer_text(interface), pointer_text(property)) || 0
|
|
137
|
+
rescue StandardError
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
@set_callback = Fiddle::Closure::BlockCaller.new(I, [P] * 8) do |_connection, _sender, object_path, interface, property, value, _error, data|
|
|
141
|
+
INSTANCES[data.to_i]&.__send__(:set_property, pointer_text(object_path), pointer_text(interface), pointer_text(property), value) ? 1 : 0
|
|
142
|
+
rescue StandardError
|
|
143
|
+
0
|
|
144
|
+
end
|
|
145
|
+
@vtable = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP * 11)
|
|
146
|
+
@vtable[0, Fiddle::SIZEOF_VOIDP * 11] = [@method_callback.to_i, @get_callback.to_i, @set_callback.to_i, *([0] * 8)].pack("J*")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_interfaces
|
|
150
|
+
error = [0].pack("J")
|
|
151
|
+
@node_info = gio.fn(:g_dbus_node_info_new_for_xml, [P, P], P).call(XML, error)
|
|
152
|
+
raise Error, "invalid AT-SPI introspection data" if @node_info.to_i.zero?
|
|
153
|
+
@interfaces = [ACCESSIBLE, COMPONENT, ACTION, APPLICATION].to_h do |name|
|
|
154
|
+
[name, gio.fn(:g_dbus_node_info_lookup_interface, [P, P], P).call(@node_info, name)]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def register(object_path)
|
|
159
|
+
names = [ACCESSIBLE, COMPONENT, ACTION]
|
|
160
|
+
names << APPLICATION if object_path == ROOT_PATH
|
|
161
|
+
ids = names.map do |name|
|
|
162
|
+
gio.fn(:g_dbus_connection_register_object, [P, P, P, P, P, P, P], U)
|
|
163
|
+
.call(@connection, object_path, @interfaces.fetch(name), @vtable, object_id, 0, 0)
|
|
164
|
+
end
|
|
165
|
+
raise Error, "cannot register AT-SPI object #{object_path}" if ids.any?(&:zero?)
|
|
166
|
+
@registrations[object_path] = ids
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def unregister(object_path)
|
|
170
|
+
@registrations.delete(object_path)&.each do |id|
|
|
171
|
+
gio.fn(:g_dbus_connection_unregister_object, [P, U], I).call(@connection, id)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def embed
|
|
176
|
+
gio.fn(:g_dbus_connection_call, [P, P, P, P, P, P, P, I, I, P, P, P], Fiddle::TYPE_VOID)
|
|
177
|
+
.call(@connection, "org.a11y.atspi.Registry", ROOT_PATH, "org.a11y.atspi.Socket", "Embed",
|
|
178
|
+
tuple(reference(@tree.root)), 0, 0, 3000, 0, 0, 0)
|
|
179
|
+
@embedded = true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def emit_change
|
|
183
|
+
parameters = tuple(string(""), int32(0), int32(0), variant(string("")), array("{sv}", []))
|
|
184
|
+
gio.fn(:g_dbus_connection_emit_signal, [P, P, P, P, P, P, P], I)
|
|
185
|
+
.call(@connection, 0, ROOT_PATH, "org.a11y.atspi.Event.Object", "VisibleDataChanged", parameters, 0)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def method_call(object_path, interface, method, parameters, invocation)
|
|
189
|
+
entry = @entries[object_path]
|
|
190
|
+
raise Error, "unknown accessible object" unless entry
|
|
191
|
+
values = case interface
|
|
192
|
+
when ACCESSIBLE then accessible_method(entry, method, parameters)
|
|
193
|
+
when COMPONENT then component_method(entry, method, parameters)
|
|
194
|
+
when ACTION then action_method(entry, method, parameters)
|
|
195
|
+
when APPLICATION then application_method(method)
|
|
196
|
+
else raise Error, "unknown accessibility interface"
|
|
197
|
+
end
|
|
198
|
+
gio.fn(:g_dbus_method_invocation_return_value, [P, P], Fiddle::TYPE_VOID).call(invocation, tuple(*Array(values)))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def accessible_method(entry, method, parameters)
|
|
202
|
+
case method
|
|
203
|
+
when "GetChildAtIndex" then reference(entry.children[parameter(parameters, 0, :int32)])
|
|
204
|
+
when "GetChildren" then array("(so)", entry.children.map { |child| reference(child) })
|
|
205
|
+
when "GetIndexInParent" then int32(entry.parent ? entry.parent.children.index(entry) : -1)
|
|
206
|
+
when "GetRelationSet" then array("(ua(so))", [])
|
|
207
|
+
when "GetRole" then uint32(ROLES.fetch(entry.node.role, 67))
|
|
208
|
+
when "GetRoleName", "GetLocalizedRoleName" then string(entry.node.role.to_s.tr("_", " "))
|
|
209
|
+
when "GetState" then array("u", states(entry).map { |state| uint32(state) })
|
|
210
|
+
when "GetAttributes" then array("{ss}", entry.node.states.map { |key, value| dict(string(key), string(value)) })
|
|
211
|
+
when "GetApplication" then reference(@tree.root)
|
|
212
|
+
when "GetInterfaces"
|
|
213
|
+
names = [ACCESSIBLE, COMPONENT]
|
|
214
|
+
names << ACTION unless entry.node.actions.empty?
|
|
215
|
+
names << APPLICATION if entry.equal?(@tree.root)
|
|
216
|
+
array("s", names.map { |name| string(name) })
|
|
217
|
+
else raise Error, "unsupported Accessible method #{method}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def component_method(entry, method, parameters)
|
|
222
|
+
bounds = @tree.bounds(entry)
|
|
223
|
+
case method
|
|
224
|
+
when "Contains"
|
|
225
|
+
point = Point.new(parameter(parameters, 0, :int32), parameter(parameters, 1, :int32))
|
|
226
|
+
boolean(bounds.contains?(point))
|
|
227
|
+
when "GetAccessibleAtPoint"
|
|
228
|
+
point = Point.new(parameter(parameters, 0, :int32), parameter(parameters, 1, :int32))
|
|
229
|
+
reference(@tree.hit(point))
|
|
230
|
+
when "GetExtents" then tuple(*[bounds.x, bounds.y, bounds.width, bounds.height].map { |value| int32(value.round) })
|
|
231
|
+
when "GetPosition" then [int32(bounds.x.round), int32(bounds.y.round)]
|
|
232
|
+
when "GetSize" then [int32(bounds.width.round), int32(bounds.height.round)]
|
|
233
|
+
when "GetLayer" then uint32(3)
|
|
234
|
+
when "GetMDIZOrder" then int16(-1)
|
|
235
|
+
when "GrabFocus"
|
|
236
|
+
point = Point.new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0)
|
|
237
|
+
boolean(Accessibility.focus_at(@window, point))
|
|
238
|
+
when "GetAlpha" then double(1.0)
|
|
239
|
+
else raise Error, "unsupported Component method #{method}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def action_method(entry, method, parameters)
|
|
244
|
+
index = parameter(parameters, 0, :int32)
|
|
245
|
+
action = entry.node.actions[index]
|
|
246
|
+
case method
|
|
247
|
+
when "GetDescription" then string(action ? "Perform #{action.to_s.tr("_", " ")}" : "")
|
|
248
|
+
when "GetName", "GetLocalizedName" then string(action&.to_s || "")
|
|
249
|
+
when "GetKeyBinding" then string("")
|
|
250
|
+
when "DoAction" then boolean(action ? Accessibility.perform(@window, entry.node, action, bounds: @tree.bounds(entry)) : false)
|
|
251
|
+
else raise Error, "unsupported Action method #{method}"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def application_method(method)
|
|
256
|
+
case method
|
|
257
|
+
when "GetLocale" then string(ENV["LANG"] || "C")
|
|
258
|
+
when "GetApplicationBusAddress" then string(ENV["AT_SPI_BUS_ADDRESS"] || "")
|
|
259
|
+
else raise Error, "unsupported Application method #{method}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def property(object_path, interface, name)
|
|
264
|
+
entry = @entries[object_path]
|
|
265
|
+
return 0 unless entry
|
|
266
|
+
case [interface, name]
|
|
267
|
+
when [ACCESSIBLE, "Name"] then string(entry.node.label || entry.node.value&.to_s || entry.node.role.to_s)
|
|
268
|
+
when [ACCESSIBLE, "Description"], [ACCESSIBLE, "HelpText"] then string("")
|
|
269
|
+
when [ACCESSIBLE, "Parent"] then reference(entry.parent)
|
|
270
|
+
when [ACCESSIBLE, "ChildCount"] then int32(entry.children.length)
|
|
271
|
+
when [ACCESSIBLE, "Locale"] then string(ENV["LANG"] || "C")
|
|
272
|
+
when [ACCESSIBLE, "AccessibleId"] then string(entry.runtime_id.to_s)
|
|
273
|
+
when [ACTION, "NActions"] then int32(entry.node.actions.length)
|
|
274
|
+
when [APPLICATION, "ToolkitName"] then string("Zaniah")
|
|
275
|
+
when [APPLICATION, "Version"], [APPLICATION, "ToolkitVersion"] then string(Zaniah::VERSION)
|
|
276
|
+
when [APPLICATION, "AtspiVersion"] then string("2.1")
|
|
277
|
+
when [APPLICATION, "InterfaceVersion"] then uint32(1)
|
|
278
|
+
when [APPLICATION, "Id"] then int32(@application_id)
|
|
279
|
+
else 0
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def set_property(object_path, interface, name, value)
|
|
284
|
+
return false unless object_path == ROOT_PATH && interface == APPLICATION && name == "Id"
|
|
285
|
+
@application_id = glib.fn(:g_variant_get_int32, [P], I).call(value)
|
|
286
|
+
true
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def states(entry)
|
|
290
|
+
node = entry.node
|
|
291
|
+
values = [25, 30]
|
|
292
|
+
values.concat([8, 24]) unless node.states[:disabled]
|
|
293
|
+
values << 3 if node.states[:busy]
|
|
294
|
+
values << 4 if node.states[:checked]
|
|
295
|
+
values << 41 if node.states.key?(:checked) || node.states.key?(:mixed)
|
|
296
|
+
values.concat([9, node.states[:expanded] ? 10 : 5]) unless node.states[:expanded].nil?
|
|
297
|
+
focusable = !node.states[:disabled] && (!node.actions.empty? || %i[textbox searchbox slider combobox].include?(node.role))
|
|
298
|
+
values << 11 if focusable
|
|
299
|
+
values << 12 if @tree.focused(@window).equal?(entry)
|
|
300
|
+
values << 7 if %i[textbox searchbox].include?(node.role) && !node.states[:readonly] && !node.states[:disabled]
|
|
301
|
+
values << 16 if node.states[:modal]
|
|
302
|
+
values << 17 if node.states[:multiline]
|
|
303
|
+
values << 18 if node.states[:multiselectable]
|
|
304
|
+
values << 20 if node.states[:pressed]
|
|
305
|
+
values << 21 if node.states[:resizable]
|
|
306
|
+
values << 22 if node.states.key?(:selected)
|
|
307
|
+
values << 23 if node.states[:selected]
|
|
308
|
+
values << 43 if node.states[:readonly]
|
|
309
|
+
values.uniq
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def reference(entry)
|
|
313
|
+
entry ? tuple(string(@unique_name), object_path(path(entry))) : tuple(string(""), object_path(NULL_PATH))
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def path(entry) = entry.path.empty? ? ROOT_PATH : "/org/a11y/atspi/accessible/#{entry.runtime_id}"
|
|
317
|
+
def window_title = @window.respond_to?(:title) ? @window.title.to_s : "Zaniah application"
|
|
318
|
+
|
|
319
|
+
def parameter(parameters, index, type)
|
|
320
|
+
child = glib.fn(:g_variant_get_child_value, [P, Fiddle::TYPE_SIZE_T], P).call(parameters, index)
|
|
321
|
+
result = glib.fn(:"g_variant_get_#{type}", [P], type == :uint32 ? U : I).call(child)
|
|
322
|
+
glib.fn(:g_variant_unref, [P], Fiddle::TYPE_VOID).call(child)
|
|
323
|
+
result
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def tuple(*values) = container(:g_variant_new_tuple, values)
|
|
327
|
+
def array(type, values)
|
|
328
|
+
variant_type = glib.fn(:g_variant_type_new, [P], P).call(type)
|
|
329
|
+
result = container(:g_variant_new_array, values, variant_type)
|
|
330
|
+
glib.fn(:g_variant_type_free, [P], Fiddle::TYPE_VOID).call(variant_type)
|
|
331
|
+
result
|
|
332
|
+
end
|
|
333
|
+
def container(function, values, prefix = nil)
|
|
334
|
+
pointers = values.empty? ? 0 : Fiddle::Pointer[values.map(&:to_i).pack("J*")]
|
|
335
|
+
arguments = prefix ? [prefix, pointers, values.length] : [pointers, values.length]
|
|
336
|
+
types = prefix ? [P, P, Fiddle::TYPE_SIZE_T] : [P, Fiddle::TYPE_SIZE_T]
|
|
337
|
+
glib.fn(function, types, P).call(*arguments)
|
|
338
|
+
end
|
|
339
|
+
def dict(key, value) = glib.fn(:g_variant_new_dict_entry, [P, P], P).call(key, value)
|
|
340
|
+
def variant(value) = glib.fn(:g_variant_new_variant, [P], P).call(value)
|
|
341
|
+
def string(value) = glib.fn(:g_variant_new_string, [P], P).call(value.to_s)
|
|
342
|
+
def object_path(value) = glib.fn(:g_variant_new_object_path, [P], P).call(value)
|
|
343
|
+
def int16(value) = glib.fn(:g_variant_new_int16, [Fiddle::TYPE_SHORT], P).call(value)
|
|
344
|
+
def int32(value) = glib.fn(:g_variant_new_int32, [I], P).call(value)
|
|
345
|
+
def uint32(value) = glib.fn(:g_variant_new_uint32, [U], P).call(value)
|
|
346
|
+
def boolean(value) = glib.fn(:g_variant_new_boolean, [I], P).call(value ? 1 : 0)
|
|
347
|
+
def double(value) = glib.fn(:g_variant_new_double, [Fiddle::TYPE_DOUBLE], P).call(value)
|
|
348
|
+
|
|
349
|
+
def return_error(invocation, error)
|
|
350
|
+
gio.fn(:g_dbus_method_invocation_return_dbus_error, [P, P, P], Fiddle::TYPE_VOID)
|
|
351
|
+
.call(invocation, "org.a11y.atspi.Error.Failed", error.message)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def pointer_text(pointer) = pointer.to_i.zero? ? "" : Fiddle::Pointer.new(pointer).to_s.force_encoding(Encoding::UTF_8)
|
|
355
|
+
def gio = (@gio ||= Zaniah::FFI::Library.new("libgio-2.0.so.0"))
|
|
356
|
+
def glib = (@glib ||= Zaniah::FFI::Library.new("libglib-2.0.so.0"))
|
|
357
|
+
def object = (@object ||= Zaniah::FFI::Library.new("libgobject-2.0.so.0"))
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "linux/service"
|
|
5
|
+
|
|
6
|
+
module Zaniah
|
|
7
|
+
module Accessibility
|
|
8
|
+
module Linux
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def publish(window, root, _changes)
|
|
12
|
+
return false unless root
|
|
13
|
+
service = services[window.object_id] ||= Service.new(window)
|
|
14
|
+
service.update(root)
|
|
15
|
+
rescue Fiddle::DLError, Errno::ENOENT, Error
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
def poll(window) = services[window.object_id]&.poll
|
|
19
|
+
def close(window) = services.delete(window.object_id)&.close
|
|
20
|
+
def services = (@services ||= {})
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Accessibility
|
|
5
|
+
module Mac
|
|
6
|
+
ELEMENTS = {}
|
|
7
|
+
ROLES = {
|
|
8
|
+
button: "AXButton", checkbox: "AXCheckBox", radio: "AXRadioButton", switch: "AXCheckBox",
|
|
9
|
+
textbox: "AXTextField", searchbox: "AXTextField", combobox: "AXComboBox", listbox: "AXList",
|
|
10
|
+
dialog: "AXDialog", table: "AXTable", row: "AXRow", cell: "AXCell", columnheader: "AXButton",
|
|
11
|
+
tree: "AXOutline", treeitem: "AXRow", list: "AXList", listitem: "AXRow", slider: "AXSlider",
|
|
12
|
+
progressbar: "AXProgressIndicator", meter: "AXLevelIndicator", link: "AXLink", image: "AXImage",
|
|
13
|
+
heading: "AXHeading", text: "AXStaticText", separator: "AXSplitter", toolbar: "AXToolbar",
|
|
14
|
+
menubar: "AXMenuBar", menu: "AXMenu", menuitem: "AXMenuItem", tab: "AXRadioButton",
|
|
15
|
+
tabpanel: "AXGroup", tooltip: "AXHelpTag", status: "AXStaticText", navigation: "AXGroup",
|
|
16
|
+
radiogroup: "AXRadioGroup", form: "AXGroup", group: "AXGroup"
|
|
17
|
+
}.freeze
|
|
18
|
+
NATIVE_ACTIONS = {
|
|
19
|
+
"AXPress" => %i[press toggle select sort expand collapse],
|
|
20
|
+
"AXIncrement" => %i[increment],
|
|
21
|
+
"AXDecrement" => %i[decrement],
|
|
22
|
+
"AXCancel" => %i[dismiss]
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def publish(window, root, _changes)
|
|
28
|
+
return unless root && defined?(Platform::Mac::O) && window.respond_to?(:view)
|
|
29
|
+
install
|
|
30
|
+
ELEMENTS.delete_if { |_element, (owner, _node)| owner.equal?(window) }
|
|
31
|
+
tree = NativeTree.new(root)
|
|
32
|
+
children = mutable_array
|
|
33
|
+
append(children, native_element(window, tree, tree.root, window.view))
|
|
34
|
+
object = window.view
|
|
35
|
+
Platform::Mac::O.send(object, "setAccessibilityElement:", 1, args: [:bool], result: :void)
|
|
36
|
+
Platform::Mac::O.send(object, "setAccessibilityRole:", Platform::Mac::O.string("AXGroup"), args: [:pointer], result: :void)
|
|
37
|
+
Platform::Mac::O.send(object, "setAccessibilityLabel:", Platform::Mac::O.string("Zaniah application"), args: [:pointer], result: :void)
|
|
38
|
+
Platform::Mac::O.send(object, "setAccessibilityChildren:", children, args: [:pointer], result: :void)
|
|
39
|
+
window.instance_variable_set(:@native_accessibility_tree, tree)
|
|
40
|
+
window.instance_variable_set(:@native_accessibility_children, children)
|
|
41
|
+
notify(window, object)
|
|
42
|
+
rescue Fiddle::DLError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def install
|
|
47
|
+
return if @installed
|
|
48
|
+
Platform::Mac::O.subclass("ZaniahAccessibilityElement", "NSAccessibilityElement") do |klass|
|
|
49
|
+
Platform::Mac::O.method(klass, "accessibilityActionNames", result: :pointer, encoding: "@@:") do |receiver, _|
|
|
50
|
+
action_names(receiver)
|
|
51
|
+
end
|
|
52
|
+
Platform::Mac::O.method(klass, "accessibilityPerformAction:", args: [:pointer], encoding: "v@:@") do |receiver, _, name|
|
|
53
|
+
perform_native(receiver, Platform::Mac::O.text(name))
|
|
54
|
+
end
|
|
55
|
+
{
|
|
56
|
+
"accessibilityPerformPress" => "AXPress",
|
|
57
|
+
"accessibilityPerformIncrement" => "AXIncrement",
|
|
58
|
+
"accessibilityPerformDecrement" => "AXDecrement"
|
|
59
|
+
}.each do |selector, action|
|
|
60
|
+
Platform::Mac::O.method(klass, selector, result: :bool, encoding: "B@:") do |receiver, _|
|
|
61
|
+
perform_native(receiver, action) ? 1 : 0
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
@installed = true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def native_element(window, tree, entry, parent)
|
|
69
|
+
o = Platform::Mac::O
|
|
70
|
+
node = entry.node
|
|
71
|
+
element = o.send(o.klass("ZaniahAccessibilityElement"), "accessibilityElementWithRole:frame:label:parent:",
|
|
72
|
+
o.string(ROLES.fetch(node.role, "AXGroup")), screen_bounds(window, tree.bounds(entry)),
|
|
73
|
+
o.string(node.label || node.value&.to_s || node.role.to_s), parent,
|
|
74
|
+
args: [:pointer, :rect, :pointer, :pointer])
|
|
75
|
+
ELEMENTS[element] = [window, node, tree.bounds(entry)]
|
|
76
|
+
o.send(element, "setAccessibilityFrameInParentSpace:", parent_bounds(tree, entry), args: [:rect], result: :void)
|
|
77
|
+
o.send(element, "setAccessibilityEnabled:", node.states[:disabled] ? 0 : 1, args: [:bool], result: :void)
|
|
78
|
+
o.send(element, "setAccessibilityValue:", native_value(node.value), args: [:pointer], result: :void) unless node.value.nil?
|
|
79
|
+
set_boolean(element, "setAccessibilitySelected:", node.states[:selected]) if node.states.key?(:selected)
|
|
80
|
+
set_boolean(element, "setAccessibilityExpanded:", node.states[:expanded]) unless node.states[:expanded].nil?
|
|
81
|
+
unless entry.children.empty?
|
|
82
|
+
children = mutable_array
|
|
83
|
+
entry.children.each { |child| append(children, native_element(window, tree, child, element)) }
|
|
84
|
+
o.send(element, "setAccessibilityChildren:", children, args: [:pointer], result: :void)
|
|
85
|
+
end
|
|
86
|
+
element
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def screen_bounds(window, bounds)
|
|
90
|
+
bounds ||= Bounds.new(0, 0, window.content_size.width, window.content_size.height)
|
|
91
|
+
Platform::Mac::O.send(window.handle, "convertRectToScreen:",
|
|
92
|
+
[bounds.x, window.content_size.height - bounds.bottom, bounds.width, bounds.height], args: [:rect], result: :rect)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parent_bounds(tree, entry)
|
|
96
|
+
bounds = tree.bounds(entry)
|
|
97
|
+
parent = entry.parent && tree.bounds(entry.parent)
|
|
98
|
+
[bounds.x - (parent&.x || 0), bounds.y - (parent&.y || 0), bounds.width, bounds.height]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def action_names(receiver)
|
|
102
|
+
_window, node, _bounds = ELEMENTS[receiver]
|
|
103
|
+
names = mutable_array
|
|
104
|
+
NATIVE_ACTIONS.each { |name, actions| append(names, Platform::Mac::O.string(name)) if node && (node.actions & actions).any? }
|
|
105
|
+
names
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def perform_native(receiver, name)
|
|
109
|
+
window, node, bounds = ELEMENTS[receiver]
|
|
110
|
+
action = NATIVE_ACTIONS[name]&.find { |candidate| node&.actions&.include?(candidate) }
|
|
111
|
+
!!(action && Accessibility.perform(window, node, action, bounds: bounds))
|
|
112
|
+
rescue StandardError
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def close(window) = ELEMENTS.delete_if { |_element, (owner, _node, _bounds)| owner.equal?(window) }
|
|
117
|
+
|
|
118
|
+
def native_value(value)
|
|
119
|
+
Platform::Mac::O.string(value.is_a?(Array) || value.is_a?(Hash) ? value.inspect : value.to_s)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def mutable_array = Platform::Mac::O.send(Platform::Mac::O.klass("NSMutableArray"), "array")
|
|
123
|
+
def append(array, value) = Platform::Mac::O.send(array, "addObject:", value, args: [:pointer], result: :void)
|
|
124
|
+
def set_boolean(element, selector, value) = Platform::Mac::O.send(element, selector, value ? 1 : 0, args: [:bool], result: :void)
|
|
125
|
+
|
|
126
|
+
def notify(window, object)
|
|
127
|
+
function = Platform::Mac::APPKIT.fn(:NSAccessibilityPostNotification,
|
|
128
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
|
|
129
|
+
function.call(object, Platform::Mac::O.string("AXLayoutChanged"))
|
|
130
|
+
if window.accessibility_tree.changes.any? { |change| change.after&.role == :status }
|
|
131
|
+
function.call(object, Platform::Mac::O.string("AXAnnouncementRequested"))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Accessibility
|
|
5
|
+
NativeEntry = Data.define(:path, :node, :parent, :children, :runtime_id)
|
|
6
|
+
|
|
7
|
+
class NativeTree
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
attr_reader :root
|
|
11
|
+
|
|
12
|
+
def initialize(root)
|
|
13
|
+
@entries = {}
|
|
14
|
+
@next_runtime_id = 1
|
|
15
|
+
@root = build(root, nil, []) if root
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each(&block) = block ? @entries.each_value(&block) : enum_for(__method__)
|
|
19
|
+
def [](path) = @entries[path]
|
|
20
|
+
|
|
21
|
+
def bounds(entry)
|
|
22
|
+
current = entry
|
|
23
|
+
while current
|
|
24
|
+
return current.node.bounds if current.node.bounds
|
|
25
|
+
current = current.parent
|
|
26
|
+
end
|
|
27
|
+
Bounds.new(0, 0, 0, 0)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def hit(point)
|
|
31
|
+
select { |entry| bounds(entry).contains?(point) }.max_by { |entry| entry.path.length }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def focused(window)
|
|
35
|
+
bounds = window.dispatcher.focused&.bounds
|
|
36
|
+
bounds && hit(Point.new(bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build(node, parent, path)
|
|
42
|
+
entry = NativeEntry.new(path: path.freeze, node: node, parent: parent, children: [], runtime_id: next_runtime_id)
|
|
43
|
+
@entries[entry.path] = entry
|
|
44
|
+
children = node.children.map.with_index { |child, index| build(child, entry, path + [index]) }.freeze
|
|
45
|
+
entry.children.replace(children)
|
|
46
|
+
entry.children.freeze
|
|
47
|
+
entry
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def next_runtime_id = (@next_runtime_id += 1) - 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Accessibility
|
|
5
|
+
Node = Data.define(:role, :label, :value, :bounds, :states, :children, :actions)
|
|
6
|
+
|
|
7
|
+
def self.node(role:, label: nil, value: nil, bounds: nil, states: {}, children: [], actions: [])
|
|
8
|
+
Node.new(role: role.to_sym, label: label&.to_s, value: value, bounds: bounds,
|
|
9
|
+
states: states.dup.freeze, children: children.dup.freeze, actions: actions.map(&:to_sym).freeze)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Accessibility
|
|
5
|
+
Change = Data.define(:kind, :path, :before, :after)
|
|
6
|
+
|
|
7
|
+
class Tree
|
|
8
|
+
attr_reader :root, :changes, :revision
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@root, @changes, @revision = nil, [], 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update(renderable, context, overlays: [])
|
|
15
|
+
nodes = [renderable, *overlays].filter_map { |item| semantic(item, context) }
|
|
16
|
+
next_root = nodes.length == 1 ? nodes.first : nodes.empty? ? nil : Accessibility.node(role: :group, label: "Application", children: nodes)
|
|
17
|
+
@changes = diff(@root, next_root).freeze
|
|
18
|
+
return false if @changes.empty?
|
|
19
|
+
@root = next_root
|
|
20
|
+
@revision += 1
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def each
|
|
25
|
+
return enum_for(__method__) unless block_given?
|
|
26
|
+
walk(@root) { |node, path| yield node, path } if @root
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find(&predicate) = each.find { |node, _path| predicate.call(node) }&.first
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def semantic(renderable, context)
|
|
34
|
+
return unless renderable
|
|
35
|
+
node = renderable.accessibility_node(context) if renderable.respond_to?(:accessibility_node)
|
|
36
|
+
return node if node.is_a?(Node)
|
|
37
|
+
children = renderable.respond_to?(:children) ? renderable.children : []
|
|
38
|
+
nodes = children.filter_map { |child| semantic(child, context) }
|
|
39
|
+
Accessibility.node(role: :group, children: nodes) unless nodes.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def diff(before, after, path = [])
|
|
43
|
+
return [] if before == after
|
|
44
|
+
return [Change.new(kind: before ? :removed : :added, path: path.freeze, before: before, after: after)] unless before && after
|
|
45
|
+
changes = []
|
|
46
|
+
if before.with(children: []) != after.with(children: [])
|
|
47
|
+
changes << Change.new(kind: :updated, path: path.freeze, before: before, after: after)
|
|
48
|
+
end
|
|
49
|
+
[before.children.length, after.children.length].max.times do |index|
|
|
50
|
+
changes.concat(diff(before.children[index], after.children[index], path + [index]))
|
|
51
|
+
end
|
|
52
|
+
changes
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def walk(node, path = [], &block)
|
|
56
|
+
yield node, path.freeze
|
|
57
|
+
node.children.each_with_index { |child, index| walk(child, path + [index], &block) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|