Flutter Engine
accessibility_text_entry.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 <UIKit/UIKit.h>
6 
7 #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
8 #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h"
9 
10 static const UIAccessibilityTraits UIAccessibilityTraitUndocumentedEmptyLine = 0x800000000000;
11 
12 @implementation FlutterInactiveTextInput {
13 }
14 
15 @synthesize tokenizer = _tokenizer;
16 @synthesize beginningOfDocument = _beginningOfDocument;
17 @synthesize endOfDocument = _endOfDocument;
18 
19 - (instancetype)init {
20  return [super init];
21 }
22 
23 - (BOOL)hasText {
24  return self.text.length > 0;
25 }
26 
27 - (NSString*)textInRange:(UITextRange*)range {
28  if (!range) {
29  return nil;
30  }
31  NSAssert([range isKindOfClass:[FlutterTextRange class]],
32  @"Expected a FlutterTextRange for range (got %@).", [range class]);
33  NSRange textRange = ((FlutterTextRange*)range).range;
34  NSAssert(textRange.location != NSNotFound, @"Expected a valid text range.");
35  return [self.text substringWithRange:textRange];
36 }
37 
38 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
39  // This method is required but not called by accessibility API for
40  // features we are using it for. It may need to be implemented if
41  // requirements change.
42 }
43 
44 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
45  // This method is required but not called by accessibility API for
46  // features we are using it for. It may need to be implemented if
47  // requirements change.
48 }
49 
50 - (void)unmarkText {
51  // This method is required but not called by accessibility API for
52  // features we are using it for. It may need to be implemented if
53  // requirements change.
54 }
55 
56 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
57  toPosition:(UITextPosition*)toPosition {
58  NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
59  NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
60  return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
61 }
62 
63 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
64  // This method is required but not called by accessibility API for
65  // features we are using it for. It may need to be implemented if
66  // requirements change.
67  return nil;
68 }
69 
70 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
71  inDirection:(UITextLayoutDirection)direction
72  offset:(NSInteger)offset {
73  // This method is required but not called by accessibility API for
74  // features we are using it for. It may need to be implemented if
75  // requirements change.
76  return nil;
77 }
78 
79 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
80  // This method is required but not called by accessibility API for
81  // features we are using it for. It may need to be implemented if
82  // requirements change.
83  return NSOrderedSame;
84 }
85 
86 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
87  // This method is required but not called by accessibility API for
88  // features we are using it for. It may need to be implemented if
89  // requirements change.
90  return 0;
91 }
92 
93 - (UITextPosition*)positionWithinRange:(UITextRange*)range
94  farthestInDirection:(UITextLayoutDirection)direction {
95  // This method is required but not called by accessibility API for
96  // features we are using it for. It may need to be implemented if
97  // requirements change.
98  return nil;
99 }
100 
101 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
102  inDirection:(UITextLayoutDirection)direction {
103  // This method is required but not called by accessibility API for
104  // features we are using it for. It may need to be implemented if
105  // requirements change.
106  return nil;
107 }
108 
109 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
110  inDirection:(UITextStorageDirection)direction {
111  // Not editable. Does not apply.
112  return UITextWritingDirectionNatural;
113 }
114 
115 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
116  forRange:(UITextRange*)range {
117  // Not editable. Does not apply.
118 }
119 
120 - (CGRect)firstRectForRange:(UITextRange*)range {
121  // This method is required but not called by accessibility API for
122  // features we are using it for. It may need to be implemented if
123  // requirements change.
124  return CGRectZero;
125 }
126 
127 - (CGRect)caretRectForPosition:(UITextPosition*)position {
128  // This method is required but not called by accessibility API for
129  // features we are using it for. It may need to be implemented if
130  // requirements change.
131  return CGRectZero;
132 }
133 
134 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
135  // This method is required but not called by accessibility API for
136  // features we are using it for. It may need to be implemented if
137  // requirements change.
138  return nil;
139 }
140 
141 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
142  // This method is required but not called by accessibility API for
143  // features we are using it for. It may need to be implemented if
144  // requirements change.
145  return nil;
146 }
147 
148 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
149  // This method is required but not called by accessibility API for
150  // features we are using it for. It may need to be implemented if
151  // requirements change.
152  return @[];
153 }
154 
155 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
156  // This method is required but not called by accessibility API for
157  // features we are using it for. It may need to be implemented if
158  // requirements change.
159  return nil;
160 }
161 
162 - (void)insertText:(NSString*)text {
163  // This method is required but not called by accessibility API for
164  // features we are using it for. It may need to be implemented if
165  // requirements change.
166 }
167 
168 - (void)deleteBackward {
169  // This method is required but not called by accessibility API for
170  // features we are using it for. It may need to be implemented if
171  // requirements change.
172 }
173 
174 @end
175 
176 @implementation TextInputSemanticsObject {
177  FlutterInactiveTextInput* _inactive_text_input;
178 }
179 
180 - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
181  uid:(int32_t)uid {
182  self = [super initWithBridge:bridge uid:uid];
183 
184  if (self) {
185  _inactive_text_input = [[FlutterInactiveTextInput alloc] init];
186  }
187 
188  return self;
189 }
190 
191 - (void)dealloc {
192  [_inactive_text_input release];
193  [super dealloc];
194 }
195 
196 #pragma mark - SemanticsObject overrides
197 
198 - (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
199  [super setSemanticsNode:node];
200  _inactive_text_input.text = @(node->value.data());
201  if ([self node].HasFlag(flutter::SemanticsFlags::kIsFocused)) {
202  // The text input view must have a non-trivial size for the accessibility
203  // system to send text editing events.
204  [self bridge]->textInputView().frame = CGRectMake(0.0, 0.0, 1.0, 1.0);
205  }
206 }
207 
208 #pragma mark - UIAccessibility overrides
209 
210 /**
211  * The UITextInput whose accessibility properties we present to UIKit as
212  * substitutes for Flutter's text field properties.
213  *
214  * When the field is currently focused (i.e. it is being edited), we use
215  * the FlutterTextInputView used by FlutterTextInputPlugin. Otherwise,
216  * we use an FlutterInactiveTextInput.
217  */
218 - (UIView<UITextInput>*)textInputSurrogate {
219  if ([self node].HasFlag(flutter::SemanticsFlags::kIsFocused)) {
220  return [self bridge]->textInputView();
221  } else {
222  return _inactive_text_input;
223  }
224 }
225 
226 - (UIView*)textInputView {
227  return [self textInputSurrogate];
228 }
229 
230 - (void)accessibilityElementDidBecomeFocused {
231  if (![self isAccessibilityBridgeAlive])
232  return;
233  [[self textInputSurrogate] accessibilityElementDidBecomeFocused];
234  [super accessibilityElementDidBecomeFocused];
235 }
236 
237 - (void)accessibilityElementDidLoseFocus {
238  if (![self isAccessibilityBridgeAlive])
239  return;
240  [[self textInputSurrogate] accessibilityElementDidLoseFocus];
241  [super accessibilityElementDidLoseFocus];
242 }
243 
244 - (BOOL)accessibilityElementIsFocused {
245  if (![self isAccessibilityBridgeAlive])
246  return false;
247  return [self node].HasFlag(flutter::SemanticsFlags::kIsFocused);
248 }
249 
250 - (BOOL)accessibilityActivate {
251  if (![self isAccessibilityBridgeAlive])
252  return false;
253  return [[self textInputSurrogate] accessibilityActivate];
254 }
255 
256 - (NSString*)accessibilityLabel {
257  if (![self isAccessibilityBridgeAlive])
258  return nil;
259 
260  NSString* label = [super accessibilityLabel];
261  if (label != nil)
262  return label;
263  return [self textInputSurrogate].accessibilityLabel;
264 }
265 
266 - (NSString*)accessibilityHint {
267  if (![self isAccessibilityBridgeAlive])
268  return nil;
269  NSString* hint = [super accessibilityHint];
270  if (hint != nil)
271  return hint;
272  return [self textInputSurrogate].accessibilityHint;
273 }
274 
275 - (NSString*)accessibilityValue {
276  if (![self isAccessibilityBridgeAlive])
277  return nil;
278  NSString* value = [super accessibilityValue];
279  if (value != nil)
280  return value;
281  return [self textInputSurrogate].accessibilityValue;
282 }
283 
284 - (UIAccessibilityTraits)accessibilityTraits {
285  if (![self isAccessibilityBridgeAlive])
286  return 0;
287  // Adding UIAccessibilityTraitKeyboardKey to the trait list so that iOS treats it like
288  // a keyboard entry control, thus adding support for text editing features, such as
289  // pinch to select text, and up/down fling to move cursor.
290  UIAccessibilityTraits results = [super accessibilityTraits] |
291  [self textInputSurrogate].accessibilityTraits |
292  UIAccessibilityTraitKeyboardKey;
293  // We remove an undocumented flag to get rid of a bug where single-tapping
294  // a text input field incorrectly says "empty line".
295  // See also: https://github.com/flutter/flutter/issues/52487
296  return results & (~UIAccessibilityTraitUndocumentedEmptyLine);
297 }
298 
299 #pragma mark - UITextInput overrides
300 
301 - (NSString*)textInRange:(UITextRange*)range {
302  return [[self textInputSurrogate] textInRange:range];
303 }
304 
305 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
306  return [[self textInputSurrogate] replaceRange:range withText:text];
307 }
308 
309 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
310  return [[self textInputSurrogate] shouldChangeTextInRange:range replacementText:text];
311 }
312 
313 - (UITextRange*)selectedTextRange {
314  return [[self textInputSurrogate] selectedTextRange];
315 }
316 
317 - (void)setSelectedTextRange:(UITextRange*)range {
318  [[self textInputSurrogate] setSelectedTextRange:range];
319 }
320 
321 - (UITextRange*)markedTextRange {
322  return [[self textInputSurrogate] markedTextRange];
323 }
324 
325 - (NSDictionary*)markedTextStyle {
326  return [[self textInputSurrogate] markedTextStyle];
327 }
328 
329 - (void)setMarkedTextStyle:(NSDictionary*)style {
330  [[self textInputSurrogate] setMarkedTextStyle:style];
331 }
332 
333 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)selectedRange {
334  [[self textInputSurrogate] setMarkedText:markedText selectedRange:selectedRange];
335 }
336 
337 - (void)unmarkText {
338  [[self textInputSurrogate] unmarkText];
339 }
340 
341 - (UITextStorageDirection)selectionAffinity {
342  return [[self textInputSurrogate] selectionAffinity];
343 }
344 
345 - (UITextPosition*)beginningOfDocument {
346  return [[self textInputSurrogate] beginningOfDocument];
347 }
348 
349 - (UITextPosition*)endOfDocument {
350  return [[self textInputSurrogate] endOfDocument];
351 }
352 
353 - (id<UITextInputDelegate>)inputDelegate {
354  return [[self textInputSurrogate] inputDelegate];
355 }
356 
357 - (void)setInputDelegate:(id<UITextInputDelegate>)delegate {
358  [[self textInputSurrogate] setInputDelegate:delegate];
359 }
360 
361 - (id<UITextInputTokenizer>)tokenizer {
362  return [[self textInputSurrogate] tokenizer];
363 }
364 
365 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
366  toPosition:(UITextPosition*)toPosition {
367  return [[self textInputSurrogate] textRangeFromPosition:fromPosition toPosition:toPosition];
368 }
369 
370 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
371  return [[self textInputSurrogate] positionFromPosition:position offset:offset];
372 }
373 
374 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
375  inDirection:(UITextLayoutDirection)direction
376  offset:(NSInteger)offset {
377  return [[self textInputSurrogate] positionFromPosition:position
378  inDirection:direction
379  offset:offset];
380 }
381 
382 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
383  return [[self textInputSurrogate] comparePosition:position toPosition:other];
384 }
385 
386 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
387  return [[self textInputSurrogate] offsetFromPosition:from toPosition:toPosition];
388 }
389 
390 - (UITextPosition*)positionWithinRange:(UITextRange*)range
391  farthestInDirection:(UITextLayoutDirection)direction {
392  return [[self textInputSurrogate] positionWithinRange:range farthestInDirection:direction];
393 }
394 
395 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
396  inDirection:(UITextLayoutDirection)direction {
397  return [[self textInputSurrogate] characterRangeByExtendingPosition:position
398  inDirection:direction];
399 }
400 
401 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
402  inDirection:(UITextStorageDirection)direction {
403  return [[self textInputSurrogate] baseWritingDirectionForPosition:position inDirection:direction];
404 }
405 
406 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
407  forRange:(UITextRange*)range {
408  [[self textInputSurrogate] setBaseWritingDirection:writingDirection forRange:range];
409 }
410 
411 - (CGRect)firstRectForRange:(UITextRange*)range {
412  return [[self textInputSurrogate] firstRectForRange:range];
413 }
414 
415 - (CGRect)caretRectForPosition:(UITextPosition*)position {
416  return [[self textInputSurrogate] caretRectForPosition:position];
417 }
418 
419 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
420  return [[self textInputSurrogate] closestPositionToPoint:point];
421 }
422 
423 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
424  return [[self textInputSurrogate] closestPositionToPoint:point withinRange:range];
425 }
426 
427 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
428  return [[self textInputSurrogate] selectionRectsForRange:range];
429 }
430 
431 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
432  return [[self textInputSurrogate] characterRangeAtPoint:point];
433 }
434 
435 - (void)insertText:(NSString*)text {
436  [[self textInputSurrogate] insertText:text];
437 }
438 
439 - (void)deleteBackward {
440  [[self textInputSurrogate] deleteBackward];
441 }
442 
443 #pragma mark - UIKeyInput overrides
444 
445 - (BOOL)hasText {
446  return [[self textInputSurrogate] hasText];
447 }
448 
449 @end
static const UIAccessibilityTraits UIAccessibilityTraitUndocumentedEmptyLine
instancetype rangeWithNSRange:(NSRange range)
uint8_t value
id< UITextInputDelegate > inputDelegate