Flutter Engine
The Flutter Engine
AccessibilityBridgeMac.mm
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h"
6
7#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
8#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h"
9#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
10#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
11#include "flutter/shell/platform/embedder/embedder.h"
12
13namespace flutter {
14
15// Native mac notifications fired. These notifications are not publicly documented.
16static NSString* const kAccessibilityLoadCompleteNotification = @"AXLoadComplete";
17static NSString* const kAccessibilityInvalidStatusChangedNotification = @"AXInvalidStatusChanged";
18static NSString* const kAccessibilityLiveRegionCreatedNotification = @"AXLiveRegionCreated";
19static NSString* const kAccessibilityLiveRegionChangedNotification = @"AXLiveRegionChanged";
20static NSString* const kAccessibilityExpandedChanged = @"AXExpandedChanged";
21static NSString* const kAccessibilityMenuItemSelectedNotification = @"AXMenuItemSelected";
22
24 __weak FlutterViewController* view_controller)
25 : flutter_engine_(flutter_engine), view_controller_(view_controller) {}
26
29 if (!view_controller_.viewLoaded || !view_controller_.view.window) {
30 // Don't need to send accessibility events if the there is no view or window.
31 return;
32 }
33 ui::AXNode* ax_node = targeted_event.node;
34 std::vector<AccessibilityBridgeMac::NSAccessibilityEvent> events =
35 MacOSEventsFromAXEvent(targeted_event.event_params.event, *ax_node);
36 for (const AccessibilityBridgeMac::NSAccessibilityEvent& event : events) {
37 if (event.user_info != nil) {
38 DispatchMacOSNotificationWithUserInfo(event.target, event.name, event.user_info);
39 } else {
41 }
42 }
43}
44
45std::vector<AccessibilityBridgeMac::NSAccessibilityEvent>
46AccessibilityBridgeMac::MacOSEventsFromAXEvent(ui::AXEventGenerator::Event event_type,
47 const ui::AXNode& ax_node) const {
48 // Gets the native_node with the node_id.
49 NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated");
50 auto platform_node_delegate = GetFlutterPlatformNodeDelegateFromID(ax_node.id()).lock();
51 NSCAssert(platform_node_delegate, @"Event target must exist in accessibility bridge.");
52 auto mac_platform_node_delegate =
53 std::static_pointer_cast<FlutterPlatformNodeDelegateMac>(platform_node_delegate);
54 gfx::NativeViewAccessible native_node = mac_platform_node_delegate->GetNativeViewAccessible();
55
56 std::vector<AccessibilityBridgeMac::NSAccessibilityEvent> events;
57 switch (event_type) {
59 if (ax_node.data().role == ax::mojom::Role::kTree) {
60 events.push_back({
61 .name = NSAccessibilitySelectedRowsChangedNotification,
62 .target = native_node,
63 .user_info = nil,
64 });
65 } else if (ax_node.data().role == ax::mojom::Role::kTextFieldWithComboBox) {
66 // Even though the selected item in the combo box has changed, don't
67 // post a focus change because this will take the focus out of
68 // the combo box where the user might be typing.
69 events.push_back({
70 .name = NSAccessibilitySelectedChildrenChangedNotification,
71 .target = native_node,
72 .user_info = nil,
73 });
74 }
75 // In all other cases, this delegate should post
76 // |NSAccessibilityFocusedUIElementChangedNotification|, but this is
77 // handled elsewhere.
78 break;
80 events.push_back({
82 .target = native_node,
83 .user_info = nil,
84 });
85 break;
87 events.push_back({
89 .target = native_node,
90 .user_info = nil,
91 });
92 break;
94 if (ui::IsTableLike(ax_node.data().role)) {
95 events.push_back({
96 .name = NSAccessibilitySelectedRowsChangedNotification,
97 .target = native_node,
98 .user_info = nil,
99 });
100 } else {
101 // VoiceOver does not read anything if selection changes on the
102 // currently focused object, and the focus did not move. Fire a
103 // selection change if the focus did not change.
104 NSAccessibilityElement* native_accessibility_node = (NSAccessibilityElement*)native_node;
105 if (native_accessibility_node.accessibilityFocusedUIElement &&
109 // Don't fire selected children change, it will sometimes override
110 // announcement of current focus.
111 break;
112 }
113 events.push_back({
114 .name = NSAccessibilitySelectedChildrenChangedNotification,
115 .target = native_node,
116 .user_info = nil,
117 });
118 }
119 break;
121 id focused = mac_platform_node_delegate->GetFocus();
122 if ([focused isKindOfClass:[FlutterTextField class]]) {
123 // If it is a text field, the selection notifications are handled by
124 // the FlutterTextField directly. Only need to make sure it is the
125 // first responder.
126 FlutterTextField* native_text_field = (FlutterTextField*)focused;
127 if (native_text_field == mac_platform_node_delegate->GetFocus()) {
128 [native_text_field startEditing];
129 }
130 break;
131 }
132 // This event always fires at root
133 events.push_back({
134 .name = NSAccessibilitySelectedTextChangedNotification,
135 .target = native_node,
136 .user_info = nil,
137 });
138 // WebKit fires a notification both on the focused object and the page
139 // root.
140 const ui::AXTreeData& tree_data = GetAXTreeData();
141 int32_t focus = tree_data.focus_id;
142 if (focus == ui::AXNode::kInvalidAXID || focus != tree_data.sel_anchor_object_id) {
143 break; // Just fire a notification on the root.
144 }
145 auto focus_node = GetFlutterPlatformNodeDelegateFromID(focus).lock();
146 if (!focus_node) {
147 break; // Just fire a notification on the root.
148 }
149 events.push_back({
150 .name = NSAccessibilitySelectedTextChangedNotification,
151 .target = focus_node->GetNativeViewAccessible(),
152 .user_info = nil,
153 });
154 break;
155 }
157 events.push_back({
158 .name = NSAccessibilityValueChangedNotification,
159 .target = native_node,
160 .user_info = nil,
161 });
162 break;
164 if (ax_node.data().role == ax::mojom::Role::kTextField) {
165 // If it is a text field, the value change notifications are handled by
166 // the FlutterTextField directly. Only need to make sure it is the
167 // first responder.
168 FlutterTextField* native_text_field =
169 (FlutterTextField*)mac_platform_node_delegate->GetNativeViewAccessible();
170 id focused = mac_platform_node_delegate->GetFocus();
171 if (!focused || native_text_field == focused) {
172 [native_text_field startEditing];
173 }
174 break;
175 }
176 events.push_back({
177 .name = NSAccessibilityValueChangedNotification,
178 .target = native_node,
179 .user_info = nil,
180 });
182 events.push_back({
183 .name = NSAccessibilityValueChangedNotification,
185 .user_info = nil,
186 });
187 }
188 break;
189 }
191 events.push_back({
193 .target = native_node,
194 .user_info = nil,
195 });
196 break;
198 events.push_back({
200 .target = native_node,
201 .user_info = nil,
202 });
203 // VoiceOver requires a live region changed notification to actually
204 // announce the live region.
205 auto live_region_events =
206 MacOSEventsFromAXEvent(ui::AXEventGenerator::Event::LIVE_REGION_CHANGED, ax_node);
207 events.insert(events.end(), live_region_events.begin(), live_region_events.end());
208 break;
209 }
211 // Uses native VoiceOver support for live regions.
212 events.push_back({
214 .target = native_node,
215 .user_info = nil,
216 });
217 break;
218 }
220 events.push_back({
221 .name = NSAccessibilityRowCountChangedNotification,
222 .target = native_node,
223 .user_info = nil,
224 });
225 break;
227 NSAccessibilityNotificationName mac_notification;
228 if (ax_node.data().role == ax::mojom::Role::kRow ||
229 ax_node.data().role == ax::mojom::Role::kTreeItem) {
230 mac_notification = NSAccessibilityRowExpandedNotification;
231 } else {
232 mac_notification = kAccessibilityExpandedChanged;
233 }
234 events.push_back({
235 .name = mac_notification,
236 .target = native_node,
237 .user_info = nil,
238 });
239 break;
240 }
242 NSAccessibilityNotificationName mac_notification;
243 if (ax_node.data().role == ax::mojom::Role::kRow ||
244 ax_node.data().role == ax::mojom::Role::kTreeItem) {
245 mac_notification = NSAccessibilityRowCollapsedNotification;
246 } else {
247 mac_notification = kAccessibilityExpandedChanged;
248 }
249 events.push_back({
250 .name = mac_notification,
251 .target = native_node,
252 .user_info = nil,
253 });
254 break;
255 }
257 events.push_back({
259 .target = native_node,
260 .user_info = nil,
261 });
262 break;
264 // NSAccessibilityCreatedNotification seems to be the only way to let
265 // Voiceover pick up layout changes.
266 events.push_back({
267 .name = NSAccessibilityCreatedNotification,
268 .target = view_controller_.view.window,
269 .user_info = nil,
270 });
271 break;
272 }
325 // There are some notifications that aren't meaningful on Mac.
326 // It's okay to skip them.
327 break;
328 }
329 return events;
330}
331
335 NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated");
336 NSCAssert(view_controller_.viewLoaded && view_controller_.view.window,
337 @"The accessibility bridge should not receive accessibility actions if the flutter view"
338 @"is not loaded or attached to a NSWindow.");
339 [flutter_engine_ dispatchSemanticsAction:action toTarget:target withData:std::move(data)];
340}
341
342std::shared_ptr<FlutterPlatformNodeDelegate>
344 return std::make_shared<FlutterPlatformNodeDelegateMac>(weak_from_this(), view_controller_);
345}
346
347// Private method
349 gfx::NativeViewAccessible native_node,
350 NSAccessibilityNotificationName mac_notification) {
351 NSCAssert(mac_notification, @"The notification must not be null.");
352 NSCAssert(native_node, @"The notification target must not be null.");
353 NSAccessibilityPostNotification(native_node, mac_notification);
354}
355
356void AccessibilityBridgeMac::DispatchMacOSNotificationWithUserInfo(
357 gfx::NativeViewAccessible native_node,
358 NSAccessibilityNotificationName mac_notification,
359 NSDictionary* user_info) {
360 NSCAssert(mac_notification, @"The notification must not be null.");
361 NSCAssert(native_node, @"The notification target must not be null.");
362 NSCAssert(user_info, @"The notification data must not be null.");
363 NSAccessibilityPostNotificationWithUserInfo(native_node, mac_notification, user_info);
364}
365
366bool AccessibilityBridgeMac::HasPendingEvent(ui::AXEventGenerator::Event event) const {
367 NSCAssert(flutter_engine_, @"Flutter engine should not be deallocated");
368 std::vector<ui::AXEventGenerator::TargetedEvent> pending_events = GetPendingEvents();
369 for (const auto& pending_event : GetPendingEvents()) {
370 if (pending_event.event_params.event == event) {
371 return true;
372 }
373 }
374 return false;
375}
376
377} // namespace flutter
ax::mojom::Event event_type
AccessibilityBridgeMac(__weak FlutterEngine *flutter_engine, __weak FlutterViewController *view_controller)
Creates an AccessibilityBridgeMacDelegate.
void OnAccessibilityEvent(ui::AXEventGenerator::TargetedEvent targeted_event) override
Handle accessibility events generated due to accessibility tree changes. These events are needed to b...
std::shared_ptr< FlutterPlatformNodeDelegate > CreateFlutterPlatformNodeDelegate() override
Creates a platform specific FlutterPlatformNodeDelegate. Ownership passes to the caller....
virtual void DispatchMacOSNotification(gfx::NativeViewAccessible native_node, NSAccessibilityNotificationName mac_notification)
Posts the given event against the given node to the macOS accessibility notification system.
void DispatchAccessibilityAction(AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data) override
Dispatch accessibility action back to the Flutter framework. These actions are generated in the nativ...
ui::AXPlatformNodeDelegate * RootDelegate() const override
std::weak_ptr< FlutterPlatformNodeDelegate > GetFlutterPlatformNodeDelegateFromID(AccessibilityNodeId id) const
Get the flutter platform node delegate with the given id from this accessibility bridge....
const ui::AXTreeData & GetAXTreeData() const
Get the ax tree data from this accessibility bridge. The tree data contains information such as the i...
const std::vector< ui::AXEventGenerator::TargetedEvent > GetPendingEvents() const
Gets all pending accessibility events generated during semantics updates. This is useful when decidin...
A Mapping like NonOwnedMapping, but uses Free as its release proc.
Definition: mapping.h:144
AXID id() const
Definition: ax_node.h:110
int32_t AXID
Definition: ax_node.h:36
static constexpr AXID kInvalidAXID
Definition: ax_node.h:41
const AXNodeData & data() const
Definition: ax_node.h:112
virtual gfx::NativeViewAccessible GetNativeViewAccessible()=0
FlutterSemanticsAction
Definition: embedder.h:113
FlKeyEvent * event
uint32_t * target
static NSString *const kAccessibilityLoadCompleteNotification
static NSString *const kAccessibilityExpandedChanged
static NSString *const kAccessibilityMenuItemSelectedNotification
static NSString *const kAccessibilityLiveRegionChangedNotification
static NSString *const kAccessibilityInvalidStatusChangedNotification
static NSString *const kAccessibilityLiveRegionCreatedNotification
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot data
Definition: switches.h:41
UnimplementedNativeViewAccessible * NativeViewAccessible
bool IsTableLike(const ax::mojom::Role role)
bool HasState(ax::mojom::State state) const
ax::mojom::Role role
Definition: ax_node_data.h:277
AXNode::AXID sel_anchor_object_id
Definition: ax_tree_data.h:62
AXNode::AXID focus_id
Definition: ax_tree_data.h:53