9 #include "flutter/fml/logging.h"
15 #pragma GCC diagnostic error "-Wundeclared-selector"
22 constexpr int32_t kSemanticObjectIdInvalid = -1;
24 class DefaultIosDelegate :
public AccessibilityBridge::IosDelegate {
26 bool IsFlutterViewControllerPresentingModalViewController(
28 if (view_controller) {
29 return view_controller.isPresentingViewController;
35 void PostAccessibilityNotification(UIAccessibilityNotifications notification,
36 id argument)
override {
37 UIAccessibilityPostNotification(notification, argument);
45 std::shared_ptr<FlutterPlatformViewsController> platform_views_controller,
46 std::unique_ptr<IosDelegate> ios_delegate)
47 : view_controller_(view_controller),
49 platform_views_controller_(std::move(platform_views_controller)),
50 last_focused_semantics_object_id_(kSemanticObjectIdInvalid),
51 objects_([[NSMutableDictionary alloc] init]),
53 ios_delegate_(ios_delegate ? std::move(ios_delegate)
54 : std::make_unique<DefaultIosDelegate>()),
57 initWithName:
@"flutter/accessibility"
58 binaryMessenger:
platform_view->GetOwnerViewController().get().engine.binaryMessenger
60 [accessibility_channel_.get() setMessageHandler:^(
id message,
FlutterReply reply) {
66 [accessibility_channel_.get() setMessageHandler:nil];
68 view_controller_.viewIfLoaded.accessibilityElements = nil;
76 last_focused_semantics_object_id_ = id;
77 [accessibility_channel_.get() sendMessage:@{
@"type" :
@"didGainFocus",
@"nodeId" : @(id)}];
81 if (last_focused_semantics_object_id_ ==
id) {
82 last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
87 flutter::SemanticsNodeUpdates nodes,
88 const flutter::CustomAccessibilityActionUpdates& actions) {
89 BOOL layoutChanged = NO;
90 BOOL scrollOccured = NO;
91 BOOL needsAnnouncement = NO;
92 for (
const auto& entry : actions) {
93 const flutter::CustomAccessibilityAction& action = entry.second;
94 actions_[action.id] = action;
96 for (
const auto& entry : nodes) {
97 const flutter::SemanticsNode& node = entry.second;
99 layoutChanged = layoutChanged || [
object nodeWillCauseLayoutChange:&node];
100 scrollOccured = scrollOccured || [
object nodeWillCauseScroll:&node];
101 needsAnnouncement = [
object nodeShouldTriggerAnnouncement:&node];
102 [
object setSemanticsNode:&node];
103 NSUInteger newChildCount = node.childrenInTraversalOrder.size();
104 NSMutableArray* newChildren =
105 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
106 for (NSUInteger i = 0; i < newChildCount; ++i) {
107 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
108 [newChildren addObject:child];
110 NSMutableArray* newChildrenInHitTestOrder =
111 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
112 for (NSUInteger i = 0; i < newChildCount; ++i) {
113 SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
114 [newChildrenInHitTestOrder addObject:child];
116 object.children = newChildren;
117 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
118 if (!node.customAccessibilityActions.empty()) {
119 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
120 [[[NSMutableArray alloc] init] autorelease];
121 for (int32_t action_id : node.customAccessibilityActions) {
122 flutter::CustomAccessibilityAction& action = actions_[action_id];
123 if (action.overrideId != -1) {
128 NSString* label = @(action.label.data());
129 SEL selector =
@selector(onCustomAccessibilityAction:);
133 selector:selector] autorelease];
134 customAction.
uid = action_id;
135 [accessibilityCustomActions addObject:customAction];
137 object.accessibilityCustomActions = accessibilityCustomActions;
140 if (needsAnnouncement) {
146 NSString* announcement =
147 [[[NSString alloc] initWithUTF8String:
object.node.label.c_str()] autorelease];
148 UIAccessibilityPostNotification(
149 UIAccessibilityAnnouncementNotification,
150 [[[NSAttributedString alloc] initWithString:announcement
152 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
159 bool routeChanged =
false;
163 if (!view_controller_.view.accessibilityElements) {
164 view_controller_.view.accessibilityElements =
165 @[ [root accessibilityContainer] ?: [NSNull
null] ];
167 NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
168 [root collectRoutes:newRoutes];
171 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
172 previous_routes_.end()) {
177 if (lastAdded == nil && [newRoutes count] > 0) {
178 int index = [newRoutes count] - 1;
179 lastAdded = [newRoutes objectAtIndex:index];
188 if (lastAdded != nil &&
189 ([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
190 previous_route_id_ = [lastAdded uid];
193 previous_routes_.clear();
195 previous_routes_.push_back([route uid]);
198 view_controller_.viewIfLoaded.accessibilityElements = nil;
201 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_ allKeys]];
203 VisitObjectsRecursivelyAndRemove(root, doomed_uids);
205 [objects_ removeObjectsForKeys:doomed_uids];
208 [
object accessibilityBridgeDidFinishUpdate];
211 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
212 layoutChanged = layoutChanged || [doomed_uids count] > 0;
215 NSString* routeName = [lastAdded routeName];
216 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
223 [objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
227 ios_delegate_->PostAccessibilityNotification(
228 UIAccessibilityLayoutChangedNotification,
230 }
else if (scrollOccured) {
234 ios_delegate_->PostAccessibilityNotification(
235 UIAccessibilityPageScrolledNotification,
242 platform_view_->DispatchSemanticsAction(uid, action, {});
246 flutter::SemanticsAction action,
247 fml::MallocMapping args) {
248 platform_view_->DispatchSemanticsAction(uid, action, std::move(args));
253 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
255 FML_DCHECK(oldObject.
node.id == newObject.
uid);
256 NSNumber* nodeId = @(oldObject.
node.id);
257 NSUInteger positionInChildlist = [oldObject.
parent.
children indexOfObject:oldObject];
258 [[oldObject retain] autorelease];
260 [oldObject.
parent replaceChildAtIndex:positionInChildlist withChild:newObject];
261 [objects removeObjectForKey:nodeId];
262 objects[nodeId] = newObject;
265 static SemanticsObject* CreateObject(
const flutter::SemanticsNode& node,
266 const fml::WeakPtr<AccessibilityBridge>& weak_ptr) {
267 if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) &&
268 !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
271 }
else if (!node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) &&
272 (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
273 node.HasFlag(flutter::SemanticsFlags::kHasCheckedState))) {
275 }
else if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) {
277 uid:node.id] autorelease];
278 }
else if (node.IsPlatformViewNode()) {
280 initWithBridge:weak_ptr
282 platformView:weak_ptr->GetPlatformViewsController()->GetFlutterTouchInterceptingViewByID(
283 node.platformViewId)] autorelease];
289 static bool DidFlagChange(
const flutter::SemanticsNode& oldNode,
290 const flutter::SemanticsNode& newNode,
291 SemanticsFlags flag) {
292 return oldNode.HasFlag(flag) != newNode.HasFlag(flag);
296 flutter::SemanticsNodeUpdates& updates) {
299 object = CreateObject(updates[uid],
GetWeakPtr());
300 objects_.get()[@(uid)] =
object;
303 auto nodeEntry = updates.find(
object.node.id);
304 if (nodeEntry != updates.end()) {
306 flutter::SemanticsNode node = nodeEntry->second;
307 if (DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsTextField) ||
308 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsReadOnly) ||
309 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasCheckedState) ||
310 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasToggledState) ||
311 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasImplicitScrolling)) {
316 ReplaceSemanticsObject(
object, newSemanticsObject, objects_.get());
317 object = newSemanticsObject;
324 void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(
SemanticsObject*
object,
325 NSMutableArray<NSNumber*>* doomed_uids) {
326 [doomed_uids removeObject:@(
object.uid)];
328 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
334 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
339 return FindFirstFocusable([objects_.get() objectForKey:@(last_focused_semantics_object_id_)]);
344 if (!currentObject) {
347 if (currentObject.isAccessibilityElement) {
348 return currentObject;
361 NSString* type = annotatedEvent[
@"type"];
362 if ([type isEqualToString:
@"announce"]) {
363 NSString* message = annotatedEvent[
@"data"][
@"message"];
364 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
366 if ([type isEqualToString:
@"focus"]) {
368 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
373 return weak_factory_.GetWeakPtr();
377 [objects_ removeAllObjects];
378 previous_route_id_ = 0;
379 previous_routes_.clear();