blob: 46224c1ea4da7919dcfb54b0a47024745c2ea355 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h"
#include <utility>
#include <vector>
#import <UIKit/UIKit.h>
#include "flutter/fml/logging.h"
#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
#include "flutter/shell/platform/darwin/ios/platform_view_ios.h"
FLUTTER_ASSERT_NOT_ARC
namespace {
constexpr int32_t kRootNodeId = 0;
flutter::SemanticsAction GetSemanticsActionForScrollDirection(
UIAccessibilityScrollDirection direction) {
// To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the
// direction the scroll bar moves in and SemanticsAction uses the direction the finger
// moves in. However, the horizontal scroll direction matches the SemanticsAction direction.
// That is way the following maps vertical opposite of the SemanticsAction, but the horizontal
// maps directly.
switch (direction) {
case UIAccessibilityScrollDirectionRight:
case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using
// _node.textDirection.
return flutter::SemanticsAction::kScrollRight;
case UIAccessibilityScrollDirectionLeft:
case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using
// _node.textDirection.
return flutter::SemanticsAction::kScrollLeft;
case UIAccessibilityScrollDirectionUp:
return flutter::SemanticsAction::kScrollDown;
case UIAccessibilityScrollDirectionDown:
return flutter::SemanticsAction::kScrollUp;
}
FML_DCHECK(false); // Unreachable
return flutter::SemanticsAction::kScrollUp;
}
} // namespace
/// A proxy class for SemanticsObject and UISwitch. For most Accessibility and
/// SemanticsObject methods it delegates to the semantics object, otherwise it
/// sends messages to the UISwitch.
@interface FlutterSwitchSemanticsObject : UISwitch
@end
@implementation FlutterSwitchSemanticsObject {
SemanticsObject* _semanticsObject;
}
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject {
self = [super init];
if (self) {
_semanticsObject = [semanticsObject retain];
}
return self;
}
- (void)dealloc {
[_semanticsObject release];
[super dealloc];
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
NSMethodSignature* result = [super methodSignatureForSelector:sel];
if (!result) {
result = [_semanticsObject methodSignatureForSelector:sel];
}
return result;
}
- (void)forwardInvocation:(NSInvocation*)anInvocation {
[anInvocation setTarget:_semanticsObject];
[anInvocation invoke];
}
- (CGRect)accessibilityFrame {
return [_semanticsObject accessibilityFrame];
}
- (id)accessibilityContainer {
return [_semanticsObject accessibilityContainer];
}
- (NSString*)accessibilityLabel {
return [_semanticsObject accessibilityLabel];
}
- (NSString*)accessibilityHint {
return [_semanticsObject accessibilityHint];
}
- (NSString*)accessibilityValue {
if ([_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
[_semanticsObject node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
self.on = YES;
} else {
self.on = NO;
}
if (![_semanticsObject isAccessibilityBridgeAlive]) {
return nil;
} else {
return [super accessibilityValue];
}
}
@end // FlutterSwitchSemanticsObject
@implementation FlutterCustomAccessibilityAction {
}
@end
/**
* Represents a semantics object that has children and hence has to be presented to the OS as a
* UIAccessibilityContainer.
*
* The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an
* object that returns YES for isAccessibilityElement cannot also implement
* UIAccessibilityContainer.
*
* With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from
* the framework, such as:
*
* SemanticsObject1
* SemanticsObject2
* SemanticsObject3
* SemanticsObject4
*
* is translated into the following hierarchy, which is understood by iOS:
*
* SemanticsObjectContainer1
* SemanticsObject1
* SemanticsObjectContainer2
* SemanticsObject2
* SemanticsObject3
* SemanticsObject4
*
* From Flutter's view of the world (the first tree seen above), we construct iOS's view of the
* world (second tree) as follows: We replace each SemanticsObjects that has children with a
* SemanticsObjectContainer, which has the original SemanticsObject and its children as children.
*
* SemanticsObjects have semantic information attached to them which is interpreted by
* VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just
* there for structure and they don't provide any semantic information to VoiceOver (they return
* NO for isAccessibilityElement).
*/
@interface SemanticsObjectContainer : UIAccessibilityElement
- (instancetype)init __attribute__((unavailable("Use initWithSemanticsObject instead")));
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
bridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge
NS_DESIGNATED_INITIALIZER;
@property(nonatomic, weak) SemanticsObject* semanticsObject;
@end
@interface SemanticsObject ()
/** Should only be called in conjunction with setting child/parent relationship. */
- (void)privateSetParent:(SemanticsObject*)parent;
@end
@implementation SemanticsObject {
fml::scoped_nsobject<SemanticsObjectContainer> _container;
NSMutableArray<SemanticsObject*>* _children;
}
#pragma mark - Override base class designated initializers
// Method declared as unavailable in the interface
- (instancetype)init {
[self release];
[super doesNotRecognizeSelector:_cmd];
return nil;
}
#pragma mark - Designated initializers
- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge uid:(int32_t)uid {
FML_DCHECK(bridge) << "bridge must be set";
FML_DCHECK(uid >= kRootNodeId);
// Initialize with the UIView as the container.
// The UIView will not necessarily be accessibility parent for this object.
// The bridge informs the OS of the actual structure via
// `accessibilityContainer` and `accessibilityElementAtIndex`.
self = [super initWithAccessibilityContainer:bridge->view()];
if (self) {
_bridge = bridge;
_uid = uid;
_children = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc {
for (SemanticsObject* child in _children) {
[child privateSetParent:nil];
}
[_children removeAllObjects];
[_children release];
_parent = nil;
_container.get().semanticsObject = nil;
[_platformViewSemanticsContainer release];
[super dealloc];
}
#pragma mark - Semantic object methods
- (BOOL)isAccessibilityBridgeAlive {
return [self bridge].get() != nil;
}
- (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
_node = *node;
}
/**
* Whether calling `setSemanticsNode:` with `node` would cause a layout change.
*/
- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node {
return [self node].rect != node->rect || [self node].transform != node->transform;
}
/**
* Whether calling `setSemanticsNode:` with `node` would cause a scroll event.
*/
- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node {
return !isnan([self node].scrollPosition) && !isnan(node->scrollPosition) &&
[self node].scrollPosition != node->scrollPosition;
}
- (BOOL)hasChildren {
if (_node.IsPlatformViewNode()) {
return YES;
}
return [self.children count] != 0;
}
- (void)privateSetParent:(SemanticsObject*)parent {
_parent = parent;
}
- (void)setChildren:(NSArray<SemanticsObject*>*)children {
for (SemanticsObject* child in _children) {
[child privateSetParent:nil];
}
[_children release];
_children = [[NSMutableArray alloc] initWithArray:children];
for (SemanticsObject* child in _children) {
[child privateSetParent:self];
}
}
- (void)replaceChildAtIndex:(NSInteger)index withChild:(SemanticsObject*)child {
SemanticsObject* oldChild = _children[index];
[oldChild privateSetParent:nil];
[child privateSetParent:self];
[_children replaceObjectAtIndex:index withObject:child];
}
#pragma mark - UIAccessibility overrides
- (BOOL)isAccessibilityElement {
if (![self isAccessibilityBridgeAlive])
return false;
// Note: hit detection will only apply to elements that report
// -isAccessibilityElement of YES. The framework will continue scanning the
// entire element tree looking for such a hit.
// We enforce in the framework that no other useful semantics are merged with these nodes.
if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute))
return false;
// If the only flag(s) set are scrolling related AND
// The only flags set are not kIsHidden OR
// The node doesn't have a label, value, or hint OR
// The only actions set are scrolling related actions.
//
// The kIsHidden flag set with any other flag just means this node is now
// hidden but still is a valid target for a11y focus in the tree, e.g. a list
// item that is currently off screen but the a11y navigation needs to know
// about.
return (([self node].flags & ~flutter::kScrollableSemanticsFlags) != 0 &&
[self node].flags != static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden)) ||
![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() ||
([self node].actions & ~flutter::kScrollableSemanticsActions) != 0;
}
- (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges {
if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute))
[edges addObject:self];
if ([self hasChildren]) {
for (SemanticsObject* child in self.children) {
[child collectRoutes:edges];
}
}
}
- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action {
if (![self node].HasAction(flutter::SemanticsAction::kCustomAction))
return NO;
int32_t action_id = action.uid;
std::vector<uint8_t> args;
args.push_back(3); // type=int32.
args.push_back(action_id);
args.push_back(action_id >> 8);
args.push_back(action_id >> 16);
args.push_back(action_id >> 24);
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kCustomAction,
std::move(args));
return YES;
}
- (NSString*)routeName {
// Returns the first non-null and non-empty semantic label of a child
// with an NamesRoute flag. Otherwise returns nil.
if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
NSString* newName = [self accessibilityLabel];
if (newName != nil && [newName length] > 0) {
return newName;
}
}
if ([self hasChildren]) {
for (SemanticsObject* child in self.children) {
NSString* newName = [child routeName];
if (newName != nil && [newName length] > 0) {
return newName;
}
}
}
return nil;
}
- (NSString*)accessibilityLabel {
if (![self isAccessibilityBridgeAlive])
return nil;
if ([self node].label.empty())
return nil;
return @([self node].label.data());
}
- (NSString*)accessibilityHint {
if (![self isAccessibilityBridgeAlive])
return nil;
if ([self node].hint.empty())
return nil;
return @([self node].hint.data());
}
- (NSString*)accessibilityValue {
if (![self isAccessibilityBridgeAlive])
return nil;
if (![self node].value.empty()) {
return @([self node].value.data());
}
// FlutterSwitchSemanticsObject should supercede these conditionals.
if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
[self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
if ([self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
[self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
return @"1";
} else {
return @"0";
}
}
return nil;
}
- (CGRect)accessibilityFrame {
if (![self isAccessibilityBridgeAlive])
return CGRectMake(0, 0, 0, 0);
if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
return [super accessibilityFrame];
}
return [self globalRect];
}
- (CGRect)globalRect {
SkMatrix44 globalTransform = [self node].transform;
for (SemanticsObject* parent = [self parent]; parent; parent = parent.parent) {
globalTransform = parent.node.transform * globalTransform;
}
SkPoint quad[4];
[self node].rect.toQuad(quad);
for (auto& point : quad) {
SkScalar vector[4] = {point.x(), point.y(), 0, 1};
globalTransform.mapScalars(vector);
point.set(vector[0] / vector[3], vector[1] / vector[3]);
}
SkRect rect;
rect.setBounds(quad, 4);
// `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
// the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
// convert.
CGFloat scale = [[[self bridge]->view() window] screen].scale;
auto result =
CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
return UIAccessibilityConvertFrameToScreenCoordinates(result, [self bridge]->view());
}
#pragma mark - UIAccessibilityElement protocol
- (id)accessibilityContainer {
if ([self hasChildren] || [self uid] == kRootNodeId) {
if (_container == nil)
_container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self
bridge:[self bridge]]);
return _container.get();
}
if ([self parent] == nil) {
// This can happen when we have released the accessibility tree but iOS is
// still holding onto our objects. iOS can take some time before it
// realizes that the tree has changed.
return nil;
}
return [[self parent] accessibilityContainer];
}
#pragma mark - UIAccessibilityAction overrides
- (BOOL)accessibilityActivate {
if (![self isAccessibilityBridgeAlive])
return NO;
if (![self node].HasAction(flutter::SemanticsAction::kTap))
return NO;
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap);
return YES;
}
- (void)accessibilityIncrement {
if (![self isAccessibilityBridgeAlive])
return;
if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) {
[self node].value = [self node].increasedValue;
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease);
}
}
- (void)accessibilityDecrement {
if (![self isAccessibilityBridgeAlive])
return;
if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) {
[self node].value = [self node].decreasedValue;
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease);
}
}
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
if (![self isAccessibilityBridgeAlive])
return NO;
flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction);
if (![self node].HasAction(action))
return NO;
[self bridge]->DispatchSemanticsAction([self uid], action);
return YES;
}
- (BOOL)accessibilityPerformEscape {
if (![self isAccessibilityBridgeAlive])
return NO;
if (![self node].HasAction(flutter::SemanticsAction::kDismiss))
return NO;
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss);
return YES;
}
#pragma mark UIAccessibilityFocus overrides
- (void)accessibilityElementDidBecomeFocused {
if (![self isAccessibilityBridgeAlive])
return;
if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen);
}
if ([self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
[self bridge]->DispatchSemanticsAction([self uid],
flutter::SemanticsAction::kDidGainAccessibilityFocus);
}
}
- (void)accessibilityElementDidLoseFocus {
if (![self isAccessibilityBridgeAlive])
return;
if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
[self bridge]->DispatchSemanticsAction([self uid],
flutter::SemanticsAction::kDidLoseAccessibilityFocus);
}
}
@end
@implementation FlutterSemanticsObject {
}
#pragma mark - Override base class designated initializers
// Method declared as unavailable in the interface
- (instancetype)init {
[self release];
[super doesNotRecognizeSelector:_cmd];
return nil;
}
#pragma mark - Designated initializers
- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge uid:(int32_t)uid {
self = [super initWithBridge:bridge uid:uid];
return self;
}
#pragma mark - UIAccessibility overrides
- (UIAccessibilityTraits)accessibilityTraits {
UIAccessibilityTraits traits = UIAccessibilityTraitNone;
if ([self node].HasAction(flutter::SemanticsAction::kIncrease) ||
[self node].HasAction(flutter::SemanticsAction::kDecrease)) {
traits |= UIAccessibilityTraitAdjustable;
}
// FlutterSwitchSemanticsObject should supercede these conditionals.
if ([self node].HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
[self node].HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
traits |= UIAccessibilityTraitButton;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected)) {
traits |= UIAccessibilityTraitSelected;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) {
traits |= UIAccessibilityTraitButton;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
traits |= UIAccessibilityTraitNotEnabled;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
traits |= UIAccessibilityTraitHeader;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) {
traits |= UIAccessibilityTraitImage;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
traits |= UIAccessibilityTraitUpdatesFrequently;
}
if ([self node].HasFlag(flutter::SemanticsFlags::kIsLink)) {
traits |= UIAccessibilityTraitLink;
}
return traits;
}
@end
@implementation FlutterPlatformViewSemanticsContainer {
SemanticsObject* _semanticsObject;
UIView* _platformView;
}
// Method declared as unavailable in the interface
- (instancetype)init {
[self release];
[super doesNotRecognizeSelector:_cmd];
return nil;
}
- (instancetype)initWithSemanticsObject:(SemanticsObject*)object {
FML_CHECK(object);
// Initialize with the UIView as the container.
// The UIView will not necessarily be accessibility parent for this object.
// The bridge informs the OS of the actual structure via
// `accessibilityContainer` and `accessibilityElementAtIndex`.
if (self = [super initWithAccessibilityContainer:object.bridge->view()]) {
_semanticsObject = object;
flutter::FlutterPlatformViewsController* controller =
object.bridge->GetPlatformViewsController();
if (controller) {
_platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view];
}
self.accessibilityElements = @[ _semanticsObject, _platformView ];
}
return self;
}
- (CGRect)accessibilityFrame {
return _semanticsObject.accessibilityFrame;
}
- (BOOL)isAccessibilityElement {
return NO;
}
- (id)accessibilityContainer {
return [_semanticsObject accessibilityContainer];
}
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
return [_platformView accessibilityScroll:direction];
}
@end
@implementation SemanticsObjectContainer {
SemanticsObject* _semanticsObject;
fml::WeakPtr<flutter::AccessibilityBridge> _bridge;
}
#pragma mark - initializers
// Method declared as unavailable in the interface
- (instancetype)init {
[self release];
[super doesNotRecognizeSelector:_cmd];
return nil;
}
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
bridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge {
FML_DCHECK(semanticsObject) << "semanticsObject must be set";
// Initialize with the UIView as the container.
// The UIView will not necessarily be accessibility parent for this object.
// The bridge informs the OS of the actual structure via
// `accessibilityContainer` and `accessibilityElementAtIndex`.
self = [super initWithAccessibilityContainer:bridge->view()];
if (self) {
_semanticsObject = semanticsObject;
_bridge = bridge;
}
return self;
}
#pragma mark - UIAccessibilityContainer overrides
- (NSInteger)accessibilityElementCount {
NSInteger count = [[_semanticsObject children] count] + 1;
return count;
}
- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
if (index < 0 || index >= [self accessibilityElementCount])
return nil;
if (index == 0) {
return _semanticsObject;
}
SemanticsObject* child = [_semanticsObject children][index - 1];
// Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer`
if (child.node.IsPlatformViewNode()) {
child.platformViewSemanticsContainer.index = index;
return child.platformViewSemanticsContainer;
}
if ([child hasChildren])
return [child accessibilityContainer];
return child;
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == _semanticsObject)
return 0;
// FlutterPlatformViewSemanticsContainer is always the last element of its parent.
if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) {
return ((FlutterPlatformViewSemanticsContainer*)element).index;
}
NSArray<SemanticsObject*>* children = [_semanticsObject children];
for (size_t i = 0; i < [children count]; i++) {
SemanticsObject* child = children[i];
if ((![child hasChildren] && child == element) ||
([child hasChildren] && [child accessibilityContainer] == element))
return i + 1;
}
return NSNotFound;
}
#pragma mark - UIAccessibilityElement protocol
- (BOOL)isAccessibilityElement {
return NO;
}
- (CGRect)accessibilityFrame {
return [_semanticsObject accessibilityFrame];
}
- (id)accessibilityContainer {
if (!_bridge) {
return nil;
}
return ([_semanticsObject uid] == kRootNodeId)
? _bridge->view()
: [[_semanticsObject parent] accessibilityContainer];
}
#pragma mark - UIAccessibilityAction overrides
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
return [_semanticsObject accessibilityScroll:direction];
}
@end
#pragma mark - AccessibilityBridge impl
namespace flutter {
AccessibilityBridge::AccessibilityBridge(UIView* view,
PlatformViewIOS* platform_view,
FlutterPlatformViewsController* platform_views_controller)
: view_(view),
platform_view_(platform_view),
platform_views_controller_(platform_views_controller),
objects_([[NSMutableDictionary alloc] init]),
weak_factory_(this),
previous_route_id_(0),
previous_routes_({}) {
accessibility_channel_.reset([[FlutterBasicMessageChannel alloc]
initWithName:@"flutter/accessibility"
binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger
codec:[FlutterStandardMessageCodec sharedInstance]]);
[accessibility_channel_.get() setMessageHandler:^(id message, FlutterReply reply) {
HandleEvent((NSDictionary*)message);
}];
}
AccessibilityBridge::~AccessibilityBridge() {
clearState();
view_.accessibilityElements = nil;
}
UIView<UITextInput>* AccessibilityBridge::textInputView() {
return [platform_view_->GetTextInputPlugin() textInputView];
}
void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
flutter::CustomAccessibilityActionUpdates actions) {
BOOL layoutChanged = NO;
BOOL scrollOccured = NO;
for (const auto& entry : actions) {
const flutter::CustomAccessibilityAction& action = entry.second;
actions_[action.id] = action;
}
for (const auto& entry : nodes) {
const flutter::SemanticsNode& node = entry.second;
SemanticsObject* object = GetOrCreateObject(node.id, nodes);
layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
[object setSemanticsNode:&node];
NSUInteger newChildCount = node.childrenInTraversalOrder.size();
NSMutableArray* newChildren =
[[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
for (NSUInteger i = 0; i < newChildCount; ++i) {
SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
[newChildren addObject:child];
}
object.children = newChildren;
if (node.customAccessibilityActions.size() > 0) {
NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
[[[NSMutableArray alloc] init] autorelease];
for (int32_t action_id : node.customAccessibilityActions) {
flutter::CustomAccessibilityAction& action = actions_[action_id];
if (action.overrideId != -1) {
// iOS does not support overriding standard actions, so we ignore any
// custom actions that have an override id provided.
continue;
}
NSString* label = @(action.label.data());
SEL selector = @selector(onCustomAccessibilityAction:);
FlutterCustomAccessibilityAction* customAction =
[[FlutterCustomAccessibilityAction alloc] initWithName:label
target:object
selector:selector];
customAction.uid = action_id;
[accessibilityCustomActions addObject:customAction];
}
object.accessibilityCustomActions = accessibilityCustomActions;
}
if (object.node.IsPlatformViewNode()) {
FlutterPlatformViewsController* controller = GetPlatformViewsController();
if (controller) {
object.platformViewSemanticsContainer =
[[FlutterPlatformViewSemanticsContainer alloc] initWithSemanticsObject:object];
}
} else if (object.platformViewSemanticsContainer) {
[object.platformViewSemanticsContainer release];
}
}
SemanticsObject* root = objects_.get()[@(kRootNodeId)];
bool routeChanged = false;
SemanticsObject* lastAdded = nil;
if (root) {
if (!view_.accessibilityElements) {
view_.accessibilityElements = @[ [root accessibilityContainer] ];
}
NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
[root collectRoutes:newRoutes];
for (SemanticsObject* route in newRoutes) {
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) !=
previous_routes_.end()) {
lastAdded = route;
}
}
if (lastAdded == nil && [newRoutes count] > 0) {
int index = [newRoutes count] - 1;
lastAdded = [newRoutes objectAtIndex:index];
}
if (lastAdded != nil && [lastAdded uid] != previous_route_id_) {
previous_route_id_ = [lastAdded uid];
routeChanged = true;
}
previous_routes_.clear();
for (SemanticsObject* route in newRoutes) {
previous_routes_.push_back([route uid]);
}
} else {
view_.accessibilityElements = nil;
}
NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_.get() allKeys]];
if (root)
VisitObjectsRecursivelyAndRemove(root, doomed_uids);
[objects_ removeObjectsForKeys:doomed_uids];
layoutChanged = layoutChanged || [doomed_uids count] > 0;
if (routeChanged) {
NSString* routeName = [lastAdded routeName];
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, routeName);
} else if (layoutChanged) {
// TODO(goderbauer): figure out which node to focus next.
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
}
if (scrollOccured) {
// TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5")
UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, @"");
}
}
void AccessibilityBridge::DispatchSemanticsAction(int32_t uid, flutter::SemanticsAction action) {
platform_view_->DispatchSemanticsAction(uid, action, {});
}
void AccessibilityBridge::DispatchSemanticsAction(int32_t uid,
flutter::SemanticsAction action,
std::vector<uint8_t> args) {
platform_view_->DispatchSemanticsAction(uid, action, std::move(args));
}
static void ReplaceSemanticsObject(SemanticsObject* oldObject,
SemanticsObject* newObject,
NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
// `newObject` should represent the same id as `oldObject`.
assert(oldObject.node.id == newObject.node.id);
NSNumber* nodeId = @(oldObject.node.id);
NSUInteger positionInChildlist = [oldObject.parent.children indexOfObject:oldObject];
[objects removeObjectForKey:nodeId];
[oldObject.parent replaceChildAtIndex:positionInChildlist withChild:newObject];
objects[nodeId] = newObject;
}
static SemanticsObject* CreateObject(const flutter::SemanticsNode& node,
fml::WeakPtr<AccessibilityBridge> weak_ptr) {
if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) &&
!node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
// Text fields are backed by objects that implement UITextInput.
return [[[TextInputSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
} else if (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
SemanticsObject* delegateObject = [[FlutterSemanticsObject alloc] initWithBridge:weak_ptr
uid:node.id];
return (SemanticsObject*)[[[FlutterSwitchSemanticsObject alloc]
initWithSemanticsObject:delegateObject] autorelease];
[delegateObject release];
} else {
return [[[FlutterSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
}
}
static bool DidFlagChange(const flutter::SemanticsNode& oldNode,
const flutter::SemanticsNode& newNode,
SemanticsFlags flag) {
return oldNode.HasFlag(flag) != newNode.HasFlag(flag);
}
SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid,
flutter::SemanticsNodeUpdates& updates) {
SemanticsObject* object = objects_.get()[@(uid)];
if (!object) {
object = CreateObject(updates[uid], GetWeakPtr());
objects_.get()[@(uid)] = object;
} else {
// Existing node case
auto nodeEntry = updates.find(object.node.id);
if (nodeEntry != updates.end()) {
// There's an update for this node
flutter::SemanticsNode node = nodeEntry->second;
if (DidFlagChange(object.node, node, flutter::SemanticsFlags::kIsTextField) ||
DidFlagChange(object.node, node, flutter::SemanticsFlags::kIsReadOnly) ||
DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasCheckedState) ||
DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasToggledState)) {
// The node changed its type. In this case, we cannot reuse the existing
// SemanticsObject implementation. Instead, we replace it with a new
// instance.
SemanticsObject* newSemanticsObject = CreateObject(node, GetWeakPtr());
ReplaceSemanticsObject(object, newSemanticsObject, objects_.get());
object = newSemanticsObject;
}
}
}
return object;
}
void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(SemanticsObject* object,
NSMutableArray<NSNumber*>* doomed_uids) {
[doomed_uids removeObject:@(object.uid)];
for (SemanticsObject* child in [object children])
VisitObjectsRecursivelyAndRemove(child, doomed_uids);
}
void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) {
NSString* type = annotatedEvent[@"type"];
if ([type isEqualToString:@"announce"]) {
NSString* message = annotatedEvent[@"data"][@"message"];
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, message);
}
}
fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void AccessibilityBridge::clearState() {
[objects_ removeAllObjects];
previous_route_id_ = 0;
previous_routes_.clear();
}
} // namespace flutter