7 #include "flutter/fml/platform/darwin/scoped_nsobject.h"
13 flutter::SemanticsAction GetSemanticsActionForScrollDirection(
14 UIAccessibilityScrollDirection direction) {
21 case UIAccessibilityScrollDirectionRight:
22 case UIAccessibilityScrollDirectionPrevious:
24 return flutter::SemanticsAction::kScrollRight;
25 case UIAccessibilityScrollDirectionLeft:
26 case UIAccessibilityScrollDirectionNext:
28 return flutter::SemanticsAction::kScrollLeft;
29 case UIAccessibilityScrollDirectionUp:
30 return flutter::SemanticsAction::kScrollDown;
31 case UIAccessibilityScrollDirectionDown:
32 return flutter::SemanticsAction::kScrollUp;
35 return flutter::SemanticsAction::kScrollUp;
39 SkM44 globalTransform = [reference node].transform;
41 globalTransform = parent.node.transform * globalTransform;
43 return globalTransform;
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);
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);
58 UIScreen* screen = [[[reference bridge]->view() window] screen];
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];
65 CGRect ConvertRectToGlobal(
SemanticsObject* reference, CGRect local_rect) {
66 SkM44 globalTransform = GetGlobalTransform(reference);
69 SkPoint::Make(local_rect.origin.x, local_rect.origin.y),
70 SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y),
71 SkPoint::Make(local_rect.origin.x + local_rect.size.width,
72 local_rect.origin.y + local_rect.size.height),
73 SkPoint::Make(local_rect.origin.x,
74 local_rect.origin.y + local_rect.size.height)
76 for (
auto& point : quad) {
77 point = ApplyTransform(point, globalTransform);
80 NSCAssert(rect.setBoundsCheck(quad, 4),
@"Transformed points can't form a rect");
81 rect.setBounds(quad, 4);
86 UIScreen* screen = [[[reference bridge]->view() window] screen];
88 CGFloat scale = screen == nil ? [UIScreen mainScreen].scale : screen.scale;
90 CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
91 return UIAccessibilityConvertFrameToScreenCoordinates(result, [reference bridge]->view());
97 @property(nonatomic, readonly) UISwitch* nativeSwitch;
102 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
104 self = [
super initWithBridge:bridge uid:uid];
106 _nativeSwitch = [[UISwitch alloc] init];
112 [_nativeSwitch release];
116 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)sel {
117 NSMethodSignature* result = [
super methodSignatureForSelector:sel];
119 result = [_nativeSwitch methodSignatureForSelector:sel];
124 - (void)forwardInvocation:(NSInvocation*)anInvocation {
125 [anInvocation setTarget:_nativeSwitch];
126 [anInvocation invoke];
129 - (NSString*)accessibilityValue {
130 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
131 [
self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
132 _nativeSwitch.on = YES;
134 _nativeSwitch.on = NO;
140 return _nativeSwitch.accessibilityValue;
144 - (UIAccessibilityTraits)accessibilityTraits {
145 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
146 _nativeSwitch.enabled = YES;
148 _nativeSwitch.enabled = NO;
151 return _nativeSwitch.accessibilityTraits;
162 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
164 self = [
super initWithBridge:bridge uid:uid];
167 [_scrollView setShowsHorizontalScrollIndicator:NO];
168 [_scrollView setShowsVerticalScrollIndicator:NO];
169 [
self.bridge->view() addSubview:_scrollView];
175 [_scrollView removeFromSuperview];
177 [_scrollView release];
190 [_scrollView setFrame:[
self accessibilityFrame]];
191 [_scrollView setContentSize:[
self contentSizeInternal]];
192 [_scrollView setContentOffset:[
self contentOffsetInternal] animated:NO];
201 - (float)scrollExtentMax {
205 float scrollExtentMax =
self.node.scrollExtentMax;
206 if (isnan(scrollExtentMax)) {
207 scrollExtentMax = 0.0f;
208 }
else if (!isfinite(scrollExtentMax)) {
211 return scrollExtentMax;
214 - (float)scrollPosition {
218 float scrollPosition =
self.node.scrollPosition;
219 if (isnan(scrollPosition)) {
220 scrollPosition = 0.0f;
222 NSCAssert(isfinite(scrollPosition),
@"The scrollPosition must not be infinity");
223 return scrollPosition;
226 - (CGSize)contentSizeInternal {
228 const SkRect& rect =
self.node.rect;
230 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
231 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [
self scrollExtentMax]);
232 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
233 result = CGRectMake(rect.x(), rect.y(), rect.width() + [
self scrollExtentMax], rect.height());
235 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
237 return ConvertRectToGlobal(
self, result).size;
240 - (CGPoint)contentOffsetInternal {
243 const SkRect& rect =
self.node.rect;
244 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
245 result = ConvertPointToGlobal(
self, CGPointMake(rect.x(), rect.y() + [
self scrollPosition]));
246 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
247 result = ConvertPointToGlobal(
self, CGPointMake(rect.x() + [
self scrollPosition], rect.y()));
251 return CGPointMake(result.x - origin.x, result.y - origin.y);
266 fml::scoped_nsobject<SemanticsObjectContainer> _container;
267 NSMutableArray<SemanticsObject*>*
_children;
272 #pragma mark - Override base class designated initializers
275 - (instancetype)init {
277 [
super doesNotRecognizeSelector:_cmd];
281 #pragma mark - Designated initializers
283 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
285 FML_DCHECK(bridge) <<
"bridge must be set";
291 self = [
super initWithAccessibilityContainer:bridge->view()];
296 _children = [[NSMutableArray alloc] init];
305 [child privateSetParent:nil];
307 [_children removeAllObjects];
308 [_childrenInHitTestOrder removeAllObjects];
310 [_childrenInHitTestOrder release];
313 _container.get().semanticsObject = nil;
318 #pragma mark - Semantic object property accesser
322 [child privateSetParent:nil];
325 _children = [[NSMutableArray alloc] initWithArray:children];
327 [child privateSetParent:self];
331 - (void)setChildrenInHitTestOrder:(NSArray<
SemanticsObject*>*)childrenInHitTestOrder {
333 [child privateSetParent:nil];
335 [_childrenInHitTestOrder release];
338 [child privateSetParent:self];
342 - (BOOL)hasChildren {
343 return [
self.children count] != 0;
346 #pragma mark - Semantic object method
348 - (BOOL)isAccessibilityBridgeAlive {
349 return [
self bridge].get() != nil;
352 - (void)setSemanticsNode:(const
flutter::SemanticsNode*)node {
356 - (void)accessibilityBridgeDidFinishUpdate {
362 - (BOOL)nodeWillCauseLayoutChange:(const
flutter::SemanticsNode*)node {
363 return [
self node].rect != node->rect || [
self node].transform != node->transform;
369 - (BOOL)nodeWillCauseScroll:(const
flutter::SemanticsNode*)node {
370 return !isnan([
self node].scrollPosition) && !isnan(node->scrollPosition) &&
371 [
self node].scrollPosition != node->scrollPosition;
378 - (BOOL)nodeShouldTriggerAnnouncement:(const
flutter::SemanticsNode*)node {
380 if (!node || !node->HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
385 if (![
self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
390 return [
self node].label != node->label;
395 [oldChild privateSetParent:nil];
396 [child privateSetParent:self];
397 [_children replaceObjectAtIndex:index withObject:child];
400 - (NSString*)routeName {
403 if ([
self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
404 NSString* newName = [
self accessibilityLabel];
405 if (newName != nil && [newName length] > 0) {
409 if ([
self hasChildren]) {
411 NSString* newName = [child routeName];
412 if (newName != nil && [newName length] > 0) {
420 - (id)nativeAccessibility {
424 #pragma mark - Semantic object private method
430 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
432 (const
flutter::StringAttributes&)attributes {
433 NSMutableAttributedString* attributedString =
434 [[[NSMutableAttributedString alloc] initWithString:string] autorelease];
435 for (
const auto& attribute : attributes) {
436 NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
437 switch (attribute->type) {
438 case flutter::StringAttributeType::kLocale: {
439 std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
440 std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
441 NSDictionary* attributeDict = @{
442 UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
444 [attributedString setAttributes:attributeDict range:range];
447 case flutter::StringAttributeType::kSpellOut: {
448 if (@available(iOS 13.0, *)) {
449 NSDictionary* attributeDict = @{
450 UIAccessibilitySpeechAttributeSpellOut : @YES,
452 [attributedString setAttributes:attributeDict range:range];
458 return attributedString;
461 - (void)showOnScreen {
462 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kShowOnScreen);
465 #pragma mark - UIAccessibility overrides
467 - (BOOL)isAccessibilityElement {
468 if (![
self isAccessibilityBridgeAlive]) {
477 if ([
self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
481 return [
self isFocusable];
484 - (bool)isFocusable {
493 return (([
self node].flags & flutter::kScrollableSemanticsFlags) != 0 &&
494 ([
self node].flags &
static_cast<int32_t
>(flutter::SemanticsFlags::kIsHidden)) != 0) ||
495 ![
self node].label.empty() || ![
self node].value.empty() || ![
self node].hint.empty() ||
496 ([
self node].actions & ~
flutter::kScrollableSemanticsActions) != 0;
500 if ([
self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
501 [edges addObject:self];
503 if ([
self hasChildren]) {
505 [child collectRoutes:edges];
511 if (![
self node].HasAction(flutter::SemanticsAction::kCustomAction)) {
514 int32_t action_id = action.
uid;
515 std::vector<uint8_t> args;
517 args.push_back(action_id);
518 args.push_back(action_id >> 8);
519 args.push_back(action_id >> 16);
520 args.push_back(action_id >> 24);
521 [
self bridge]->DispatchSemanticsAction(
522 [
self uid], flutter::SemanticsAction::kCustomAction,
523 fml::MallocMapping::Copy(args.data(), args.size() *
sizeof(uint8_t)));
527 - (NSString*)accessibilityIdentifier {
528 if (![
self isAccessibilityBridgeAlive]) {
532 if ([
self node].identifier.empty()) {
535 return @([
self node].identifier.data());
538 - (NSString*)accessibilityLabel {
539 if (![
self isAccessibilityBridgeAlive]) {
542 NSString* label = nil;
543 if (![
self node].label.empty()) {
544 label = @([
self node].label.data());
546 if (![
self node].tooltip.empty()) {
547 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @([
self node].tooltip.data())]
548 : @([
self node].tooltip.data());
553 - (bool)containsPoint:(CGPoint)point {
555 return CGRectContainsPoint([
self globalRect], point);
559 - (id)search:(CGPoint)point {
562 if ([child containsPoint:point]) {
563 id childSearchResult = [child search:point];
564 if (childSearchResult != nil) {
565 return childSearchResult;
570 if ([
self containsPoint:point] && [
self isFocusable]) {
571 return self.nativeAccessibility;
583 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
584 return [
self search:point];
588 - (BOOL)accessibilityScrollToVisible {
594 - (BOOL)accessibilityScrollToVisibleWithChild:(
id)child {
596 [child showOnScreen];
602 - (NSAttributedString*)accessibilityAttributedLabel {
603 NSString* label = [
self accessibilityLabel];
604 if (label.length == 0) {
607 return [
self createAttributedStringFromString:label withAttributes:[
self node].labelAttributes];
610 - (NSString*)accessibilityHint {
611 if (![
self isAccessibilityBridgeAlive]) {
615 if ([
self node].hint.empty()) {
618 return @([
self node].hint.data());
621 - (NSAttributedString*)accessibilityAttributedHint {
622 NSString* hint = [
self accessibilityHint];
623 if (hint.length == 0) {
626 return [
self createAttributedStringFromString:hint withAttributes:[
self node].hintAttributes];
629 - (NSString*)accessibilityValue {
630 if (![
self isAccessibilityBridgeAlive]) {
634 if (![
self node].value.empty()) {
635 return @([
self node].value.data());
639 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
644 if ([
self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
645 [
self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
646 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
647 [
self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
657 - (NSAttributedString*)accessibilityAttributedValue {
658 NSString* value = [
self accessibilityValue];
659 if (value.length == 0) {
662 return [
self createAttributedStringFromString:value withAttributes:[
self node].valueAttributes];
665 - (CGRect)accessibilityFrame {
666 if (![
self isAccessibilityBridgeAlive]) {
667 return CGRectMake(0, 0, 0, 0);
670 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
671 return [
super accessibilityFrame];
673 return [
self globalRect];
676 - (CGRect)globalRect {
677 const SkRect& rect = [
self node].rect;
678 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
679 return ConvertRectToGlobal(
self, localRect);
682 #pragma mark - UIAccessibilityElement protocol
684 - (void)setAccessibilityContainer:(
id)container {
689 - (id)accessibilityContainer {
698 if (![
self isAccessibilityBridgeAlive]) {
702 if ([
self hasChildren] || [
self uid] ==
kRootNodeId) {
703 if (_container == nil) {
705 bridge:[
self bridge]]);
707 return _container.get();
709 if ([
self parent] == nil) {
715 return [[
self parent] accessibilityContainer];
718 #pragma mark - UIAccessibilityAction overrides
720 - (BOOL)accessibilityActivate {
721 if (![
self isAccessibilityBridgeAlive]) {
724 if (![
self node].HasAction(flutter::SemanticsAction::kTap)) {
727 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kTap);
731 - (void)accessibilityIncrement {
732 if (![
self isAccessibilityBridgeAlive]) {
735 if ([
self node].HasAction(flutter::SemanticsAction::kIncrease)) {
736 [
self node].value = [
self node].increasedValue;
737 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kIncrease);
741 - (void)accessibilityDecrement {
742 if (![
self isAccessibilityBridgeAlive]) {
745 if ([
self node].HasAction(flutter::SemanticsAction::kDecrease)) {
746 [
self node].value = [
self node].decreasedValue;
747 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kDecrease);
751 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
752 if (![
self isAccessibilityBridgeAlive]) {
755 flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction);
756 if (![
self node].HasAction(action)) {
759 [
self bridge]->DispatchSemanticsAction([
self uid], action);
763 - (BOOL)accessibilityPerformEscape {
764 if (![
self isAccessibilityBridgeAlive]) {
767 if (![
self node].HasAction(flutter::SemanticsAction::kDismiss)) {
770 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kDismiss);
774 #pragma mark UIAccessibilityFocus overrides
776 - (void)accessibilityElementDidBecomeFocused {
777 if (![
self isAccessibilityBridgeAlive]) {
780 [
self bridge]->AccessibilityObjectDidBecomeFocused([
self uid]);
781 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsHidden) ||
782 [
self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
785 if ([
self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
786 [
self bridge]->DispatchSemanticsAction([
self uid],
787 flutter::SemanticsAction::kDidGainAccessibilityFocus);
791 - (void)accessibilityElementDidLoseFocus {
792 if (![
self isAccessibilityBridgeAlive]) {
795 [
self bridge]->AccessibilityObjectDidLoseFocus([
self uid]);
796 if ([
self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
797 [
self bridge]->DispatchSemanticsAction([
self uid],
798 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
807 #pragma mark - Override base class designated initializers
810 - (instancetype)init {
812 [
super doesNotRecognizeSelector:_cmd];
816 #pragma mark - Designated initializers
818 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
820 self = [
super initWithBridge:bridge uid:uid];
824 #pragma mark - UIAccessibility overrides
826 - (UIAccessibilityTraits)accessibilityTraits {
827 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
828 if ([
self node].HasAction(flutter::SemanticsAction::kIncrease) ||
829 [
self node].HasAction(flutter::SemanticsAction::kDecrease)) {
830 traits |= UIAccessibilityTraitAdjustable;
833 if ([
self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
834 [
self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
835 traits |= UIAccessibilityTraitButton;
837 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) {
838 traits |= UIAccessibilityTraitSelected;
840 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsButton)) {
841 traits |= UIAccessibilityTraitButton;
843 if ([
self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
844 ![
self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
845 traits |= UIAccessibilityTraitNotEnabled;
847 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
848 traits |= UIAccessibilityTraitHeader;
850 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsImage)) {
851 traits |= UIAccessibilityTraitImage;
853 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
854 traits |= UIAccessibilityTraitUpdatesFrequently;
856 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsLink)) {
857 traits |= UIAccessibilityTraitLink;
859 if (traits == UIAccessibilityTraitNone && ![
self hasChildren] &&
860 [[
self accessibilityLabel] length] != 0 &&
861 ![
self node].HasFlag(flutter::SemanticsFlags::kIsTextField)) {
862 traits = UIAccessibilityTraitStaticText;
870 @property(nonatomic, retain) UIView* platformView;
875 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
879 _platformView = [platformView retain];
886 [_platformView release];
892 return _platformView;
899 fml::WeakPtr<flutter::AccessibilityBridgeIos>
_bridge;
902 #pragma mark - initializers
905 - (instancetype)init {
907 [
super doesNotRecognizeSelector:_cmd];
912 bridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge {
913 FML_DCHECK(semanticsObject) <<
"semanticsObject must be set";
918 self = [
super initWithAccessibilityContainer:bridge->view()];
921 _semanticsObject = semanticsObject;
928 #pragma mark - UIAccessibilityContainer overrides
930 - (NSInteger)accessibilityElementCount {
931 NSInteger count = [[_semanticsObject children] count] + 1;
935 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
936 if (index < 0 || index >= [
self accessibilityElementCount]) {
940 return _semanticsObject.nativeAccessibility;
945 if ([child hasChildren]) {
946 return [child accessibilityContainer];
951 - (NSInteger)indexOfAccessibilityElement:(
id)element {
952 if (element == _semanticsObject.nativeAccessibility) {
956 NSArray<SemanticsObject*>* children = [_semanticsObject children];
957 for (
size_t i = 0; i < [children count]; i++) {
967 #pragma mark - UIAccessibilityElement protocol
969 - (BOOL)isAccessibilityElement {
973 - (CGRect)accessibilityFrame {
974 return [_semanticsObject accessibilityFrame];
977 - (id)accessibilityContainer {
983 : [[_semanticsObject parent] accessibilityContainer];
986 #pragma mark - UIAccessibilityAction overrides
988 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
989 return [_semanticsObject accessibilityScroll:direction];