5#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
9#include "flutter/fml/logging.h"
10#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
11#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
12#import "flutter/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.h"
13#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
15#pragma GCC diagnostic error "-Wundeclared-selector"
22constexpr int32_t kSemanticObjectIdInvalid = -1;
24class DefaultIosDelegate :
public AccessibilityBridge::IosDelegate {
26 bool IsFlutterViewControllerPresentingModalViewController(
28 if (view_controller) {
35 void PostAccessibilityNotification(UIAccessibilityNotifications notification,
36 id argument)
override {
37 UIAccessibilityPostNotification(notification, argument);
45 std::shared_ptr<FlutterPlatformViewsController> platform_views_controller,
46 std::unique_ptr<IosDelegate> ios_delegate)
47 : view_controller_(view_controller),
49 platform_views_controller_(
std::move(platform_views_controller)),
50 last_focused_semantics_object_id_(kSemanticObjectIdInvalid),
51 objects_([[NSMutableDictionary alloc]
init]),
53 ios_delegate_(ios_delegate ? std::move(ios_delegate)
54 :
std::make_unique<DefaultIosDelegate>()),
57 initWithName:
@"flutter/accessibility"
58 binaryMessenger:
platform_view->GetOwnerViewController().get().engine.binaryMessenger
61 HandleEvent((NSDictionary*)
message);
65AccessibilityBridge::~AccessibilityBridge() {
66 [accessibility_channel_.get() setMessageHandler:nil];
68 view_controller_.viewIfLoaded.accessibilityElements = nil;
71UIView<UITextInput>* AccessibilityBridge::textInputView() {
72 return [[platform_view_->GetOwnerViewController().get().engine
textInputPlugin] textInputView];
75void AccessibilityBridge::AccessibilityObjectDidBecomeFocused(int32_t
id) {
76 last_focused_semantics_object_id_ =
id;
77 [accessibility_channel_.get() sendMessage:@{
@"type" :
@"didGainFocus",
@"nodeId" : @(
id)}];
80void AccessibilityBridge::AccessibilityObjectDidLoseFocus(int32_t
id) {
81 if (last_focused_semantics_object_id_ ==
id) {
82 last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
86void AccessibilityBridge::UpdateSemantics(
89 BOOL layoutChanged = NO;
90 BOOL scrollOccured = NO;
91 BOOL needsAnnouncement = NO;
92 for (
const auto& entry : actions) {
96 for (
const auto& entry : nodes) {
99 layoutChanged = layoutChanged || [
object nodeWillCauseLayoutChange:&node];
100 scrollOccured = scrollOccured || [
object nodeWillCauseScroll:&node];
101 needsAnnouncement = [
object nodeShouldTriggerAnnouncement:&node];
102 [
object setSemanticsNode:&node];
104 NSMutableArray* newChildren =
105 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
106 for (NSUInteger
i = 0;
i < newChildCount; ++
i) {
108 [newChildren addObject:child];
110 NSMutableArray* newChildrenInHitTestOrder =
111 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
112 for (NSUInteger
i = 0;
i < newChildCount; ++
i) {
114 [newChildrenInHitTestOrder addObject:child];
116 object.children = newChildren;
117 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
119 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
120 [[[NSMutableArray alloc]
init] autorelease];
123 if (
action.overrideId != -1) {
128 NSString* label = @(
action.label.data());
129 SEL selector =
@selector(onCustomAccessibilityAction:);
133 selector:selector] autorelease];
134 customAction.
uid = action_id;
135 [accessibilityCustomActions addObject:customAction];
137 object.accessibilityCustomActions = accessibilityCustomActions;
140 if (needsAnnouncement) {
146 NSString* announcement =
147 [[[NSString alloc] initWithUTF8String:
object.node.label.c_str()] autorelease];
148 UIAccessibilityPostNotification(
149 UIAccessibilityAnnouncementNotification,
150 [[[NSAttributedString alloc] initWithString:announcement
152 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
159 bool routeChanged =
false;
163 if (!view_controller_.view.accessibilityElements) {
164 view_controller_.view.accessibilityElements =
165 @[ [
root accessibilityContainer] ?: [NSNull null] ];
167 NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc]
init] autorelease];
168 [
root collectRoutes:newRoutes];
171 if (
std::find(previous_routes_.begin(), previous_routes_.end(), [
route uid]) ==
172 previous_routes_.end()) {
177 if (lastAdded == nil && [newRoutes
count] > 0) {
178 int index = [newRoutes
count] - 1;
179 lastAdded = [newRoutes objectAtIndex:index];
188 if (lastAdded != nil &&
189 ([lastAdded uid] != previous_route_id_ || [newRoutes
count] != previous_routes_.size())) {
190 previous_route_id_ = [lastAdded uid];
193 previous_routes_.clear();
195 previous_routes_.push_back([
route uid]);
198 view_controller_.viewIfLoaded.accessibilityElements = nil;
201 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_ allKeys]];
203 VisitObjectsRecursivelyAndRemove(
root, doomed_uids);
205 [objects_ removeObjectsForKeys:doomed_uids];
208 [
object accessibilityBridgeDidFinishUpdate];
211 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
212 layoutChanged = layoutChanged || [doomed_uids
count] > 0;
215 NSString* routeName = [lastAdded routeName];
216 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
223 [objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
227 ios_delegate_->PostAccessibilityNotification(
228 UIAccessibilityLayoutChangedNotification,
229 (routeChanged ||
next != lastFocused) ?
next.nativeAccessibility : NULL);
230 }
else if (scrollOccured) {
234 ios_delegate_->PostAccessibilityNotification(
235 UIAccessibilityPageScrolledNotification,
236 FindNextFocusableIfNecessary().nativeAccessibility);
242 platform_view_->DispatchSemanticsAction(uid,
action, {});
248 platform_view_->DispatchSemanticsAction(uid,
action, std::move(
args));
253 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
256 NSNumber* nodeId = @(oldObject.
node.
id);
257 NSUInteger positionInChildlist = [oldObject.
parent.
children indexOfObject:oldObject];
258 [[oldObject retain] autorelease];
260 [oldObject.
parent replaceChildAtIndex:positionInChildlist withChild:newObject];
261 [objects removeObjectForKey:nodeId];
262 objects[nodeId] = newObject;
277 uid:node.
id] autorelease];
280 initWithBridge:weak_ptr
282 platformView:weak_ptr->GetPlatformViewsController()->GetFlutterTouchInterceptingViewByID(
299 object = CreateObject(updates[uid], GetWeakPtr());
300 objects_.get()[@(uid)] =
object;
303 auto nodeEntry = updates.find(
object.node.
id);
304 if (nodeEntry != updates.end()) {
315 SemanticsObject* newSemanticsObject = CreateObject(node, GetWeakPtr());
316 ReplaceSemanticsObject(
object, newSemanticsObject, objects_.get());
317 object = newSemanticsObject;
324void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(
SemanticsObject*
object,
325 NSMutableArray<NSNumber*>* doomed_uids) {
326 [doomed_uids removeObject:@(
object.uid)];
328 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
334 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
339 return FindFirstFocusable([objects_.get() objectForKey:@(last_focused_semantics_object_id_)]);
344 if (!currentObject) {
348 return currentObject;
360void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) {
361 NSString*
type = annotatedEvent[
@"type"];
362 if ([
type isEqualToString:
@"announce"]) {
363 NSString*
message = annotatedEvent[
@"data"][
@"message"];
364 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification,
message);
366 if ([
type isEqualToString:
@"focus"]) {
368 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
373 return weak_factory_.GetWeakPtr();
376void AccessibilityBridge::clearState() {
377 [objects_ removeAllObjects];
378 previous_route_id_ = 0;
379 previous_routes_.clear();
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
#define FLUTTER_ASSERT_NOT_ARC
static float next(float f)
constexpr int32_t kRootNodeId
int find(T *array, int N, T item)
AccessibilityBridge()
Creates a new instance of a accessibility bridge.
A Mapping like NonOwnedMapping, but uses Free as its release proc.
FlutterSemanticsFlag flag
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
#define FML_DCHECK(condition)
BOOL isPresentingViewController()
flutter::SemanticsNode node
BOOL isAccessibilityElement()
NSArray< SemanticsObject * > * children
FlutterTextInputPlugin * textInputPlugin
@ kIsInMutuallyExclusiveGroup
std::unordered_map< int32_t, SemanticsNode > SemanticsNodeUpdates
std::unordered_map< int32_t, CustomAccessibilityAction > CustomAccessibilityActionUpdates
static void DispatchSemanticsAction(JNIEnv *env, jobject jcaller, jlong shell_holder, jint id, jint action, jobject args, jint args_position)
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace Enable an endless trace buffer The default is a ring buffer This is useful when very old events need to viewed For during application launch Memory usage will continue to grow indefinitely however route
std::vector< int32_t > childrenInHitTestOrder
bool HasFlag(SemanticsFlags flag) const
std::vector< int32_t > customAccessibilityActions
bool IsPlatformViewNode() const
std::vector< int32_t > childrenInTraversalOrder