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/logging.h"
11 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
12 
13 static const char _kTextAffinityDownstream[] = "TextAffinity.downstream";
14 static const char _kTextAffinityUpstream[] = "TextAffinity.upstream";
15 // A delay before enabling the accessibility of FlutterTextInputView after
16 // it is activated.
17 static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5;
18 
19 // The "canonical" invalid CGRect, similar to CGRectNull, used to
20 // indicate a CGRect involved in firstRectForRange calculation is
21 // invalid. The specific value is chosen so that if firstRectForRange
22 // returns kInvalidFirstRect, iOS will not show the IME candidates view.
23 const CGRect kInvalidFirstRect = {{-1, -1}, {9999, 9999}};
24 
25 // The `bounds` value a FlutterTextInputView returns when the floating cursor
26 // is activated in that view.
27 //
28 // DO NOT use extremely large values (such as CGFloat_MAX) in this rect, for that
29 // will significantly reduce the precision of the floating cursor's coordinates.
30 //
31 // It is recommended for this CGRect to be roughly centered at caretRectForPosition
32 // (which currently always return CGRectZero), so the initial floating cursor will
33 // be placed at (0, 0).
34 // See the comments in beginFloatingCursorAtPoint and caretRectForPosition.
35 const CGRect kSpacePanBounds = {{-2500, -2500}, {5000, 5000}};
36 
37 #pragma mark - TextInput channel method names.
38 // See https://api.flutter.dev/flutter/services/SystemChannels/textInput-constant.html
39 static NSString* const kShowMethod = @"TextInput.show";
40 static NSString* const kHideMethod = @"TextInput.hide";
41 static NSString* const kSetClientMethod = @"TextInput.setClient";
42 static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
43 static NSString* const kClearClientMethod = @"TextInput.clearClient";
44 static NSString* const kSetEditableSizeAndTransformMethod =
45  @"TextInput.setEditableSizeAndTransform";
46 static NSString* const kSetMarkedTextRectMethod = @"TextInput.setMarkedTextRect";
47 static NSString* const kFinishAutofillContextMethod = @"TextInput.finishAutofillContext";
48 
49 #pragma mark - TextInputConfiguration Field Names
50 static NSString* const kSecureTextEntry = @"obscureText";
51 static NSString* const kKeyboardType = @"inputType";
52 static NSString* const kKeyboardAppearance = @"keyboardAppearance";
53 static NSString* const kInputAction = @"inputAction";
54 static NSString* const kEnableDeltaModel = @"enableDeltaModel";
55 
56 static NSString* const kSmartDashesType = @"smartDashesType";
57 static NSString* const kSmartQuotesType = @"smartQuotesType";
58 
59 static NSString* const kAssociatedAutofillFields = @"fields";
60 
61 // TextInputConfiguration.autofill and sub-field names
62 static NSString* const kAutofillProperties = @"autofill";
63 static NSString* const kAutofillId = @"uniqueIdentifier";
64 static NSString* const kAutofillEditingValue = @"editingValue";
65 static NSString* const kAutofillHints = @"hints";
66 
67 static NSString* const kAutocorrectionType = @"autocorrect";
68 
69 #pragma mark - Static Functions
70 
71 // "TextInputType.none" is a made-up input type that's typically
72 // used when there's an in-app virtual keyboard. If
73 // "TextInputType.none" is specified, disable the system
74 // keyboard.
75 static BOOL ShouldShowSystemKeyboard(NSDictionary* type) {
76  NSString* inputType = type[@"name"];
77  return ![inputType isEqualToString:@"TextInputType.none"];
78 }
79 static UIKeyboardType ToUIKeyboardType(NSDictionary* type) {
80  NSString* inputType = type[@"name"];
81  if ([inputType isEqualToString:@"TextInputType.address"]) {
82  return UIKeyboardTypeDefault;
83  }
84  if ([inputType isEqualToString:@"TextInputType.datetime"]) {
85  return UIKeyboardTypeNumbersAndPunctuation;
86  }
87  if ([inputType isEqualToString:@"TextInputType.emailAddress"]) {
88  return UIKeyboardTypeEmailAddress;
89  }
90  if ([inputType isEqualToString:@"TextInputType.multiline"]) {
91  return UIKeyboardTypeDefault;
92  }
93  if ([inputType isEqualToString:@"TextInputType.name"]) {
94  return UIKeyboardTypeNamePhonePad;
95  }
96  if ([inputType isEqualToString:@"TextInputType.number"]) {
97  if ([type[@"signed"] boolValue]) {
98  return UIKeyboardTypeNumbersAndPunctuation;
99  }
100  if ([type[@"decimal"] boolValue]) {
101  return UIKeyboardTypeDecimalPad;
102  }
103  return UIKeyboardTypeNumberPad;
104  }
105  if ([inputType isEqualToString:@"TextInputType.phone"]) {
106  return UIKeyboardTypePhonePad;
107  }
108  if ([inputType isEqualToString:@"TextInputType.text"]) {
109  return UIKeyboardTypeDefault;
110  }
111  if ([inputType isEqualToString:@"TextInputType.url"]) {
112  return UIKeyboardTypeURL;
113  }
114  return UIKeyboardTypeDefault;
115 }
116 
117 static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary* type) {
118  NSString* textCapitalization = type[@"textCapitalization"];
119  if ([textCapitalization isEqualToString:@"TextCapitalization.characters"]) {
120  return UITextAutocapitalizationTypeAllCharacters;
121  } else if ([textCapitalization isEqualToString:@"TextCapitalization.sentences"]) {
122  return UITextAutocapitalizationTypeSentences;
123  } else if ([textCapitalization isEqualToString:@"TextCapitalization.words"]) {
124  return UITextAutocapitalizationTypeWords;
125  }
126  return UITextAutocapitalizationTypeNone;
127 }
128 
129 static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) {
130  // Where did the term "unspecified" come from? iOS has a "default" and Android
131  // has "unspecified." These 2 terms seem to mean the same thing but we need
132  // to pick just one. "unspecified" was chosen because "default" is often a
133  // reserved word in languages with switch statements (dart, java, etc).
134  if ([inputType isEqualToString:@"TextInputAction.unspecified"]) {
135  return UIReturnKeyDefault;
136  }
137 
138  if ([inputType isEqualToString:@"TextInputAction.done"]) {
139  return UIReturnKeyDone;
140  }
141 
142  if ([inputType isEqualToString:@"TextInputAction.go"]) {
143  return UIReturnKeyGo;
144  }
145 
146  if ([inputType isEqualToString:@"TextInputAction.send"]) {
147  return UIReturnKeySend;
148  }
149 
150  if ([inputType isEqualToString:@"TextInputAction.search"]) {
151  return UIReturnKeySearch;
152  }
153 
154  if ([inputType isEqualToString:@"TextInputAction.next"]) {
155  return UIReturnKeyNext;
156  }
157 
158  if (@available(iOS 9.0, *)) {
159  if ([inputType isEqualToString:@"TextInputAction.continueAction"]) {
160  return UIReturnKeyContinue;
161  }
162  }
163 
164  if ([inputType isEqualToString:@"TextInputAction.join"]) {
165  return UIReturnKeyJoin;
166  }
167 
168  if ([inputType isEqualToString:@"TextInputAction.route"]) {
169  return UIReturnKeyRoute;
170  }
171 
172  if ([inputType isEqualToString:@"TextInputAction.emergencyCall"]) {
173  return UIReturnKeyEmergencyCall;
174  }
175 
176  if ([inputType isEqualToString:@"TextInputAction.newline"]) {
177  return UIReturnKeyDefault;
178  }
179 
180  // Present default key if bad input type is given.
181  return UIReturnKeyDefault;
182 }
183 
184 static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
185  if (!hints || hints.count == 0) {
186  // If no hints are specified, use the default content type nil.
187  return nil;
188  }
189 
190  NSString* hint = hints[0];
191  if (@available(iOS 10.0, *)) {
192  if ([hint isEqualToString:@"addressCityAndState"]) {
193  return UITextContentTypeAddressCityAndState;
194  }
195 
196  if ([hint isEqualToString:@"addressState"]) {
197  return UITextContentTypeAddressState;
198  }
199 
200  if ([hint isEqualToString:@"addressCity"]) {
201  return UITextContentTypeAddressCity;
202  }
203 
204  if ([hint isEqualToString:@"sublocality"]) {
205  return UITextContentTypeSublocality;
206  }
207 
208  if ([hint isEqualToString:@"streetAddressLine1"]) {
209  return UITextContentTypeStreetAddressLine1;
210  }
211 
212  if ([hint isEqualToString:@"streetAddressLine2"]) {
213  return UITextContentTypeStreetAddressLine2;
214  }
215 
216  if ([hint isEqualToString:@"countryName"]) {
217  return UITextContentTypeCountryName;
218  }
219 
220  if ([hint isEqualToString:@"fullStreetAddress"]) {
221  return UITextContentTypeFullStreetAddress;
222  }
223 
224  if ([hint isEqualToString:@"postalCode"]) {
225  return UITextContentTypePostalCode;
226  }
227 
228  if ([hint isEqualToString:@"location"]) {
229  return UITextContentTypeLocation;
230  }
231 
232  if ([hint isEqualToString:@"creditCardNumber"]) {
233  return UITextContentTypeCreditCardNumber;
234  }
235 
236  if ([hint isEqualToString:@"email"]) {
237  return UITextContentTypeEmailAddress;
238  }
239 
240  if ([hint isEqualToString:@"jobTitle"]) {
241  return UITextContentTypeJobTitle;
242  }
243 
244  if ([hint isEqualToString:@"givenName"]) {
245  return UITextContentTypeGivenName;
246  }
247 
248  if ([hint isEqualToString:@"middleName"]) {
249  return UITextContentTypeMiddleName;
250  }
251 
252  if ([hint isEqualToString:@"familyName"]) {
253  return UITextContentTypeFamilyName;
254  }
255 
256  if ([hint isEqualToString:@"name"]) {
257  return UITextContentTypeName;
258  }
259 
260  if ([hint isEqualToString:@"namePrefix"]) {
261  return UITextContentTypeNamePrefix;
262  }
263 
264  if ([hint isEqualToString:@"nameSuffix"]) {
265  return UITextContentTypeNameSuffix;
266  }
267 
268  if ([hint isEqualToString:@"nickname"]) {
269  return UITextContentTypeNickname;
270  }
271 
272  if ([hint isEqualToString:@"organizationName"]) {
273  return UITextContentTypeOrganizationName;
274  }
275 
276  if ([hint isEqualToString:@"telephoneNumber"]) {
277  return UITextContentTypeTelephoneNumber;
278  }
279  }
280 
281  if (@available(iOS 11.0, *)) {
282  if ([hint isEqualToString:@"password"]) {
283  return UITextContentTypePassword;
284  }
285  }
286 
287  if (@available(iOS 12.0, *)) {
288  if ([hint isEqualToString:@"oneTimeCode"]) {
289  return UITextContentTypeOneTimeCode;
290  }
291 
292  if ([hint isEqualToString:@"newPassword"]) {
293  return UITextContentTypeNewPassword;
294  }
295  }
296 
297  return hints[0];
298 }
299 
300 // Retrieves the autofillId from an input field's configuration. Returns
301 // nil if the field is nil and the input field is not a password field.
302 static NSString* AutofillIdFromDictionary(NSDictionary* dictionary) {
303  NSDictionary* autofill = dictionary[kAutofillProperties];
304  if (autofill) {
305  return autofill[kAutofillId];
306  }
307 
308  // When autofill is nil, the field may still need an autofill id
309  // if the field is for password.
310  return [dictionary[kSecureTextEntry] boolValue] ? @"password" : nil;
311 }
312 
313 // # Autofill Implementation Notes:
314 //
315 // Currently there're 2 types of autofills on iOS:
316 // - Regular autofill, including contact information and one-time-code,
317 // takes place in the form of predictive text in the quick type bar.
318 // This type of autofill does not save user input, and the keyboard
319 // currently only populates the focused field when a predictive text entry
320 // is selected by the user.
321 //
322 // - Password autofill, includes automatic strong password and regular
323 // password autofill. The former happens automatically when a
324 // "new password" field is detected and focused, and only that password
325 // field will be populated. The latter appears in the quick type bar when
326 // an eligible input field (which either has a UITextContentTypePassword
327 // contentType, or is a secure text entry) becomes the first responder, and may
328 // fill both the username and the password fields. iOS will attempt
329 // to save user input for both kinds of password fields. It's relatively
330 // tricky to deal with password autofill since it can autofill more than one
331 // field at a time and may employ heuristics based on what other text fields
332 // are in the same view controller.
333 //
334 // When a flutter text field is focused, and autofill is not explicitly disabled
335 // for it ("autofillable"), the framework collects its attributes and checks if
336 // it's in an AutofillGroup, and collects the attributes of other autofillable
337 // text fields in the same AutofillGroup if so. The attributes are sent to the
338 // text input plugin via a "TextInput.setClient" platform channel message. If
339 // autofill is disabled for a text field, its "autofill" field will be nil in
340 // the configuration json.
341 //
342 // The text input plugin then tries to determine which kind of autofill the text
343 // field needs. If the AutofillGroup the text field belongs to contains an
344 // autofillable text field that's password related, this text 's autofill type
345 // will be FlutterAutofillTypePassword. If autofill is disabled for a text field,
346 // then its type will be FlutterAutofillTypeNone. Otherwise the text field will
347 // have an autofill type of FlutterAutofillTypeRegular.
348 //
349 // The text input plugin creates a new UIView for every FlutterAutofillTypeNone
350 // text field. The UIView instance is never reused for other flutter text fields
351 // since the software keyboard often uses the identity of a UIView to distinguish
352 // different views and provides the same predictive text suggestions or restore
353 // the composing region if a UIView is reused for a different flutter text field.
354 //
355 // The text input plugin creates a new "autofill context" if the text field has
356 // the type of FlutterAutofillTypePassword, to represent the AutofillGroup of
357 // the text field, and creates one FlutterTextInputView for every text field in
358 // the AutofillGroup.
359 //
360 // The text input plugin will try to reuse a UIView if a flutter text field's
361 // type is FlutterAutofillTypeRegular, and has the same autofill id.
362 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
363  // The field does not have autofillable content. Additionally if
364  // the field is currently in the autofill context, it will be
365  // removed from the context without triggering autofill save.
366  FlutterAutofillTypeNone,
367  FlutterAutofillTypeRegular,
368  FlutterAutofillTypePassword,
369 };
370 
371 static BOOL IsFieldPasswordRelated(NSDictionary* configuration) {
372  if (@available(iOS 10.0, *)) {
373  // Autofill is explicitly disabled if the id isn't present.
374  if (!AutofillIdFromDictionary(configuration)) {
375  return NO;
376  }
377 
378  BOOL isSecureTextEntry = [configuration[kSecureTextEntry] boolValue];
379  if (isSecureTextEntry) {
380  return YES;
381  }
382 
383  NSDictionary* autofill = configuration[kAutofillProperties];
384  UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
385 
386  if (@available(iOS 11.0, *)) {
387  if ([contentType isEqualToString:UITextContentTypePassword] ||
388  [contentType isEqualToString:UITextContentTypeUsername]) {
389  return YES;
390  }
391  }
392 
393  if (@available(iOS 12.0, *)) {
394  if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
395  return YES;
396  }
397  }
398  }
399  return NO;
400 }
401 
402 static FlutterAutofillType AutofillTypeOf(NSDictionary* configuration) {
403  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
404  if (IsFieldPasswordRelated(field)) {
405  return FlutterAutofillTypePassword;
406  }
407  }
408 
409  if (IsFieldPasswordRelated(configuration)) {
410  return FlutterAutofillTypePassword;
411  }
412 
413  if (@available(iOS 10.0, *)) {
414  NSDictionary* autofill = configuration[kAutofillProperties];
415  UITextContentType contentType = ToUITextContentType(autofill[kAutofillHints]);
416  return !autofill || [contentType isEqualToString:@""] ? FlutterAutofillTypeNone
417  : FlutterAutofillTypeRegular;
418  }
419 
420  return FlutterAutofillTypeNone;
421 }
422 
423 static BOOL IsApproximatelyEqual(float x, float y, float delta) {
424  return fabsf(x - y) <= delta;
425 }
426 
427 // Checks whether point should be considered closer to selectionRect compared to
428 // otherSelectionRect.
429 //
430 // If checkRightBoundary is set, the right-center point on selectionRect and
431 // otherSelectionRect will be used instead of the left-center point.
432 //
433 // This uses special (empirically determined using a 1st gen iPad pro, 9.7" model running
434 // iOS 14.7.1) logic for determining the closer rect, rather than a simple distance calculation.
435 // First, the closer vertical distance is determined. Within the closest y distance, if the point is
436 // above the bottom of the closest rect, the x distance will be minimized; however, if the point is
437 // below the bottom of the rect, the x value will be maximized.
438 static BOOL IsSelectionRectCloserToPoint(CGPoint point,
439  CGRect selectionRect,
440  CGRect otherSelectionRect,
441  BOOL checkRightBoundary) {
442  CGPoint pointForSelectionRect =
443  CGPointMake(selectionRect.origin.x + (checkRightBoundary ? selectionRect.size.width : 0),
444  selectionRect.origin.y + selectionRect.size.height * 0.5);
445  float yDist = fabs(pointForSelectionRect.y - point.y);
446  float xDist = fabs(pointForSelectionRect.x - point.x);
447 
448  CGPoint pointForOtherSelectionRect =
449  CGPointMake(otherSelectionRect.origin.x + (checkRightBoundary ? selectionRect.size.width : 0),
450  otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
451  float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
452  float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
453 
454  // This serves a similar purpose to IsApproximatelyEqual, allowing a little buffer before
455  // declaring something closer vertically to account for the small variations in size and position
456  // of SelectionRects, especially when dealing with emoji.
457  BOOL isCloserVertically = yDist < yDistOther - 1;
458  BOOL isEqualVertically = IsApproximatelyEqual(yDist, yDistOther, 1);
459  BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
460  BOOL isCloserHorizontally = xDist <= xDistOther;
461  BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
462  BOOL isFartherToRight =
463  selectionRect.origin.x + (checkRightBoundary ? selectionRect.size.width : 0) >
464  otherSelectionRect.origin.x;
465  return (isCloserVertically ||
466  (isEqualVertically && ((isAboveBottomOfLine && isCloserHorizontally) ||
467  (isBelowBottomOfLine && isFartherToRight))));
468 }
469 
470 // Checks whether Scribble features are possibly available – meaning this is an iPad running iOS
471 // 14 or higher.
473  if (@available(iOS 14.0, *)) {
474  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
475  return YES;
476  }
477  }
478  return NO;
479 }
480 
481 #pragma mark - FlutterTextPosition
482 
483 @implementation FlutterTextPosition
484 
485 + (instancetype)positionWithIndex:(NSUInteger)index {
486  return [[[FlutterTextPosition alloc] initWithIndex:index] autorelease];
487 }
488 
489 - (instancetype)initWithIndex:(NSUInteger)index {
490  self = [super init];
491  if (self) {
492  _index = index;
493  }
494  return self;
495 }
496 
497 @end
498 
499 #pragma mark - FlutterTextRange
500 
501 @implementation FlutterTextRange
502 
503 + (instancetype)rangeWithNSRange:(NSRange)range {
504  return [[[FlutterTextRange alloc] initWithNSRange:range] autorelease];
505 }
506 
507 - (instancetype)initWithNSRange:(NSRange)range {
508  self = [super init];
509  if (self) {
510  _range = range;
511  }
512  return self;
513 }
514 
515 - (UITextPosition*)start {
516  return [FlutterTextPosition positionWithIndex:self.range.location];
517 }
518 
519 - (UITextPosition*)end {
520  return [FlutterTextPosition positionWithIndex:self.range.location + self.range.length];
521 }
522 
523 - (BOOL)isEmpty {
524  return self.range.length == 0;
525 }
526 
527 - (id)copyWithZone:(NSZone*)zone {
528  return [[FlutterTextRange allocWithZone:zone] initWithNSRange:self.range];
529 }
530 
531 - (BOOL)isEqualTo:(FlutterTextRange*)other {
532  return NSEqualRanges(self.range, other.range);
533 }
534 @end
535 
536 #pragma mark - FlutterTokenizer
537 
538 @interface FlutterTokenizer ()
539 
540 @property(nonatomic, assign) FlutterTextInputView* textInputView;
541 
542 @end
543 
544 @implementation FlutterTokenizer
545 
546 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
547  NSAssert([textInput isKindOfClass:[FlutterTextInputView class]],
548  @"The FlutterTokenizer can only be used in a FlutterTextInputView");
549  self = [super initWithTextInput:textInput];
550  if (self) {
551  _textInputView = (FlutterTextInputView*)textInput;
552  }
553  return self;
554 }
555 
556 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
557  withGranularity:(UITextGranularity)granularity
558  inDirection:(UITextDirection)direction {
559  UITextRange* result;
560  switch (granularity) {
561  case UITextGranularityLine:
562  // The default UITextInputStringTokenizer does not handle line granularity
563  // correctly. We need to implement our own line tokenizer.
564  result = [self lineEnclosingPosition:position];
565  break;
566  case UITextGranularityCharacter:
567  case UITextGranularityWord:
568  case UITextGranularitySentence:
569  case UITextGranularityParagraph:
570  case UITextGranularityDocument:
571  // The UITextInputStringTokenizer can handle all these cases correctly.
572  result = [super rangeEnclosingPosition:position
573  withGranularity:granularity
574  inDirection:direction];
575  break;
576  }
577  return result;
578 }
579 
580 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position {
581  // Gets the first line break position after the input position.
582  NSString* textAfter = [_textInputView
583  textInRange:[_textInputView textRangeFromPosition:position
584  toPosition:[_textInputView endOfDocument]]];
585  NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
586  NSInteger offSetToLineBreak = [linesAfter firstObject].length;
587  UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
588  offset:offSetToLineBreak];
589  // Gets the first line break position before the input position.
590  NSString* textBefore = [_textInputView
591  textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
592  toPosition:position]];
593  NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
594  NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
595  UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
596  offset:-offSetFromLineBreak];
597 
598  return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
599 }
600 
601 @end
602 
603 #pragma mark - FlutterTextSelectionRect
604 
605 @implementation FlutterTextSelectionRect
606 
607 @synthesize rect = _rect;
608 @synthesize writingDirection = _writingDirection;
609 @synthesize containsStart = _containsStart;
610 @synthesize containsEnd = _containsEnd;
611 @synthesize isVertical = _isVertical;
612 
613 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
614  position:(NSUInteger)position
615  writingDirection:(NSWritingDirection)writingDirection
616  containsStart:(BOOL)containsStart
617  containsEnd:(BOOL)containsEnd
618  isVertical:(BOOL)isVertical {
619  return [[[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect
620  position:position
621  writingDirection:writingDirection
622  containsStart:containsStart
623  containsEnd:containsEnd
624  isVertical:isVertical] autorelease];
625 }
626 
627 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
628  return [[[FlutterTextSelectionRect alloc] initWithRectAndInfo:rect
629  position:position
630  writingDirection:UITextWritingDirectionNatural
631  containsStart:NO
632  containsEnd:NO
633  isVertical:NO] autorelease];
634 }
635 
636 - (instancetype)initWithRectAndInfo:(CGRect)rect
637  position:(NSUInteger)position
638  writingDirection:(NSWritingDirection)writingDirection
639  containsStart:(BOOL)containsStart
640  containsEnd:(BOOL)containsEnd
641  isVertical:(BOOL)isVertical {
642  self = [super init];
643  if (self) {
644  self.rect = rect;
645  self.position = position;
646  self.writingDirection = writingDirection;
647  self.containsStart = containsStart;
648  self.containsEnd = containsEnd;
649  self.isVertical = isVertical;
650  }
651  return self;
652 }
653 
654 @end
655 
656 #pragma mark - FlutterTextPlaceholder
657 
658 @implementation FlutterTextPlaceholder
659 
660 - (NSArray<UITextSelectionRect*>*)rects {
661  // Returning anything other than an empty array here seems to cause PencilKit to enter an
662  // infinite loop of allocating placeholders until the app crashes
663  return @[];
664 }
665 
666 @end
667 
668 // A FlutterTextInputView that masquerades as a UITextField, and forwards
669 // selectors it can't respond to to a shared UITextField instance.
670 //
671 // Relevant API docs claim that password autofill supports any custom view
672 // that adopts the UITextInput protocol, automatic strong password seems to
673 // currently only support UITextFields, and password saving only supports
674 // UITextFields and UITextViews, as of iOS 13.5.
676 @property(nonatomic, retain, readonly) UITextField* textField;
677 @end
678 
679 @implementation FlutterSecureTextInputView {
680  UITextField* _textField;
681 }
682 
683 - (void)dealloc {
684  [_textField release];
685  [super dealloc];
686 }
687 
688 - (UITextField*)textField {
689  if (!_textField) {
690  _textField = [[UITextField alloc] init];
691  }
692  return _textField;
693 }
694 
695 - (BOOL)isKindOfClass:(Class)aClass {
696  return [super isKindOfClass:aClass] || (aClass == [UITextField class]);
697 }
698 
699 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
700  NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
701  if (!signature) {
702  signature = [self.textField methodSignatureForSelector:aSelector];
703  }
704  return signature;
705 }
706 
707 - (void)forwardInvocation:(NSInvocation*)anInvocation {
708  [anInvocation invokeWithTarget:self.textField];
709 }
710 
711 @end
712 
713 @interface FlutterTextInputView ()
714 @property(nonatomic, copy) NSString* autofillId;
715 @property(nonatomic, readonly) CATransform3D editableTransform;
716 @property(nonatomic, assign) CGRect markedRect;
717 @property(nonatomic) BOOL isVisibleToAutofill;
718 @property(nonatomic, assign) BOOL accessibilityEnabled;
719 @property(nonatomic, retain) UITextInteraction* textInteraction API_AVAILABLE(ios(13.0));
720 
721 - (void)setEditableTransform:(NSArray*)matrix;
722 @end
723 
724 @implementation FlutterTextInputView {
725  int _textInputClient;
726  const char* _selectionAffinity;
728  UIInputViewController* _inputViewController;
730  FlutterScribbleInteractionStatus _scribbleInteractionStatus;
732  // Whether to show the system keyboard when this view
733  // becomes the first responder. Typically set to false
734  // when the app shows its own in-flutter keyboard.
737  // The view has reached end of life, and is no longer
738  // allowed to access its textInputDelegate.
740 }
741 
742 @synthesize tokenizer = _tokenizer;
743 
744 - (instancetype)init {
745  self = [super init];
746  if (self) {
747  _textInputClient = 0;
749 
750  // UITextInput
751  _text = [[NSMutableString alloc] init];
752  _markedText = [[NSMutableString alloc] init];
753  _selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)];
754  _markedRect = kInvalidFirstRect;
756  _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
757  // Initialize with the zero matrix which is not
758  // an affine transform.
759  _editableTransform = CATransform3D();
760  _isFloatingCursorActive = false;
761 
762  // UITextInputTraits
763  _autocapitalizationType = UITextAutocapitalizationTypeSentences;
764  _autocorrectionType = UITextAutocorrectionTypeDefault;
765  _spellCheckingType = UITextSpellCheckingTypeDefault;
766  _enablesReturnKeyAutomatically = NO;
767  _keyboardAppearance = UIKeyboardAppearanceDefault;
768  _keyboardType = UIKeyboardTypeDefault;
769  _returnKeyType = UIReturnKeyDone;
770  _secureTextEntry = NO;
771  _enableDeltaModel = NO;
772  _accessibilityEnabled = NO;
773  _decommissioned = NO;
774  if (@available(iOS 11.0, *)) {
775  _smartQuotesType = UITextSmartQuotesTypeYes;
776  _smartDashesType = UITextSmartDashesTypeYes;
777  }
778  _selectionRects = [[NSArray alloc] init];
779 
780  if (@available(iOS 14.0, *)) {
781  UIScribbleInteraction* interaction =
782  [[[UIScribbleInteraction alloc] initWithDelegate:self] autorelease];
783  [self addInteraction:interaction];
784  }
785  }
786 
787  return self;
788 }
789 
790 - (void)configureWithDictionary:(NSDictionary*)configuration {
791  NSAssert(!_decommissioned, @"Attempt to reuse a decommissioned view, for %@", configuration);
792  NSDictionary* inputType = configuration[kKeyboardType];
793  NSString* keyboardAppearance = configuration[kKeyboardAppearance];
794  NSDictionary* autofill = configuration[kAutofillProperties];
795 
796  self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
797  self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
798 
800  self.keyboardType = ToUIKeyboardType(inputType);
801  self.returnKeyType = ToUIReturnKeyType(configuration[kInputAction]);
802  self.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
803 
804  if (@available(iOS 11.0, *)) {
805  NSString* smartDashesType = configuration[kSmartDashesType];
806  // This index comes from the SmartDashesType enum in the framework.
807  bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
808  self.smartDashesType =
809  smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
810  NSString* smartQuotesType = configuration[kSmartQuotesType];
811  // This index comes from the SmartQuotesType enum in the framework.
812  bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
813  self.smartQuotesType =
814  smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
815  }
816  if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
817  self.keyboardAppearance = UIKeyboardAppearanceDark;
818  } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
819  self.keyboardAppearance = UIKeyboardAppearanceLight;
820  } else {
821  self.keyboardAppearance = UIKeyboardAppearanceDefault;
822  }
823  NSString* autocorrect = configuration[kAutocorrectionType];
824  self.autocorrectionType = autocorrect && ![autocorrect boolValue]
825  ? UITextAutocorrectionTypeNo
826  : UITextAutocorrectionTypeDefault;
827  if (@available(iOS 10.0, *)) {
828  self.autofillId = AutofillIdFromDictionary(configuration);
829  if (autofill == nil) {
830  self.textContentType = @"";
831  } else {
832  self.textContentType = ToUITextContentType(autofill[kAutofillHints]);
833  [self setTextInputState:autofill[kAutofillEditingValue]];
834  NSAssert(_autofillId, @"The autofill configuration must contain an autofill id");
835  }
836  // The input field needs to be visible for the system autofill
837  // to find it.
838  self.isVisibleToAutofill = autofill || _secureTextEntry;
839  }
840 }
841 
842 - (UITextContentType)textContentType {
843  return _textContentType;
844 }
845 
846 // Prevent UIKit from showing selection handles or highlights. This is needed
847 // because Scribble interactions require the view to have it's actual frame on
848 // the screen.
849 - (UIColor*)insertionPointColor {
850  return [UIColor clearColor];
851 }
852 
853 - (UIColor*)selectionBarColor {
854  return [UIColor clearColor];
855 }
856 
857 - (UIColor*)selectionHighlightColor {
858  return [UIColor clearColor];
859 }
860 
861 - (UIInputViewController*)inputViewController {
863  return nil;
864  }
865 
866  if (!_inputViewController) {
867  _inputViewController = [[UIInputViewController alloc] init];
868  }
869  return _inputViewController;
870 }
871 
872 - (id<FlutterTextInputDelegate>)textInputDelegate {
873  return _decommissioned ? nil : _textInputDelegate;
874 }
875 
876 // Declares that the view has reached end of life, and
877 // is no longer allowed to access its textInputDelegate.
878 //
879 // UIKit may retain this view (even after it's been removed
880 // from the view hierarchy) so that it may outlive the plugin/engine,
881 // in which case _textInputDelegate will become a dangling pointer.
882 
883 // The text input plugin needs to call decommission when it should
884 // not have access to its FlutterTextInputDelegate any more.
885 - (void)decommission {
886  _decommissioned = YES;
887 }
888 
889 - (void)dealloc {
890  [_text release];
891  [_markedText release];
892  [_markedTextRange release];
893  [_selectedTextRange release];
894  [_tokenizer release];
895  [_autofillId release];
896  [_inputViewController release];
897  [_selectionRects release];
898  [_markedTextStyle release];
899  [_textContentType release];
900  [_textInteraction release];
901  [super dealloc];
902 }
903 
904 - (void)setTextInputClient:(int)client {
905  _textInputClient = client;
906  _hasPlaceholder = NO;
907 }
908 
909 - (void)setTextInputState:(NSDictionary*)state {
910  NSString* newText = state[@"text"];
911  BOOL textChanged = ![self.text isEqualToString:newText];
912  if (textChanged) {
913  [self.inputDelegate textWillChange:self];
914  [self.text setString:newText];
915  }
916  NSInteger composingBase = [state[@"composingBase"] intValue];
917  NSInteger composingExtent = [state[@"composingExtent"] intValue];
918  NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
919  ABS(composingBase - composingExtent))
920  forText:self.text];
921 
922  self.markedTextRange =
923  composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil;
924 
925  NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue]
926  extent:[state[@"selectionExtent"] intValue]
927  forText:self.text];
928 
929  NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range];
930  if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
931  [self.inputDelegate selectionWillChange:self];
932 
933  [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
934 
936  if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) {
938  }
939  [self.inputDelegate selectionDidChange:self];
940  }
941 
942  if (textChanged) {
943  [self.inputDelegate textDidChange:self];
944  }
945 }
946 
947 // Forward touches to the viewResponder to allow tapping inside the UITextField as normal.
948 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
949  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
950  [self resetScribbleInteractionStatusIfEnding];
951  [self.viewResponder touchesBegan:touches withEvent:event];
952 }
953 
954 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
955  [self.viewResponder touchesMoved:touches withEvent:event];
956 }
957 
958 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
959  [self.viewResponder touchesEnded:touches withEvent:event];
960 }
961 
962 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
963  [self.viewResponder touchesCancelled:touches withEvent:event];
964 }
965 
966 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
967  [self.viewResponder touchesEstimatedPropertiesUpdated:touches];
968 }
969 
970 // Extracts the selection information from the editing state dictionary.
971 //
972 // The state may contain an invalid selection, such as when no selection was
973 // explicitly set in the framework. This is handled here by setting the
974 // selection to (0,0). In contrast, Android handles this situation by
975 // clearing the selection, but the result in both cases is that the cursor
976 // is placed at the beginning of the field.
977 - (NSRange)clampSelectionFromBase:(int)selectionBase
978  extent:(int)selectionExtent
979  forText:(NSString*)text {
980  int loc = MIN(selectionBase, selectionExtent);
981  int len = ABS(selectionExtent - selectionBase);
982  return loc < 0 ? NSMakeRange(0, 0)
983  : [self clampSelection:NSMakeRange(loc, len) forText:self.text];
984 }
985 
986 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
987  NSUInteger start = MIN(MAX(range.location, 0), text.length);
988  NSUInteger length = MIN(range.length, text.length - start);
989  return NSMakeRange(start, length);
990 }
991 
992 - (BOOL)isVisibleToAutofill {
993  return self.frame.size.width > 0 && self.frame.size.height > 0;
994 }
995 
996 // An input view is generally ignored by password autofill attempts, if it's
997 // not the first responder and is zero-sized. For input fields that are in the
998 // autofill context but do not belong to the current autofill group, setting
999 // their frames to CGRectZero prevents ios autofill from taking them into
1000 // account.
1001 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1002  // This probably needs to change (think it is getting overwritten by the updateSizeAndTransform
1003  // stuff for now).
1004  self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1005 }
1006 
1007 #pragma mark UIScribbleInteractionDelegate
1008 
1009 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1010  API_AVAILABLE(ios(14.0)) {
1011  _scribbleInteractionStatus = FlutterScribbleInteractionStatusStarted;
1012  [_textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1013 }
1014 
1015 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1016  API_AVAILABLE(ios(14.0)) {
1017  _scribbleInteractionStatus = FlutterScribbleInteractionStatusEnding;
1018  [_textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1019 }
1020 
1021 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1022  shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1023  return YES;
1024 }
1025 
1026 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1027  API_AVAILABLE(ios(14.0)) {
1028  return NO;
1029 }
1030 
1031 #pragma mark - UIResponder Overrides
1032 
1033 - (BOOL)canBecomeFirstResponder {
1034  // Only the currently focused input field can
1035  // become the first responder. This prevents iOS
1036  // from changing focus by itself (the framework
1037  // focus will be out of sync if that happens).
1038  return _textInputClient != 0;
1039 }
1040 
1041 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
1042  // When scribble is available, the FlutterTextInputView will display the native toolbar unless
1043  // these text editing actions are disabled.
1044  if (IsScribbleAvailable()) {
1045  return NO;
1046  }
1047  if (action == @selector(paste:)) {
1048  // Forbid pasting images, memojis, or other non-string content.
1049  return [UIPasteboard generalPasteboard].string != nil;
1050  }
1051 
1052  return [super canPerformAction:action withSender:sender];
1053 }
1054 
1055 #pragma mark - UIResponderStandardEditActions Overrides
1056 
1057 - (void)cut:(id)sender {
1058  [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1059  [self replaceRange:_selectedTextRange withText:@""];
1060 }
1061 
1062 - (void)copy:(id)sender {
1063  [UIPasteboard generalPasteboard].string = [self textInRange:_selectedTextRange];
1064 }
1065 
1066 - (void)paste:(id)sender {
1067  NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1068  if (pasteboardString != nil) {
1069  [self insertText:pasteboardString];
1070  }
1071 }
1072 
1073 - (void)delete:(id)sender {
1074  [self replaceRange:_selectedTextRange withText:@""];
1075 }
1076 
1077 - (void)selectAll:(id)sender {
1078  [self setSelectedTextRange:[self textRangeFromPosition:[self beginningOfDocument]
1079  toPosition:[self endOfDocument]]];
1080 }
1081 
1082 #pragma mark - UITextInput Overrides
1083 
1084 - (id<UITextInputTokenizer>)tokenizer {
1085  if (_tokenizer == nil) {
1086  _tokenizer = [[FlutterTokenizer alloc] initWithTextInput:self];
1087  }
1088  return _tokenizer;
1089 }
1090 
1091 - (UITextRange*)selectedTextRange {
1092  return [[_selectedTextRange copy] autorelease];
1093 }
1094 
1095 // Change the range of selected text, without notifying the framework.
1096 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1098  UITextRange* oldSelectedRange = _selectedTextRange;
1099  if (self.hasText) {
1100  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1102  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1103  } else {
1104  _selectedTextRange = [selectedTextRange copy];
1105  }
1106  [oldSelectedRange release];
1107  }
1108 }
1109 
1110 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1111  [self setSelectedTextRangeLocal:selectedTextRange];
1112 
1113  if (_enableDeltaModel) {
1114  [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1115  } else {
1116  [self updateEditingState];
1117  }
1118 
1119  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1120  _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1121  NSAssert([selectedTextRange isKindOfClass:[FlutterTextRange class]],
1122  @"Expected a FlutterTextRange for range (got %@).", [selectedTextRange class]);
1123  FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
1124  if (flutterTextRange.range.length > 0) {
1125  [_textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1126  }
1127  }
1128 
1129  [self resetScribbleInteractionStatusIfEnding];
1130 }
1131 
1132 - (id)insertDictationResultPlaceholder {
1133  return @"";
1134 }
1135 
1136 - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
1137 }
1138 
1139 - (NSString*)textInRange:(UITextRange*)range {
1140  if (!range) {
1141  return nil;
1142  }
1143  NSAssert([range isKindOfClass:[FlutterTextRange class]],
1144  @"Expected a FlutterTextRange for range (got %@).", [range class]);
1145  NSRange textRange = ((FlutterTextRange*)range).range;
1146  NSAssert(textRange.location != NSNotFound, @"Expected a valid text range.");
1147  // Sanitize the range to prevent going out of bounds.
1148  NSUInteger location = MIN(textRange.location, self.text.length);
1149  NSUInteger length = MIN(self.text.length - location, textRange.length);
1150  NSRange safeRange = NSMakeRange(location, length);
1151  return [self.text substringWithRange:safeRange];
1152 }
1153 
1154 // Replace the text within the specified range with the given text,
1155 // without notifying the framework.
1156 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1157  NSRange selectedRange = _selectedTextRange.range;
1158 
1159  // Adjust the text selection:
1160  // * reduce the length by the intersection length
1161  // * adjust the location by newLength - oldLength + intersectionLength
1162  NSRange intersectionRange = NSIntersectionRange(range, selectedRange);
1163  if (range.location <= selectedRange.location) {
1164  selectedRange.location += text.length - range.length;
1165  }
1166  if (intersectionRange.location != NSNotFound) {
1167  selectedRange.location += intersectionRange.length;
1168  selectedRange.length -= intersectionRange.length;
1169  }
1170 
1171  [self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
1172  withString:text];
1173  [self setSelectedTextRangeLocal:[FlutterTextRange
1174  rangeWithNSRange:[self clampSelection:selectedRange
1175  forText:self.text]]];
1176 }
1177 
1178 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1179  NSString* textBeforeChange = [[self.text copy] autorelease];
1180  NSRange replaceRange = ((FlutterTextRange*)range).range;
1181  [self replaceRangeLocal:replaceRange withText:text];
1182  if (_enableDeltaModel) {
1183  NSRange nextReplaceRange = [self clampSelection:replaceRange forText:textBeforeChange];
1184  [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1185  [textBeforeChange UTF8String],
1186  flutter::TextRange(
1187  nextReplaceRange.location,
1188  nextReplaceRange.location + nextReplaceRange.length),
1189  [text UTF8String])];
1190  } else {
1191  [self updateEditingState];
1192  }
1193 }
1194 
1195 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1196  if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
1197  [self.textInputDelegate flutterTextInputView:self
1198  performAction:FlutterTextInputActionNewline
1199  withClient:_textInputClient];
1200  return YES;
1201  }
1202 
1203  if ([text isEqualToString:@"\n"]) {
1204  FlutterTextInputAction action;
1205  switch (self.returnKeyType) {
1206  case UIReturnKeyDefault:
1207  action = FlutterTextInputActionUnspecified;
1208  break;
1209  case UIReturnKeyDone:
1210  action = FlutterTextInputActionDone;
1211  break;
1212  case UIReturnKeyGo:
1213  action = FlutterTextInputActionGo;
1214  break;
1215  case UIReturnKeySend:
1216  action = FlutterTextInputActionSend;
1217  break;
1218  case UIReturnKeySearch:
1219  case UIReturnKeyGoogle:
1220  case UIReturnKeyYahoo:
1221  action = FlutterTextInputActionSearch;
1222  break;
1223  case UIReturnKeyNext:
1224  action = FlutterTextInputActionNext;
1225  break;
1226  case UIReturnKeyContinue:
1227  action = FlutterTextInputActionContinue;
1228  break;
1229  case UIReturnKeyJoin:
1230  action = FlutterTextInputActionJoin;
1231  break;
1232  case UIReturnKeyRoute:
1233  action = FlutterTextInputActionRoute;
1234  break;
1235  case UIReturnKeyEmergencyCall:
1236  action = FlutterTextInputActionEmergencyCall;
1237  break;
1238  }
1239 
1240  [self.textInputDelegate flutterTextInputView:self
1241  performAction:action
1242  withClient:_textInputClient];
1243  return NO;
1244  }
1245 
1246  return YES;
1247 }
1248 
1249 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1250  NSString* textBeforeChange = [[self.text copy] autorelease];
1251  NSRange selectedRange = _selectedTextRange.range;
1252  NSRange markedTextRange = ((FlutterTextRange*)self.markedTextRange).range;
1253  NSRange actualReplacedRange;
1254 
1255  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone ||
1256  _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1257  return;
1258  }
1259 
1260  if (markedText == nil) {
1261  markedText = @"";
1262  }
1263 
1264  if (markedTextRange.length > 0) {
1265  // Replace text in the marked range with the new text.
1266  [self replaceRangeLocal:markedTextRange withText:markedText];
1267  actualReplacedRange = markedTextRange;
1268  markedTextRange.length = markedText.length;
1269  } else {
1270  // Replace text in the selected range with the new text.
1271  actualReplacedRange = selectedRange;
1272  [self replaceRangeLocal:selectedRange withText:markedText];
1273  markedTextRange = NSMakeRange(selectedRange.location, markedText.length);
1274  }
1275 
1276  self.markedTextRange =
1277  markedTextRange.length > 0 ? [FlutterTextRange rangeWithNSRange:markedTextRange] : nil;
1278 
1279  NSUInteger selectionLocation = markedSelectedRange.location + markedTextRange.location;
1280  selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length);
1281  [self setSelectedTextRangeLocal:[FlutterTextRange
1282  rangeWithNSRange:[self clampSelection:selectedRange
1283  forText:self.text]]];
1284  if (_enableDeltaModel) {
1285  NSRange nextReplaceRange = [self clampSelection:actualReplacedRange forText:textBeforeChange];
1286  [self updateEditingStateWithDelta:flutter::TextEditingDelta(
1287  [textBeforeChange UTF8String],
1288  flutter::TextRange(
1289  nextReplaceRange.location,
1290  nextReplaceRange.location + nextReplaceRange.length),
1291  [markedText UTF8String])];
1292  } else {
1293  [self updateEditingState];
1294  }
1295 }
1296 
1297 - (void)unmarkText {
1298  if (!self.markedTextRange) {
1299  return;
1300  }
1301  self.markedTextRange = nil;
1302  if (_enableDeltaModel) {
1303  [self updateEditingStateWithDelta:flutter::TextEditingDelta([self.text UTF8String])];
1304  } else {
1305  [self updateEditingState];
1306  }
1307 }
1308 
1309 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1310  toPosition:(UITextPosition*)toPosition {
1311  NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
1312  NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
1313  if (toIndex >= fromIndex) {
1314  return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
1315  } else {
1316  // toIndex may be less than fromIndex, because
1317  // UITextInputStringTokenizer does not handle CJK characters
1318  // well in some cases. See:
1319  // https://github.com/flutter/flutter/issues/58750#issuecomment-644469521
1320  // Swap fromPosition and toPosition to match the behavior of native
1321  // UITextViews.
1322  return [FlutterTextRange rangeWithNSRange:NSMakeRange(toIndex, fromIndex - toIndex)];
1323  }
1324 }
1325 
1326 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1327  return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location;
1328 }
1329 
1330 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1331  NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position);
1332  return MIN(position + charRange.length, self.text.length);
1333 }
1334 
1335 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1336  NSUInteger offsetPosition = ((FlutterTextPosition*)position).index;
1337 
1338  NSInteger newLocation = (NSInteger)offsetPosition + offset;
1339  if (newLocation < 0 || newLocation > (NSInteger)self.text.length) {
1340  return nil;
1341  }
1342 
1343  if (_scribbleInteractionStatus != FlutterScribbleInteractionStatusNone) {
1344  return [FlutterTextPosition positionWithIndex:newLocation];
1345  }
1346 
1347  if (offset >= 0) {
1348  for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) {
1349  offsetPosition = [self incrementOffsetPosition:offsetPosition];
1350  }
1351  } else {
1352  for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1353  offsetPosition = [self decrementOffsetPosition:offsetPosition];
1354  }
1355  }
1356  return [FlutterTextPosition positionWithIndex:offsetPosition];
1357 }
1358 
1359 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1360  inDirection:(UITextLayoutDirection)direction
1361  offset:(NSInteger)offset {
1362  // TODO(cbracken) Add RTL handling.
1363  switch (direction) {
1364  case UITextLayoutDirectionLeft:
1365  case UITextLayoutDirectionUp:
1366  return [self positionFromPosition:position offset:offset * -1];
1367  case UITextLayoutDirectionRight:
1368  case UITextLayoutDirectionDown:
1369  return [self positionFromPosition:position offset:1];
1370  }
1371 }
1372 
1373 - (UITextPosition*)beginningOfDocument {
1375 }
1376 
1377 - (UITextPosition*)endOfDocument {
1378  return [FlutterTextPosition positionWithIndex:self.text.length];
1379 }
1380 
1381 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1382  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1383  NSUInteger otherIndex = ((FlutterTextPosition*)other).index;
1384  if (positionIndex < otherIndex) {
1385  return NSOrderedAscending;
1386  }
1387  if (positionIndex > otherIndex) {
1388  return NSOrderedDescending;
1389  }
1390  return NSOrderedSame;
1391 }
1392 
1393 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1394  return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index;
1395 }
1396 
1397 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1398  farthestInDirection:(UITextLayoutDirection)direction {
1399  NSUInteger index;
1400  switch (direction) {
1401  case UITextLayoutDirectionLeft:
1402  case UITextLayoutDirectionUp:
1403  index = ((FlutterTextPosition*)range.start).index;
1404  break;
1405  case UITextLayoutDirectionRight:
1406  case UITextLayoutDirectionDown:
1407  index = ((FlutterTextPosition*)range.end).index;
1408  break;
1409  }
1410  return [FlutterTextPosition positionWithIndex:index];
1411 }
1412 
1413 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1414  inDirection:(UITextLayoutDirection)direction {
1415  NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
1416  NSUInteger startIndex;
1417  NSUInteger endIndex;
1418  switch (direction) {
1419  case UITextLayoutDirectionLeft:
1420  case UITextLayoutDirectionUp:
1421  startIndex = [self decrementOffsetPosition:positionIndex];
1422  endIndex = positionIndex;
1423  break;
1424  case UITextLayoutDirectionRight:
1425  case UITextLayoutDirectionDown:
1426  startIndex = positionIndex;
1427  endIndex = [self incrementOffsetPosition:positionIndex];
1428  break;
1429  }
1430  return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)];
1431 }
1432 
1433 #pragma mark - UITextInput text direction handling
1434 
1435 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1436  inDirection:(UITextStorageDirection)direction {
1437  // TODO(cbracken) Add RTL handling.
1438  return UITextWritingDirectionNatural;
1439 }
1440 
1441 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1442  forRange:(UITextRange*)range {
1443  // TODO(cbracken) Add RTL handling.
1444 }
1445 
1446 #pragma mark - UITextInput cursor, selection rect handling
1447 
1448 - (void)setMarkedRect:(CGRect)markedRect {
1449  _markedRect = markedRect;
1450  // Invalidate the cache.
1452 }
1453 
1454 // This method expects a 4x4 perspective matrix
1455 // stored in a NSArray in column-major order.
1456 - (void)setEditableTransform:(NSArray*)matrix {
1457  CATransform3D* transform = &_editableTransform;
1458 
1459  transform->m11 = [matrix[0] doubleValue];
1460  transform->m12 = [matrix[1] doubleValue];
1461  transform->m13 = [matrix[2] doubleValue];
1462  transform->m14 = [matrix[3] doubleValue];
1463 
1464  transform->m21 = [matrix[4] doubleValue];
1465  transform->m22 = [matrix[5] doubleValue];
1466  transform->m23 = [matrix[6] doubleValue];
1467  transform->m24 = [matrix[7] doubleValue];
1468 
1469  transform->m31 = [matrix[8] doubleValue];
1470  transform->m32 = [matrix[9] doubleValue];
1471  transform->m33 = [matrix[10] doubleValue];
1472  transform->m34 = [matrix[11] doubleValue];
1473 
1474  transform->m41 = [matrix[12] doubleValue];
1475  transform->m42 = [matrix[13] doubleValue];
1476  transform->m43 = [matrix[14] doubleValue];
1477  transform->m44 = [matrix[15] doubleValue];
1478 
1479  // Invalidate the cache.
1481 }
1482 
1483 // The following methods are required to support force-touch cursor positioning
1484 // and to position the
1485 // candidates view for multi-stage input methods (e.g., Japanese) when using a
1486 // physical keyboard.
1487 
1488 - (CGRect)firstRectForRange:(UITextRange*)range {
1489  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1490  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1491  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1492  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1493  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1494  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1495  if (_markedTextRange != nil) {
1496  // The candidates view can't be shown if _editableTransform is not affine,
1497  // or markedRect is invalid.
1498  if (CGRectEqualToRect(kInvalidFirstRect, _markedRect) ||
1499  !CATransform3DIsAffine(_editableTransform)) {
1500  return kInvalidFirstRect;
1501  }
1502 
1503  if (CGRectEqualToRect(_cachedFirstRect, kInvalidFirstRect)) {
1504  // If the width returned is too small, that means the framework sent us
1505  // the caret rect instead of the marked text rect. Expand it to 0.1 so
1506  // the IME candidates view show up.
1507  double nonZeroWidth = MAX(_markedRect.size.width, 0.1);
1508  CGRect rect = _markedRect;
1509  rect.size = CGSizeMake(nonZeroWidth, rect.size.height);
1511  CGRectApplyAffineTransform(rect, CATransform3DGetAffineTransform(_editableTransform));
1512  }
1513 
1514  return _cachedFirstRect;
1515  }
1516 
1517  if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusNone &&
1518  _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1519  [_textInputDelegate flutterTextInputView:self
1520  showAutocorrectionPromptRectForStart:start
1521  end:end
1522  withClient:_textInputClient];
1523  }
1524 
1525  NSUInteger first = start;
1526  if (end < start) {
1527  first = end;
1528  }
1529  FlutterTextRange* textRange = [FlutterTextRange
1530  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1531  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1532  BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1533  BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1534  BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.range.length > first;
1535  BOOL nextSelectionRectIsAfterStartOfRange =
1536  !isLastSelectionRect && _selectionRects[i + 1].position > first;
1537  if (startsOnOrBeforeStartOfRange &&
1538  (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1539  return _selectionRects[i].rect;
1540  }
1541  }
1542 
1543  return CGRectZero;
1544 }
1545 
1546 - (CGRect)caretRectForPosition:(UITextPosition*)position {
1547  // TODO(cbracken) Implement.
1548 
1549  // As of iOS 14.4, this call is used by iOS's
1550  // _UIKeyboardTextSelectionController to determine the position
1551  // of the floating cursor when the user force touches the space
1552  // bar to initiate floating cursor.
1553  //
1554  // It is recommended to return a value that's roughly the
1555  // center of kSpacePanBounds to make sure the floating cursor
1556  // has ample space in all directions and does not hit kSpacePanBounds.
1557  // See the comments in beginFloatingCursorAtPoint.
1558  return CGRectZero;
1559 }
1560 
1561 - (CGRect)bounds {
1562  return _isFloatingCursorActive ? kSpacePanBounds : super.bounds;
1563 }
1564 
1565 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
1566  if ([_selectionRects count] == 0) {
1567  NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
1568  @"Expected a FlutterTextPosition for position (got %@).",
1569  [_selectedTextRange.start class]);
1570  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1571  return [FlutterTextPosition positionWithIndex:currentIndex];
1572  }
1573 
1575  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1576  return [self closestPositionToPoint:point withinRange:range];
1577 }
1578 
1579 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
1580  // At least in the simulator, swapping to the Japanese keyboard crashes the app as this method
1581  // is called immediately with a UITextRange with a UITextPosition rather than FlutterTextPosition
1582  // for the start and end.
1583  if (![range.start isKindOfClass:[FlutterTextPosition class]]) {
1584  return @[];
1585  }
1586  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1587  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1588  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1589  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1590  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1591  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1592  NSMutableArray* rects = [[[NSMutableArray alloc] init] autorelease];
1593  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1594  if (_selectionRects[i].position >= start && _selectionRects[i].position <= end) {
1595  float width = _selectionRects[i].rect.size.width;
1596  if (start == end) {
1597  width = 0;
1598  }
1599  CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
1600  width, _selectionRects[i].rect.size.height);
1603  position:_selectionRects[i].position
1604  writingDirection:UITextWritingDirectionNatural
1605  containsStart:(i == 0)
1606  containsEnd:(i == fml::RangeForCharactersInRange(
1607  self.text, NSMakeRange(0, self.text.length))
1608  .length)
1609  isVertical:NO];
1610  [rects addObject:selectionRect];
1611  }
1612  }
1613  return rects;
1614 }
1615 
1616 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
1617  NSAssert([range.start isKindOfClass:[FlutterTextPosition class]],
1618  @"Expected a FlutterTextPosition for range.start (got %@).", [range.start class]);
1619  NSAssert([range.end isKindOfClass:[FlutterTextPosition class]],
1620  @"Expected a FlutterTextPosition for range.end (got %@).", [range.end class]);
1621  NSUInteger start = ((FlutterTextPosition*)range.start).index;
1622  NSUInteger end = ((FlutterTextPosition*)range.end).index;
1623 
1624  NSUInteger _closestIndex = 0;
1625  CGRect _closestRect = CGRectZero;
1626  NSUInteger _closestPosition = 0;
1627  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1628  NSUInteger position = _selectionRects[i].position;
1629  if (position >= start && position <= end) {
1630  BOOL isFirst = _closestIndex == 0;
1631  if (isFirst || IsSelectionRectCloserToPoint(point, _selectionRects[i].rect, _closestRect,
1632  /*checkRightBoundary=*/NO)) {
1633  _closestIndex = i;
1634  _closestRect = _selectionRects[i].rect;
1635  _closestPosition = position;
1636  }
1637  }
1638  }
1639 
1640  FlutterTextRange* textRange = [FlutterTextRange
1641  rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1642 
1643  if ([_selectionRects count] > 0 && textRange.range.length == end) {
1644  NSUInteger i = [_selectionRects count] - 1;
1645  NSUInteger position = _selectionRects[i].position + 1;
1646  if (position <= end) {
1647  if (IsSelectionRectCloserToPoint(point, _selectionRects[i].rect, _closestRect,
1648  /*checkRightBoundary=*/YES)) {
1649  _closestIndex = [_selectionRects count];
1650  _closestPosition = position;
1651  }
1652  }
1653  }
1654 
1655  return [FlutterTextPosition positionWithIndex:_closestPosition];
1656 }
1657 
1658 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
1659  // TODO(cbracken) Implement.
1660  NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
1661  return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
1662 }
1663 
1664 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
1665  // For "beginFloatingCursorAtPoint" and "updateFloatingCursorAtPoint", "point" is roughly:
1666  //
1667  // CGPoint(
1668  // width >= 0 ? point.x.clamp(boundingBox.left, boundingBox.right) : point.x,
1669  // height >= 0 ? point.y.clamp(boundingBox.top, boundingBox.bottom) : point.y,
1670  // )
1671  // where
1672  // point = keyboardPanGestureRecognizer.translationInView(textInputView) +
1673  // caretRectForPosition boundingBox = self.convertRect(bounds, fromView:textInputView)
1674  // bounds = self._selectionClipRect ?? self.bounds
1675  //
1676  // It's tricky to provide accurate "bounds" and "caretRectForPosition" so it's preferred to
1677  // bypass the clamping and implement the same clamping logic in the framework where we have easy
1678  // access to the bounding box of the input field and the caret location.
1679  //
1680  // The current implementation returns kSpacePanBounds for "bounds" when
1681  // "_isFloatingCursorActive" is true. kSpacePanBounds centers "caretRectForPosition" so the
1682  // floating cursor has enough clearance in all directions to move around.
1683  //
1684  // It seems impossible to use a negative "width" or "height", as the "convertRect"
1685  // call always turns a CGRect's negative dimensions into non-negative values, e.g.,
1686  // (1, 2, -3, -4) would become (-2, -2, 3, 4).
1687  _isFloatingCursorActive = true;
1688  // This makes sure UITextSelectionView.interactionAssistant is not nil so
1689  // UITextSelectionView has access to this view (and its bounds). Otherwise
1690  // floating cursor breaks: https://github.com/flutter/flutter/issues/70267.
1691  if (@available(iOS 13.0, *)) {
1692  self.textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1693  self.textInteraction.textInput = self;
1694  [self addInteraction:_textInteraction];
1695  }
1696  [self.textInputDelegate flutterTextInputView:self
1697  updateFloatingCursor:FlutterFloatingCursorDragStateStart
1698  withClient:_textInputClient
1699  withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
1700 }
1701 
1702 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
1703  _isFloatingCursorActive = true;
1704  [self.textInputDelegate flutterTextInputView:self
1705  updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
1706  withClient:_textInputClient
1707  withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
1708 }
1709 
1710 - (void)endFloatingCursor {
1711  _isFloatingCursorActive = false;
1712  if (@available(iOS 13.0, *)) {
1713  if (_textInteraction != NULL) {
1714  [self removeInteraction:_textInteraction];
1715  self.textInteraction = NULL;
1716  }
1717  }
1718  [self.textInputDelegate flutterTextInputView:self
1719  updateFloatingCursor:FlutterFloatingCursorDragStateEnd
1720  withClient:_textInputClient
1721  withPosition:@{@"X" : @(0), @"Y" : @(0)}];
1722 }
1723 
1724 #pragma mark - UIKeyInput Overrides
1725 
1726 - (void)updateEditingState {
1727  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
1728  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
1729 
1730  // Empty compositing range is represented by the framework's TextRange.empty.
1731  NSInteger composingBase = -1;
1732  NSInteger composingExtent = -1;
1733  if (self.markedTextRange != nil) {
1734  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
1735  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
1736  }
1737 
1738  NSDictionary* state = @{
1739  @"selectionBase" : @(selectionBase),
1740  @"selectionExtent" : @(selectionExtent),
1741  @"selectionAffinity" : @(_selectionAffinity),
1742  @"selectionIsDirectional" : @(false),
1743  @"composingBase" : @(composingBase),
1744  @"composingExtent" : @(composingExtent),
1745  @"text" : [NSString stringWithString:self.text],
1746  };
1747 
1748  if (_textInputClient == 0 && _autofillId != nil) {
1749  [self.textInputDelegate flutterTextInputView:self
1750  updateEditingClient:_textInputClient
1751  withState:state
1752  withTag:_autofillId];
1753  } else {
1754  [self.textInputDelegate flutterTextInputView:self
1755  updateEditingClient:_textInputClient
1756  withState:state];
1757  }
1758 }
1759 
1760 - (void)updateEditingStateWithDelta:(flutter::TextEditingDelta)delta {
1761  NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
1762  NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
1763 
1764  // Empty compositing range is represented by the framework's TextRange.empty.
1765  NSInteger composingBase = -1;
1766  NSInteger composingExtent = -1;
1767  if (self.markedTextRange != nil) {
1768  composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
1769  composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
1770  }
1771 
1772  NSDictionary* deltaToFramework = @{
1773  @"oldText" : @(delta.old_text().c_str()),
1774  @"deltaText" : @(delta.delta_text().c_str()),
1775  @"deltaStart" : @(delta.delta_start()),
1776  @"deltaEnd" : @(delta.delta_end()),
1777  @"selectionBase" : @(selectionBase),
1778  @"selectionExtent" : @(selectionExtent),
1779  @"selectionAffinity" : @(_selectionAffinity),
1780  @"selectionIsDirectional" : @(false),
1781  @"composingBase" : @(composingBase),
1782  @"composingExtent" : @(composingExtent),
1783  };
1784 
1785  NSDictionary* deltas = @{
1786  @"deltas" : @[ deltaToFramework ],
1787  };
1788 
1789  [self.textInputDelegate flutterTextInputView:self
1790  updateEditingClient:_textInputClient
1791  withDelta:deltas];
1792 }
1793 
1794 - (BOOL)hasText {
1795  return self.text.length > 0;
1796 }
1797 
1798 - (void)insertText:(NSString*)text {
1799  NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
1800  [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
1801  NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
1802  @"Expected a FlutterTextPosition for position (got %@).",
1803  [_selectedTextRange.start class]);
1804  NSUInteger insertPosition = ((FlutterTextPosition*)_selectedTextRange.start).index;
1805  for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1806  NSUInteger rectPosition = _selectionRects[i].position;
1807  if (rectPosition == insertPosition) {
1808  for (NSUInteger j = 0; j <= text.length; j++) {
1809  [copiedRects
1810  addObject:[FlutterTextSelectionRect selectionRectWithRect:_selectionRects[i].rect
1811  position:rectPosition + j]];
1812  }
1813  } else {
1814  if (rectPosition > insertPosition) {
1815  rectPosition = rectPosition + text.length;
1816  }
1817  [copiedRects addObject:[FlutterTextSelectionRect selectionRectWithRect:_selectionRects[i].rect
1818  position:rectPosition]];
1819  }
1820  }
1821 
1822  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1823  [self resetScribbleInteractionStatusIfEnding];
1824  self.selectionRects = copiedRects;
1825  [copiedRects release];
1827  [self replaceRange:_selectedTextRange withText:text];
1828 }
1829 
1830 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
1831  [_textInputDelegate flutterTextInputView:self
1832  insertTextPlaceholderWithSize:size
1833  withClient:_textInputClient];
1834  _hasPlaceholder = YES;
1835  return [[[FlutterTextPlaceholder alloc] init] autorelease];
1836 }
1837 
1838 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
1839  _hasPlaceholder = NO;
1840  [_textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
1841 }
1842 
1843 - (void)deleteBackward {
1845  _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1846  [self resetScribbleInteractionStatusIfEnding];
1847 
1848  // When deleting Thai vowel, _selectedTextRange has location
1849  // but does not have length, so we have to manually set it.
1850  // In addition, we needed to delete only a part of grapheme cluster
1851  // because it is the expected behavior of Thai input.
1852  // https://github.com/flutter/flutter/issues/24203
1853  // https://github.com/flutter/flutter/issues/21745
1854  // https://github.com/flutter/flutter/issues/39399
1855  //
1856  // This is needed for correct handling of the deletion of Thai vowel input.
1857  // TODO(cbracken): Get a good understanding of expected behavior of Thai
1858  // input and ensure that this is the correct solution.
1859  // https://github.com/flutter/flutter/issues/28962
1860  if (_selectedTextRange.isEmpty && [self hasText]) {
1861  UITextRange* oldSelectedRange = _selectedTextRange;
1862  NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
1863  if (oldRange.location > 0) {
1864  NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
1866  [oldSelectedRange release];
1867  }
1868  }
1869 
1870  if (!_selectedTextRange.isEmpty) {
1871  [self replaceRange:_selectedTextRange withText:@""];
1872  }
1873 }
1874 
1875 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
1876  UIAccessibilityPostNotification(notification, target);
1877 }
1878 
1879 - (void)accessibilityElementDidBecomeFocused {
1880  if ([self accessibilityElementIsFocused]) {
1881  // For most of the cases, this flutter text input view should never
1882  // receive the focus. If we do receive the focus, we make the best effort
1883  // to send the focus back to the real text field.
1884  FML_DCHECK(_backingTextInputAccessibilityObject);
1885  [self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
1886  target:_backingTextInputAccessibilityObject];
1887  }
1888 }
1889 
1890 - (BOOL)accessibilityElementsHidden {
1891  return !_accessibilityEnabled;
1892 }
1893 
1895  if (_scribbleInteractionStatus == FlutterScribbleInteractionStatusEnding) {
1896  _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
1897  }
1898 }
1899 
1900 @end
1901 
1902 /**
1903  * Hides `FlutterTextInputView` from iOS accessibility system so it
1904  * does not show up twice, once where it is in the `UIView` hierarchy,
1905  * and a second time as part of the `SemanticsObject` hierarchy.
1906  *
1907  * This prevents the `FlutterTextInputView` from receiving the focus
1908  * due to swiping gesture.
1909  *
1910  * There are other cases the `FlutterTextInputView` may receive
1911  * focus. One example is during screen changes, the accessibility
1912  * tree will undergo a dramatic structural update. The Voiceover may
1913  * decide to focus the `FlutterTextInputView` that is not involved
1914  * in the structural update instead. If that happens, the
1915  * `FlutterTextInputView` will make a best effort to direct the
1916  * focus back to the `SemanticsObject`.
1917  */
1919 }
1920 
1921 @end
1922 
1924 }
1925 
1926 - (BOOL)accessibilityElementsHidden {
1927  return YES;
1928 }
1929 
1930 @end
1931 
1933 - (void)enableActiveViewAccessibility;
1934 @end
1935 
1936 @interface FlutterTimerProxy : NSObject
1937 @property(nonatomic, assign) FlutterTextInputPlugin* target;
1938 @end
1939 
1940 @implementation FlutterTimerProxy
1941 
1942 + (instancetype)proxyWithTarget:(FlutterTextInputPlugin*)target {
1943  FlutterTimerProxy* proxy = [[self new] autorelease];
1944  if (proxy) {
1945  proxy.target = target;
1946  }
1947  return proxy;
1948 }
1949 
1950 - (void)enableActiveViewAccessibility {
1951  [self.target enableActiveViewAccessibility];
1952 }
1953 
1954 @end
1955 
1956 @interface FlutterTextInputPlugin ()
1957 // The current password-autofillable input fields that have yet to be saved.
1958 @property(nonatomic, readonly)
1959  NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
1960 @property(nonatomic, retain) FlutterTextInputView* activeView;
1961 @property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider;
1962 @property(nonatomic, readonly) id<FlutterViewResponder> viewResponder;
1963 @end
1964 
1965 @implementation FlutterTextInputPlugin {
1966  NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
1967 }
1968 
1969 @synthesize textInputDelegate = _textInputDelegate;
1970 
1971 - (instancetype)init {
1972  self = [super init];
1973 
1974  if (self) {
1975  _autofillContext = [[NSMutableDictionary alloc] init];
1976  _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
1977  _scribbleElements = [[NSMutableDictionary alloc] init];
1978  // Initialize activeView with a dummy view to keep tests
1979  // passing. This dummy view needs to be replace once the
1980  // framework initializes an input connection, and thus
1981  // should never have access to the textInputDelegate.
1982  _activeView = [[FlutterTextInputView alloc] init];
1983  [_activeView decommission];
1984  }
1985 
1986  return self;
1987 }
1988 
1989 - (void)dealloc {
1990  [self hideTextInput];
1991  _activeView.textInputDelegate = nil;
1992  [_activeView release];
1993  [_inputHider release];
1994  for (FlutterTextInputView* autofillView in _autofillContext.allValues) {
1995  autofillView.textInputDelegate = nil;
1996  }
1997  [_autofillContext release];
1998  [_scribbleElements release];
1999  [super dealloc];
2000 }
2001 
2002 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2003  if (_enableFlutterTextInputViewAccessibilityTimer) {
2004  [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2005  [_enableFlutterTextInputViewAccessibilityTimer release];
2006  _enableFlutterTextInputViewAccessibilityTimer = nil;
2007  }
2008 }
2009 
2010 - (UIView<UITextInput>*)textInputView {
2011  return _activeView;
2012 }
2013 
2014 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
2015  NSString* method = call.method;
2016  id args = call.arguments;
2017  if ([method isEqualToString:kShowMethod]) {
2018  [self showTextInput];
2019  result(nil);
2020  } else if ([method isEqualToString:kHideMethod]) {
2021  [self hideTextInput];
2022  result(nil);
2023  } else if ([method isEqualToString:kSetClientMethod]) {
2024  [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2025  result(nil);
2026  } else if ([method isEqualToString:kSetEditingStateMethod]) {
2027  [self setTextInputEditingState:args];
2028  result(nil);
2029  } else if ([method isEqualToString:kClearClientMethod]) {
2030  [self clearTextInputClient];
2031  result(nil);
2032  } else if ([method isEqualToString:kSetEditableSizeAndTransformMethod]) {
2033  [self setEditableSizeAndTransform:args];
2034  result(nil);
2035  } else if ([method isEqualToString:kSetMarkedTextRectMethod]) {
2036  [self updateMarkedRect:args];
2037  result(nil);
2038  } else if ([method isEqualToString:kFinishAutofillContextMethod]) {
2039  [self triggerAutofillSave:[args boolValue]];
2040  result(nil);
2041  } else if ([method isEqualToString:@"TextInput.setSelectionRects"]) {
2042  [self setSelectionRects:args];
2043  result(nil);
2044  } else {
2046  }
2047 }
2048 
2049 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2050  [_activeView setEditableTransform:dictionary[@"transform"]];
2051  if (IsScribbleAvailable()) {
2052  // This is necessary to set up where the scribble interactable element will be.
2053  int leftIndex = 12;
2054  int topIndex = 13;
2055  _inputHider.frame =
2056  CGRectMake([dictionary[@"transform"][leftIndex] intValue],
2057  [dictionary[@"transform"][topIndex] intValue], [dictionary[@"width"] intValue],
2058  [dictionary[@"height"] intValue]);
2059  _activeView.frame =
2060  CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
2061  _activeView.tintColor = [UIColor clearColor];
2062  }
2063 }
2064 
2065 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2066  NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
2067  dictionary[@"height"] != nil,
2068  @"Expected a dictionary representing a CGRect, got %@", dictionary);
2069  CGRect rect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
2070  [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
2071  _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ? kInvalidFirstRect : rect;
2072 }
2073 
2074 - (void)setSelectionRects:(NSArray*)rects {
2075  NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2076  [[[NSMutableArray alloc] initWithCapacity:[rects count]] autorelease];
2077  for (NSUInteger i = 0; i < [rects count]; i++) {
2078  NSArray<NSNumber*>* rect = rects[i];
2079  [rectsAsRect
2080  addObject:[FlutterTextSelectionRect
2081  selectionRectWithRect:CGRectMake([rect[0] floatValue], [rect[1] floatValue],
2082  [rect[2] floatValue], [rect[3] floatValue])
2083  position:[rect[4] unsignedIntegerValue]]];
2084  }
2085  _activeView.selectionRects = rectsAsRect;
2086 }
2087 
2088 - (void)showTextInput {
2089  _activeView.textInputDelegate = _textInputDelegate;
2090  _activeView.viewResponder = _viewResponder;
2091  [self addToInputParentViewIfNeeded:_activeView];
2092  // Adds a delay to prevent the text view from receiving accessibility
2093  // focus in case it is activated during semantics updates.
2094  //
2095  // One common case is when the app navigates to a page with an auto
2096  // focused text field. The text field will activate the FlutterTextInputView
2097  // with a semantics update sent to the engine. The voiceover will focus
2098  // the newly attached active view while performing accessibility update.
2099  // This results in accessibility focus stuck at the FlutterTextInputView.
2100  if (!_enableFlutterTextInputViewAccessibilityTimer) {
2101  _enableFlutterTextInputViewAccessibilityTimer =
2102  [[NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2103  target:[FlutterTimerProxy proxyWithTarget:self]
2104  selector:@selector(enableActiveViewAccessibility)
2105  userInfo:nil
2106  repeats:NO] retain];
2107  }
2108  [_activeView becomeFirstResponder];
2109 }
2110 
2111 - (void)enableActiveViewAccessibility {
2112  if (_activeView.isFirstResponder) {
2113  _activeView.accessibilityEnabled = YES;
2114  }
2115  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2116 }
2117 
2118 - (void)hideTextInput {
2119  [self removeEnableFlutterTextInputViewAccessibilityTimer];
2120  _activeView.accessibilityEnabled = NO;
2121  [_activeView resignFirstResponder];
2122  [_activeView removeFromSuperview];
2123  [_inputHider removeFromSuperview];
2124 }
2125 
2126 - (void)triggerAutofillSave:(BOOL)saveEntries {
2127  [_activeView resignFirstResponder];
2128 
2129  if (saveEntries) {
2130  // Make all the input fields in the autofill context visible,
2131  // then remove them to trigger autofill save.
2132  [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2133  [_autofillContext removeAllObjects];
2134  [self changeInputViewsAutofillVisibility:YES];
2135  } else {
2136  [_autofillContext removeAllObjects];
2137  }
2138 
2139  [self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2140  [self addToInputParentViewIfNeeded:_activeView];
2141 }
2142 
2143 - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
2144  [self resetAllClientIds];
2145  // Hide all input views from autofill, only make those in the new configuration visible
2146  // to autofill.
2147  [self changeInputViewsAutofillVisibility:NO];
2148 
2149  // Update the current active view.
2150  switch (AutofillTypeOf(configuration)) {
2151  case FlutterAutofillTypeNone:
2152  self.activeView = [self createInputViewWith:configuration];
2153  break;
2154  case FlutterAutofillTypeRegular:
2155  // If the group does not involve password autofill, only install the
2156  // input view that's being focused.
2157  self.activeView = [self updateAndShowAutofillViews:nil
2158  focusedField:configuration
2159  isPasswordRelated:NO];
2160  break;
2161  case FlutterAutofillTypePassword:
2162  self.activeView = [self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2163  focusedField:configuration
2164  isPasswordRelated:YES];
2165  break;
2166  }
2167  [_activeView setTextInputClient:client];
2168  [_activeView reloadInputViews];
2169 
2170  // Clean up views that no longer need to be in the view hierarchy, according to
2171  // the current autofill context. The "garbage" input views are already made
2172  // invisible to autofill and they can't `becomeFirstResponder`, we only remove
2173  // them to free up resources and reduce the number of input views in the view
2174  // hierarchy.
2175  //
2176  // The garbage views are decommissioned immediately, but the removeFromSuperview
2177  // call is scheduled on the runloop and delayed by 0.1s so we don't remove the
2178  // text fields immediately (which seems to make the keyboard flicker).
2179  // See: https://github.com/flutter/flutter/issues/64628.
2180  [self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2181 }
2182 
2183 // Creates and shows an input field that is not password related and has no autofill
2184 // info. This method returns a new FlutterTextInputView instance when called, since
2185 // UIKit uses the identity of `UITextInput` instances (or the identity of the input
2186 // views) to decide whether the IME's internal states should be reset. See:
2187 // https://github.com/flutter/flutter/issues/79031 .
2188 - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration {
2189  NSString* autofillId = AutofillIdFromDictionary(configuration);
2190  if (autofillId) {
2191  [_autofillContext removeObjectForKey:autofillId];
2192  }
2193  FlutterTextInputView* newView = [[FlutterTextInputView alloc] init];
2194  [newView configureWithDictionary:configuration];
2195  [self addToInputParentViewIfNeeded:newView];
2196  newView.textInputDelegate = _textInputDelegate;
2197 
2198  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
2199  NSString* autofillId = AutofillIdFromDictionary(field);
2200  if (autofillId && AutofillTypeOf(field) == FlutterAutofillTypeNone) {
2201  [_autofillContext removeObjectForKey:autofillId];
2202  }
2203  }
2204  return [newView autorelease];
2205 }
2206 
2207 - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields
2208  focusedField:(NSDictionary*)focusedField
2209  isPasswordRelated:(BOOL)isPassword {
2210  FlutterTextInputView* focused = nil;
2211  NSString* focusedId = AutofillIdFromDictionary(focusedField);
2212  NSAssert(focusedId, @"autofillId must not be null for the focused field: %@", focusedField);
2213 
2214  if (!fields) {
2215  // DO NOT push the current autofillable input fields to the context even
2216  // if it's password-related, because it is not in an autofill group.
2217  focused = [self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2218  [_autofillContext removeObjectForKey:focusedId];
2219  }
2220 
2221  for (NSDictionary* field in fields) {
2222  NSString* autofillId = AutofillIdFromDictionary(field);
2223  NSAssert(autofillId, @"autofillId must not be null for field: %@", field);
2224 
2225  BOOL hasHints = AutofillTypeOf(field) != FlutterAutofillTypeNone;
2226  BOOL isFocused = [focusedId isEqualToString:autofillId];
2227 
2228  if (isFocused) {
2229  focused = [self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2230  }
2231 
2232  if (hasHints) {
2233  // Push the current input field to the context if it has hints.
2234  _autofillContext[autofillId] = isFocused ? focused
2235  : [self getOrCreateAutofillableView:field
2236  isPasswordAutofill:isPassword];
2237  } else {
2238  // Mark for deletion.
2239  [_autofillContext removeObjectForKey:autofillId];
2240  }
2241  }
2242 
2243  NSAssert(focused, @"The current focused input view must not be nil.");
2244  return focused;
2245 }
2246 
2247 // Returns a new non-reusable input view (and put it into the view hierarchy), or get the
2248 // view from the current autofill context, if an input view with the same autofill id
2249 // already exists in the context.
2250 // This is generally used for input fields that are autofillable (UIKit tracks these veiws
2251 // for autofill purposes so they should not be reused for a different type of views).
2252 - (FlutterTextInputView*)getOrCreateAutofillableView:(NSDictionary*)field
2253  isPasswordAutofill:(BOOL)needsPasswordAutofill {
2254  NSString* autofillId = AutofillIdFromDictionary(field);
2255  FlutterTextInputView* inputView = _autofillContext[autofillId];
2256  if (!inputView) {
2257  inputView =
2258  needsPasswordAutofill ? [FlutterSecureTextInputView alloc] : [FlutterTextInputView alloc];
2259  inputView = [[inputView init] autorelease];
2260  [self addToInputParentViewIfNeeded:inputView];
2261  }
2262 
2263  inputView.textInputDelegate = _textInputDelegate;
2264  [inputView configureWithDictionary:field];
2265  return inputView;
2266 }
2267 
2268 // The UIView to add FlutterTextInputViews to.
2269 - (UIView*)hostView {
2270  NSAssert(self.viewController.view != nullptr,
2271  @"The application must have a HostView since the keyboard client "
2272  @"must be part of the responder chain to function");
2273  return self.viewController.view;
2274 }
2275 
2276 // The UIView to add FlutterTextInputViews to.
2277 - (NSArray<UIView*>*)textInputViews {
2278  return _inputHider.subviews;
2279 }
2280 
2281 // Decommissions (See the "decommission" method on FlutterTextInputView) and removes
2282 // every installed input field, unless it's in the current autofill context.
2283 //
2284 // The active view will be decommissioned and removed from its superview too, if
2285 // includeActiveView is YES.
2286 // When clearText is YES, the text on the input fields will be set to empty before
2287 // they are removed from the view hierarchy, to avoid triggering autofill save.
2288 // If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and
2289 // will be delayed by 0.1s so we don't remove the text fields immediately (which seems
2290 // to make the keyboard flicker).
2291 // See: https://github.com/flutter/flutter/issues/64628.
2292 
2293 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
2294  clearText:(BOOL)clearText
2295  delayRemoval:(BOOL)delayRemoval {
2296  for (UIView* view in self.textInputViews) {
2297  if ([view isKindOfClass:[FlutterTextInputView class]] &&
2298  (includeActiveView || view != _activeView)) {
2300  if (_autofillContext[inputView.autofillId] != view) {
2301  if (clearText) {
2302  [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
2303  }
2304  [inputView decommission];
2305  if (delayRemoval) {
2306  [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
2307  } else {
2308  [inputView removeFromSuperview];
2309  }
2310  }
2311  }
2312  }
2313 }
2314 
2315 // Changes the visibility of every FlutterTextInputView currently in the
2316 // view hierarchy.
2317 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
2318  for (UIView* view in self.textInputViews) {
2319  if ([view isKindOfClass:[FlutterTextInputView class]]) {
2320  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2321  inputView.isVisibleToAutofill = newVisibility;
2322  }
2323  }
2324 }
2325 
2326 // Resets the client id of every FlutterTextInputView in the view hierarchy
2327 // to 0.
2328 // Called before establishing a new text input connection.
2329 // For views in the current autofill context, they need to
2330 // stay in the view hierachy but should not be allowed to
2331 // send messages (other than autofill related ones) to the
2332 // framework.
2333 - (void)resetAllClientIds {
2334  for (UIView* view in self.textInputViews) {
2335  if ([view isKindOfClass:[FlutterTextInputView class]]) {
2336  FlutterTextInputView* inputView = (FlutterTextInputView*)view;
2337  [inputView setTextInputClient:0];
2338  }
2339  }
2340 }
2341 
2342 - (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
2343  if (![inputView isDescendantOfView:_inputHider]) {
2344  [_inputHider addSubview:inputView];
2345  }
2346  UIView* parentView = self.hostView;
2347  if (_inputHider.superview != parentView) {
2348  [parentView addSubview:_inputHider];
2349  }
2350 }
2351 
2352 - (void)setTextInputEditingState:(NSDictionary*)state {
2353  [_activeView setTextInputState:state];
2354 }
2355 
2356 - (void)clearTextInputClient {
2357  [_activeView setTextInputClient:0];
2358  _activeView.frame = CGRectZero;
2359 }
2360 
2361 #pragma mark UIIndirectScribbleInteractionDelegate
2362 
2363 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2364  isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
2365  API_AVAILABLE(ios(14.0)) {
2366  return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
2367 }
2368 
2369 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2370  focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
2371  referencePoint:(CGPoint)focusReferencePoint
2372  completion:(void (^)(UIResponder<UITextInput>* focusedInput))completion
2373  API_AVAILABLE(ios(14.0)) {
2374  _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
2375  [_indirectScribbleDelegate flutterTextInputPlugin:self
2376  focusElement:elementIdentifier
2377  atPoint:focusReferencePoint
2378  result:^(id _Nullable result) {
2379  _activeView.scribbleFocusStatus =
2380  FlutterScribbleFocusStatusFocused;
2381  completion(_activeView);
2382  }];
2383 }
2384 
2385 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2386  shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
2387  API_AVAILABLE(ios(14.0)) {
2388  return NO;
2389 }
2390 
2391 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2392  willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2393  API_AVAILABLE(ios(14.0)) {
2394 }
2395 
2396 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2397  didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
2398  API_AVAILABLE(ios(14.0)) {
2399 }
2400 
2401 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2402  frameForElement:(UIScribbleElementIdentifier)elementIdentifier
2403  API_AVAILABLE(ios(14.0)) {
2404  NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
2405  if (elementValue == nil) {
2406  return CGRectZero;
2407  }
2408  return [elementValue CGRectValue];
2409 }
2410 
2411 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
2412  requestElementsInRect:(CGRect)rect
2413  completion:
2414  (void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
2415  API_AVAILABLE(ios(14.0)) {
2416  [_indirectScribbleDelegate
2417  flutterTextInputPlugin:self
2418  requestElementsInRect:rect
2419  result:^(id _Nullable result) {
2420  NSMutableArray<UIScribbleElementIdentifier>* elements =
2421  [[[NSMutableArray alloc] init] autorelease];
2422  if ([result isKindOfClass:[NSArray class]]) {
2423  for (NSArray* elementArray in result) {
2424  [elements addObject:elementArray[0]];
2425  [_scribbleElements
2426  setObject:[NSValue
2427  valueWithCGRect:CGRectMake(
2428  [elementArray[1] floatValue],
2429  [elementArray[2] floatValue],
2430  [elementArray[3] floatValue],
2431  [elementArray[4] floatValue])]
2432  forKey:elementArray[0]];
2433  }
2434  }
2435  completion(elements);
2436  }];
2437 }
2438 
2439 #pragma mark - Methods related to Scribble support
2440 
2441 - (void)setupIndirectScribbleInteraction:(id<FlutterViewResponder>)viewResponder {
2442  if (_viewResponder != viewResponder) {
2443  if (@available(iOS 14.0, *)) {
2444  UIView* parentView = viewResponder.view;
2445  if (parentView != nil) {
2446  UIIndirectScribbleInteraction* scribbleInteraction = [[[UIIndirectScribbleInteraction alloc]
2447  initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self] autorelease];
2448  [parentView addInteraction:scribbleInteraction];
2449  }
2450  }
2451  }
2452  _viewResponder = viewResponder;
2453 }
2454 
2455 - (void)resetViewResponder {
2456  _viewResponder = nil;
2457 }
2458 
2459 #pragma mark -
2460 #pragma mark FlutterKeySecondaryResponder
2461 
2462 /**
2463  * Handles key down events received from the view controller, responding YES if
2464  * the event was handled.
2465  */
2466 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
2467  return NO;
2468 }
2469 @end
G_BEGIN_DECLS FlValue * args
static NSString *const kSmartDashesType
KeyCallType type
bool _isFloatingCursorActive
static FlutterAutofillType AutofillTypeOf(NSDictionary *configuration)
static NSString *const kSmartQuotesType
FlutterTextRange * _selectedTextRange
bool _isSystemKeyboardEnabled
static const char _kTextAffinityDownstream[]
#define FML_DCHECK(condition)
Definition: logging.h:86
FlutterViewController * viewController
NSRange _range
static NSString *const kInputAction
static NSString *const kSetEditingStateMethod
A change in the state of an input field.
static BOOL IsSelectionRectCloserToPoint(CGPoint point, CGRect selectionRect, CGRect otherSelectionRect, BOOL checkRightBoundary)
BOOL _hasPlaceholder
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder NSMutableString * markedText
static NSString *const kShowMethod
static NSString *const kSecureTextEntry
id< FlutterTextInputDelegate > textInputDelegate
GAsyncResult * result
constexpr std::size_t size(T(&array)[N])
Definition: size.h:13
CATransform3D _editableTransform
static NSString *const kClearClientMethod
instancetype rangeWithNSRange:(NSRange range)
UIReturnKeyType returnKeyType
NSRange RangeForCharacterAtIndex(NSString *text, NSUInteger index)
instancetype positionWithIndex:(NSUInteger index)
static NSString *const kSetClientMethod
static NSString *const kAutofillId
FlKeyEvent * event
FlutterTextInputPlugin * target
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static UIKeyboardType ToUIKeyboardType(NSDictionary *type)
UITextRange * markedTextRange
static NSString *const kAssociatedAutofillFields
static const char _kTextAffinityUpstream[]
uint32_t * target
static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary *type)
const CGRect kSpacePanBounds
const char * _selectionAffinity
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds
static NSString *const kAutofillEditingValue
CGRect _cachedFirstRect
static NSString *const kSetMarkedTextRectMethod
static BOOL IsFieldPasswordRelated(NSDictionary *configuration)
SemanticsAction action
NSWritingDirection writingDirection
static NSString *const kEnableDeltaModel
static UIReturnKeyType ToUIReturnKeyType(NSString *inputType)
static BOOL ShouldShowSystemKeyboard(NSDictionary *type)
const CGRect kInvalidFirstRect
void(^ FlutterResult)(id _Nullable result)
int32_t width
size_t length
BOOL _decommissioned
static NSString *const kSetEditableSizeAndTransformMethod
instancetype selectionRectWithRectAndInfo:position:writingDirection:containsStart:containsEnd:isVertical:(CGRect rect, [position] NSUInteger position, [writingDirection] NSWritingDirection writingDirection, [containsStart] BOOL containsStart, [containsEnd] BOOL containsEnd, [isVertical] BOOL isVertical)
FlutterScribbleInteractionStatus _scribbleInteractionStatus
static NSString *const kAutocorrectionType
int BOOL
Definition: windows_types.h:37
static NSString *const kKeyboardAppearance
static BOOL IsScribbleAvailable()
static NSString *const kAutofillProperties
UIInputViewController * _inputViewController
FlView * view
UITextRange * selectedTextRange
id< FlutterViewResponder > viewResponder
std::u16string text
static NSString * AutofillIdFromDictionary(NSDictionary *dictionary)
int32_t id
instancetype selectionRectWithRect:position:(CGRect rect, [position] NSUInteger position)
static NSString *const kAutofillHints
static UITextContentType ToUITextContentType(NSArray< NSString *> *hints)
static NSString *const kKeyboardType
UIKeyboardAppearance keyboardAppearance
fml::scoped_nsobject< UIPointerInteraction > _pointerInteraction API_AVAILABLE(ios(13.4))
AtkStateType state
static NSString *const kHideMethod
void resetScribbleInteractionStatusIfEnding
static NSString *const kFinishAutofillContextMethod
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
static BOOL IsApproximatelyEqual(float x, float y, float delta)