Flutter Engine
SemanticsObject.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/ios/framework/Source/SemanticsObject.h"
6 
7 #include "flutter/fml/platform/darwin/scoped_nsobject.h"
8 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
9 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
10 
11 namespace {
12 
13 flutter::SemanticsAction GetSemanticsActionForScrollDirection(
14  UIAccessibilityScrollDirection direction) {
15  // To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the
16  // direction the scroll bar moves in and SemanticsAction uses the direction the finger
17  // moves in. However, the horizontal scroll direction matches the SemanticsAction direction.
18  // That is way the following maps vertical opposite of the SemanticsAction, but the horizontal
19  // maps directly.
20  switch (direction) {
21  case UIAccessibilityScrollDirectionRight:
22  case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using
23  // _node.textDirection.
25  case UIAccessibilityScrollDirectionLeft:
26  case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using
27  // _node.textDirection.
29  case UIAccessibilityScrollDirectionUp:
31  case UIAccessibilityScrollDirectionDown:
33  }
34  FML_DCHECK(false); // Unreachable
36 }
37 
38 SkM44 GetGlobalTransform(SemanticsObject* reference) {
39  SkM44 globalTransform = [reference node].transform;
40  for (SemanticsObject* parent = [reference parent]; parent; parent = parent.parent) {
41  globalTransform = parent.node.transform * globalTransform;
42  }
43  return globalTransform;
44 }
45 
46 SkPoint ApplyTransform(SkPoint& point, const SkM44& transform) {
47  SkV4 vector = transform.map(point.x(), point.y(), 0, 1);
48  return SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
49 }
50 
51 CGPoint ConvertPointToGlobal(SemanticsObject* reference, CGPoint local_point) {
52  SkM44 globalTransform = GetGlobalTransform(reference);
53  SkPoint point = SkPoint::Make(local_point.x, local_point.y);
54  point = ApplyTransform(point, globalTransform);
55  // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
56  // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
57  // convert.
58  UIScreen* screen = [[[reference bridge]->view() window] screen];
59  // Screen can be nil if the FlutterView is covered by another native view.
60  CGFloat scale = screen == nil ? [UIScreen mainScreen].scale : screen.scale;
61  auto result = CGPointMake(point.x() / scale, point.y() / scale);
62  return [[reference bridge]->view() convertPoint:result toView:nil];
63 }
64 
65 CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
66  SkM44 globalTransform = GetGlobalTransform(reference);
67 
68  SkPoint quad[4] = {
69  SkPoint::Make(local_rect.origin.x, local_rect.origin.y), // top left
70  SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y), // top right
71  SkPoint::Make(local_rect.origin.x + local_rect.size.width,
72  local_rect.origin.y + local_rect.size.height), // bottom right
73  SkPoint::Make(local_rect.origin.x,
74  local_rect.origin.y + local_rect.size.height) // bottom left
75  };
76  for (auto& point : quad) {
77  point = ApplyTransform(point, globalTransform);
78  }
79  SkRect rect;
80  NSCAssert(rect.setBoundsCheck(quad, 4), @"Transformed points can't form a rect");
81  rect.setBounds(quad, 4);
82 
83  // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
84  // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
85  // convert.
86  UIScreen* screen = [[[reference bridge]->view() window] screen];
87  // Screen can be nil if the FlutterView is covered by another native view.
88  CGFloat scale = screen == nil ? [UIScreen mainScreen].scale : screen.scale;
89  auto result =
90  CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
91  return UIAccessibilityConvertFrameToScreenCoordinates(result, [reference bridge]->view());
92 }
93 
94 } // namespace
95 
96 @implementation FlutterSwitchSemanticsObject {
97  UISwitch* _nativeSwitch;
98 }
99 
100 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
101  uid:(int32_t)uid {
102  self = [super initWithBridge:bridge uid:uid];
103  if (self) {
104  _nativeSwitch = [[UISwitch alloc] init];
105  }
106  return self;
107 }
108 
109 - (void)dealloc {
110  [_nativeSwitch release];
111  [super dealloc];
112 }
113 
114 - (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
115  NSMethodSignature* result = [super methodSignatureForSelector:sel];
116  if (!result) {
117  result = [_nativeSwitch methodSignatureForSelector:sel];
118  }
119  return result;
120 }
121 
122 - (void)forwardInvocation:(NSInvocation*)anInvocation {
123  [anInvocation setTarget:_nativeSwitch];
124  [anInvocation invoke];
125 }
126 
127 - (NSString*)accessibilityValue {
128  if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
129  [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
130  _nativeSwitch.on = YES;
131  } else {
132  _nativeSwitch.on = NO;
133  }
134 
135  if (![self isAccessibilityBridgeAlive]) {
136  return nil;
137  } else {
138  return _nativeSwitch.accessibilityValue;
139  }
140 }
141 
142 - (UIAccessibilityTraits)accessibilityTraits {
143  return _nativeSwitch.accessibilityTraits;
144 }
145 
146 @end // FlutterSwitchSemanticsObject
147 
149 @property(nonatomic, strong) FlutterSemanticsScrollView* scrollView;
150 @end
151 
152 @implementation FlutterScrollableSemanticsObject {
154 }
155 
156 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
157  uid:(int32_t)uid {
158  self = [super initWithBridge:bridge uid:uid];
159  if (self) {
160  _scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
161  [_scrollView setShowsHorizontalScrollIndicator:NO];
162  [_scrollView setShowsVerticalScrollIndicator:NO];
163  [self.bridge->view() addSubview:_scrollView];
164  }
165  return self;
166 }
167 
168 - (void)dealloc {
169  [_scrollView removeFromSuperview];
170  [_scrollView release];
171  [super dealloc];
172 }
173 
174 - (void)accessibilityBridgeDidFinishUpdate {
175  // In order to make iOS think this UIScrollView is scrollable, the following
176  // requirements must be true.
177  // 1. contentSize must be bigger than the frame size.
178  // 2. The scrollable isAccessibilityElement must return YES
179  //
180  // Once the requirements are met, the iOS uses contentOffset to determine
181  // what scroll actions are available. e.g. If the view scrolls vertically and
182  // contentOffset is 0.0, only the scroll down action is available.
183  [_scrollView setFrame:[self accessibilityFrame]];
184  [_scrollView setContentSize:[self contentSizeInternal]];
185  [_scrollView setContentOffset:[self contentOffsetInternal] animated:NO];
186 }
187 
188 - (id)nativeAccessibility {
189  return _scrollView;
190 }
191 
192 // private methods
193 
194 - (float)scrollExtentMax {
195  if (![self isAccessibilityBridgeAlive]) {
196  return 0.0f;
197  }
198  float scrollExtentMax = self.node.scrollExtentMax;
199  if (isnan(scrollExtentMax)) {
200  scrollExtentMax = 0.0f;
201  } else if (!isfinite(scrollExtentMax)) {
202  scrollExtentMax = kScrollExtentMaxForInf + [self scrollPosition];
203  }
204  return scrollExtentMax;
205 }
206 
207 - (float)scrollPosition {
208  if (![self isAccessibilityBridgeAlive]) {
209  return 0.0f;
210  }
211  float scrollPosition = self.node.scrollPosition;
212  if (isnan(scrollPosition)) {
213  scrollPosition = 0.0f;
214  }
215  NSCAssert(isfinite(scrollPosition), @"The scrollPosition must not be infinity");
216  return scrollPosition;
217 }
218 
219 - (CGSize)contentSizeInternal {
220  CGRect result;
221  const SkRect& rect = self.node.rect;
222 
224  result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [self scrollExtentMax]);
226  result = CGRectMake(rect.x(), rect.y(), rect.width() + [self scrollExtentMax], rect.height());
227  } else {
228  result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
229  }
230  return ConvertRectToGlobal(self, result).size;
231 }
232 
233 - (CGPoint)contentOffsetInternal {
234  CGPoint result;
235  CGPoint origin = _scrollView.frame.origin;
236  const SkRect& rect = self.node.rect;
238  result = ConvertPointToGlobal(self, CGPointMake(rect.x(), rect.y() + [self scrollPosition]));
240  result = ConvertPointToGlobal(self, CGPointMake(rect.x() + [self scrollPosition], rect.y()));
241  } else {
242  result = origin;
243  }
244  return CGPointMake(result.x - origin.x, result.y - origin.y);
245 }
246 
247 @end // FlutterScrollableSemanticsObject
248 
249 @implementation FlutterCustomAccessibilityAction {
250 }
251 @end
252 
253 @interface SemanticsObject ()
254 /** Should only be called in conjunction with setting child/parent relationship. */
255 - (void)privateSetParent:(SemanticsObject*)parent;
256 @end
257 
258 @implementation SemanticsObject {
260  NSMutableArray<SemanticsObject*>* _children;
262 }
263 
264 #pragma mark - Override base class designated initializers
265 
266 // Method declared as unavailable in the interface
267 - (instancetype)init {
268  [self release];
269  [super doesNotRecognizeSelector:_cmd];
270  return nil;
271 }
272 
273 #pragma mark - Designated initializers
274 
275 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
276  uid:(int32_t)uid {
277  FML_DCHECK(bridge) << "bridge must be set";
278  FML_DCHECK(uid >= kRootNodeId);
279  // Initialize with the UIView as the container.
280  // The UIView will not necessarily be accessibility parent for this object.
281  // The bridge informs the OS of the actual structure via
282  // `accessibilityContainer` and `accessibilityElementAtIndex`.
283  self = [super initWithAccessibilityContainer:bridge->view()];
284 
285  if (self) {
286  _bridge = bridge;
287  _uid = uid;
288  _children = [[NSMutableArray alloc] init];
289  }
290 
291  return self;
292 }
293 
294 - (void)dealloc {
295  for (SemanticsObject* child in _children) {
296  [child privateSetParent:nil];
297  }
298  [_children removeAllObjects];
299  [_children release];
300  _parent = nil;
301  _container.get().semanticsObject = nil;
302  [_platformViewSemanticsContainer release];
303  _inDealloc = YES;
304  [super dealloc];
305 }
306 
307 #pragma mark - Semantic object property accesser
308 
309 - (void)setChildren:(NSArray<SemanticsObject*>*)children {
310  for (SemanticsObject* child in _children) {
311  [child privateSetParent:nil];
312  }
313  [_children release];
314  _children = [[NSMutableArray alloc] initWithArray:children];
315  for (SemanticsObject* child in _children) {
316  [child privateSetParent:self];
317  }
318 }
319 
320 - (BOOL)hasChildren {
321  if (_node.IsPlatformViewNode()) {
322  return YES;
323  }
324  return [self.children count] != 0;
325 }
326 
327 #pragma mark - Semantic object method
328 
329 - (BOOL)isAccessibilityBridgeAlive {
330  return [self bridge].get() != nil;
331 }
332 
333 - (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
334  _node = *node;
335 }
336 
337 - (void)accessibilityBridgeDidFinishUpdate { /* Do nothing by default */
338 }
339 
340 /**
341  * Whether calling `setSemanticsNode:` with `node` would cause a layout change.
342  */
343 - (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node {
344  return [self node].rect != node->rect || [self node].transform != node->transform;
345 }
346 
347 /**
348  * Whether calling `setSemanticsNode:` with `node` would cause a scroll event.
349  */
350 - (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node {
351  return !isnan([self node].scrollPosition) && !isnan(node->scrollPosition) &&
352  [self node].scrollPosition != node->scrollPosition;
353 }
354 
355 /**
356  * Whether calling `setSemanticsNode:` with `node` should trigger an
357  * announcement.
358  */
359 - (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node {
360  // The node dropped the live region flag, if it ever had one.
361  if (!node || !node->HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
362  return NO;
363  }
364 
365  // The node has gained a new live region flag, always announce.
366  if (![self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
367  return YES;
368  }
369 
370  // The label has updated, and the new node has a live region flag.
371  return [self node].label != node->label;
372 }
373 
374 - (void)replaceChildAtIndex:(NSInteger)index withChild:(SemanticsObject*)child {
375  SemanticsObject* oldChild = _children[index];
376  [oldChild privateSetParent:nil];
377  [child privateSetParent:self];
378  [_children replaceObjectAtIndex:index withObject:child];
379 }
380 
381 - (NSString*)routeName {
382  // Returns the first non-null and non-empty semantic label of a child
383  // with an NamesRoute flag. Otherwise returns nil.
384  if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
385  NSString* newName = [self accessibilityLabel];
386  if (newName != nil && [newName length] > 0) {
387  return newName;
388  }
389  }
390  if ([self hasChildren]) {
391  for (SemanticsObject* child in self.children) {
392  NSString* newName = [child routeName];
393  if (newName != nil && [newName length] > 0) {
394  return newName;
395  }
396  }
397  }
398  return nil;
399 }
400 
401 - (id)nativeAccessibility {
402  return self;
403 }
404 
405 #pragma mark - Semantic object private method
406 
407 - (void)privateSetParent:(SemanticsObject*)parent {
408  _parent = parent;
409 }
410 
411 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
412  withAttributes:
413  (const flutter::StringAttributes&)attributes {
414  NSMutableAttributedString* attributedString =
415  [[NSMutableAttributedString alloc] initWithString:string];
416  for (const auto& attribute : attributes) {
417  NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
418  switch (attribute->type) {
420  std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
421  std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
422  NSDictionary* attributeDict = @{
423  UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
424  };
425  [attributedString setAttributes:attributeDict range:range];
426  break;
427  }
429  if (@available(iOS 13.0, *)) {
430  NSDictionary* attributeDict = @{
431  UIAccessibilitySpeechAttributeSpellOut : @YES,
432  };
433  [attributedString setAttributes:attributeDict range:range];
434  }
435  break;
436  }
437  }
438  }
439  return attributedString;
440 }
441 
442 #pragma mark - UIAccessibility overrides
443 
444 - (BOOL)isAccessibilityElement {
445  if (![self isAccessibilityBridgeAlive])
446  return false;
447 
448  // Note: hit detection will only apply to elements that report
449  // -isAccessibilityElement of YES. The framework will continue scanning the
450  // entire element tree looking for such a hit.
451 
452  // We enforce in the framework that no other useful semantics are merged with these nodes.
453  if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute))
454  return false;
455 
456  // If the node is scrollable AND hidden OR
457  // The node has a label, value, or hint OR
458  // The node has non-scrolling related actions.
459  //
460  // The kIsHidden flag set with the scrollable flag means this node is now
461  // hidden but still is a valid target for a11y focus in the tree, e.g. a list
462  // item that is currently off screen but the a11y navigation needs to know
463  // about.
464  return (([self node].flags & flutter::kScrollableSemanticsFlags) != 0 &&
465  ([self node].flags & static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden)) != 0) ||
466  ![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() ||
467  ([self node].actions & ~flutter::kScrollableSemanticsActions) != 0;
468 }
469 
470 - (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges {
471  if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute))
472  [edges addObject:self];
473  if ([self hasChildren]) {
474  for (SemanticsObject* child in self.children) {
475  [child collectRoutes:edges];
476  }
477  }
478 }
479 
480 - (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action {
481  if (![self node].HasAction(flutter::SemanticsAction::kCustomAction))
482  return NO;
483  int32_t action_id = action.uid;
484  std::vector<uint8_t> args;
485  args.push_back(3); // type=int32.
486  args.push_back(action_id);
487  args.push_back(action_id >> 8);
488  args.push_back(action_id >> 16);
489  args.push_back(action_id >> 24);
490  [self bridge]->DispatchSemanticsAction(
492  fml::MallocMapping::Copy(args.data(), args.size() * sizeof(uint8_t)));
493  return YES;
494 }
495 
496 - (NSString*)accessibilityLabel {
497  if (![self isAccessibilityBridgeAlive])
498  return nil;
499  NSString* label = nil;
500  if (![self node].label.empty()) {
501  label = @([self node].label.data());
502  }
503  if (![self node].tooltip.empty()) {
504  label = label ? [NSString stringWithFormat:@"%@\n%@", label, @([self node].tooltip.data())]
505  : @([self node].tooltip.data());
506  }
507  return label;
508 }
509 
510 - (NSAttributedString*)accessibilityAttributedLabel {
511  NSString* label = [self accessibilityLabel];
512  if (label.length == 0)
513  return nil;
514  return [self createAttributedStringFromString:label withAttributes:[self node].labelAttributes];
515 }
516 
517 - (NSString*)accessibilityHint {
518  if (![self isAccessibilityBridgeAlive])
519  return nil;
520 
521  if ([self node].hint.empty())
522  return nil;
523  return @([self node].hint.data());
524 }
525 
526 - (NSAttributedString*)accessibilityAttributedHint {
527  NSString* hint = [self accessibilityHint];
528  if (hint.length == 0)
529  return nil;
530  return [self createAttributedStringFromString:hint withAttributes:[self node].hintAttributes];
531 }
532 
533 - (NSString*)accessibilityValue {
534  if (![self isAccessibilityBridgeAlive])
535  return nil;
536 
537  if (![self node].value.empty()) {
538  return @([self node].value.data());
539  }
540 
541  // FlutterSwitchSemanticsObject should supercede these conditionals.
542  if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
544  if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
545  [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
546  return @"1";
547  } else {
548  return @"0";
549  }
550  }
551 
552  return nil;
553 }
554 
555 - (NSAttributedString*)accessibilityAttributedValue {
556  NSString* value = [self accessibilityValue];
557  if (value.length == 0)
558  return nil;
559  return [self createAttributedStringFromString:value withAttributes:[self node].valueAttributes];
560 }
561 
562 - (CGRect)accessibilityFrame {
563  if (![self isAccessibilityBridgeAlive])
564  return CGRectMake(0, 0, 0, 0);
565 
566  if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
567  return [super accessibilityFrame];
568  }
569  return [self globalRect];
570 }
571 
572 - (CGRect)globalRect {
573  const SkRect& rect = [self node].rect;
574  CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
575  return ConvertRectToGlobal(self, localRect);
576 }
577 
578 #pragma mark - UIAccessibilityElement protocol
579 
580 - (void)setAccessibilityContainer:(id)container {
581  // Explicit noop. The containers are calculated lazily in `accessibilityContainer`.
582  // See also: https://github.com/flutter/flutter/issues/54366
583 }
584 
585 - (id)accessibilityContainer {
586  if (_inDealloc) {
587  // In iOS9, `accessibilityContainer` will be called by `[UIAccessibilityElementSuperCategory
588  // dealloc]` during `[super dealloc]`. And will crash when accessing `_children` which has
589  // called `[_children release]` in `[SemanticsObject dealloc]`.
590  // https://github.com/flutter/flutter/issues/87247
591  return nil;
592  }
593 
594  if (![self isAccessibilityBridgeAlive]) {
595  return nil;
596  }
597 
598  if ([self hasChildren] || [self uid] == kRootNodeId) {
599  if (_container == nil)
600  _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self
601  bridge:[self bridge]]);
602  return _container.get();
603  }
604  if ([self parent] == nil) {
605  // This can happen when we have released the accessibility tree but iOS is
606  // still holding onto our objects. iOS can take some time before it
607  // realizes that the tree has changed.
608  return nil;
609  }
610  return [[self parent] accessibilityContainer];
611 }
612 
613 #pragma mark - UIAccessibilityAction overrides
614 
615 - (BOOL)accessibilityActivate {
616  if (![self isAccessibilityBridgeAlive])
617  return NO;
618  if (![self node].HasAction(flutter::SemanticsAction::kTap))
619  return NO;
620  [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap);
621  return YES;
622 }
623 
624 - (void)accessibilityIncrement {
625  if (![self isAccessibilityBridgeAlive])
626  return;
627  if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) {
628  [self node].value = [self node].increasedValue;
629  [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease);
630  }
631 }
632 
633 - (void)accessibilityDecrement {
634  if (![self isAccessibilityBridgeAlive])
635  return;
636  if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) {
637  [self node].value = [self node].decreasedValue;
638  [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease);
639  }
640 }
641 
642 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
643  if (![self isAccessibilityBridgeAlive])
644  return NO;
645  flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction);
646  if (![self node].HasAction(action))
647  return NO;
648  [self bridge]->DispatchSemanticsAction([self uid], action);
649  return YES;
650 }
651 
652 - (BOOL)accessibilityPerformEscape {
653  if (![self isAccessibilityBridgeAlive])
654  return NO;
655  if (![self node].HasAction(flutter::SemanticsAction::kDismiss))
656  return NO;
657  [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss);
658  return YES;
659 }
660 
661 #pragma mark UIAccessibilityFocus overrides
662 
663 - (void)accessibilityElementDidBecomeFocused {
664  if (![self isAccessibilityBridgeAlive])
665  return;
666  [self bridge]->AccessibilityObjectDidBecomeFocused([self uid]);
667  if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) ||
668  [self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
669  [self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen);
670  }
672  [self bridge]->DispatchSemanticsAction([self uid],
674  }
675 }
676 
677 - (void)accessibilityElementDidLoseFocus {
678  if (![self isAccessibilityBridgeAlive])
679  return;
680  [self bridge]->AccessibilityObjectDidLoseFocus([self uid]);
682  [self bridge]->DispatchSemanticsAction([self uid],
684  }
685 }
686 
687 @end
688 
689 @implementation FlutterSemanticsObject {
690 }
691 
692 #pragma mark - Override base class designated initializers
693 
694 // Method declared as unavailable in the interface
695 - (instancetype)init {
696  [self release];
697  [super doesNotRecognizeSelector:_cmd];
698  return nil;
699 }
700 
701 #pragma mark - Designated initializers
702 
703 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
704  uid:(int32_t)uid {
705  self = [super initWithBridge:bridge uid:uid];
706  return self;
707 }
708 
709 #pragma mark - UIAccessibility overrides
710 
711 - (UIAccessibilityTraits)accessibilityTraits {
712  UIAccessibilityTraits traits = UIAccessibilityTraitNone;
713  if ([self node].HasAction(flutter::SemanticsAction::kIncrease) ||
714  [self node].HasAction(flutter::SemanticsAction::kDecrease)) {
715  traits |= UIAccessibilityTraitAdjustable;
716  }
717  // FlutterSwitchSemanticsObject should supercede these conditionals.
718  if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
720  traits |= UIAccessibilityTraitButton;
721  }
722  if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) {
723  traits |= UIAccessibilityTraitSelected;
724  }
725  if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) {
726  traits |= UIAccessibilityTraitButton;
727  }
728  if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
729  ![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
730  traits |= UIAccessibilityTraitNotEnabled;
731  }
732  if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
733  traits |= UIAccessibilityTraitHeader;
734  }
735  if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) {
736  traits |= UIAccessibilityTraitImage;
737  }
738  if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
739  traits |= UIAccessibilityTraitUpdatesFrequently;
740  }
741  if ([self node].HasFlag(flutter::SemanticsFlags::kIsLink)) {
742  traits |= UIAccessibilityTraitLink;
743  }
744  if (traits == UIAccessibilityTraitNone && ![self hasChildren] &&
745  [[self accessibilityLabel] length] != 0 &&
746  ![self node].HasFlag(flutter::SemanticsFlags::kIsTextField)) {
747  traits = UIAccessibilityTraitStaticText;
748  }
749  return traits;
750 }
751 
752 @end
753 
755 @property(nonatomic, assign) SemanticsObject* semanticsObject;
756 @property(nonatomic, strong) UIView* platformView;
757 @end
758 
760 
761 // Method declared as unavailable in the interface
762 - (instancetype)init {
763  [self release];
764  [super doesNotRecognizeSelector:_cmd];
765  return nil;
766 }
767 
768 - (instancetype)initWithSemanticsObject:(SemanticsObject*)object {
769  FML_CHECK(object);
770  // Initialize with the UIView as the container.
771  // The UIView will not necessarily be accessibility parent for this object.
772  // The bridge informs the OS of the actual structure via
773  // `accessibilityContainer` and `accessibilityElementAtIndex`.
774  if (self = [super initWithAccessibilityContainer:object.bridge->view()]) {
775  _semanticsObject = object;
776  auto controller = object.bridge->GetPlatformViewsController();
777  if (controller) {
778  _platformView = [controller->GetPlatformViewByID(object.node.platformViewId) retain];
779  }
780  }
781  return self;
782 }
783 
784 - (void)dealloc {
785  [_platformView release];
786  _platformView = nil;
787  [super dealloc];
788 }
789 
790 #pragma mark - UIAccessibilityContainer overrides
791 
792 - (NSInteger)accessibilityElementCount {
793  // This container should only contain 2 elements:
794  // 1. The semantic object that represents this container.
795  // 2. The platform view object.
796  return 2;
797 }
798 
799 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
800  FML_DCHECK(index < 2);
801  if (index == 0) {
802  return _semanticsObject.nativeAccessibility;
803  } else {
804  return _platformView;
805  }
806 }
807 
808 - (NSInteger)indexOfAccessibilityElement:(id)element {
809  FML_DCHECK(element == _semanticsObject || element == _platformView);
810  if (element == _semanticsObject) {
811  return 0;
812  } else {
813  return 1;
814  }
815 }
816 
817 #pragma mark - UIAccessibilityElement overrides
818 
819 - (CGRect)accessibilityFrame {
820  return _semanticsObject.accessibilityFrame;
821 }
822 
823 - (BOOL)isAccessibilityElement {
824  return NO;
825 }
826 
827 - (id)accessibilityContainer {
828  return [_semanticsObject accessibilityContainer];
829 }
830 
831 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
832  return [_platformView accessibilityScroll:direction];
833 }
834 
835 @end
836 
837 @implementation SemanticsObjectContainer {
838  SemanticsObject* _semanticsObject;
840 }
841 
842 #pragma mark - initializers
843 
844 // Method declared as unavailable in the interface
845 - (instancetype)init {
846  [self release];
847  [super doesNotRecognizeSelector:_cmd];
848  return nil;
849 }
850 
851 - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
852  bridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge {
853  FML_DCHECK(semanticsObject) << "semanticsObject must be set";
854  // Initialize with the UIView as the container.
855  // The UIView will not necessarily be accessibility parent for this object.
856  // The bridge informs the OS of the actual structure via
857  // `accessibilityContainer` and `accessibilityElementAtIndex`.
858  self = [super initWithAccessibilityContainer:bridge->view()];
859 
860  if (self) {
861  _semanticsObject = semanticsObject;
862  _bridge = bridge;
863  }
864 
865  return self;
866 }
867 
868 #pragma mark - UIAccessibilityContainer overrides
869 
870 - (NSInteger)accessibilityElementCount {
871  NSInteger count = [[_semanticsObject children] count] + 1;
872  return count;
873 }
874 
875 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
876  if (index < 0 || index >= [self accessibilityElementCount])
877  return nil;
878  if (index == 0) {
879  return _semanticsObject.nativeAccessibility;
880  }
881 
882  SemanticsObject* child = [_semanticsObject children][index - 1];
883 
884  // Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer`
885  if (child.node.IsPlatformViewNode()) {
887  return child.platformViewSemanticsContainer;
888  }
889 
890  if ([child hasChildren])
891  return [child accessibilityContainer];
892  return child.nativeAccessibility;
893 }
894 
895 - (NSInteger)indexOfAccessibilityElement:(id)element {
896  if (element == _semanticsObject)
897  return 0;
898 
899  // FlutterPlatformViewSemanticsContainer is always the last element of its parent.
900  if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) {
901  return ((FlutterPlatformViewSemanticsContainer*)element).index;
902  }
903 
904  NSArray<SemanticsObject*>* children = [_semanticsObject children];
905  for (size_t i = 0; i < [children count]; i++) {
906  SemanticsObject* child = children[i];
907  if ((![child hasChildren] && child == element) ||
908  ([child hasChildren] && [child accessibilityContainer] == element))
909  return i + 1;
910  }
911  return NSNotFound;
912 }
913 
914 #pragma mark - UIAccessibilityElement protocol
915 
916 - (BOOL)isAccessibilityElement {
917  return NO;
918 }
919 
920 - (CGRect)accessibilityFrame {
921  return [_semanticsObject accessibilityFrame];
922 }
923 
924 - (id)accessibilityContainer {
925  if (!_bridge) {
926  return nil;
927  }
928  return ([_semanticsObject uid] == kRootNodeId)
929  ? _bridge->view()
930  : [[_semanticsObject parent] accessibilityContainer];
931 }
932 
933 #pragma mark - UIAccessibilityAction overrides
934 
935 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
936  return [_semanticsObject accessibilityScroll:direction];
937 }
938 
939 @end
G_BEGIN_DECLS FlValue * args
const int kVerticalScrollSemanticsActions
fml::WeakPtr< flutter::AccessibilityBridgeIos > _bridge
constexpr float kScrollExtentMaxForInf
bool IsPlatformViewNode() const
#define FML_DCHECK(condition)
Definition: logging.h:86
FlutterPlatformViewSemanticsContainer * platformViewSemanticsContainer
auto WeakPtr(std::shared_ptr< T > pointer)
GAsyncResult * result
const int kScrollableSemanticsFlags
const int kHorizontalScrollSemanticsActions
std::vector< StringAttributePtr > StringAttributes
FlutterSemanticsFlag flags
Definition: ascii_trie.cc:9
const int kScrollableSemanticsActions
BOOL _inDealloc
uint8_t value
const FlutterSemanticsNode * node
Definition: fl_view.cc:83
fml::scoped_nsobject< UIScrollView > _scrollView
virtual std::shared_ptr< FlutterPlatformViewsController > GetPlatformViewsController() const =0
fml::WeakPtr< flutter::AccessibilityBridgeIos > bridge
SemanticsAction action
SemanticsObject * parent
size_t length
static MallocMapping Copy(const T *begin, const T *end)
Definition: mapping.h:147
int BOOL
Definition: windows_types.h:37
constexpr int32_t kRootNodeId
#define FML_CHECK(condition)
Definition: logging.h:68
FlView * view
NSMutableArray< SemanticsObject * > * _children
FlutterSemanticsAction actions
The set of semantics actions applicable to this node.
Definition: embedder.h:802
SemanticsObject * semanticsObject
flutter::SemanticsNode node
int32_t id
virtual UIView * view() const =0