Flutter Engine
FlutterTextInputPlugin.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 "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
6 
7 #import <Foundation/Foundation.h>
8 #import <UIKit/UIKit.h>
9 
10 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
11 
12 static const char _kTextAffinityDownstream[] = "TextAffinity.downstream";
13 static const char _kTextAffinityUpstream[] = "TextAffinity.upstream";
14 
15 // The "canonical" invalid CGRect, similar to CGRectNull, used to
16 // indicate a CGRect involved in firstRectForRange calculation is
17 // invalid. The specific value is chosen so that if firstRectForRange
18 // returns kInvalidFirstRect, iOS will not show the IME candidates view.
19 const CGRect kInvalidFirstRect = {{-1, -1}, {9999, 9999}};
20 
21 #pragma mark - TextInputConfiguration Field Names
22 static NSString* const kSecureTextEntry = @"obscureText";
23 static NSString* const kKeyboardType = @"inputType";
24 static NSString* const kKeyboardAppearance = @"keyboardAppearance";
25 static NSString* const kInputAction = @"inputAction";
26 
27 static NSString* const kSmartDashesType = @"smartDashesType";
28 static NSString* const kSmartQuotesType = @"smartQuotesType";
29 
30 static NSString* const kAssociatedAutofillFields = @"fields";
31 
32 // TextInputConfiguration.autofill and sub-field names
33 static NSString* const kAutofillProperties = @"autofill";
34 static NSString* const kAutofillId = @"uniqueIdentifier";
35 static NSString* const kAutofillEditingValue = @"editingValue";
36 static NSString* const kAutofillHints = @"hints";
37 
38 static NSString* const kAutocorrectionType = @"autocorrect";
39 
40 #pragma mark - Static Functions
41 
42 static UIKeyboardType ToUIKeyboardType(NSDictionary* type) {
43  NSString* inputType = type[@"name"];
44  if ([inputType isEqualToString:@"TextInputType.address"])
45  return UIKeyboardTypeDefault;
46  if ([inputType isEqualToString:@"TextInputType.datetime"])
47  return UIKeyboardTypeNumbersAndPunctuation;
48  if ([inputType isEqualToString:@"TextInputType.emailAddress"])
49  return UIKeyboardTypeEmailAddress;
50  if ([inputType isEqualToString:@"TextInputType.multiline"])
51  return UIKeyboardTypeDefault;
52  if ([inputType isEqualToString:@"TextInputType.name"])
53  return UIKeyboardTypeNamePhonePad;
54  if ([inputType isEqualToString:@"TextInputType.number"]) {
55  if ([type[@"signed"] boolValue])
56  return UIKeyboardTypeNumbersAndPunctuation;
57  if ([type[@"decimal"] boolValue])
58  return UIKeyboardTypeDecimalPad;
59  return UIKeyboardTypeNumberPad;
60  }
61  if ([inputType isEqualToString:@"TextInputType.phone"])
62  return UIKeyboardTypePhonePad;
63  if ([inputType isEqualToString:@"TextInputType.text"])
64  return UIKeyboardTypeDefault;
65  if ([inputType isEqualToString:@"TextInputType.url"])
66  return UIKeyboardTypeURL;
67  return UIKeyboardTypeDefault;
68 }
69 
70 static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary* type) {
71  NSString* textCapitalization = type[@"textCapitalization"];
72  if ([textCapitalization isEqualToString:@"TextCapitalization.characters"]) {
73  return UITextAutocapitalizationTypeAllCharacters;
74  } else if ([textCapitalization isEqualToString:@"TextCapitalization.sentences"]) {
75  return UITextAutocapitalizationTypeSentences;
76  } else if ([textCapitalization isEqualToString:@"TextCapitalization.words"]) {
77  return UITextAutocapitalizationTypeWords;
78  }
79  return UITextAutocapitalizationTypeNone;
80 }
81 
82 static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) {
83  // Where did the term "unspecified" come from? iOS has a "default" and Android
84  // has "unspecified." These 2 terms seem to mean the same thing but we need
85  // to pick just one. "unspecified" was chosen because "default" is often a
86  // reserved word in languages with switch statements (dart, java, etc).
87  if ([inputType isEqualToString:@"TextInputAction.unspecified"])
88  return UIReturnKeyDefault;
89 
90  if ([inputType isEqualToString:@"TextInputAction.done"])
91  return UIReturnKeyDone;
92 
93  if ([inputType isEqualToString:@"TextInputAction.go"])
94  return UIReturnKeyGo;
95 
96  if ([inputType isEqualToString:@"TextInputAction.send"])
97  return UIReturnKeySend;
98 
99  if ([inputType isEqualToString:@"TextInputAction.search"])
100  return UIReturnKeySearch;
101 
102  if ([inputType isEqualToString:@"TextInputAction.next"])
103  return UIReturnKeyNext;
104 
105  if (@available(iOS 9.0, *))
106  if ([inputType isEqualToString:@"TextInputAction.continueAction"])
107  return UIReturnKeyContinue;
108 
109  if ([inputType isEqualToString:@"TextInputAction.join"])
110  return UIReturnKeyJoin;
111 
112  if ([inputType isEqualToString:@"TextInputAction.route"])
113  return UIReturnKeyRoute;
114 
115  if ([inputType isEqualToString:@"TextInputAction.emergencyCall"])
116  return UIReturnKeyEmergencyCall;
117 
118  if ([inputType isEqualToString:@"TextInputAction.newline"])
119  return UIReturnKeyDefault;
120 
121  // Present default key if bad input type is given.
122  return UIReturnKeyDefault;
123 }
124 
125 static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
126  if (hints == nil || hints.count == 0) {
127  return @"";
128  }
129 
130  NSString* hint = hints[0];
131  if (@available(iOS 10.0, *)) {
132  if ([hint isEqualToString:@"addressCityAndState"]) {
133  return UITextContentTypeAddressCityAndState;
134  }
135 
136  if ([hint isEqualToString:@"addressState"]) {
137  return UITextContentTypeAddressState;
138  }
139 
140  if ([hint isEqualToString:@"addressCity"]) {
141  return UITextContentTypeAddressCity;
142  }
143 
144  if ([hint isEqualToString:@"sublocality"]) {
145  return UITextContentTypeSublocality;
146  }
147 
148  if ([hint isEqualToString:@"streetAddressLine1"]) {
149  return UITextContentTypeStreetAddressLine1;
150  }
151 
152  if ([hint isEqualToString:@"streetAddressLine2"]) {
153  return UITextContentTypeStreetAddressLine2;
154  }
155 
156  if ([hint isEqualToString:@"countryName"]) {
157  return UITextContentTypeCountryName;
158  }
159 
160  if ([hint isEqualToString:@"fullStreetAddress"]) {
161  return UITextContentTypeFullStreetAddress;
162  }
163 
164  if ([hint isEqualToString:@"postalCode"]) {
165  return UITextContentTypePostalCode;
166  }
167 
168  if ([hint isEqualToString:@"location"]) {
169  return UITextContentTypeLocation;
170  }
171 
172  if ([hint isEqualToString:@"creditCardNumber"]) {
173  return UITextContentTypeCreditCardNumber;
174  }
175 
176  if ([hint isEqualToString:@"email"]) {
177  return UITextContentTypeEmailAddress;
178  }
179 
180  if ([hint isEqualToString:@"jobTitle"]) {
181  return UITextContentTypeJobTitle;
182  }
183 
184  if ([hint isEqualToString:@"givenName"]) {
185  return UITextContentTypeGivenName;
186  }
187 
188  if ([hint isEqualToString:@"middleName"]) {
189  return UITextContentTypeMiddleName;
190  }
191 
192  if ([hint isEqualToString:@"familyName"]) {
193  return UITextContentTypeFamilyName;
194  }
195 
196  if ([hint isEqualToString:@"name"]) {
197  return UITextContentTypeName;
198  }
199 
200  if ([hint isEqualToString:@"namePrefix"]) {
201  return UITextContentTypeNamePrefix;
202  }
203 
204  if ([hint isEqualToString:@"nameSuffix"]) {
205  return UITextContentTypeNameSuffix;
206  }
207 
208  if ([hint isEqualToString:@"nickname"]) {
209  return UITextContentTypeNickname;
210  }
211 
212  if ([hint isEqualToString:@"organizationName"]) {
213  return UITextContentTypeOrganizationName;
214  }
215 
216  if ([hint isEqualToString:@"telephoneNumber"]) {
217  return UITextContentTypeTelephoneNumber;
218  }
219  }
220 
221  if (@available(iOS 11.0, *)) {
222  if ([hint isEqualToString:@"password"]) {
223  return UITextContentTypePassword;
224  }
225  }
226 
227  if (@available(iOS 12.0, *)) {
228  if ([hint isEqualToString:@"oneTimeCode"]) {
229  return UITextContentTypeOneTimeCode;
230  }
231 
232  if ([hint isEqualToString:@"newPassword"]) {
233  return UITextContentTypeNewPassword;
234  }
235  }
236 
237  return hints[0];
238 }
239 
240 // Retrieves the autofillId from an input field's configuration. Returns
241 // nil if the field is nil and the input field is not a password field.
242 static NSString* autofillIdFromDictionary(NSDictionary* dictionary) {
243  NSDictionary* autofill = dictionary[kAutofillProperties];
244  if (autofill) {
245  return autofill[kAutofillId];
246  }
247 
248  // When autofill is nil, the field may still need an autofill id
249  // if the field is for password.
250  return [dictionary[kSecureTextEntry] boolValue] ? @"password" : nil;
251 }
252 
253 // There're 2 types of autofills on native iOS:
254 // - Regular autofill, includes contact information autofill and
255 // one-time-code autofill, takes place in the form of predictive
256 // text in the quick type bar. This type of autofill does not save
257 // user input.
258 // - Password autofill, includes automatic strong password and regular
259 // password autofill. The former happens automatically when a
260 // "new password" field is detected, and only that password field
261 // will be populated. The latter appears in the quick type bar when
262 // an eligible input field becomes the first responder, and may
263 // fill both the username and the password fields. iOS will attempt
264 // to save user input for both kinds of password fields.
265 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
266  // The field does not have autofillable content. Additionally if
267  // the field is currently in the autofill context, it will be
268  // removed from the context without triggering autofill save.
269  FlutterAutofillTypeNone,
270  FlutterAutofillTypeRegular,
271  FlutterAutofillTypePassword,
272 };
273 
274 static BOOL isFieldPasswordRelated(NSDictionary* configuration) {
275  if (@available(iOS 10.0, *)) {
276  BOOL isSecureTextEntry = [configuration[kSecureTextEntry] boolValue];
277  if (isSecureTextEntry)
278  return YES;
279 
280  if (!autofillIdFromDictionary(configuration)) {
281  return NO;
282  }
283  NSDictionary* autofill = configuration[kAutofillProperties];
284  UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
285 
286  if (@available(iOS 11.0, *)) {
287  if ([contentType isEqualToString:UITextContentTypePassword] ||
288  [contentType isEqualToString:UITextContentTypeUsername]) {
289  return YES;
290  }
291  }
292 
293  if (@available(iOS 12.0, *)) {
294  if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
295  return YES;
296  }
297  }
298  }
299  return NO;
300 }
301 
302 static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
303  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
304  if (isFieldPasswordRelated(field)) {
305  return FlutterAutofillTypePassword;
306  }
307  }
308 
309  if (isFieldPasswordRelated(configuration)) {
310  return FlutterAutofillTypePassword;
311  }
312 
313  if (@available(iOS 10.0, *)) {
314  NSDictionary* autofill = configuration[kAutofillProperties];
315  UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
316  return [contentType isEqualToString:@""] ? FlutterAutofillTypeNone : FlutterAutofillTypeRegular;
317  }
318 
319  return FlutterAutofillTypeNone;
320 }
321 
322 #pragma mark - FlutterTextPosition
323 
324 @implementation FlutterTextPosition
325 
326 + (instancetype)positionWithIndex:(NSUInteger)index {
327  return [[[FlutterTextPosition alloc] initWithIndex:index] autorelease];
328 }
329 
330 - (instancetype)initWithIndex:(NSUInteger)index {
331  self = [super init];
332  if (self) {
333  _index = index;
334  }
335  return self;
336 }
337 
338 @end
339 
340 #pragma mark - FlutterTextRange
341 
342 @implementation FlutterTextRange
343 
344 + (instancetype)rangeWithNSRange:(NSRange)range {
345  return [[[FlutterTextRange alloc] initWithNSRange:range] autorelease];
346 }
347 
348 - (instancetype)initWithNSRange:(NSRange)range {
349  self = [super init];
350  if (self) {
351  _range = range;
352  }
353  return self;
354 }
355 
356 - (UITextPosition*)start {
357  return [FlutterTextPosition positionWithIndex:self.range.location];
358 }
359 
360 - (UITextPosition*)end {
361  return [FlutterTextPosition positionWithIndex:self.range.location + self.range.length];
362 }
363 
364 - (BOOL)isEmpty {
365  return self.range.length == 0;
366 }
367 
368 - (id)copyWithZone:(NSZone*)zone {
369  return [[FlutterTextRange allocWithZone:zone] initWithNSRange:self.range];
370 }
371 
372 - (BOOL)isEqualTo:(FlutterTextRange*)other {
373  return NSEqualRanges(self.range, other.range);
374 }
375 @end
376 
377 // A FlutterTextInputView that masquerades as a UITextField, and forwards
378 // selectors it can't respond to to a shared UITextField instance.
379 //
380 // Relevant API docs claim that password autofill supports any custom view
381 // that adopts the UITextInput protocol, automatic strong password seems to
382 // currently only support UITextFields, and password saving only supports
383 // UITextFields and UITextViews, as of iOS 13.5.
385 @property(nonatomic, strong, readonly) UITextField* textField;
386 @end
387 
388 @implementation FlutterSecureTextInputView {
389  UITextField* _textField;
390 }
391 
392 - (void)dealloc {
393  [_textField release];
394  [super dealloc];
395 }
396 
397 - (UITextField*)textField {
398  if (!_textField) {
399  _textField = [[UITextField alloc] init];
400  }
401  return _textField;
402 }
403 
404 - (BOOL)isKindOfClass:(Class)aClass {
405  return [super isKindOfClass:aClass] || (aClass == [UITextField class]);
406 }
407 
408 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
409  NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
410  if (!signature) {
411  signature = [self.textField methodSignatureForSelector:aSelector];
412  }
413  return signature;
414 }
415 
416 - (void)forwardInvocation:(NSInvocation*)anInvocation {
417  [anInvocation invokeWithTarget:self.textField];
418 }
419 
420 @end
421 
422 @interface FlutterTextInputView ()
423 @property(nonatomic, copy) NSString* autofillId;
424 @property(nonatomic, readonly) CATransform3D editableTransform;
425 @property(nonatomic, assign) CGRect markedRect;
426 @property(nonatomic) BOOL isVisibleToAutofill;
427 
428 - (void)setEditableTransform:(NSArray*)matrix;
429 @end
430 
431 @implementation FlutterTextInputView {
432  int _textInputClient;
433  const char* _selectionAffinity;
436 }
437 
438 @synthesize tokenizer = _tokenizer;
439 
440 - (instancetype)init {
441  self = [super init];
442  if (self) {
443  _textInputClient = 0;
445 
446  // UITextInput
447  _text = [[NSMutableString alloc] init];
448  _markedText = [[NSMutableString alloc] init];
449  _selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)];
450  _markedRect = kInvalidFirstRect;
452  // Initialize with the zero matrix which is not
453  // an affine transform.
454  _editableTransform = CATransform3D();
455 
456  // UITextInputTraits
457  _autocapitalizationType = UITextAutocapitalizationTypeSentences;
458  _autocorrectionType = UITextAutocorrectionTypeDefault;
459  _spellCheckingType = UITextSpellCheckingTypeDefault;
460  _enablesReturnKeyAutomatically = NO;
461  _keyboardAppearance = UIKeyboardAppearanceDefault;
462  _keyboardType = UIKeyboardTypeDefault;
463  _returnKeyType = UIReturnKeyDone;
464  _secureTextEntry = NO;
465  if (@available(iOS 11.0, *)) {
466  _smartQuotesType = UITextSmartQuotesTypeYes;
467  _smartDashesType = UITextSmartDashesTypeYes;
468  }
469  }
470 
471  return self;
472 }
473 
474 - (void)configureWithDictionary:(NSDictionary*)configuration {
475  NSDictionary* inputType = configuration[kKeyboardType];
476  NSString* keyboardAppearance = configuration[kKeyboardAppearance];
477  NSDictionary* autofill = configuration[kAutofillProperties];
478 
479  self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
480  self.keyboardType = ToUIKeyboardType(inputType);
481  self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]);
482  self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
483 
484  if (@available(iOS 11.0, *)) {
485  NSString* smartDashesType = configuration[kSmartDashesType];
486  // This index comes from the SmartDashesType enum in the framework.
487  bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
488  self.smartDashesType =
489  smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
490  NSString* smartQuotesType = configuration[kSmartQuotesType];
491  // This index comes from the SmartQuotesType enum in the framework.
492  bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
493  self.smartQuotesType =
494  smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
495  }
496  if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
497  self.keyboardAppearance = UIKeyboardAppearanceDark;
498  } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
499  self.keyboardAppearance = UIKeyboardAppearanceLight;
500  } else {
501  self.keyboardAppearance = UIKeyboardAppearanceDefault;
502  }
503  NSString* autocorrect = configuration[kAutocorrectionType];
504  self.autocorrectionType = autocorrect && ![autocorrect boolValue]
505  ? UITextAutocorrectionTypeNo
506  : UITextAutocorrectionTypeDefault;
507  if (@available(iOS 10.0, *)) {
508  self.autofillId = autofillIdFromDictionary(configuration);
509  if (autofill == nil) {
510  self.textContentType = @"";
511  } else {
512  self.textContentType = ToUITextContentType(autofill[kAutofillHints]);
513  [self setTextInputState:autofill[kAutofillEditingValue]];
514  NSAssert(_autofillId, @"The autofill configuration must contain an autofill id");
515  }
516  // The input field needs to be visible for the system autofill
517  // to find it.
518  self.isVisibleToAutofill = autofill || _secureTextEntry;
519  }
520 }
521 
522 - (UITextContentType)textContentType {
523  return _textContentType;
524 }
525 
526 - (void)dealloc {
527  [_text release];
528  [_markedText release];
529  [_markedTextRange release];
530  [_selectedTextRange release];
531  [_tokenizer release];
532  [_autofillId release];
533  [super dealloc];
534 }
535 
536 - (void)setTextInputClient:(int)client {
537  _textInputClient = client;
538 }
539 
540 - (void)setTextInputState:(NSDictionary*)state {
541  NSString* newText = state[@"text"];
542  BOOL textChanged = ![self.text isEqualToString:newText];
543  if (textChanged) {
544  [self.inputDelegate textWillChange:self];
545  [self.text setString:newText];
546  }
547  NSInteger composingBase = [state[@"composingBase"] intValue];
548  NSInteger composingExtent = [state[@"composingExtent"] intValue];
549  NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
550  ABS(composingBase - composingExtent))
551  forText:self.text];
552 
553  self.markedTextRange =
554  composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil;
555 
556  NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue]
557  extent:[state[@"selectionExtent"] intValue]
558  forText:self.text];
559 
560  NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range];
561  if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
562  [self.inputDelegate selectionWillChange:self];
563 
564  [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
565 
567  if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)])
569  [self.inputDelegate selectionDidChange:self];
570  }
571 
572  if (textChanged) {
573  [self.inputDelegate textDidChange:self];
574  }
575 }
576 
577 // Extracts the selection information from the editing state dictionary.
578 //
579 // The state may contain an invalid selection, such as when no selection was
580 // explicitly set in the framework. This is handled here by setting the
581 // selection to (0,0). In contrast, Android handles this situation by
582 // clearing the selection, but the result in both cases is that the cursor
583 // is placed at the beginning of the field.
584 - (NSRange)clampSelectionFromBase:(int)selectionBase
585  extent:(int)selectionExtent
586  forText:(NSString*)text {
587  int loc = MIN(selectionBase, selectionExtent);
588  int len = ABS(selectionExtent - selectionBase);
589  return loc < 0 ? NSMakeRange(0, 0)
590  : [self clampSelection:NSMakeRange(loc, len) forText:self.text];
591 }
592 
593 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
594  int start = MIN(MAX(range.location, 0), text.length);
595  int length = MIN(range.length, text.length - start);
596  return NSMakeRange(start, length);
597 }
598 
599 - (BOOL)isVisibleToAutofill {
600  return self.frame.size.width > 0 && self.frame.size.height > 0;
601 }
602 
603 // An input view is generally ignored by password autofill attempts, if it's
604 // not the first responder and is zero-sized. For input fields that are in the
605 // autofill context but do not belong to the current autofill group, setting
606 // their frames to CGRectZero prevents ios autofill from taking them into
607 // account.
608 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
609  self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
610 }
611 
612 #pragma mark - UIResponder Overrides
613 
614 - (BOOL)canBecomeFirstResponder {
615  // Only the currently focused input field can
616  // become the first responder. This prevents iOS
617  // from changing focus by itself (the framework
618  // focus will be out of sync if that happens).
619  return _textInputClient != 0;
620 }
621 
622 #pragma mark - UITextInput Overrides
623 
624 - (id<UITextInputTokenizer>)tokenizer {
625  if (_tokenizer == nil) {
626  _tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];
627  }
628  return _tokenizer;
629 }
630 
631 - (UITextRange*)selectedTextRange {
632  return [[_selectedTextRange copy] autorelease];
633 }
634 
635 // Change the range of selected text, without notifying the framework.
636 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
637  if (_selectedTextRange != selectedTextRange) {
638  UITextRange* oldSelectedRange = _selectedTextRange;
639  if (self.hasText) {
640  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
642  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
643  } else {
644  _selectedTextRange = [selectedTextRange copy];
645  }
646  [oldSelectedRange release];
647  }
648 }
649 
650 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
651  [self setSelectedTextRangeLocal:selectedTextRange];
652  [self updateEditingState];
653 }
654 
655 - (id)insertDictationResultPlaceholder {
656  return @"";
657 }
658 
659 - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
660 }
661 
662 - (NSString*)textInRange:(UITextRange*)range {
663  if (!range) {
664  return nil;
665  }
666  NSAssert([range isKindOfClass:[FlutterTextRange class]],
667  @"Expected a FlutterTextRange for range (got %@).", [range class]);
668  NSRange textRange = ((FlutterTextRange*)range).range;
669  NSAssert(textRange.location != NSNotFound, @"Expected a valid text range.");
670  return [self.text substringWithRange:textRange];
671 }
672 
673 // Replace the text within the specified range with the given text,
674 // without notifying the framework.
675 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
676  NSRange selectedRange = _selectedTextRange.range;
677 
678  // Adjust the text selection:
679  // * reduce the length by the intersection length
680  // * adjust the location by newLength - oldLength + intersectionLength
681  NSRange intersectionRange = NSIntersectionRange(range, selectedRange);
682  if (range.location <= selectedRange.location)
683  selectedRange.location += text.length - range.length;
684  if (intersectionRange.location != NSNotFound) {
685  selectedRange.location += intersectionRange.length;
686  selectedRange.length -= intersectionRange.length;
687  }
688 
689  [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
690  withString:text];
691  [self setSelectedTextRangeLocal:[FlutterTextRange
692  rangeWithNSRange:[self clampSelection:selectedRange
693  forText:self.text]]];
694 }
695 
696 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
697  NSRange replaceRange = ((FlutterTextRange*)range).range;
698  [self replaceRangeLocal:replaceRange withText:text];
699  [self updateEditingState];
700 }
701 
702 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
703  if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
704  [_textInputDelegate performAction:FlutterTextInputActionNewline withClient:_textInputClient];
705  return YES;
706  }
707 
708  if ([text isEqualToString:@"\n"]) {
709  FlutterTextInputAction action;
710  switch (self.returnKeyType) {
711  case UIReturnKeyDefault:
712  action = FlutterTextInputActionUnspecified;
713  break;
714  case UIReturnKeyDone:
715  action = FlutterTextInputActionDone;
716  break;
717  case UIReturnKeyGo:
718  action = FlutterTextInputActionGo;
719  break;
720  case UIReturnKeySend:
721  action = FlutterTextInputActionSend;
722  break;
723  case UIReturnKeySearch:
724  case UIReturnKeyGoogle:
725  case UIReturnKeyYahoo:
726  action = FlutterTextInputActionSearch;
727  break;
728  case UIReturnKeyNext:
729  action = FlutterTextInputActionNext;
730  break;
731  case UIReturnKeyContinue:
732  action = FlutterTextInputActionContinue;
733  break;
734  case UIReturnKeyJoin:
735  action = FlutterTextInputActionJoin;
736  break;
737  case UIReturnKeyRoute:
738  action = FlutterTextInputActionRoute;
739  break;
740  case UIReturnKeyEmergencyCall:
741  action = FlutterTextInputActionEmergencyCall;
742  break;
743  }
744 
745  [_textInputDelegate performAction:action withClient:_textInputClient];
746  return NO;
747  }
748 
749  return YES;
750 }
751 
752 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
753  NSRange selectedRange = _selectedTextRange.range;
754  NSRange markedTextRange = ((FlutterTextRange*)self.markedTextRange).range;
755 
756  if (markedText == nil)
757  markedText = @"";
758 
759  if (markedTextRange.length > 0) {
760  // Replace text in the marked range with the new text.
761  [self replaceRangeLocal:markedTextRange withText:markedText];
762  markedTextRange.length = markedText.length;
763  } else {
764  // Replace text in the selected range with the new text.
765  [self replaceRangeLocal:selectedRange withText:markedText];
766  markedTextRange = NSMakeRange(selectedRange.location, markedText.length);
767  }
768 
769  self.markedTextRange =
770  markedTextRange.length > 0 ? [FlutterTextRange rangeWithNSRange:markedTextRange] : nil;
771 
772  NSUInteger selectionLocation = markedSelectedRange.location + markedTextRange.location;
773  selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length);
774  [self setSelectedTextRangeLocal:[FlutterTextRange
775  rangeWithNSRange:[self clampSelection:selectedRange
776  forText:self.text]]];
777  [self updateEditingState];
778 }
779 
780 - (void)unmarkText {
781  if (!self.markedTextRange)
782  return;
783  self.markedTextRange = nil;
784  [self updateEditingState];
785 }
786 
787 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
788  toPosition:(UITextPosition*)toPosition {
789  NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
790  NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
791  if (toIndex >= fromIndex) {
792  return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
793  } else {
794  // toIndex may be less than fromIndex, because
795  // UITextInputStringTokenizer does not handle CJK characters
796  // well in some cases. See:
797  // https://github.com/flutter/flutter/issues/58750#issuecomment-644469521
798  // Swap fromPosition and toPosition to match the behavior of native
799  // UITextViews.
800  return [FlutterTextRange rangeWithNSRange:NSMakeRange(toIndex, fromIndex - toIndex)];
801  }
802 }
803 
804 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
805  return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location;
806 }
807 
808 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
809  NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position);
810  return MIN(position + charRange.length, self.text.length);
811 }
812 
813 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
814  NSUInteger offsetPosition = ((FlutterTextPosition*)position).index;
815 
816  NSInteger newLocation = (NSInteger)offsetPosition + offset;
817  if (newLocation < 0 || newLocation > (NSInteger)self.text.length) {
818  return nil;
819  }
820 
821  if (offset >= 0) {
822  for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i)
823  offsetPosition = [self incrementOffsetPosition:offsetPosition];
824  } else {
825  for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i)
826  offsetPosition = [self decrementOffsetPosition:offsetPosition];
827  }
828  return [FlutterTextPosition positionWithIndex:offsetPosition];
829 }
830 
831 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
832  inDirection:(UITextLayoutDirection)direction
833  offset:(NSInteger)offset {
834  // TODO(cbracken) Add RTL handling.
835  switch (direction) {
836  case UITextLayoutDirectionLeft:
837  case UITextLayoutDirectionUp:
838  return [self positionFromPosition:position offset:offset * -1];
839  case UITextLayoutDirectionRight:
840  case UITextLayoutDirectionDown:
841  return [self positionFromPosition:position offset:1];
842  }
843 }
844 
845 - (UITextPosition*)beginningOfDocument {
847 }
848 
849 - (UITextPosition*)endOfDocument {
850  return [FlutterTextPosition positionWithIndex:self.text.length];
851 }
852 
853 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
854  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
855  NSUInteger otherIndex = ((FlutterTextPosition*)other).index;
856  if (positionIndex < otherIndex)
857  return NSOrderedAscending;
858  if (positionIndex > otherIndex)
859  return NSOrderedDescending;
860  return NSOrderedSame;
861 }
862 
863 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
864  return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index;
865 }
866 
867 - (UITextPosition*)positionWithinRange:(UITextRange*)range
868  farthestInDirection:(UITextLayoutDirection)direction {
869  NSUInteger index;
870  switch (direction) {
871  case UITextLayoutDirectionLeft:
872  case UITextLayoutDirectionUp:
873  index = ((FlutterTextPosition*)range.start).index;
874  break;
875  case UITextLayoutDirectionRight:
876  case UITextLayoutDirectionDown:
877  index = ((FlutterTextPosition*)range.end).index;
878  break;
879  }
880  return [FlutterTextPosition positionWithIndex:index];
881 }
882 
883 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
884  inDirection:(UITextLayoutDirection)direction {
885  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
886  NSUInteger startIndex;
887  NSUInteger endIndex;
888  switch (direction) {
889  case UITextLayoutDirectionLeft:
890  case UITextLayoutDirectionUp:
891  startIndex = [self decrementOffsetPosition:positionIndex];
892  endIndex = positionIndex;
893  break;
894  case UITextLayoutDirectionRight:
895  case UITextLayoutDirectionDown:
896  startIndex = positionIndex;
897  endIndex = [self incrementOffsetPosition:positionIndex];
898  break;
899  }
900  return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)];
901 }
902 
903 #pragma mark - UITextInput text direction handling
904 
905 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
906  inDirection:(UITextStorageDirection)direction {
907  // TODO(cbracken) Add RTL handling.
908  return UITextWritingDirectionNatural;
909 }
910 
911 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
912  forRange:(UITextRange*)range {
913  // TODO(cbracken) Add RTL handling.
914 }
915 
916 #pragma mark - UITextInput cursor, selection rect handling
917 
918 - (void)setMarkedRect:(CGRect)markedRect {
919  _markedRect = markedRect;
920  // Invalidate the cache.
922 }
923 
924 // This method expects a 4x4 perspective matrix
925 // stored in a NSArray in column-major order.
926 - (void)setEditableTransform:(NSArray*)matrix {
927  CATransform3D* transform = &_editableTransform;
928 
929  transform->m11 = [matrix[0] doubleValue];
930  transform->m12 = [matrix[1] doubleValue];
931  transform->m13 = [matrix[2] doubleValue];
932  transform->m14 = [matrix[3] doubleValue];
933 
934  transform->m21 = [matrix[4] doubleValue];
935  transform->m22 = [matrix[5] doubleValue];
936  transform->m23 = [matrix[6] doubleValue];
937  transform->m24 = [matrix[7] doubleValue];
938 
939  transform->m31 = [matrix[8] doubleValue];
940  transform->m32 = [matrix[9] doubleValue];
941  transform->m33 = [matrix[10] doubleValue];
942  transform->m34 = [matrix[11] doubleValue];
943 
944  transform->m41 = [matrix[12] doubleValue];
945  transform->m42 = [matrix[13] doubleValue];
946  transform->m43 = [matrix[14] doubleValue];
947  transform->m44 = [matrix[15] doubleValue];
948 
949  // Invalidate the cache.
951 }
952 
953 // The following methods are required to support force-touch cursor positioning
954 // and to position the
955 // candidates view for multi-stage input methods (e.g., Japanese) when using a
956 // physical keyboard.
957 
958 - (CGRect)firstRectForRange:(UITextRange*)range {
959  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
960  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
961  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
962  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
963 
964  NSUInteger start = ((FlutterTextPosition*)range.start).index;
965  NSUInteger end = ((FlutterTextPosition*)range.end).index;
966  if (_markedTextRange != nil) {
967  // The candidates view can't be shown if _editableTransform is not affine,
968  // or markedRect is invalid.
969  if (CGRectEqualToRect(kInvalidFirstRect, _markedRect) ||
970  !CATransform3DIsAffine(_editableTransform)) {
971  return kInvalidFirstRect;
972  }
973 
974  if (CGRectEqualToRect(_cachedFirstRect, kInvalidFirstRect)) {
975  // If the width returned is too small, that means the framework sent us
976  // the caret rect instead of the marked text rect. Expand it to 0.1 so
977  // the IME candidates view show up.
978  double nonZeroWidth = MAX(_markedRect.size.width, 0.1);
979  CGRect rect = _markedRect;
980  rect.size = CGSizeMake(nonZeroWidth, rect.size.height);
982  CGRectApplyAffineTransform(rect, CATransform3DGetAffineTransform(_editableTransform));
983  }
984 
985  return _cachedFirstRect;
986  }
987 
988  [_textInputDelegate showAutocorrectionPromptRectForStart:start
989  end:end
990  withClient:_textInputClient];
991  // TODO(cbracken) Implement.
992  return CGRectZero;
993 }
994 
995 - (CGRect)caretRectForPosition:(UITextPosition*)position {
996  // TODO(cbracken) Implement.
997  return CGRectZero;
998 }
999 
1000 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1001  // TODO(cbracken) Implement.
1002  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1003  return [FlutterTextPosition positionWithIndex:currentIndex];
1004 }
1005 
1006 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1007  // TODO(cbracken) Implement.
1008  return @[];
1009 }
1010 
1011 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1012  // TODO(cbracken) Implement.
1013  return range.start;
1014 }
1015 
1016 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1017  // TODO(cbracken) Implement.
1018  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1019  return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
1020 }
1021 
1022 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
1023  [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart
1024  withClient:_textInputClient
1025  withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
1026 }
1027 
1028 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
1029  [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
1030  withClient:_textInputClient
1031  withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
1032 }
1033 
1034 - (void)endFloatingCursor {
1035  [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd
1036  withClient:_textInputClient
1037  withPosition:@{@"X" : @(0), @"Y" : @(0)}];
1038 }
1039 
1040 #pragma mark - UIKeyInput Overrides
1041 
1042 - (void)updateEditingState {
1043  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
1044  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
1045 
1046  // Empty compositing range is represented by the framework's TextRange.empty.
1047  NSInteger composingBase = -1;
1048  NSInteger composingExtent = -1;
1049  if (self.markedTextRange != nil) {
1050  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
1051  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
1052  }
1053 
1054  NSDictionary* state = @{
1055  @"selectionBase" : @(selectionBase),
1056  @"selectionExtent" : @(selectionExtent),
1057  @"selectionAffinity" : @(_selectionAffinity),
1058  @"selectionIsDirectional" : @(false),
1059  @"composingBase" : @(composingBase),
1060  @"composingExtent" : @(composingExtent),
1061  @"text" : [NSString stringWithString:self.text],
1062  };
1063 
1064  if (_textInputClient == 0 && _autofillId != nil) {
1065  [_textInputDelegate updateEditingClient:_textInputClient withState:state withTag:_autofillId];
1066  } else {
1067  [_textInputDelegate updateEditingClient:_textInputClient withState:state];
1068  }
1069 }
1070 
1071 - (BOOL)hasText {
1072  return self.text.length > 0;
1073 }
1074 
1075 - (void)insertText:(NSString*)text {
1077  [self replaceRange:_selectedTextRange withText:text];
1078 }
1079 
1080 - (void)deleteBackward {
1082 
1083  // When deleting Thai vowel, _selectedTextRange has location
1084  // but does not have length, so we have to manually set it.
1085  // In addition, we needed to delete only a part of grapheme cluster
1086  // because it is the expected behavior of Thai input.
1087  // https://github.com/flutter/flutter/issues/24203
1088  // https://github.com/flutter/flutter/issues/21745
1089  // https://github.com/flutter/flutter/issues/39399
1090  //
1091  // This is needed for correct handling of the deletion of Thai vowel input.
1092  // TODO(cbracken): Get a good understanding of expected behavior of Thai
1093  // input and ensure that this is the correct solution.
1094  // https://github.com/flutter/flutter/issues/28962
1095  if (_selectedTextRange.isEmpty && [self hasText]) {
1096  UITextRange* oldSelectedRange = _selectedTextRange;
1097  NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
1098  if (oldRange.location > 0) {
1099  NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
1101  [oldSelectedRange release];
1102  }
1103  }
1104 
1105  if (!_selectedTextRange.isEmpty)
1106  [self replaceRange:_selectedTextRange withText:@""];
1107 }
1108 
1109 - (BOOL)accessibilityElementsHidden {
1110  // We are hiding this accessibility element.
1111  // There are 2 accessible elements involved in text entry in 2 different parts of the view
1112  // hierarchy. This `FlutterTextInputView` is injected at the top of key window. We use this as a
1113  // `UITextInput` protocol to bridge text edit events between Flutter and iOS.
1114  //
1115  // We also create ur own custom `UIAccessibilityElements` tree with our `SemanticsObject` to
1116  // mimic the semantics tree from Flutter. We want the text field to be represented as a
1117  // `TextInputSemanticsObject` in that `SemanticsObject` tree rather than in this
1118  // `FlutterTextInputView` bridge which doesn't appear above a text field from the Flutter side.
1119  return YES;
1120 }
1121 
1122 @end
1123 
1125 @property(nonatomic, strong) FlutterTextInputView* reusableInputView;
1126 
1127 // The current password-autofillable input fields that have yet to be saved.
1128 @property(nonatomic, readonly)
1129  NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
1130 @property(nonatomic, assign) FlutterTextInputView* activeView;
1131 @end
1132 
1133 @implementation FlutterTextInputPlugin
1134 
1135 @synthesize textInputDelegate = _textInputDelegate;
1136 
1137 - (instancetype)init {
1138  self = [super init];
1139 
1140  if (self) {
1141  _reusableInputView = [[FlutterTextInputView alloc] init];
1142  _reusableInputView.secureTextEntry = NO;
1143  _autofillContext = [[NSMutableDictionary alloc] init];
1144  _activeView = _reusableInputView;
1145  }
1146 
1147  return self;
1148 }
1149 
1150 - (void)dealloc {
1151  [self hideTextInput];
1152  [_reusableInputView release];
1153  [_autofillContext release];
1154 
1155  [super dealloc];
1156 }
1157 
1158 - (UIView<UITextInput>*)textInputView {
1159  return _activeView;
1160 }
1161 
1162 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
1163  NSString* method = call.method;
1164  id args = call.arguments;
1165  if ([method isEqualToString:@"TextInput.show"]) {
1166  [self showTextInput];
1167  result(nil);
1168  } else if ([method isEqualToString:@"TextInput.hide"]) {
1169  [self hideTextInput];
1170  result(nil);
1171  } else if ([method isEqualToString:@"TextInput.setClient"]) {
1172  [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
1173  result(nil);
1174  } else if ([method isEqualToString:@"TextInput.setEditingState"]) {
1175  [self setTextInputEditingState:args];
1176  result(nil);
1177  } else if ([method isEqualToString:@"TextInput.clearClient"]) {
1178  [self clearTextInputClient];
1179  result(nil);
1180  } else if ([method isEqualToString:@"TextInput.setEditableSizeAndTransform"]) {
1181  [self setEditableSizeAndTransform:args];
1182  result(nil);
1183  } else if ([method isEqualToString:@"TextInput.setMarkedTextRect"]) {
1184  [self updateMarkedRect:args];
1185  result(nil);
1186  } else if ([method isEqualToString:@"TextInput.finishAutofillContext"]) {
1187  [self triggerAutofillSave:[args boolValue]];
1188  result(nil);
1189  } else {
1191  }
1192 }
1193 
1194 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
1195  [_activeView setEditableTransform:dictionary[@"transform"]];
1196 }
1197 
1198 - (void)updateMarkedRect:(NSDictionary*)dictionary {
1199  NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
1200  dictionary[@"height"] != nil,
1201  @"Expected a dictionary representing a CGRect, got %@", dictionary);
1202  CGRect rect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
1203  [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
1204  _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect;
1205 }
1206 
1207 - (void)showTextInput {
1208  _activeView.textInputDelegate = _textInputDelegate;
1209  [self addToInputParentViewIfNeeded:_activeView];
1210  [_activeView becomeFirstResponder];
1211 }
1212 
1213 - (void)hideTextInput {
1214  [_activeView resignFirstResponder];
1215 }
1216 
1217 - (void)triggerAutofillSave:(BOOL)saveEntries {
1218  [self hideTextInput];
1219 
1220  if (saveEntries) {
1221  // Make all the input fields in the autofill context visible,
1222  // then remove them to trigger autofill save.
1223  [self cleanUpViewHierarchy:YES clearText:YES];
1224  [_autofillContext removeAllObjects];
1225  [self changeInputViewsAutofillVisibility:YES];
1226  } else {
1227  [_autofillContext removeAllObjects];
1228  }
1229 
1230  [self cleanUpViewHierarchy:YES clearText:!saveEntries];
1231  [self addToInputParentViewIfNeeded:_activeView];
1232 }
1233 
1234 - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
1235  [self resetAllClientIds];
1236  // Hide all input views from autofill, only make those in the new configuration visible
1237  // to autofill.
1238  [self changeInputViewsAutofillVisibility:NO];
1239  switch (autofillTypeOf(configuration)) {
1240  case FlutterAutofillTypeNone:
1241  _activeView = [self updateAndShowReusableInputView:configuration];
1242  break;
1243  case FlutterAutofillTypeRegular:
1244  // If the group does not involve password autofill, only install the
1245  // input view that's being focused.
1246  _activeView = [self updateAndShowAutofillViews:nil
1247  focusedField:configuration
1248  isPasswordRelated:NO];
1249  break;
1250  case FlutterAutofillTypePassword:
1251  _activeView = [self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
1252  focusedField:configuration
1253  isPasswordRelated:YES];
1254  break;
1255  }
1256 
1257  [_activeView setTextInputClient:client];
1258  [_activeView reloadInputViews];
1259 
1260  // Clean up views that no longer need to be in the view hierarchy, according to
1261  // the current autofill context. The "garbage" input views are already made
1262  // invisible to autofill and they can't `becomeFirstResponder`, we only remove
1263  // them to free up resources and reduce the number of input views in the view
1264  // hierarchy.
1265  //
1266  // This is scheduled on the runloop and delayed by 0.1s so we don't remove the
1267  // text fields immediately (which seems to make the keyboard flicker).
1268  // See: https://github.com/flutter/flutter/issues/64628.
1269  [self performSelector:@selector(collectGarbageInputViews) withObject:nil afterDelay:0.1];
1270 }
1271 
1272 // Updates and shows an input field that is not password related and has no autofill
1273 // hints. This method re-configures and reuses an existing instance of input field
1274 // instead of creating a new one.
1275 // Also updates the current autofill context.
1276 - (FlutterTextInputView*)updateAndShowReusableInputView:(NSDictionary*)configuration {
1277  // It's possible that the configuration of this non-autofillable input view has
1278  // an autofill configuration without hints. If it does, remove it from the context.
1279  NSString* autofillId = autofillIdFromDictionary(configuration);
1280  if (autofillId) {
1281  [_autofillContext removeObjectForKey:autofillId];
1282  }
1283 
1284  [_reusableInputView configureWithDictionary:configuration];
1285  [self addToInputParentViewIfNeeded:_reusableInputView];
1286  _reusableInputView.textInputDelegate = _textInputDelegate;
1287 
1288  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
1289  NSString* autofillId = autofillIdFromDictionary(field);
1290  if (autofillId && autofillTypeOf(field) == FlutterAutofillTypeNone) {
1291  [_autofillContext removeObjectForKey:autofillId];
1292  }
1293  }
1294  return _reusableInputView;
1295 }
1296 
1297 - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
1298  focusedField:(NSDictionary*)focusedField
1299  isPasswordRelated:(BOOL)isPassword {
1300  FlutterTextInputView* focused = nil;
1301  NSString* focusedId = autofillIdFromDictionary(focusedField);
1302  NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField);
1303 
1304  if (!fields) {
1305  // DO NOT push the current autofillable input fields to the context even
1306  // if it's password-related, because it is not in an autofill group.
1307  focused = [self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
1308  [_autofillContext removeObjectForKey:focusedId];
1309  }
1310 
1311  for (NSDictionary* field in fields) {
1312  NSString* autofillId = autofillIdFromDictionary(field);
1313  NSAssert(autofillId, @"autofillId must not be null for field: %@", field);
1314 
1315  BOOL hasHints = autofillTypeOf(field) != FlutterAutofillTypeNone;
1316  BOOL isFocused = [focusedId isEqualToString:autofillId];
1317 
1318  if (isFocused) {
1319  focused = [self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
1320  }
1321 
1322  if (hasHints) {
1323  // Push the current input field to the context if it has hints.
1324  _autofillContext[autofillId] = isFocused ? focused
1325  : [self getOrCreateAutofillableView:field
1326  isPasswordAutofill:isPassword];
1327  } else {
1328  // Mark for deletion;
1329  [_autofillContext removeObjectForKey:autofillId];
1330  }
1331  }
1332 
1333  NSAssert(focused, @"The current focused input view must not be nil.");
1334  return focused;
1335 }
1336 
1337 // Returns a new non-reusable input view (and put it into the view hierarchy), or get the
1338 // view from the current autofill context, if an input view with the same autofill id
1339 // already exists in the context.
1340 // This is generally used for input fields that are autofillable (UIKit tracks these veiws
1341 // for autofill purposes so they should not be reused for a different type of views).
1342 - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field
1343  isPasswordAutofill:(BOOL)needsPasswordAutofill {
1344  NSString* autofillId = autofillIdFromDictionary(field);
1345  FlutterTextInputView* inputView = _autofillContext[autofillId];
1346  if (!inputView) {
1347  inputView =
1348  needsPasswordAutofill ? [FlutterSecureTextInputView alloc] : [FlutterTextInputView alloc];
1349  inputView = [[inputView init] autorelease];
1350  [self addToInputParentViewIfNeeded:inputView];
1351  }
1352 
1353  inputView.textInputDelegate = _textInputDelegate;
1354  [inputView configureWithDictionary:field];
1355  return inputView;
1356 }
1357 
1358 // The UIView to add FlutterTextInputViews to.
1359 - (UIView*)textInputParentView {
1360  UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
1361  NSAssert(keyWindow != nullptr,
1362  @"The application must have a key window since the keyboard client "
1363  @"must be part of the responder chain to function");
1364  return keyWindow;
1365 }
1366 
1367 // Removes every installed input field, unless it's in the current autofill
1368 // context. May remove the active view too if includeActiveView is YES.
1369 // When clearText is YES, the text on the input fields will be set to empty before
1370 // they are removed from the view hierarchy, to avoid triggering autofill save.
1371 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText {
1372  for (UIView* view in self.textInputParentView.subviews) {
1373  if ([view isKindOfClass:[FlutterTextInputView class]] &&
1374  (includeActiveView || view != _activeView)) {
1375  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
1376  if (_autofillContext[inputView.autofillId] != view) {
1377  if (clearText) {
1378  [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
1379  }
1380  [view removeFromSuperview];
1381  }
1382  }
1383  }
1384 }
1385 
1386 - (void)collectGarbageInputViews {
1387  [self cleanUpViewHierarchy:NO clearText:YES];
1388 }
1389 
1390 // Changes the visibility of every FlutterTextInputView currently in the
1391 // view hierarchy.
1392 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
1393  for (UIView* view in self.textInputParentView.subviews) {
1394  if ([view isKindOfClass:[FlutterTextInputView class]]) {
1395  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
1396  inputView.isVisibleToAutofill = newVisibility;
1397  }
1398  }
1399 }
1400 
1401 // Resets the client id of every FlutterTextInputView in the view hierarchy
1402 // to 0. Called when a new text input connection will be established.
1403 - (void)resetAllClientIds {
1404  for (UIView* view in self.textInputParentView.subviews) {
1405  if ([view isKindOfClass:[FlutterTextInputView class]]) {
1406  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
1407  [inputView setTextInputClient:0];
1408  }
1409  }
1410 }
1411 
1412 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
1413  UIView* parentView = self.textInputParentView;
1414  if (inputView.superview != parentView) {
1415  [parentView addSubview:inputView];
1416  }
1417 }
1418 
1419 - (void)setTextInputEditingState:(NSDictionary*)state {
1420  [_activeView setTextInputState:state];
1421 }
1422 
1423 - (void)clearTextInputClient {
1424  [_activeView setTextInputClient:0];
1425 }
1426 
1427 @end
G_BEGIN_DECLS FlValue * args
static NSString *const kSmartDashesType
static NSString *const kSmartQuotesType
FlutterTextRange * _selectedTextRange
static const char _kTextAffinityDownstream[]
NSRange _range
static NSString *const kInputAction
static FlutterAutofillType autofillTypeOf(NSDictionary *configuration)
id< FlutterTextInputDelegate > textInputDelegate
static NSString *const kSecureTextEntry
instancetype rangeWithNSRange:(NSRange range)
NSRange RangeForCharacterAtIndex(NSString *text, NSUInteger index)
instancetype positionWithIndex:(NSUInteger index)
static BOOL isFieldPasswordRelated(NSDictionary *configuration)
static NSString *const kAutofillId
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static UIKeyboardType ToUIKeyboardType(NSDictionary *type)
static NSString *const kAssociatedAutofillFields
static const char _kTextAffinityUpstream[]
UIView< UITextInput > * textInputView()
static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary *type)
const char * _selectionAffinity
static NSString *const kAutofillEditingValue
CGRect _cachedFirstRect
SemanticsAction action
size_t length
static UIReturnKeyType ToUIReturnKeyType(NSString *inputType)
const CGRect kInvalidFirstRect
void(^ FlutterResult)(id _Nullable result)
FLUTTER_EXPORT NSObject const * FlutterMethodNotImplemented
static NSString *const kAutocorrectionType
static NSString *const kKeyboardAppearance
static NSString *const kAutofillProperties
static NSString * autofillIdFromDictionary(NSDictionary *dictionary)
int32_t id
static NSString *const kAutofillHints
static UITextContentType ToUITextContentType(NSArray< NSString *> *hints)
static NSString *const kKeyboardType
id< FlutterTextInputDelegate > textInputDelegate