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*)accessibilityLabel {
528 if (![
self isAccessibilityBridgeAlive]) {
531 NSString* label = nil;
532 if (![
self node].label.empty()) {
533 label = @([
self node].label.data());
535 if (![
self node].tooltip.empty()) {
536 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @([
self node].tooltip.data())]
537 : @([
self node].tooltip.data());
542 - (bool)containsPoint:(CGPoint)point {
544 return CGRectContainsPoint([
self globalRect], point);
548 - (
id)search:(CGPoint)point {
551 if ([child containsPoint:point]) {
552 id childSearchResult = [child search:point];
553 if (childSearchResult != nil) {
554 return childSearchResult;
559 if ([
self containsPoint:point] && [
self isFocusable]) {
560 return self.nativeAccessibility;
572 - (
id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
573 return [
self search:point];
577 - (BOOL)accessibilityScrollToVisible {
583 - (BOOL)accessibilityScrollToVisibleWithChild:(
id)child {
585 [child showOnScreen];
591 - (NSAttributedString*)accessibilityAttributedLabel {
592 NSString* label = [
self accessibilityLabel];
593 if (label.length == 0) {
596 return [
self createAttributedStringFromString:label withAttributes:[
self node].labelAttributes];
599 - (NSString*)accessibilityHint {
600 if (![
self isAccessibilityBridgeAlive]) {
604 if ([
self node].hint.empty()) {
607 return @([
self node].hint.data());
610 - (NSAttributedString*)accessibilityAttributedHint {
611 NSString* hint = [
self accessibilityHint];
612 if (hint.length == 0) {
615 return [
self createAttributedStringFromString:hint withAttributes:[
self node].hintAttributes];
618 - (NSString*)accessibilityValue {
619 if (![
self isAccessibilityBridgeAlive]) {
623 if (![
self node].value.empty()) {
624 return @([
self node].value.data());
628 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
633 if ([
self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
634 [
self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
635 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
636 [
self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
646 - (NSAttributedString*)accessibilityAttributedValue {
647 NSString* value = [
self accessibilityValue];
648 if (value.length == 0) {
651 return [
self createAttributedStringFromString:value withAttributes:[
self node].valueAttributes];
654 - (CGRect)accessibilityFrame {
655 if (![
self isAccessibilityBridgeAlive]) {
656 return CGRectMake(0, 0, 0, 0);
659 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
660 return [
super accessibilityFrame];
662 return [
self globalRect];
665 - (CGRect)globalRect {
666 const SkRect& rect = [
self node].rect;
667 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
668 return ConvertRectToGlobal(
self, localRect);
671 #pragma mark - UIAccessibilityElement protocol
673 - (void)setAccessibilityContainer:(
id)container {
678 - (
id)accessibilityContainer {
687 if (![
self isAccessibilityBridgeAlive]) {
691 if ([
self hasChildren] || [
self uid] ==
kRootNodeId) {
692 if (_container == nil) {
694 bridge:[
self bridge]]);
696 return _container.get();
698 if ([
self parent] == nil) {
704 return [[
self parent] accessibilityContainer];
707 #pragma mark - UIAccessibilityAction overrides
709 - (BOOL)accessibilityActivate {
710 if (![
self isAccessibilityBridgeAlive]) {
713 if (![
self node].HasAction(flutter::SemanticsAction::kTap)) {
716 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kTap);
720 - (void)accessibilityIncrement {
721 if (![
self isAccessibilityBridgeAlive]) {
724 if ([
self node].HasAction(flutter::SemanticsAction::kIncrease)) {
725 [
self node].value = [
self node].increasedValue;
726 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kIncrease);
730 - (void)accessibilityDecrement {
731 if (![
self isAccessibilityBridgeAlive]) {
734 if ([
self node].HasAction(flutter::SemanticsAction::kDecrease)) {
735 [
self node].value = [
self node].decreasedValue;
736 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kDecrease);
740 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
741 if (![
self isAccessibilityBridgeAlive]) {
744 flutter::SemanticsAction
action = GetSemanticsActionForScrollDirection(direction);
745 if (![
self node].HasAction(
action)) {
748 [
self bridge]->DispatchSemanticsAction([
self uid],
action);
752 - (BOOL)accessibilityPerformEscape {
753 if (![
self isAccessibilityBridgeAlive]) {
756 if (![
self node].HasAction(flutter::SemanticsAction::kDismiss)) {
759 [
self bridge]->DispatchSemanticsAction([
self uid], flutter::SemanticsAction::kDismiss);
763 #pragma mark UIAccessibilityFocus overrides
765 - (void)accessibilityElementDidBecomeFocused {
766 if (![
self isAccessibilityBridgeAlive]) {
769 [
self bridge]->AccessibilityObjectDidBecomeFocused([
self uid]);
770 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsHidden) ||
771 [
self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
774 if ([
self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
775 [
self bridge]->DispatchSemanticsAction([
self uid],
776 flutter::SemanticsAction::kDidGainAccessibilityFocus);
780 - (void)accessibilityElementDidLoseFocus {
781 if (![
self isAccessibilityBridgeAlive]) {
784 [
self bridge]->AccessibilityObjectDidLoseFocus([
self uid]);
785 if ([
self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
786 [
self bridge]->DispatchSemanticsAction([
self uid],
787 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
796 #pragma mark - Override base class designated initializers
799 - (instancetype)init {
801 [
super doesNotRecognizeSelector:_cmd];
805 #pragma mark - Designated initializers
807 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
809 self = [
super initWithBridge:bridge uid:uid];
813 #pragma mark - UIAccessibility overrides
815 - (UIAccessibilityTraits)accessibilityTraits {
816 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
817 if ([
self node].HasAction(flutter::SemanticsAction::kIncrease) ||
818 [
self node].HasAction(flutter::SemanticsAction::kDecrease)) {
819 traits |= UIAccessibilityTraitAdjustable;
822 if ([
self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
823 [
self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
824 traits |= UIAccessibilityTraitButton;
826 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) {
827 traits |= UIAccessibilityTraitSelected;
829 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsButton)) {
830 traits |= UIAccessibilityTraitButton;
832 if ([
self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
833 ![
self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
834 traits |= UIAccessibilityTraitNotEnabled;
836 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
837 traits |= UIAccessibilityTraitHeader;
839 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsImage)) {
840 traits |= UIAccessibilityTraitImage;
842 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
843 traits |= UIAccessibilityTraitUpdatesFrequently;
845 if ([
self node].HasFlag(flutter::SemanticsFlags::kIsLink)) {
846 traits |= UIAccessibilityTraitLink;
848 if (traits == UIAccessibilityTraitNone && ![
self hasChildren] &&
849 [[
self accessibilityLabel] length] != 0 &&
850 ![
self node].HasFlag(flutter::SemanticsFlags::kIsTextField)) {
851 traits = UIAccessibilityTraitStaticText;
859 @property(nonatomic, retain) UIView* platformView;
864 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
868 _platformView = [platformView retain];
875 [_platformView release];
881 return _platformView;
888 fml::WeakPtr<flutter::AccessibilityBridgeIos>
_bridge;
891 #pragma mark - initializers
894 - (instancetype)init {
896 [
super doesNotRecognizeSelector:_cmd];
901 bridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge {
902 FML_DCHECK(semanticsObject) <<
"semanticsObject must be set";
907 self = [
super initWithAccessibilityContainer:bridge->view()];
910 _semanticsObject = semanticsObject;
917 #pragma mark - UIAccessibilityContainer overrides
919 - (NSInteger)accessibilityElementCount {
920 NSInteger count = [[_semanticsObject children] count] + 1;
924 - (nullable
id)accessibilityElementAtIndex:(NSInteger)index {
925 if (index < 0 || index >= [
self accessibilityElementCount]) {
929 return _semanticsObject.nativeAccessibility;
934 if ([child hasChildren]) {
935 return [child accessibilityContainer];
940 - (NSInteger)indexOfAccessibilityElement:(
id)element {
941 if (element == _semanticsObject.nativeAccessibility) {
945 NSArray<SemanticsObject*>* children = [_semanticsObject children];
946 for (
size_t i = 0; i < [children count]; i++) {
956 #pragma mark - UIAccessibilityElement protocol
958 - (BOOL)isAccessibilityElement {
962 - (CGRect)accessibilityFrame {
963 return [_semanticsObject accessibilityFrame];
966 - (
id)accessibilityContainer {
972 : [[_semanticsObject parent] accessibilityContainer];
975 #pragma mark - UIAccessibilityAction overrides
977 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
978 return [_semanticsObject accessibilityScroll:direction];