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