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