8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIKit.h>
11 #include "unicode/uchar.h"
13 #include "flutter/fml/logging.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
15 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
39 #pragma mark - TextInput channel method names.
48 @"TextInput.setEditableSizeAndTransform";
59 @"TextInput.onPointerMoveForInteractiveKeyboard";
61 @"TextInput.onPointerUpForInteractiveKeyboard";
63 #pragma mark - TextInputConfiguration Field Names
84 #pragma mark - Static Functions
87 static BOOL
IsEmoji(NSString* text, NSRange charRange) {
89 BOOL gotCodePoint = [text getBytes:&codePoint
90 maxLength:
sizeof(codePoint)
92 encoding:NSUTF32StringEncoding
96 return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
104 NSString* inputType = type[
@"name"];
105 return ![inputType isEqualToString:
@"TextInputType.none"];
108 NSString* inputType = type[
@"name"];
109 if ([inputType isEqualToString:
@"TextInputType.address"]) {
110 return UIKeyboardTypeDefault;
112 if ([inputType isEqualToString:
@"TextInputType.datetime"]) {
113 return UIKeyboardTypeNumbersAndPunctuation;
115 if ([inputType isEqualToString:
@"TextInputType.emailAddress"]) {
116 return UIKeyboardTypeEmailAddress;
118 if ([inputType isEqualToString:
@"TextInputType.multiline"]) {
119 return UIKeyboardTypeDefault;
121 if ([inputType isEqualToString:
@"TextInputType.name"]) {
122 return UIKeyboardTypeNamePhonePad;
124 if ([inputType isEqualToString:
@"TextInputType.number"]) {
125 if ([type[
@"signed"] boolValue]) {
126 return UIKeyboardTypeNumbersAndPunctuation;
128 if ([type[
@"decimal"] boolValue]) {
129 return UIKeyboardTypeDecimalPad;
131 return UIKeyboardTypeNumberPad;
133 if ([inputType isEqualToString:
@"TextInputType.phone"]) {
134 return UIKeyboardTypePhonePad;
136 if ([inputType isEqualToString:
@"TextInputType.text"]) {
137 return UIKeyboardTypeDefault;
139 if ([inputType isEqualToString:
@"TextInputType.url"]) {
140 return UIKeyboardTypeURL;
142 if ([inputType isEqualToString:
@"TextInputType.visiblePassword"]) {
143 return UIKeyboardTypeASCIICapable;
145 if ([inputType isEqualToString:
@"TextInputType.webSearch"]) {
146 return UIKeyboardTypeWebSearch;
148 if ([inputType isEqualToString:
@"TextInputType.twitter"]) {
149 return UIKeyboardTypeTwitter;
151 return UIKeyboardTypeDefault;
155 NSString* textCapitalization = type[
@"textCapitalization"];
156 if ([textCapitalization isEqualToString:
@"TextCapitalization.characters"]) {
157 return UITextAutocapitalizationTypeAllCharacters;
158 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.sentences"]) {
159 return UITextAutocapitalizationTypeSentences;
160 }
else if ([textCapitalization isEqualToString:
@"TextCapitalization.words"]) {
161 return UITextAutocapitalizationTypeWords;
163 return UITextAutocapitalizationTypeNone;
171 if ([inputType isEqualToString:
@"TextInputAction.unspecified"]) {
172 return UIReturnKeyDefault;
175 if ([inputType isEqualToString:
@"TextInputAction.done"]) {
176 return UIReturnKeyDone;
179 if ([inputType isEqualToString:
@"TextInputAction.go"]) {
180 return UIReturnKeyGo;
183 if ([inputType isEqualToString:
@"TextInputAction.send"]) {
184 return UIReturnKeySend;
187 if ([inputType isEqualToString:
@"TextInputAction.search"]) {
188 return UIReturnKeySearch;
191 if ([inputType isEqualToString:
@"TextInputAction.next"]) {
192 return UIReturnKeyNext;
195 if ([inputType isEqualToString:
@"TextInputAction.continueAction"]) {
196 return UIReturnKeyContinue;
199 if ([inputType isEqualToString:
@"TextInputAction.join"]) {
200 return UIReturnKeyJoin;
203 if ([inputType isEqualToString:
@"TextInputAction.route"]) {
204 return UIReturnKeyRoute;
207 if ([inputType isEqualToString:
@"TextInputAction.emergencyCall"]) {
208 return UIReturnKeyEmergencyCall;
211 if ([inputType isEqualToString:
@"TextInputAction.newline"]) {
212 return UIReturnKeyDefault;
216 return UIReturnKeyDefault;
220 if (!hints || hints.count == 0) {
225 NSString* hint = hints[0];
226 if ([hint isEqualToString:
@"addressCityAndState"]) {
227 return UITextContentTypeAddressCityAndState;
230 if ([hint isEqualToString:
@"addressState"]) {
231 return UITextContentTypeAddressState;
234 if ([hint isEqualToString:
@"addressCity"]) {
235 return UITextContentTypeAddressCity;
238 if ([hint isEqualToString:
@"sublocality"]) {
239 return UITextContentTypeSublocality;
242 if ([hint isEqualToString:
@"streetAddressLine1"]) {
243 return UITextContentTypeStreetAddressLine1;
246 if ([hint isEqualToString:
@"streetAddressLine2"]) {
247 return UITextContentTypeStreetAddressLine2;
250 if ([hint isEqualToString:
@"countryName"]) {
251 return UITextContentTypeCountryName;
254 if ([hint isEqualToString:
@"fullStreetAddress"]) {
255 return UITextContentTypeFullStreetAddress;
258 if ([hint isEqualToString:
@"postalCode"]) {
259 return UITextContentTypePostalCode;
262 if ([hint isEqualToString:
@"location"]) {
263 return UITextContentTypeLocation;
266 if ([hint isEqualToString:
@"creditCardNumber"]) {
267 return UITextContentTypeCreditCardNumber;
270 if ([hint isEqualToString:
@"email"]) {
271 return UITextContentTypeEmailAddress;
274 if ([hint isEqualToString:
@"jobTitle"]) {
275 return UITextContentTypeJobTitle;
278 if ([hint isEqualToString:
@"givenName"]) {
279 return UITextContentTypeGivenName;
282 if ([hint isEqualToString:
@"middleName"]) {
283 return UITextContentTypeMiddleName;
286 if ([hint isEqualToString:
@"familyName"]) {
287 return UITextContentTypeFamilyName;
290 if ([hint isEqualToString:
@"name"]) {
291 return UITextContentTypeName;
294 if ([hint isEqualToString:
@"namePrefix"]) {
295 return UITextContentTypeNamePrefix;
298 if ([hint isEqualToString:
@"nameSuffix"]) {
299 return UITextContentTypeNameSuffix;
302 if ([hint isEqualToString:
@"nickname"]) {
303 return UITextContentTypeNickname;
306 if ([hint isEqualToString:
@"organizationName"]) {
307 return UITextContentTypeOrganizationName;
310 if ([hint isEqualToString:
@"telephoneNumber"]) {
311 return UITextContentTypeTelephoneNumber;
314 if ([hint isEqualToString:
@"password"]) {
315 return UITextContentTypePassword;
318 if ([hint isEqualToString:
@"oneTimeCode"]) {
319 return UITextContentTypeOneTimeCode;
322 if ([hint isEqualToString:
@"newPassword"]) {
323 return UITextContentTypeNewPassword;
391 typedef NS_ENUM(NSInteger, FlutterAutofillType) {
395 kFlutterAutofillTypeNone,
396 kFlutterAutofillTypeRegular,
397 kFlutterAutofillTypePassword,
407 if (isSecureTextEntry) {
414 if ([contentType isEqualToString:UITextContentTypePassword] ||
415 [contentType isEqualToString:UITextContentTypeUsername]) {
419 if ([contentType isEqualToString:UITextContentTypeNewPassword]) {
429 return kFlutterAutofillTypePassword;
434 return kFlutterAutofillTypePassword;
439 return !autofill || [contentType isEqualToString:
@""] ? kFlutterAutofillTypeNone
440 : kFlutterAutofillTypeRegular;
444 return fabsf(x - y) <= delta;
470 CGRect selectionRect,
471 BOOL selectionRectIsRTL,
472 BOOL useTrailingBoundaryOfSelectionRect,
473 CGRect otherSelectionRect,
474 BOOL otherSelectionRectIsRTL,
475 CGFloat verticalPrecision) {
477 if (CGRectContainsPoint(
479 selectionRect.origin.x + ((useTrailingBoundaryOfSelectionRect ^ selectionRectIsRTL)
480 ? 0.5 * selectionRect.size.width
482 selectionRect.origin.y, 0.5 * selectionRect.size.width, selectionRect.size.height),
487 CGPoint pointForSelectionRect = CGPointMake(
488 selectionRect.origin.x +
489 (selectionRectIsRTL ^ useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0),
490 selectionRect.origin.y + selectionRect.size.height * 0.5);
491 float yDist = fabs(pointForSelectionRect.y - point.y);
492 float xDist = fabs(pointForSelectionRect.x - point.x);
495 CGPoint pointForOtherSelectionRect = CGPointMake(
496 otherSelectionRect.origin.x + (otherSelectionRectIsRTL ? otherSelectionRect.size.width : 0),
497 otherSelectionRect.origin.y + otherSelectionRect.size.height * 0.5);
498 float yDistOther = fabs(pointForOtherSelectionRect.y - point.y);
499 float xDistOther = fabs(pointForOtherSelectionRect.x - point.x);
504 BOOL isCloserVertically = yDist < yDistOther - verticalPrecision;
506 BOOL isAboveBottomOfLine = point.y <= selectionRect.origin.y + selectionRect.size.height;
507 BOOL isCloserHorizontally = xDist < xDistOther;
508 BOOL isBelowBottomOfLine = point.y > selectionRect.origin.y + selectionRect.size.height;
511 if (selectionRectIsRTL) {
512 isFarther = selectionRect.origin.x < otherSelectionRect.origin.x;
514 isFarther = selectionRect.origin.x +
515 (useTrailingBoundaryOfSelectionRect ? selectionRect.size.width : 0) >
516 otherSelectionRect.origin.x;
518 return (isCloserVertically ||
519 (isEqualVertically &&
520 ((isAboveBottomOfLine && isCloserHorizontally) || (isBelowBottomOfLine && isFarther))));
523 #pragma mark - FlutterTextPosition
527 + (instancetype)positionWithIndex:(NSUInteger)index {
528 return [[
FlutterTextPosition alloc] initWithIndex:index affinity:UITextStorageDirectionForward];
531 + (instancetype)positionWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
535 - (instancetype)initWithIndex:(NSUInteger)index affinity:(UITextStorageDirection)affinity {
546 #pragma mark - FlutterTextRange
550 + (instancetype)rangeWithNSRange:(NSRange)range {
554 - (instancetype)initWithNSRange:(NSRange)range {
562 - (UITextPosition*)start {
564 affinity:UITextStorageDirectionForward];
567 - (UITextPosition*)end {
569 affinity:UITextStorageDirectionBackward];
573 return self.range.length == 0;
576 - (id)copyWithZone:(NSZone*)zone {
581 return NSEqualRanges(
self.
range, other.
range);
585 #pragma mark - FlutterTokenizer
595 - (instancetype)initWithTextInput:(UIResponder<UITextInput>*)textInput {
597 @"The FlutterTokenizer can only be used in a FlutterTextInputView");
598 self = [
super initWithTextInput:textInput];
605 - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position
606 withGranularity:(UITextGranularity)granularity
607 inDirection:(UITextDirection)direction {
609 switch (granularity) {
610 case UITextGranularityLine:
613 result = [
self lineEnclosingPosition:position inDirection:direction];
615 case UITextGranularityCharacter:
616 case UITextGranularityWord:
617 case UITextGranularitySentence:
618 case UITextGranularityParagraph:
619 case UITextGranularityDocument:
621 result = [
super rangeEnclosingPosition:position
622 withGranularity:granularity
623 inDirection:direction];
629 - (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
630 inDirection:(UITextDirection)direction {
632 if (@available(iOS 17.0, *)) {
637 if (flutterPosition.
index > _textInputView.text.length ||
638 (flutterPosition.
index == _textInputView.text.length &&
639 direction == UITextStorageDirectionForward)) {
645 NSString* textAfter = [_textInputView
646 textInRange:[_textInputView textRangeFromPosition:position
647 toPosition:[_textInputView endOfDocument]]];
648 NSArray<NSString*>* linesAfter = [textAfter componentsSeparatedByString:@"\n"];
649 NSInteger offSetToLineBreak = [linesAfter firstObject].length;
650 UITextPosition* lineBreakAfter = [_textInputView positionFromPosition:position
651 offset:offSetToLineBreak];
653 NSString* textBefore = [_textInputView
654 textInRange:[_textInputView textRangeFromPosition:[_textInputView beginningOfDocument]
655 toPosition:position]];
656 NSArray<NSString*>* linesBefore = [textBefore componentsSeparatedByString:@"\n"];
657 NSInteger offSetFromLineBreak = [linesBefore lastObject].length;
658 UITextPosition* lineBreakBefore = [_textInputView positionFromPosition:position
659 offset:-offSetFromLineBreak];
661 return [_textInputView textRangeFromPosition:lineBreakBefore toPosition:lineBreakAfter];
666 #pragma mark - FlutterTextSelectionRect
671 @synthesize rect = _rect;
677 + (instancetype)selectionRectWithRectAndInfo:(CGRect)rect
678 position:(NSUInteger)position
679 writingDirection:(NSWritingDirection)writingDirection
680 containsStart:(BOOL)containsStart
681 containsEnd:(BOOL)containsEnd
682 isVertical:(BOOL)isVertical {
685 writingDirection:writingDirection
686 containsStart:containsStart
687 containsEnd:containsEnd
688 isVertical:isVertical];
691 + (instancetype)selectionRectWithRect:(CGRect)rect position:(NSUInteger)position {
694 writingDirection:NSWritingDirectionNatural
700 + (instancetype)selectionRectWithRect:(CGRect)rect
701 position:(NSUInteger)position
702 writingDirection:(NSWritingDirection)writingDirection {
705 writingDirection:writingDirection
711 - (instancetype)initWithRectAndInfo:(CGRect)rect
712 position:(NSUInteger)position
713 writingDirection:(NSWritingDirection)writingDirection
714 containsStart:(BOOL)containsStart
715 containsEnd:(BOOL)containsEnd
716 isVertical:(BOOL)isVertical {
730 return _writingDirection == NSWritingDirectionRightToLeft;
735 #pragma mark - FlutterTextPlaceholder
739 - (NSArray<UITextSelectionRect*>*)rects {
755 @property(nonatomic, retain, readonly) UITextField*
textField;
759 UITextField* _textField;
764 _textField = [[UITextField alloc] init];
769 - (BOOL)isKindOfClass:(Class)aClass {
770 return [
super isKindOfClass:aClass] || (aClass == [UITextField class]);
773 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)aSelector {
774 NSMethodSignature* signature = [
super methodSignatureForSelector:aSelector];
776 signature = [
self.textField methodSignatureForSelector:aSelector];
781 - (void)forwardInvocation:(NSInvocation*)anInvocation {
782 [anInvocation invokeWithTarget:self.textField];
788 @property(nonatomic, readonly, weak) id<FlutterTextInputDelegate> textInputDelegate;
789 @property(nonatomic, readonly) UIView* hostView;
794 @property(nonatomic, copy) NSString* autofillId;
795 @property(nonatomic, readonly) CATransform3D editableTransform;
796 @property(nonatomic, assign) CGRect markedRect;
798 @property(nonatomic, assign) BOOL preventCursorDismissWhenResignFirstResponder;
799 @property(nonatomic) BOOL isVisibleToAutofill;
800 @property(nonatomic, assign) BOOL accessibilityEnabled;
801 @property(nonatomic, assign)
int textInputClient;
805 @property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
806 @property(nonatomic, assign) CGRect editMenuTargetRect;
807 @property(nonatomic, strong) NSArray<NSDictionary*>* editMenuItems;
809 - (void)setEditableTransform:(NSArray*)matrix;
813 int _textInputClient;
830 @synthesize tokenizer = _tokenizer;
833 self = [
super initWithFrame:CGRectZero];
836 _textInputClient = 0;
838 _preventCursorDismissWhenResignFirstResponder = NO;
841 _text = [[NSMutableString alloc] init];
846 _pendingDeltas = [[NSMutableArray alloc] init];
849 _editableTransform = CATransform3D();
852 _autocapitalizationType = UITextAutocapitalizationTypeSentences;
853 _autocorrectionType = UITextAutocorrectionTypeDefault;
854 _spellCheckingType = UITextSpellCheckingTypeDefault;
855 _enablesReturnKeyAutomatically = NO;
856 _keyboardAppearance = UIKeyboardAppearanceDefault;
857 _keyboardType = UIKeyboardTypeDefault;
858 _returnKeyType = UIReturnKeyDone;
859 _secureTextEntry = NO;
860 _enableDeltaModel = NO;
862 _accessibilityEnabled = NO;
863 _smartQuotesType = UITextSmartQuotesTypeYes;
864 _smartDashesType = UITextSmartDashesTypeYes;
865 _selectionRects = [[NSArray alloc] init];
867 if (@available(iOS 14.0, *)) {
868 UIScribbleInteraction* interaction = [[UIScribbleInteraction alloc] initWithDelegate:self];
869 [
self addInteraction:interaction];
873 if (@available(iOS 16.0, *)) {
874 _editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
875 [
self addInteraction:_editMenuInteraction];
881 - (void)handleSearchWebAction {
882 [
self.textInputDelegate flutterTextInputView:self
883 searchWebWithSelectedText:[
self textInRange:_selectedTextRange]];
886 - (void)handleLookUpAction {
887 [
self.textInputDelegate flutterTextInputView:self
888 lookUpSelectedText:[
self textInRange:_selectedTextRange]];
891 - (void)handleShareAction {
892 [
self.textInputDelegate flutterTextInputView:self
893 shareSelectedText:[
self textInRange:_selectedTextRange]];
897 - (UICommand*)searchCommandWithSelector:(
SEL)selector
898 element:(UIMenuElement*)element API_AVAILABLE(ios(16.0)) {
899 if ([element isKindOfClass:UICommand.class]) {
900 UICommand* command = (UICommand*)element;
901 return command.action == selector ? command : nil;
902 }
else if ([element isKindOfClass:UIMenu.class]) {
903 NSArray<UIMenuElement*>* children = ((UIMenu*)element).children;
904 for (UIMenuElement* child in children) {
905 UICommand* result = [
self searchCommandWithSelector:selector element:child];
916 - (void)addBasicEditingCommandToItems:(NSMutableArray*)items
918 selector:(
SEL)selector
919 suggestedMenu:(UIMenu*)suggestedMenu {
920 UICommand* command = [
self searchCommandWithSelector:selector element:suggestedMenu];
922 [items addObject:command];
924 NSString* errorMessage =
925 [NSString stringWithFormat:@"Cannot find context menu item of type \"%@\".", type];
926 [FlutterLogger logError:errorMessage];
930 - (void)addAdditionalBasicCommandToItems:(NSMutableArray*)items
932 selector:(
SEL)selector
933 encodedItem:(NSDictionary<NSString*,
id>*)encodedItem {
934 NSString* title = encodedItem[@"title"];
936 UICommand* command = [UICommand commandWithTitle:title
940 [items addObject:command];
942 NSString* errorMessage =
943 [NSString stringWithFormat:@"Missing title for context menu item of type \"%@\".", type];
944 [FlutterLogger logError:errorMessage];
948 - (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
949 menuForConfiguration:(UIEditMenuConfiguration*)configuration
950 suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
951 UIMenu* suggestedMenu = [UIMenu menuWithChildren:suggestedActions];
952 if (!_editMenuItems) {
953 return suggestedMenu;
956 NSMutableArray* items = [NSMutableArray array];
957 for (NSDictionary<NSString*, id>* encodedItem in _editMenuItems) {
958 NSString* type = encodedItem[@"type"];
959 if ([type isEqualToString:
@"copy"]) {
960 [
self addBasicEditingCommandToItems:items
962 selector:@selector(copy:)
963 suggestedMenu:suggestedMenu];
964 }
else if ([type isEqualToString:
@"paste"]) {
965 [
self addBasicEditingCommandToItems:items
967 selector:@selector(paste:)
968 suggestedMenu:suggestedMenu];
969 }
else if ([type isEqualToString:
@"cut"]) {
970 [
self addBasicEditingCommandToItems:items
972 selector:@selector(cut:)
973 suggestedMenu:suggestedMenu];
974 }
else if ([type isEqualToString:
@"delete"]) {
975 [
self addBasicEditingCommandToItems:items
977 selector:@selector(delete:)
978 suggestedMenu:suggestedMenu];
979 }
else if ([type isEqualToString:
@"selectAll"]) {
980 [
self addBasicEditingCommandToItems:items
982 selector:@selector(selectAll:)
983 suggestedMenu:suggestedMenu];
984 }
else if ([type isEqualToString:
@"searchWeb"]) {
985 [
self addAdditionalBasicCommandToItems:items
987 selector:@selector(handleSearchWebAction)
988 encodedItem:encodedItem];
989 }
else if ([type isEqualToString:
@"share"]) {
990 [
self addAdditionalBasicCommandToItems:items
992 selector:@selector(handleShareAction)
993 encodedItem:encodedItem];
994 }
else if ([type isEqualToString:
@"lookUp"]) {
995 [
self addAdditionalBasicCommandToItems:items
997 selector:@selector(handleLookUpAction)
998 encodedItem:encodedItem];
999 }
else if ([type isEqualToString:
@"captureTextFromCamera"]) {
1000 if (@available(iOS 15.0, *)) {
1001 [
self addBasicEditingCommandToItems:items
1003 selector:@selector(captureTextFromCamera:)
1004 suggestedMenu:suggestedMenu];
1008 return [UIMenu menuWithChildren:items];
1011 - (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
1012 willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
1013 animator:(
id<UIEditMenuInteractionAnimating>)animator
1014 API_AVAILABLE(ios(16.0)) {
1015 [
self.textInputDelegate flutterTextInputView:self
1016 willDismissEditMenuWithTextInputClient:_textInputClient];
1019 - (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
1020 targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
1021 return _editMenuTargetRect;
1024 - (void)showEditMenuWithTargetRect:(CGRect)targetRect
1025 items:(NSArray<NSDictionary*>*)items API_AVAILABLE(ios(16.0)) {
1026 _editMenuTargetRect = targetRect;
1027 _editMenuItems = items;
1028 UIEditMenuConfiguration* config =
1029 [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
1030 [
self.editMenuInteraction presentEditMenuWithConfiguration:config];
1034 [
self.editMenuInteraction dismissMenu];
1037 - (void)configureWithDictionary:(NSDictionary*)configuration {
1038 NSDictionary* inputType = configuration[kKeyboardType];
1040 NSDictionary* autofill = configuration[kAutofillProperties];
1042 self.secureTextEntry = [configuration[kSecureTextEntry] boolValue];
1043 self.enableDeltaModel = [configuration[kEnableDeltaModel] boolValue];
1050 NSString* smartDashesType = configuration[kSmartDashesType];
1052 bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
1053 self.smartDashesType = smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
1054 NSString* smartQuotesType = configuration[kSmartQuotesType];
1056 bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
1057 self.smartQuotesType = smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
1059 self.keyboardAppearance = UIKeyboardAppearanceDark;
1061 self.keyboardAppearance = UIKeyboardAppearanceLight;
1063 self.keyboardAppearance = UIKeyboardAppearanceDefault;
1065 NSString* autocorrect = configuration[kAutocorrectionType];
1066 bool autocorrectIsDisabled = autocorrect && ![autocorrect boolValue];
1067 self.autocorrectionType =
1068 autocorrectIsDisabled ? UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault;
1069 self.spellCheckingType =
1070 autocorrectIsDisabled ? UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault;
1072 if (autofill == nil) {
1073 self.textContentType =
@"";
1076 [
self setTextInputState:autofill[kAutofillEditingValue]];
1077 NSAssert(_autofillId,
@"The autofill configuration must contain an autofill id");
1081 self.isVisibleToAutofill = autofill || _secureTextEntry;
1084 - (UITextContentType)textContentType {
1085 return _textContentType;
1098 - (UIColor*)insertionPointColor {
1099 return [UIColor clearColor];
1102 - (UIColor*)selectionBarColor {
1103 return [UIColor clearColor];
1106 - (UIColor*)selectionHighlightColor {
1107 return [UIColor clearColor];
1110 - (UIInputViewController*)inputViewController {
1122 return _textInputPlugin.textInputDelegate;
1125 - (BOOL)respondsToSelector:(
SEL)selector {
1126 if (@available(iOS 17.0, *)) {
1128 if (selector ==
@selector(insertionPointColor)) {
1132 return [
super respondsToSelector:selector];
1135 - (void)setTextInputClient:(
int)client {
1136 _textInputClient = client;
1140 - (UITextInteraction*)textInteraction
API_AVAILABLE(ios(13.0)) {
1141 if (!_textInteraction) {
1142 _textInteraction = [UITextInteraction textInteractionForMode:UITextInteractionModeEditable];
1143 _textInteraction.textInput =
self;
1145 return _textInteraction;
1148 - (void)setTextInputState:(NSDictionary*)state {
1155 [
self addInteraction:self.textInteraction];
1158 NSString* newText = state[@"text"];
1159 BOOL textChanged = ![
self.text isEqualToString:newText];
1161 [
self.inputDelegate textWillChange:self];
1162 [
self.text setString:newText];
1164 NSInteger composingBase = [state[@"composingBase"] intValue];
1165 NSInteger composingExtent = [state[@"composingExtent"] intValue];
1166 NSRange composingRange = [
self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
1167 ABS(composingBase - composingExtent))
1170 self.markedTextRange =
1173 NSRange selectedRange = [
self clampSelectionFromBase:[state[@"selectionBase"] intValue]
1174 extent:[state[@"selectionExtent"] intValue]
1177 NSRange oldSelectedRange = [(
FlutterTextRange*)
self.selectedTextRange range];
1178 if (!NSEqualRanges(selectedRange, oldSelectedRange)) {
1179 [
self.inputDelegate selectionWillChange:self];
1187 [
self.inputDelegate selectionDidChange:self];
1191 [
self.inputDelegate textDidChange:self];
1194 if (_textInteraction) {
1195 [
self removeInteraction:_textInteraction];
1200 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1201 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
1202 [
self resetScribbleInteractionStatusIfEnding];
1203 [
self.viewResponder touchesBegan:touches withEvent:event];
1206 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1207 [
self.viewResponder touchesMoved:touches withEvent:event];
1210 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1211 [
self.viewResponder touchesEnded:touches withEvent:event];
1214 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1215 [
self.viewResponder touchesCancelled:touches withEvent:event];
1218 - (void)touchesEstimatedPropertiesUpdated:(NSSet*)touches {
1219 [
self.viewResponder touchesEstimatedPropertiesUpdated:touches];
1229 - (NSRange)clampSelectionFromBase:(
int)selectionBase
1230 extent:(
int)selectionExtent
1231 forText:(NSString*)text {
1232 int loc = MIN(selectionBase, selectionExtent);
1233 int len = ABS(selectionExtent - selectionBase);
1234 return loc < 0 ? NSMakeRange(0, 0)
1235 : [
self clampSelection:NSMakeRange(loc, len) forText:
self.text];
1238 - (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
1239 NSUInteger start = MIN(MAX(range.location, 0), text.length);
1240 NSUInteger length = MIN(range.length, text.length - start);
1241 return NSMakeRange(start, length);
1244 - (BOOL)isVisibleToAutofill {
1245 return self.frame.size.width > 0 &&
self.frame.size.height > 0;
1253 - (void)setIsVisibleToAutofill:(BOOL)isVisibleToAutofill {
1256 self.frame = isVisibleToAutofill ? CGRectMake(0, 0, 1, 1) : CGRectZero;
1259 #pragma mark UIScribbleInteractionDelegate
1264 if (@available(iOS 14.0, *)) {
1265 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
1272 - (void)scribbleInteractionWillBeginWriting:(UIScribbleInteraction*)interaction
1273 API_AVAILABLE(ios(14.0)) {
1275 [
self.textInputDelegate flutterTextInputViewScribbleInteractionBegan:self];
1278 - (void)scribbleInteractionDidFinishWriting:(UIScribbleInteraction*)interaction
1279 API_AVAILABLE(ios(14.0)) {
1281 [
self.textInputDelegate flutterTextInputViewScribbleInteractionFinished:self];
1284 - (BOOL)scribbleInteraction:(UIScribbleInteraction*)interaction
1285 shouldBeginAtLocation:(CGPoint)location API_AVAILABLE(ios(14.0)) {
1289 - (BOOL)scribbleInteractionShouldDelayFocus:(UIScribbleInteraction*)interaction
1290 API_AVAILABLE(ios(14.0)) {
1294 #pragma mark - UIResponder Overrides
1296 - (BOOL)canBecomeFirstResponder {
1301 return _textInputClient != 0;
1304 - (BOOL)resignFirstResponder {
1305 BOOL success = [
super resignFirstResponder];
1307 if (!_preventCursorDismissWhenResignFirstResponder) {
1308 [
self.textInputDelegate flutterTextInputView:self
1309 didResignFirstResponderWithTextInputClient:_textInputClient];
1315 - (BOOL)canPerformAction:(
SEL)action withSender:(
id)sender {
1316 if (action ==
@selector(paste:)) {
1318 return [UIPasteboard generalPasteboard].hasStrings;
1319 }
else if (action ==
@selector(copy:) || action ==
@selector(cut:) ||
1320 action ==
@selector(
delete:)) {
1321 return [
self textInRange:_selectedTextRange].length > 0;
1322 }
else if (action ==
@selector(selectAll:)) {
1323 return self.hasText;
1324 }
else if (action ==
@selector(captureTextFromCamera:)) {
1325 if (@available(iOS 15.0, *)) {
1330 return [
super canPerformAction:action withSender:sender];
1333 #pragma mark - UIResponderStandardEditActions Overrides
1335 - (void)cut:(
id)sender {
1336 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1337 [
self replaceRange:_selectedTextRange withText:@""];
1340 - (void)copy:(
id)sender {
1341 [UIPasteboard generalPasteboard].string = [
self textInRange:_selectedTextRange];
1344 - (void)paste:(
id)sender {
1345 NSString* pasteboardString = [UIPasteboard generalPasteboard].string;
1346 if (pasteboardString != nil) {
1347 [
self insertText:pasteboardString];
1351 - (void)delete:(
id)sender {
1352 [
self replaceRange:_selectedTextRange withText:@""];
1355 - (void)selectAll:(
id)sender {
1356 [
self setSelectedTextRange:[
self textRangeFromPosition:[
self beginningOfDocument]
1357 toPosition:[
self endOfDocument]]];
1360 #pragma mark - UITextInput Overrides
1362 - (id<UITextInputTokenizer>)tokenizer {
1363 if (_tokenizer == nil) {
1370 return [_selectedTextRange copy];
1374 - (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
1379 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
1386 - (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
1391 [
self setSelectedTextRangeLocal:selectedTextRange];
1393 if (_enableDeltaModel) {
1394 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1396 [
self updateEditingState];
1400 _scribbleFocusStatus == FlutterScribbleFocusStatusFocused) {
1404 if (flutterTextRange.
range.length > 0) {
1405 [
self.textInputDelegate flutterTextInputView:self showToolbar:_textInputClient];
1409 [
self resetScribbleInteractionStatusIfEnding];
1412 - (id)insertDictationResultPlaceholder {
1416 - (void)removeDictationResultPlaceholder:(
id)placeholder willInsertResult:(BOOL)willInsertResult {
1419 - (NSString*)textInRange:(UITextRange*)range {
1424 @"Expected a FlutterTextRange for range (got %@).", [range
class]);
1426 if (textRange.location == NSNotFound) {
1435 NSUInteger location = MIN(textRange.location,
self.text.length);
1436 NSUInteger length = MIN(
self.text.length - location, textRange.length);
1437 NSRange safeRange = NSMakeRange(location, length);
1438 return [
self.text substringWithRange:safeRange];
1443 - (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
1444 [
self.text replaceCharactersInRange:[
self clampSelection:range forText:self.text]
1450 const NSRange newSelectionRange =
1451 [
self clampSelection:NSMakeRange(range.location + text.length, 0) forText:self.text];
1454 self.markedTextRange = nil;
1457 - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
1458 NSString* textBeforeChange = [
self.text copy];
1460 [
self replaceRangeLocal:replaceRange withText:text];
1461 if (_enableDeltaModel) {
1462 NSRange nextReplaceRange = [
self clampSelection:replaceRange forText:textBeforeChange];
1463 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1464 [textBeforeChange UTF8String],
1466 nextReplaceRange.location,
1467 nextReplaceRange.location + nextReplaceRange.length),
1468 [text UTF8String])];
1470 [
self updateEditingState];
1474 - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
1477 self.temporarilyDeletedComposedCharacter = nil;
1479 if (
self.
returnKeyType == UIReturnKeyDefault && [text isEqualToString:
@"\n"]) {
1480 [
self.textInputDelegate flutterTextInputView:self
1481 performAction:FlutterTextInputActionNewline
1482 withClient:_textInputClient];
1486 if ([text isEqualToString:
@"\n"]) {
1487 FlutterTextInputAction action;
1489 case UIReturnKeyDefault:
1490 action = FlutterTextInputActionUnspecified;
1492 case UIReturnKeyDone:
1493 action = FlutterTextInputActionDone;
1496 action = FlutterTextInputActionGo;
1498 case UIReturnKeySend:
1499 action = FlutterTextInputActionSend;
1501 case UIReturnKeySearch:
1502 case UIReturnKeyGoogle:
1503 case UIReturnKeyYahoo:
1504 action = FlutterTextInputActionSearch;
1506 case UIReturnKeyNext:
1507 action = FlutterTextInputActionNext;
1509 case UIReturnKeyContinue:
1510 action = FlutterTextInputActionContinue;
1512 case UIReturnKeyJoin:
1513 action = FlutterTextInputActionJoin;
1515 case UIReturnKeyRoute:
1516 action = FlutterTextInputActionRoute;
1518 case UIReturnKeyEmergencyCall:
1519 action = FlutterTextInputActionEmergencyCall;
1523 [
self.textInputDelegate flutterTextInputView:self
1524 performAction:action
1525 withClient:_textInputClient];
1534 - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
1535 NSString* textBeforeChange = [
self.text copy];
1538 _scribbleFocusStatus != FlutterScribbleFocusStatusUnfocused) {
1542 if (markedText == nil) {
1547 const NSRange& actualReplacedRange = currentMarkedTextRange && !currentMarkedTextRange.isEmpty
1548 ? currentMarkedTextRange.
range
1552 [
self.text replaceCharactersInRange:actualReplacedRange withString:markedText];
1554 const NSRange newMarkedRange = NSMakeRange(actualReplacedRange.location, markedText.length);
1555 self.markedTextRange =
1558 [
self setSelectedTextRangeLocal:
1560 rangeWithNSRange:[
self clampSelection:NSMakeRange(markedSelectedRange.location +
1561 newMarkedRange.location,
1562 markedSelectedRange.length)
1563 forText:self.text]]];
1564 if (_enableDeltaModel) {
1565 NSRange nextReplaceRange = [
self clampSelection:actualReplacedRange forText:textBeforeChange];
1566 [
self updateEditingStateWithDelta:flutter::TextEditingDelta(
1567 [textBeforeChange UTF8String],
1569 nextReplaceRange.location,
1570 nextReplaceRange.location + nextReplaceRange.length),
1571 [markedText UTF8String])];
1573 [
self updateEditingState];
1577 - (void)unmarkText {
1581 self.markedTextRange = nil;
1582 if (_enableDeltaModel) {
1583 [
self updateEditingStateWithDelta:flutter::TextEditingDelta([
self.text UTF8String])];
1585 [
self updateEditingState];
1589 - (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
1590 toPosition:(UITextPosition*)toPosition {
1593 if (toIndex >= fromIndex) {
1606 - (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
1607 return fml::RangeForCharacterAtIndex(
self.text, MAX(0, position - 1)).location;
1610 - (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
1611 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, position);
1612 return MIN(position + charRange.length,
self.text.length);
1615 - (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
1618 NSInteger newLocation = (NSInteger)offsetPosition + offset;
1619 if (newLocation < 0 || newLocation > (NSInteger)
self.text.length) {
1628 for (NSInteger i = 0; i < offset && offsetPosition <
self.text.length; ++i) {
1629 offsetPosition = [
self incrementOffsetPosition:offsetPosition];
1632 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) {
1633 offsetPosition = [
self decrementOffsetPosition:offsetPosition];
1639 - (UITextPosition*)positionFromPosition:(UITextPosition*)position
1640 inDirection:(UITextLayoutDirection)direction
1641 offset:(NSInteger)offset {
1643 switch (direction) {
1644 case UITextLayoutDirectionLeft:
1645 case UITextLayoutDirectionUp:
1646 return [
self positionFromPosition:position offset:offset * -1];
1647 case UITextLayoutDirectionRight:
1648 case UITextLayoutDirectionDown:
1649 return [
self positionFromPosition:position offset:1];
1653 - (UITextPosition*)beginningOfDocument {
1657 - (UITextPosition*)endOfDocument {
1659 affinity:UITextStorageDirectionBackward];
1662 - (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
1665 if (positionIndex < otherIndex) {
1666 return NSOrderedAscending;
1668 if (positionIndex > otherIndex) {
1669 return NSOrderedDescending;
1673 if (positionAffinity == otherAffinity) {
1674 return NSOrderedSame;
1676 if (positionAffinity == UITextStorageDirectionBackward) {
1678 return NSOrderedAscending;
1681 return NSOrderedDescending;
1684 - (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
1688 - (UITextPosition*)positionWithinRange:(UITextRange*)range
1689 farthestInDirection:(UITextLayoutDirection)direction {
1691 UITextStorageDirection affinity;
1692 switch (direction) {
1693 case UITextLayoutDirectionLeft:
1694 case UITextLayoutDirectionUp:
1696 affinity = UITextStorageDirectionForward;
1698 case UITextLayoutDirectionRight:
1699 case UITextLayoutDirectionDown:
1701 affinity = UITextStorageDirectionBackward;
1707 - (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
1708 inDirection:(UITextLayoutDirection)direction {
1710 NSUInteger startIndex;
1711 NSUInteger endIndex;
1712 switch (direction) {
1713 case UITextLayoutDirectionLeft:
1714 case UITextLayoutDirectionUp:
1715 startIndex = [
self decrementOffsetPosition:positionIndex];
1716 endIndex = positionIndex;
1718 case UITextLayoutDirectionRight:
1719 case UITextLayoutDirectionDown:
1720 startIndex = positionIndex;
1721 endIndex = [
self incrementOffsetPosition:positionIndex];
1727 #pragma mark - UITextInput text direction handling
1729 - (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
1730 inDirection:(UITextStorageDirection)direction {
1732 return UITextWritingDirectionNatural;
1735 - (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
1736 forRange:(UITextRange*)range {
1740 #pragma mark - UITextInput cursor, selection rect handling
1742 - (void)setMarkedRect:(CGRect)markedRect {
1743 _markedRect = markedRect;
1750 - (void)setEditableTransform:(NSArray*)matrix {
1751 CATransform3D* transform = &_editableTransform;
1753 transform->m11 = [matrix[0] doubleValue];
1754 transform->m12 = [matrix[1] doubleValue];
1755 transform->m13 = [matrix[2] doubleValue];
1756 transform->m14 = [matrix[3] doubleValue];
1758 transform->m21 = [matrix[4] doubleValue];
1759 transform->m22 = [matrix[5] doubleValue];
1760 transform->m23 = [matrix[6] doubleValue];
1761 transform->m24 = [matrix[7] doubleValue];
1763 transform->m31 = [matrix[8] doubleValue];
1764 transform->m32 = [matrix[9] doubleValue];
1765 transform->m33 = [matrix[10] doubleValue];
1766 transform->m34 = [matrix[11] doubleValue];
1768 transform->m41 = [matrix[12] doubleValue];
1769 transform->m42 = [matrix[13] doubleValue];
1770 transform->m43 = [matrix[14] doubleValue];
1771 transform->m44 = [matrix[15] doubleValue];
1780 CGPoint points[] = {
1781 incomingRect.origin,
1782 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
1783 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
1784 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
1785 incomingRect.origin.y + incomingRect.size.height)};
1787 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
1788 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
1790 for (
int i = 0; i < 4; i++) {
1791 const CGPoint point = points[i];
1793 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
1794 _editableTransform.m41;
1795 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
1796 _editableTransform.m42;
1798 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
1799 _editableTransform.m44;
1803 }
else if (w != 1.0) {
1808 origin.x = MIN(origin.x, x);
1809 origin.y = MIN(origin.y, y);
1810 farthest.x = MAX(farthest.x, x);
1811 farthest.y = MAX(farthest.y, y);
1813 return CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y);
1822 - (CGRect)firstRectForRange:(UITextRange*)range {
1824 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
1826 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
1829 if (_markedTextRange != nil) {
1840 CGRect rect = _markedRect;
1841 if (CGRectIsEmpty(rect)) {
1842 rect = CGRectInset(rect, -0.1, 0);
1847 UIView* hostView = _textInputPlugin.hostView;
1848 NSAssert(hostView == nil || [
self isDescendantOfView:hostView],
@"%@ is not a descendant of %@",
1850 return hostView ? [hostView convertRect:_cachedFirstRect toView:self] :
_cachedFirstRect;
1854 _scribbleFocusStatus == FlutterScribbleFocusStatusUnfocused) {
1855 if (@available(iOS 17.0, *)) {
1865 [
self.textInputDelegate flutterTextInputView:self
1866 showAutocorrectionPromptRectForStart:start
1868 withClient:_textInputClient];
1876 if (@available(iOS 17, *)) {
1882 NSUInteger first = start;
1887 CGRect startSelectionRect = CGRectNull;
1888 CGRect endSelectionRect = CGRectNull;
1891 CGFloat minY = CGFLOAT_MAX;
1892 CGFloat maxY = CGFLOAT_MIN;
1895 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
1896 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
1897 BOOL startsOnOrBeforeStartOfRange = _selectionRects[i].position <= first;
1898 BOOL isLastSelectionRect = i + 1 == [_selectionRects count];
1899 BOOL endOfTextIsAfterStartOfRange = isLastSelectionRect && textRange.
range.length > first;
1900 BOOL nextSelectionRectIsAfterStartOfRange =
1901 !isLastSelectionRect && _selectionRects[i + 1].position > first;
1902 if (startsOnOrBeforeStartOfRange &&
1903 (endOfTextIsAfterStartOfRange || nextSelectionRectIsAfterStartOfRange)) {
1905 if (@available(iOS 17, *)) {
1906 startSelectionRect = _selectionRects[i].rect;
1908 return _selectionRects[i].rect;
1911 if (!CGRectIsNull(startSelectionRect)) {
1912 minY = fmin(minY, CGRectGetMinY(_selectionRects[i].rect));
1913 maxY = fmax(maxY, CGRectGetMaxY(_selectionRects[i].rect));
1914 BOOL endsOnOrAfterEndOfRange = _selectionRects[i].position >= end - 1;
1915 BOOL nextSelectionRectIsOnNextLine =
1916 !isLastSelectionRect &&
1921 CGRectGetMidY(_selectionRects[i + 1].rect) > CGRectGetMaxY(_selectionRects[i].rect);
1922 if (endsOnOrAfterEndOfRange || isLastSelectionRect || nextSelectionRectIsOnNextLine) {
1923 endSelectionRect = _selectionRects[i].rect;
1928 if (CGRectIsNull(startSelectionRect) || CGRectIsNull(endSelectionRect)) {
1932 CGFloat minX = fmin(CGRectGetMinX(startSelectionRect), CGRectGetMinX(endSelectionRect));
1933 CGFloat maxX = fmax(CGRectGetMaxX(startSelectionRect), CGRectGetMaxX(endSelectionRect));
1934 return CGRectMake(minX, minY, maxX - minX, maxY - minY);
1942 NSArray<UITextSelectionRect*>* rects = [
self
1944 rangeWithNSRange:fml::RangeForCharactersInRange(
1948 (index >= (NSInteger)self.text.length)
1951 if (rects.count == 0) {
1957 CGRect characterAfterCaret = rects[0].rect;
1962 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1963 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1965 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1966 characterAfterCaret.size.height);
1968 }
else if (rects.count == 2 && affinity == UITextStorageDirectionForward) {
1971 CGRect characterAfterCaret = rects[1].rect;
1976 return CGRectMake(characterAfterCaret.origin.x + characterAfterCaret.size.width,
1977 characterAfterCaret.origin.y, 0, characterAfterCaret.size.height);
1979 return CGRectMake(characterAfterCaret.origin.x, characterAfterCaret.origin.y, 0,
1980 characterAfterCaret.size.height);
1989 CGRect characterBeforeCaret = rects[0].rect;
1992 return CGRectMake(characterBeforeCaret.origin.x, characterBeforeCaret.origin.y, 0,
1993 characterBeforeCaret.size.height);
1995 return CGRectMake(characterBeforeCaret.origin.x + characterBeforeCaret.size.width,
1996 characterBeforeCaret.origin.y, 0, characterBeforeCaret.size.height);
2000 - (UITextPosition*)closestPositionToPoint:(CGPoint)point {
2001 if ([_selectionRects count] == 0) {
2003 @"Expected a FlutterTextPosition for position (got %@).",
2006 UITextStorageDirection currentAffinity =
2012 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, NSMakeRange(0, self.text.length))];
2013 return [
self closestPositionToPoint:point withinRange:range];
2016 - (NSArray*)selectionRectsForRange:(UITextRange*)range {
2024 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
2026 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
2029 NSMutableArray* rects = [[NSMutableArray alloc] init];
2030 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2031 if (_selectionRects[i].position >= start &&
2032 (_selectionRects[i].position < end ||
2033 (start == end && _selectionRects[i].position <= end))) {
2034 float width = _selectionRects[i].rect.size.width;
2038 CGRect rect = CGRectMake(_selectionRects[i].rect.origin.x, _selectionRects[i].rect.origin.y,
2039 width, _selectionRects[i].rect.size.height);
2042 position:_selectionRects[i].position
2046 self.text, NSMakeRange(0, self.text.length))
2049 [rects addObject:selectionRect];
2055 - (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
2057 @"Expected a FlutterTextPosition for range.start (got %@).", [range.start
class]);
2059 @"Expected a FlutterTextPosition for range.end (got %@).", [range.end
class]);
2069 NSUInteger _closestRectIndex = 0;
2070 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2071 NSUInteger position = _selectionRects[i].position;
2072 if (position >= start && position <= end) {
2075 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2076 NO, _selectionRects[_closestRectIndex].rect,
2077 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2079 _closestRectIndex = i;
2086 affinity:UITextStorageDirectionForward];
2092 for (NSUInteger i = MAX(0, _closestRectIndex - 1);
2093 i < MIN(_closestRectIndex + 2, [_selectionRects count]); i++) {
2094 NSUInteger position = _selectionRects[i].position + 1;
2095 if (position >= start && position <= end) {
2097 point, _selectionRects[i].rect, _selectionRects[i].isRTL,
2098 YES, _selectionRects[_closestRectIndex].rect,
2099 _selectionRects[_closestRectIndex].isRTL, verticalPrecision)) {
2102 affinity:UITextStorageDirectionBackward];
2107 return closestPosition;
2110 - (UITextRange*)characterRangeAtPoint:(CGPoint)point {
2113 return [
FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
2144 - (void)beginFloatingCursorAtPoint:(CGPoint)point {
2161 [
self.textInputDelegate flutterTextInputView:self
2162 updateFloatingCursor:FlutterFloatingCursorDragStateStart
2163 withClient:_textInputClient
2164 withPosition:@{@"X" : @0, @"Y" : @0}];
2167 - (void)updateFloatingCursorAtPoint:(CGPoint)point {
2168 [
self.textInputDelegate flutterTextInputView:self
2169 updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
2170 withClient:_textInputClient
2172 @"X" : @(point.x - _floatingCursorOffset.x),
2173 @"Y" : @(point.y - _floatingCursorOffset.y)
2177 - (void)endFloatingCursor {
2179 [
self.textInputDelegate flutterTextInputView:self
2180 updateFloatingCursor:FlutterFloatingCursorDragStateEnd
2181 withClient:_textInputClient
2182 withPosition:@{@"X" : @0, @"Y" : @0}];
2185 #pragma mark - UIKeyInput Overrides
2187 - (void)updateEditingState {
2192 NSInteger composingBase = -1;
2193 NSInteger composingExtent = -1;
2198 NSDictionary* state = @{
2199 @"selectionBase" : @(selectionBase),
2200 @"selectionExtent" : @(selectionExtent),
2202 @"selectionIsDirectional" : @(
false),
2203 @"composingBase" : @(composingBase),
2204 @"composingExtent" : @(composingExtent),
2205 @"text" : [NSString stringWithString:self.text],
2208 if (_textInputClient == 0 && _autofillId != nil) {
2209 [
self.textInputDelegate flutterTextInputView:self
2210 updateEditingClient:_textInputClient
2212 withTag:_autofillId];
2214 [
self.textInputDelegate flutterTextInputView:self
2215 updateEditingClient:_textInputClient
2220 - (void)updateEditingStateWithDelta:(
flutter::TextEditingDelta)delta {
2225 NSInteger composingBase = -1;
2226 NSInteger composingExtent = -1;
2232 NSDictionary* deltaToFramework = @{
2233 @"oldText" : @(delta.old_text().c_str()),
2234 @"deltaText" : @(delta.delta_text().c_str()),
2235 @"deltaStart" : @(delta.delta_start()),
2236 @"deltaEnd" : @(delta.delta_end()),
2237 @"selectionBase" : @(selectionBase),
2238 @"selectionExtent" : @(selectionExtent),
2240 @"selectionIsDirectional" : @(
false),
2241 @"composingBase" : @(composingBase),
2242 @"composingExtent" : @(composingExtent),
2245 [_pendingDeltas addObject:deltaToFramework];
2247 if (_pendingDeltas.count == 1) {
2249 dispatch_async(dispatch_get_main_queue(), ^{
2251 if (strongSelf && strongSelf.pendingDeltas.count > 0) {
2252 NSDictionary* deltas = @{
2253 @"deltas" : strongSelf.pendingDeltas,
2256 [strongSelf.textInputDelegate flutterTextInputView:strongSelf
2257 updateEditingClient:strongSelf->_textInputClient
2259 [strongSelf.pendingDeltas removeAllObjects];
2266 return self.text.length > 0;
2269 - (void)insertText:(NSString*)text {
2270 if (
self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
2271 [text characterAtIndex:0] == [
self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
2275 text =
self.temporarilyDeletedComposedCharacter;
2276 self.temporarilyDeletedComposedCharacter = nil;
2279 NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
2280 [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
2282 @"Expected a FlutterTextPosition for position (got %@).",
2285 for (NSUInteger i = 0; i < [_selectionRects count]; i++) {
2286 NSUInteger rectPosition = _selectionRects[i].position;
2287 if (rectPosition == insertPosition) {
2288 for (NSUInteger j = 0; j <= text.length; j++) {
2295 if (rectPosition > insertPosition) {
2296 rectPosition = rectPosition + text.length;
2305 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2306 [
self resetScribbleInteractionStatusIfEnding];
2307 self.selectionRects = copiedRects;
2309 [
self replaceRange:_selectedTextRange withText:text];
2312 - (UITextPlaceholder*)insertTextPlaceholderWithSize:(CGSize)size API_AVAILABLE(ios(13.0)) {
2313 [
self.textInputDelegate flutterTextInputView:self
2314 insertTextPlaceholderWithSize:size
2315 withClient:_textInputClient];
2320 - (void)removeTextPlaceholder:(UITextPlaceholder*)textPlaceholder API_AVAILABLE(ios(13.0)) {
2322 [
self.textInputDelegate flutterTextInputView:self removeTextPlaceholder:_textInputClient];
2325 - (void)deleteBackward {
2327 _scribbleFocusStatus = FlutterScribbleFocusStatusUnfocused;
2328 [
self resetScribbleInteractionStatusIfEnding];
2345 if (oldRange.location > 0) {
2346 NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
2350 NSRange charRange = fml::RangeForCharacterAtIndex(
self.text, oldRange.location - 1);
2351 if (
IsEmoji(
self.text, charRange)) {
2352 newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
2364 NSString* deletedText = [
self.text substringWithRange:_selectedTextRange.range];
2365 NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
2366 self.temporarilyDeletedComposedCharacter =
2367 [deletedText substringWithRange:deleteFirstCharacterRange];
2369 [
self replaceRange:_selectedTextRange withText:@""];
2373 - (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(
id)target {
2374 UIAccessibilityPostNotification(notification, target);
2377 - (void)accessibilityElementDidBecomeFocused {
2378 if ([
self accessibilityElementIsFocused]) {
2382 FML_DCHECK(_backingTextInputAccessibilityObject);
2383 [
self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
2384 target:_backingTextInputAccessibilityObject];
2388 - (BOOL)accessibilityElementsHidden {
2389 return !_accessibilityEnabled;
2398 #pragma mark - Key Events Handling
2399 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2400 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2401 [_textInputPlugin.viewController pressesBegan:presses withEvent:event];
2404 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2405 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2406 [_textInputPlugin.viewController pressesChanged:presses withEvent:event];
2409 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2410 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2411 [_textInputPlugin.viewController pressesEnded:presses withEvent:event];
2414 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2415 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2416 [_textInputPlugin.viewController pressesCancelled:presses withEvent:event];
2445 - (BOOL)accessibilityElementsHidden {
2452 - (void)enableActiveViewAccessibility;
2469 - (void)enableActiveViewAccessibility {
2470 [
self.target enableActiveViewAccessibility];
2477 @property(nonatomic, readonly)
2478 NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
2483 @property(nonatomic, strong) UIView* keyboardViewContainer;
2484 @property(nonatomic, strong) UIView* keyboardView;
2485 @property(nonatomic, strong) UIView* cachedFirstResponder;
2486 @property(nonatomic, assign) CGRect keyboardRect;
2487 @property(nonatomic, assign) CGFloat previousPointerYPosition;
2488 @property(nonatomic, assign) CGFloat pointerYVelocity;
2492 NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
2496 self = [
super init];
2499 _textInputDelegate = textInputDelegate;
2500 _autofillContext = [[NSMutableDictionary alloc] init];
2502 _scribbleElements = [[NSMutableDictionary alloc] init];
2503 _keyboardViewContainer = [[UIView alloc] init];
2505 [[NSNotificationCenter defaultCenter] addObserver:self
2506 selector:@selector(handleKeyboardWillShow:)
2507 name:UIKeyboardWillShowNotification
2514 - (void)handleKeyboardWillShow:(NSNotification*)notification {
2515 NSDictionary* keyboardInfo = [notification userInfo];
2516 NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
2517 _keyboardRect = [keyboardFrameEnd CGRectValue];
2521 [
self hideTextInput];
2524 - (void)removeEnableFlutterTextInputViewAccessibilityTimer {
2525 if (_enableFlutterTextInputViewAccessibilityTimer) {
2526 [_enableFlutterTextInputViewAccessibilityTimer invalidate];
2527 _enableFlutterTextInputViewAccessibilityTimer = nil;
2531 - (UIView<UITextInput>*)textInputView {
2536 [
self hideTextInput];
2540 NSString* method = call.
method;
2543 [
self showTextInput];
2545 }
else if ([method isEqualToString:
kHideMethod]) {
2546 [
self hideTextInput];
2549 [
self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
2553 [
self setPlatformViewTextInputClient];
2556 [
self setTextInputEditingState:args];
2559 [
self clearTextInputClient];
2562 [
self setEditableSizeAndTransform:args];
2565 [
self updateMarkedRect:args];
2568 [
self triggerAutofillSave:[args boolValue]];
2574 [
self setSelectionRects:args];
2577 [
self setSelectionRects:args];
2580 [
self startLiveTextInput];
2583 [
self updateConfig:args];
2586 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2587 [
self handlePointerMove:pointerY];
2590 CGFloat pointerY = (CGFloat)[args[
@"pointerY"] doubleValue];
2591 [
self handlePointerUp:pointerY];
2598 - (void)handlePointerUp:(CGFloat)pointerY {
2599 if (_keyboardView.superview != nil) {
2602 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2603 CGFloat screenHeight = screen.bounds.size.height;
2604 CGFloat keyboardHeight = _keyboardRect.size.height;
2606 BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2607 [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2609 double keyboardDestination =
2610 shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2611 _keyboardViewContainer.frame = CGRectMake(
2612 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2613 _keyboardViewContainer.frame.size.height);
2615 completion:^(BOOL finished) {
2616 if (shouldDismissKeyboardBasedOnVelocity) {
2617 [
self.textInputDelegate flutterTextInputView:self.activeView
2618 didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2619 [
self dismissKeyboardScreenshot];
2621 [
self showKeyboardAndRemoveScreenshot];
2627 - (void)dismissKeyboardScreenshot {
2628 for (UIView* subView in _keyboardViewContainer.subviews) {
2629 [subView removeFromSuperview];
2633 - (void)showKeyboardAndRemoveScreenshot {
2634 [UIView setAnimationsEnabled:NO];
2635 [_cachedFirstResponder becomeFirstResponder];
2639 dispatch_get_main_queue(), ^{
2640 [UIView setAnimationsEnabled:YES];
2641 [
self dismissKeyboardScreenshot];
2645 - (void)handlePointerMove:(CGFloat)pointerY {
2647 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2648 CGFloat screenHeight = screen.bounds.size.height;
2649 CGFloat keyboardHeight = _keyboardRect.size.height;
2650 if (screenHeight - keyboardHeight <= pointerY) {
2652 if (_keyboardView.superview == nil) {
2654 [
self takeKeyboardScreenshotAndDisplay];
2655 [
self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
2657 [
self setKeyboardContainerHeight:pointerY];
2658 _pointerYVelocity = _previousPointerYPosition - pointerY;
2661 if (_keyboardView.superview != nil) {
2663 _keyboardViewContainer.frame = _keyboardRect;
2664 _pointerYVelocity = _previousPointerYPosition - pointerY;
2667 _previousPointerYPosition = pointerY;
2670 - (void)setKeyboardContainerHeight:(CGFloat)pointerY {
2671 CGRect frameRect = _keyboardRect;
2672 frameRect.origin.y = pointerY;
2673 _keyboardViewContainer.frame = frameRect;
2676 - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate {
2677 [UIView setAnimationsEnabled:NO];
2679 _cachedFirstResponder =
2681 ? flutterApplication.keyWindow.flutterFirstResponder
2682 :
self.viewController.flutterWindowSceneIfViewLoaded.keyWindow.flutterFirstResponder;
2684 _activeView.preventCursorDismissWhenResignFirstResponder = YES;
2685 [_cachedFirstResponder resignFirstResponder];
2686 _activeView.preventCursorDismissWhenResignFirstResponder = NO;
2687 [UIView setAnimationsEnabled:YES];
2690 - (void)takeKeyboardScreenshotAndDisplay {
2692 UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2693 UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES];
2694 keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect
2695 afterScreenUpdates:YES
2696 withCapInsets:UIEdgeInsetsZero];
2697 _keyboardView = keyboardSnap;
2698 [_keyboardViewContainer addSubview:_keyboardView];
2699 if (_keyboardViewContainer.superview == nil) {
2701 UIView* rootView = flutterApplication
2702 ? flutterApplication.delegate.window.rootViewController.view
2703 :
self.viewController.viewIfLoaded.window.rootViewController.view;
2704 [rootView addSubview:_keyboardViewContainer];
2706 _keyboardViewContainer.layer.zPosition = NSIntegerMax;
2707 _keyboardViewContainer.frame = _keyboardRect;
2710 - (BOOL)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2711 if (!
self.activeView.isFirstResponder) {
2714 NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2715 CGRect globalTargetRect = CGRectMake(
2716 [encodedTargetRect[
@"x"] doubleValue], [encodedTargetRect[
@"y"] doubleValue],
2717 [encodedTargetRect[
@"width"] doubleValue], [encodedTargetRect[
@"height"] doubleValue]);
2718 CGRect localTargetRect = [
self.hostView convertRect:globalTargetRect toView:self.activeView];
2719 [
self.activeView showEditMenuWithTargetRect:localTargetRect items:args[@"items"]];
2723 - (void)hideEditMenu {
2724 [
self.activeView hideEditMenu];
2727 - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2728 NSArray* transform = dictionary[@"transform"];
2729 [_activeView setEditableTransform:transform];
2730 const int leftIndex = 12;
2731 const int topIndex = 13;
2735 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2736 [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2738 CGRectMake(0, 0, [dictionary[
@"width"] intValue], [dictionary[
@"height"] intValue]);
2739 _activeView.tintColor = [UIColor clearColor];
2744 if (@available(iOS 17, *)) {
2752 CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2757 - (void)updateMarkedRect:(NSDictionary*)dictionary {
2758 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
2759 dictionary[
@"height"] != nil,
2760 @"Expected a dictionary representing a CGRect, got %@", dictionary);
2761 CGRect rect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
2762 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
2763 _activeView.markedRect = rect.size.width < 0 && rect.size.height < 0 ?
kInvalidFirstRect : rect;
2766 - (void)setSelectionRects:(NSArray*)encodedRects {
2767 NSMutableArray<FlutterTextSelectionRect*>* rectsAsRect =
2768 [[NSMutableArray alloc] initWithCapacity:[encodedRects count]];
2769 for (NSUInteger i = 0; i < [encodedRects count]; i++) {
2770 NSArray<NSNumber*>* encodedRect = encodedRects[i];
2772 selectionRectWithRect:CGRectMake([encodedRect[0] floatValue],
2773 [encodedRect[1] floatValue],
2774 [encodedRect[2] floatValue],
2775 [encodedRect[3] floatValue])
2776 position:[encodedRect[4] unsignedIntegerValue]
2777 writingDirection:[encodedRect[5] unsignedIntegerValue] == 1
2778 ? NSWritingDirectionLeftToRight
2779 : NSWritingDirectionRightToLeft]];
2785 _activeView.selectionRects = rectsAsRect;
2788 - (void)startLiveTextInput {
2789 if (@available(iOS 15.0, *)) {
2790 if (_activeView == nil || !_activeView.isFirstResponder) {
2793 [_activeView captureTextFromCamera:nil];
2797 - (void)showTextInput {
2798 _activeView.viewResponder = _viewResponder;
2799 [
self addToInputParentViewIfNeeded:_activeView];
2808 if (!_enableFlutterTextInputViewAccessibilityTimer) {
2809 _enableFlutterTextInputViewAccessibilityTimer =
2810 [NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
2812 selector:@selector(enableActiveViewAccessibility)
2816 [_activeView becomeFirstResponder];
2819 - (void)enableActiveViewAccessibility {
2820 if (_activeView.isFirstResponder) {
2821 _activeView.accessibilityEnabled = YES;
2823 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2826 - (void)hideTextInput {
2827 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2828 _activeView.accessibilityEnabled = NO;
2829 [_activeView resignFirstResponder];
2830 [_activeView removeFromSuperview];
2831 [_inputHider removeFromSuperview];
2834 - (void)triggerAutofillSave:(BOOL)saveEntries {
2835 [_activeView resignFirstResponder];
2840 [
self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO];
2841 [_autofillContext removeAllObjects];
2842 [
self changeInputViewsAutofillVisibility:YES];
2844 [_autofillContext removeAllObjects];
2847 [
self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO];
2848 [
self addToInputParentViewIfNeeded:_activeView];
2851 - (void)setPlatformViewTextInputClient {
2855 [
self removeEnableFlutterTextInputViewAccessibilityTimer];
2856 _activeView.accessibilityEnabled = NO;
2857 [_activeView removeFromSuperview];
2858 [_inputHider removeFromSuperview];
2861 - (void)setTextInputClient:(
int)client withConfiguration:(NSDictionary*)configuration {
2862 [
self resetAllClientIds];
2865 [
self changeInputViewsAutofillVisibility:NO];
2869 case kFlutterAutofillTypeNone:
2870 self.activeView = [
self createInputViewWith:configuration];
2872 case kFlutterAutofillTypeRegular:
2875 self.activeView = [
self updateAndShowAutofillViews:nil
2876 focusedField:configuration
2877 isPasswordRelated:NO];
2879 case kFlutterAutofillTypePassword:
2880 self.activeView = [
self updateAndShowAutofillViews:configuration[kAssociatedAutofillFields]
2881 focusedField:configuration
2882 isPasswordRelated:YES];
2885 [_activeView setTextInputClient:client];
2886 [_activeView reloadInputViews];
2898 [
self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES];
2909 [_autofillContext removeObjectForKey:autofillId];
2912 [newView configureWithDictionary:configuration];
2913 [
self addToInputParentViewIfNeeded:newView];
2917 if (autofillId &&
AutofillTypeOf(field) == kFlutterAutofillTypeNone) {
2918 [_autofillContext removeObjectForKey:autofillId];
2925 focusedField:(NSDictionary*)focusedField
2926 isPasswordRelated:(BOOL)isPassword {
2929 NSAssert(focusedId,
@"autofillId must not be null for the focused field: %@", focusedField);
2934 focused = [
self getOrCreateAutofillableView:focusedField isPasswordAutofill:isPassword];
2935 [_autofillContext removeObjectForKey:focusedId];
2938 for (NSDictionary* field in fields) {
2940 NSAssert(autofillId,
@"autofillId must not be null for field: %@", field);
2942 BOOL hasHints =
AutofillTypeOf(field) != kFlutterAutofillTypeNone;
2943 BOOL isFocused = [focusedId isEqualToString:autofillId];
2946 focused = [
self getOrCreateAutofillableView:field isPasswordAutofill:isPassword];
2951 _autofillContext[autofillId] = isFocused ? focused
2952 : [
self getOrCreateAutofillableView:field
2953 isPasswordAutofill:isPassword];
2956 [_autofillContext removeObjectForKey:autofillId];
2960 NSAssert(focused,
@"The current focused input view must not be nil.");
2970 isPasswordAutofill:(BOOL)needsPasswordAutofill {
2976 inputView = [inputView initWithOwner:self];
2977 [
self addToInputParentViewIfNeeded:inputView];
2980 [inputView configureWithDictionary:field];
2985 - (UIView*)hostView {
2986 UIView* host = _viewController.view;
2987 NSAssert(host !=
nullptr,
2988 @"The application must have a host view since the keyboard client "
2989 @"must be part of the responder chain to function. The host view controller is %@",
2995 - (NSArray<UIView*>*)textInputViews {
2996 return _inputHider.subviews;
3009 - (void)cleanUpViewHierarchy:(BOOL)includeActiveView
3010 clearText:(BOOL)clearText
3011 delayRemoval:(BOOL)delayRemoval {
3012 for (UIView* view in
self.textInputViews) {
3014 (includeActiveView || view != _activeView)) {
3016 if (_autofillContext[inputView.autofillId] != view) {
3018 [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""];
3021 [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1];
3023 [inputView removeFromSuperview];
3032 - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
3033 for (UIView* view in
self.textInputViews) {
3036 inputView.isVisibleToAutofill = newVisibility;
3048 - (void)resetAllClientIds {
3049 for (UIView* view in
self.textInputViews) {
3052 [inputView setTextInputClient:0];
3058 if (![inputView isDescendantOfView:_inputHider]) {
3059 [_inputHider addSubview:inputView];
3062 if (_viewController.view == nil) {
3068 UIView* parentView =
self.hostView;
3069 if (_inputHider.superview != parentView) {
3070 [parentView addSubview:_inputHider];
3074 - (void)setTextInputEditingState:(NSDictionary*)state {
3075 [_activeView setTextInputState:state];
3078 - (void)clearTextInputClient {
3079 [_activeView setTextInputClient:0];
3080 _activeView.frame = CGRectZero;
3083 - (void)updateConfig:(NSDictionary*)dictionary {
3084 BOOL isSecureTextEntry = [dictionary[kSecureTextEntry] boolValue];
3085 for (UIView* view in
self.textInputViews) {
3092 if (inputView.isSecureTextEntry != isSecureTextEntry) {
3093 inputView.secureTextEntry = isSecureTextEntry;
3094 [inputView reloadInputViews];
3100 #pragma mark UIIndirectScribbleInteractionDelegate
3102 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3103 isElementFocused:(UIScribbleElementIdentifier)elementIdentifier
3104 API_AVAILABLE(ios(14.0)) {
3105 return _activeView.scribbleFocusStatus == FlutterScribbleFocusStatusFocused;
3108 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3109 focusElementIfNeeded:(UIScribbleElementIdentifier)elementIdentifier
3110 referencePoint:(CGPoint)focusReferencePoint
3111 completion:(
void (^)(UIResponder<UITextInput>* focusedInput))completion
3112 API_AVAILABLE(ios(14.0)) {
3113 _activeView.scribbleFocusStatus = FlutterScribbleFocusStatusFocusing;
3114 [_indirectScribbleDelegate flutterTextInputPlugin:self
3115 focusElement:elementIdentifier
3116 atPoint:focusReferencePoint
3117 result:^(id _Nullable result) {
3118 _activeView.scribbleFocusStatus =
3119 FlutterScribbleFocusStatusFocused;
3120 completion(_activeView);
3124 - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3125 shouldDelayFocusForElement:(UIScribbleElementIdentifier)elementIdentifier
3126 API_AVAILABLE(ios(14.0)) {
3130 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3131 willBeginWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3132 API_AVAILABLE(ios(14.0)) {
3135 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3136 didFinishWritingInElement:(UIScribbleElementIdentifier)elementIdentifier
3137 API_AVAILABLE(ios(14.0)) {
3140 - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3141 frameForElement:(UIScribbleElementIdentifier)elementIdentifier
3142 API_AVAILABLE(ios(14.0)) {
3143 NSValue* elementValue = [_scribbleElements objectForKey:elementIdentifier];
3144 if (elementValue == nil) {
3147 return [elementValue CGRectValue];
3150 - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
3151 requestElementsInRect:(CGRect)rect
3153 (
void (^)(NSArray<UIScribbleElementIdentifier>* elements))completion
3154 API_AVAILABLE(ios(14.0)) {
3155 [_indirectScribbleDelegate
3156 flutterTextInputPlugin:self
3157 requestElementsInRect:rect
3158 result:^(id _Nullable result) {
3159 NSMutableArray<UIScribbleElementIdentifier>* elements =
3160 [[NSMutableArray alloc] init];
3161 if ([result isKindOfClass:[NSArray class]]) {
3162 for (NSArray* elementArray in result) {
3163 [elements addObject:elementArray[0]];
3166 valueWithCGRect:CGRectMake(
3167 [elementArray[1] floatValue],
3168 [elementArray[2] floatValue],
3169 [elementArray[3] floatValue],
3170 [elementArray[4] floatValue])]
3171 forKey:elementArray[0]];
3174 completion(elements);
3178 #pragma mark - Methods related to Scribble support
3182 if (@available(iOS 14.0, *)) {
3184 if (parentView != nil) {
3185 UIIndirectScribbleInteraction* scribbleInteraction = [[UIIndirectScribbleInteraction alloc]
3186 initWithDelegate:(id<UIIndirectScribbleInteractionDelegate>)self];
3187 [parentView addInteraction:scribbleInteraction];
3194 - (void)resetViewResponder {
3195 _viewResponder = nil;
3199 #pragma mark FlutterKeySecondaryResponder
3205 - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) {
3214 - (id)flutterFirstResponder {
3215 if (
self.isFirstResponder) {
3218 for (UIView* subView in
self.subviews) {
3219 UIView* firstResponder = subView.flutterFirstResponder;
3220 if (firstResponder) {
3221 return firstResponder;
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
CGRect localRectFromFrameworkTransform
void resetScribbleInteractionStatusIfEnding
UITextRange * markedTextRange
id< UITextInputDelegate > inputDelegate
API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder UITextRange * selectedTextRange
instancetype initWithOwner
id< FlutterViewResponder > viewResponder
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
UIReturnKeyType returnKeyType
CGRect caretRectForPosition
UIKeyboardAppearance keyboardAppearance
static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary *type)
static NSString *const kSetMarkedTextRectMethod
static NSString *const kFinishAutofillContextMethod
bool _isFloatingCursorActive
static BOOL IsEmoji(NSString *text, NSRange charRange)
static NSString *const kAutofillHints
static NSString *const kAutofillEditingValue
FlutterTextRange * _selectedTextRange
static NSString *const kSecureTextEntry
bool _enableInteractiveSelection
static BOOL ShouldShowSystemKeyboard(NSDictionary *type)
static NSString *const kShowMethod
static NSString *const kUpdateConfigMethod
static UIKeyboardType ToUIKeyboardType(NSDictionary *type)
CGPoint _floatingCursorOffset
static NSString *const kSmartDashesType
static FLUTTER_ASSERT_ARC const char kTextAffinityDownstream[]
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds
static NSString *const kSetSelectionRectsMethod
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static NSString *const kAutocorrectionType
static NSString *const kSetPlatformViewClientMethod
static NSString *const kAssociatedAutofillFields
static NSString *const kKeyboardType
static const NSTimeInterval kKeyboardAnimationDelaySeconds
static NSString *const kSetEditingStateMethod
UIInputViewController * _inputViewController
static NSString *const kAutofillId
static NSString *const kOnInteractiveKeyboardPointerUpMethod
bool _isSystemKeyboardEnabled
static NSString *const kClearClientMethod
static NSString *const kKeyboardAppearance
const CGRect kInvalidFirstRect
static FlutterAutofillType AutofillTypeOf(NSDictionary *configuration)
static NSString *const kSmartQuotesType
static NSString *const kEnableDeltaModel
FlutterScribbleInteractionStatus _scribbleInteractionStatus
static NSString *const kSetClientMethod
static NSString *const kEnableInteractiveSelection
static NSString *const kInputAction
static NSString *const kStartLiveTextInputMethod
static NSString *const kAutofillProperties
static BOOL IsFieldPasswordRelated(NSDictionary *configuration)
static const NSTimeInterval kKeyboardAnimationTimeToCompleteion
static BOOL IsApproximatelyEqual(float x, float y, float delta)
static NSString *const kDeprecatedSetSelectionRectsMethod
static const char kTextAffinityUpstream[]
static NSString * AutofillIdFromDictionary(NSDictionary *dictionary)
static UIReturnKeyType ToUIReturnKeyType(NSString *inputType)
static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, CGRect selectionRect, BOOL selectionRectIsRTL, BOOL useTrailingBoundaryOfSelectionRect, CGRect otherSelectionRect, BOOL otherSelectionRectIsRTL, CGFloat verticalPrecision)
static NSString *const kHideMethod
static UITextContentType ToUITextContentType(NSArray< NSString * > *hints)
static NSString *const kSetEditableSizeAndTransformMethod
static NSString *const kOnInteractiveKeyboardPointerMoveMethod
const char * _selectionAffinity
FlutterTextInputPlugin * textInputPlugin
UIApplication * application
instancetype positionWithIndex:(NSUInteger index)
instancetype positionWithIndex:affinity:(NSUInteger index,[affinity] UITextStorageDirection affinity)
UITextStorageDirection affinity
instancetype rangeWithNSRange:(NSRange range)
NSWritingDirection writingDirection
instancetype selectionRectWithRect:position:writingDirection:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection)
instancetype selectionRectWithRectAndInfo:position:writingDirection:containsStart:containsEnd:isVertical:(CGRect rect,[position] NSUInteger position,[writingDirection] NSWritingDirection writingDirection,[containsStart] BOOL containsStart,[containsEnd] BOOL containsEnd,[isVertical] BOOL isVertical)
FlutterTextInputPlugin * target