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);
46 std::unique_ptr<IosDelegate> ios_delegate)
47 : view_controller_(view_controller),
49 platform_views_controller_(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().engine.binaryMessenger
60 [accessibility_channel_ setMessageHandler:^(
id message,
FlutterReply reply) {
66 [accessibility_channel_ setMessageHandler:nil];
68 view_controller_.viewIfLoaded.accessibilityElements = nil;
76 last_focused_semantics_object_id_ = id;
77 [accessibility_channel_ 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 = [[NSMutableArray alloc] initWithCapacity:newChildCount];
105 for (NSUInteger i = 0; i < newChildCount; ++i) {
106 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
107 [newChildren addObject:child];
109 NSMutableArray* newChildrenInHitTestOrder =
110 [[NSMutableArray alloc] initWithCapacity:newChildCount];
111 for (NSUInteger i = 0; i < newChildCount; ++i) {
112 SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
113 [newChildrenInHitTestOrder addObject:child];
115 object.children = newChildren;
116 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
117 if (!node.customAccessibilityActions.empty()) {
118 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
119 [[NSMutableArray alloc] init];
120 for (int32_t action_id : node.customAccessibilityActions) {
121 flutter::CustomAccessibilityAction& action = actions_[action_id];
122 if (action.overrideId != -1) {
127 NSString* label = @(action.label.data());
128 SEL selector =
@selector(onCustomAccessibilityAction:);
133 customAction.
uid = action_id;
134 [accessibilityCustomActions addObject:customAction];
136 object.accessibilityCustomActions = accessibilityCustomActions;
139 if (needsAnnouncement) {
145 NSString* announcement = [[NSString alloc] initWithUTF8String:
object.node.label.c_str()];
146 UIAccessibilityPostNotification(
147 UIAccessibilityAnnouncementNotification,
148 [[NSAttributedString alloc] initWithString:announcement
150 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
157 bool routeChanged =
false;
161 if (!view_controller_.view.accessibilityElements) {
162 view_controller_.view.accessibilityElements =
163 @[ [root accessibilityContainer] ?: [NSNull
null] ];
165 NSMutableArray<SemanticsObject*>* newRoutes = [[NSMutableArray alloc] init];
166 [root collectRoutes:newRoutes];
169 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
170 previous_routes_.end()) {
175 if (lastAdded == nil && [newRoutes count] > 0) {
176 int index = [newRoutes count] - 1;
177 lastAdded = [newRoutes objectAtIndex:index];
186 if (lastAdded != nil &&
187 ([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
188 previous_route_id_ = [lastAdded uid];
191 previous_routes_.clear();
193 previous_routes_.push_back([route uid]);
196 view_controller_.viewIfLoaded.accessibilityElements = nil;
199 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:objects_.allKeys];
201 VisitObjectsRecursivelyAndRemove(root, doomed_uids);
203 [objects_ removeObjectsForKeys:doomed_uids];
206 [
object accessibilityBridgeDidFinishUpdate];
209 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
210 layoutChanged = layoutChanged || [doomed_uids count] > 0;
213 NSString* routeName = [lastAdded routeName];
214 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
220 SemanticsObject* lastFocused = [objects_ objectForKey:@(last_focused_semantics_object_id_)];
224 ios_delegate_->PostAccessibilityNotification(
225 UIAccessibilityLayoutChangedNotification,
227 }
else if (scrollOccured) {
231 ios_delegate_->PostAccessibilityNotification(
232 UIAccessibilityPageScrolledNotification,
239 platform_view_->DispatchSemanticsAction(uid, action, {});
243 flutter::SemanticsAction action,
244 fml::MallocMapping args) {
245 platform_view_->DispatchSemanticsAction(uid, action, std::move(args));
250 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
252 FML_DCHECK(oldObject.
node.id == newObject.
uid);
253 NSNumber* nodeId = @(oldObject.
node.id);
254 NSUInteger positionInChildlist = [oldObject.
parent.
children indexOfObject:oldObject];
256 [oldObject.
parent replaceChildAtIndex:positionInChildlist withChild:newObject];
257 [objects removeObjectForKey:nodeId];
258 objects[nodeId] = newObject;
261 static SemanticsObject* CreateObject(
const flutter::SemanticsNode& node,
262 const fml::WeakPtr<AccessibilityBridge>& weak_ptr) {
263 if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) &&
264 !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
267 }
else if (!node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) &&
268 (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
269 node.HasFlag(flutter::SemanticsFlags::kHasCheckedState))) {
271 }
else if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) {
273 }
else if (node.IsPlatformViewNode()) {
275 weak_ptr->GetPlatformViewsController();
277 [platformViewsController flutterTouchInterceptingViewForId:node.platformViewId];
280 platformView:touchInterceptingView];
286 static bool DidFlagChange(
const flutter::SemanticsNode& oldNode,
287 const flutter::SemanticsNode& newNode,
288 SemanticsFlags flag) {
289 return oldNode.HasFlag(flag) != newNode.HasFlag(flag);
293 flutter::SemanticsNodeUpdates& updates) {
296 object = CreateObject(updates[uid],
GetWeakPtr());
297 objects_[@(uid)] =
object;
300 auto nodeEntry = updates.find(
object.node.id);
301 if (nodeEntry != updates.end()) {
303 flutter::SemanticsNode node = nodeEntry->second;
304 if (DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsTextField) ||
305 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsReadOnly) ||
306 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasCheckedState) ||
307 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasToggledState) ||
308 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasImplicitScrolling)) {
313 ReplaceSemanticsObject(
object, newSemanticsObject, objects_);
314 object = newSemanticsObject;
321 void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(
SemanticsObject*
object,
322 NSMutableArray<NSNumber*>* doomed_uids) {
323 [doomed_uids removeObject:@(
object.uid)];
325 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
331 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
336 return FindFirstFocusable(objects_[@(last_focused_semantics_object_id_)]);
341 if (!currentObject) {
344 if (currentObject.isAccessibilityElement) {
345 return currentObject;
358 NSString* type = annotatedEvent[
@"type"];
359 if ([type isEqualToString:
@"announce"]) {
360 NSString* message = annotatedEvent[
@"data"][
@"message"];
361 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
363 if ([type isEqualToString:
@"focus"]) {
365 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
370 return weak_factory_.GetWeakPtr();
374 [objects_ removeAllObjects];
375 previous_route_id_ = 0;
376 previous_routes_.clear();