blob: 225b1e51bd069ed1a75e5ec2fb8fedb44a205948 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
#include "flutter/fml/platform/darwin/string_range_sanitization.h"
#include <Foundation/Foundation.h>
#include <UIKit/UIKit.h>
static const char _kTextAffinityDownstream[] = "TextAffinity.downstream";
static const char _kTextAffinityUpstream[] = "TextAffinity.upstream";
static UIKeyboardType ToUIKeyboardType(NSDictionary* type) {
NSString* inputType = type[@"name"];
if ([inputType isEqualToString:@"TextInputType.address"])
return UIKeyboardTypeDefault;
if ([inputType isEqualToString:@"TextInputType.datetime"])
return UIKeyboardTypeNumbersAndPunctuation;
if ([inputType isEqualToString:@"TextInputType.emailAddress"])
return UIKeyboardTypeEmailAddress;
if ([inputType isEqualToString:@"TextInputType.multiline"])
return UIKeyboardTypeDefault;
if ([inputType isEqualToString:@"TextInputType.name"])
return UIKeyboardTypeNamePhonePad;
if ([inputType isEqualToString:@"TextInputType.number"]) {
if ([type[@"signed"] boolValue])
return UIKeyboardTypeNumbersAndPunctuation;
if ([type[@"decimal"] boolValue])
return UIKeyboardTypeDecimalPad;
return UIKeyboardTypeNumberPad;
}
if ([inputType isEqualToString:@"TextInputType.phone"])
return UIKeyboardTypePhonePad;
if ([inputType isEqualToString:@"TextInputType.text"])
return UIKeyboardTypeDefault;
if ([inputType isEqualToString:@"TextInputType.url"])
return UIKeyboardTypeURL;
return UIKeyboardTypeDefault;
}
static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary* type) {
NSString* textCapitalization = type[@"textCapitalization"];
if ([textCapitalization isEqualToString:@"TextCapitalization.characters"]) {
return UITextAutocapitalizationTypeAllCharacters;
} else if ([textCapitalization isEqualToString:@"TextCapitalization.sentences"]) {
return UITextAutocapitalizationTypeSentences;
} else if ([textCapitalization isEqualToString:@"TextCapitalization.words"]) {
return UITextAutocapitalizationTypeWords;
}
return UITextAutocapitalizationTypeNone;
}
static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) {
// Where did the term "unspecified" come from? iOS has a "default" and Android
// has "unspecified." These 2 terms seem to mean the same thing but we need
// to pick just one. "unspecified" was chosen because "default" is often a
// reserved word in languages with switch statements (dart, java, etc).
if ([inputType isEqualToString:@"TextInputAction.unspecified"])
return UIReturnKeyDefault;
if ([inputType isEqualToString:@"TextInputAction.done"])
return UIReturnKeyDone;
if ([inputType isEqualToString:@"TextInputAction.go"])
return UIReturnKeyGo;
if ([inputType isEqualToString:@"TextInputAction.send"])
return UIReturnKeySend;
if ([inputType isEqualToString:@"TextInputAction.search"])
return UIReturnKeySearch;
if ([inputType isEqualToString:@"TextInputAction.next"])
return UIReturnKeyNext;
if (@available(iOS 9.0, *))
if ([inputType isEqualToString:@"TextInputAction.continueAction"])
return UIReturnKeyContinue;
if ([inputType isEqualToString:@"TextInputAction.join"])
return UIReturnKeyJoin;
if ([inputType isEqualToString:@"TextInputAction.route"])
return UIReturnKeyRoute;
if ([inputType isEqualToString:@"TextInputAction.emergencyCall"])
return UIReturnKeyEmergencyCall;
if ([inputType isEqualToString:@"TextInputAction.newline"])
return UIReturnKeyDefault;
// Present default key if bad input type is given.
return UIReturnKeyDefault;
}
static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
if (hints == nil || hints.count == 0) {
return @"";
}
NSString* hint = hints[0];
if (@available(iOS 10.0, *)) {
if ([hint isEqualToString:@"addressCityAndState"]) {
return UITextContentTypeAddressCityAndState;
}
if ([hint isEqualToString:@"addressState"]) {
return UITextContentTypeAddressState;
}
if ([hint isEqualToString:@"addressCity"]) {
return UITextContentTypeAddressCity;
}
if ([hint isEqualToString:@"sublocality"]) {
return UITextContentTypeSublocality;
}
if ([hint isEqualToString:@"streetAddressLine1"]) {
return UITextContentTypeStreetAddressLine1;
}
if ([hint isEqualToString:@"streetAddressLine2"]) {
return UITextContentTypeStreetAddressLine2;
}
if ([hint isEqualToString:@"countryName"]) {
return UITextContentTypeCountryName;
}
if ([hint isEqualToString:@"fullStreetAddress"]) {
return UITextContentTypeFullStreetAddress;
}
if ([hint isEqualToString:@"postalCode"]) {
return UITextContentTypePostalCode;
}
if ([hint isEqualToString:@"location"]) {
return UITextContentTypeLocation;
}
if ([hint isEqualToString:@"creditCardNumber"]) {
return UITextContentTypeCreditCardNumber;
}
if ([hint isEqualToString:@"email"]) {
return UITextContentTypeEmailAddress;
}
if ([hint isEqualToString:@"jobTitle"]) {
return UITextContentTypeJobTitle;
}
if ([hint isEqualToString:@"givenName"]) {
return UITextContentTypeGivenName;
}
if ([hint isEqualToString:@"middleName"]) {
return UITextContentTypeMiddleName;
}
if ([hint isEqualToString:@"familyName"]) {
return UITextContentTypeFamilyName;
}
if ([hint isEqualToString:@"name"]) {
return UITextContentTypeName;
}
if ([hint isEqualToString:@"namePrefix"]) {
return UITextContentTypeNamePrefix;
}
if ([hint isEqualToString:@"nameSuffix"]) {
return UITextContentTypeNameSuffix;
}
if ([hint isEqualToString:@"nickname"]) {
return UITextContentTypeNickname;
}
if ([hint isEqualToString:@"organizationName"]) {
return UITextContentTypeOrganizationName;
}
if ([hint isEqualToString:@"telephoneNumber"]) {
return UITextContentTypeTelephoneNumber;
}
}
if (@available(iOS 11.0, *)) {
if ([hint isEqualToString:@"password"]) {
return UITextContentTypePassword;
}
}
if (@available(iOS 12.0, *)) {
if ([hint isEqualToString:@"oneTimeCode"]) {
return UITextContentTypeOneTimeCode;
}
if ([hint isEqualToString:@"newPassword"]) {
return UITextContentTypeNewPassword;
}
}
return hints[0];
}
static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
NSDictionary* autofill = dictionary[@"autofill"];
return autofill == nil ? nil : autofill[@"uniqueIdentifier"];
}
#pragma mark - FlutterTextPosition
@implementation FlutterTextPosition
+ (instancetype)positionWithIndex:(NSUInteger)index {
return [[[FlutterTextPosition alloc] initWithIndex:index] autorelease];
}
- (instancetype)initWithIndex:(NSUInteger)index {
self = [super init];
if (self) {
_index = index;
}
return self;
}
@end
#pragma mark - FlutterTextRange
@implementation FlutterTextRange
+ (instancetype)rangeWithNSRange:(NSRange)range {
return [[[FlutterTextRange alloc] initWithNSRange:range] autorelease];
}
- (instancetype)initWithNSRange:(NSRange)range {
self = [super init];
if (self) {
_range = range;
}
return self;
}
- (UITextPosition*)start {
return [FlutterTextPosition positionWithIndex:self.range.location];
}
- (UITextPosition*)end {
return [FlutterTextPosition positionWithIndex:self.range.location + self.range.length];
}
- (BOOL)isEmpty {
return self.range.length == 0;
}
- (id)copyWithZone:(NSZone*)zone {
return [[FlutterTextRange allocWithZone:zone] initWithNSRange:self.range];
}
- (BOOL)isEqualTo:(FlutterTextRange*)other {
return NSEqualRanges(self.range, other.range);
}
@end
@interface FlutterTextInputView ()
@property(nonatomic, copy) NSString* autofillId;
@end
@implementation FlutterTextInputView {
int _textInputClient;
const char* _selectionAffinity;
FlutterTextRange* _selectedTextRange;
}
@synthesize tokenizer = _tokenizer;
- (instancetype)init {
self = [super init];
if (self) {
_textInputClient = 0;
_selectionAffinity = _kTextAffinityUpstream;
// UITextInput
_text = [[NSMutableString alloc] init];
_markedText = [[NSMutableString alloc] init];
_selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)];
// UITextInputTraits
_autocapitalizationType = UITextAutocapitalizationTypeSentences;
_autocorrectionType = UITextAutocorrectionTypeDefault;
_spellCheckingType = UITextSpellCheckingTypeDefault;
_enablesReturnKeyAutomatically = NO;
_keyboardAppearance = UIKeyboardAppearanceDefault;
_keyboardType = UIKeyboardTypeDefault;
_returnKeyType = UIReturnKeyDone;
_secureTextEntry = NO;
if (@available(iOS 11.0, *)) {
_smartQuotesType = UITextSmartQuotesTypeYes;
_smartDashesType = UITextSmartDashesTypeYes;
}
}
return self;
}
- (void)dealloc {
[_text release];
[_markedText release];
[_markedTextRange release];
[_selectedTextRange release];
[_tokenizer release];
[_autofillId release];
[super dealloc];
}
- (void)setTextInputClient:(int)client {
_textInputClient = client;
}
- (void)setTextInputState:(NSDictionary*)state {
NSString* newText = state[@"text"];
BOOL textChanged = ![self.text isEqualToString:newText];
if (textChanged) {
[self.inputDelegate textWillChange:self];
[self.text setString:newText];
}
BOOL needsEditingStateUpdate = textChanged;
NSInteger composingBase = [state[@"composingBase"] intValue];
NSInteger composingExtent = [state[@"composingExtent"] intValue];
NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent),
ABS(composingBase - composingExtent))
forText:self.text];
FlutterTextRange* newMarkedRange =
composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil;
needsEditingStateUpdate =
needsEditingStateUpdate || newMarkedRange == nil
? self.markedTextRange == nil
: [newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange];
self.markedTextRange = newMarkedRange;
NSInteger selectionBase = [state[@"selectionBase"] intValue];
NSInteger selectionExtent = [state[@"selectionExtent"] intValue];
NSRange selectedRange = [self clampSelection:NSMakeRange(MIN(selectionBase, selectionExtent),
ABS(selectionBase - selectionExtent))
forText:self.text];
NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range];
if (selectedRange.location != oldSelectedRange.location ||
selectedRange.length != oldSelectedRange.length) {
needsEditingStateUpdate = YES;
[self.inputDelegate selectionWillChange:self];
[self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:selectedRange]
updateEditingState:NO];
_selectionAffinity = _kTextAffinityDownstream;
if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)])
_selectionAffinity = _kTextAffinityUpstream;
[self.inputDelegate selectionDidChange:self];
}
if (textChanged) {
[self.inputDelegate textDidChange:self];
}
if (needsEditingStateUpdate) {
// For consistency with Android behavior, send an update to the framework.
[self updateEditingState];
}
}
- (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
int start = MIN(MAX(range.location, 0), text.length);
int length = MIN(range.length, text.length - start);
return NSMakeRange(start, length);
}
#pragma mark - UIResponder Overrides
- (BOOL)canBecomeFirstResponder {
// Only the currently focused input field can
// become the first responder. This prevents iOS
// from changing focus by itself.
return _textInputClient != 0;
}
#pragma mark - UITextInput Overrides
- (id<UITextInputTokenizer>)tokenizer {
if (_tokenizer == nil) {
_tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];
}
return _tokenizer;
}
- (UITextRange*)selectedTextRange {
return [[_selectedTextRange copy] autorelease];
}
- (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
[self setSelectedTextRange:selectedTextRange updateEditingState:YES];
}
- (void)setSelectedTextRange:(UITextRange*)selectedTextRange updateEditingState:(BOOL)update {
if (_selectedTextRange != selectedTextRange) {
UITextRange* oldSelectedRange = _selectedTextRange;
if (self.hasText) {
FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange;
_selectedTextRange = [[FlutterTextRange
rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy];
} else {
_selectedTextRange = [selectedTextRange copy];
}
[oldSelectedRange release];
if (update)
[self updateEditingState];
}
}
- (id)insertDictationResultPlaceholder {
return @"";
}
- (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult {
}
- (NSString*)textInRange:(UITextRange*)range {
if (!range) {
return nil;
}
NSAssert([range isKindOfClass:[FlutterTextRange class]],
@"Expected a FlutterTextRange for range (got %@).", [range class]);
NSRange textRange = ((FlutterTextRange*)range).range;
NSAssert(textRange.location != NSNotFound, @"Expected a valid text range.");
return [self.text substringWithRange:textRange];
}
- (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
NSRange replaceRange = ((FlutterTextRange*)range).range;
NSRange selectedRange = _selectedTextRange.range;
// Adjust the text selection:
// * reduce the length by the intersection length
// * adjust the location by newLength - oldLength + intersectionLength
NSRange intersectionRange = NSIntersectionRange(replaceRange, selectedRange);
if (replaceRange.location <= selectedRange.location)
selectedRange.location += text.length - replaceRange.length;
if (intersectionRange.location != NSNotFound) {
selectedRange.location += intersectionRange.length;
selectedRange.length -= intersectionRange.length;
}
[self.text replaceCharactersInRange:[self clampSelection:replaceRange forText:self.text]
withString:text];
[self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange
forText:self.text]]
updateEditingState:NO];
[self updateEditingState];
}
- (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
[_textInputDelegate performAction:FlutterTextInputActionNewline withClient:_textInputClient];
return YES;
}
if ([text isEqualToString:@"\n"]) {
FlutterTextInputAction action;
switch (self.returnKeyType) {
case UIReturnKeyDefault:
action = FlutterTextInputActionUnspecified;
break;
case UIReturnKeyDone:
action = FlutterTextInputActionDone;
break;
case UIReturnKeyGo:
action = FlutterTextInputActionGo;
break;
case UIReturnKeySend:
action = FlutterTextInputActionSend;
break;
case UIReturnKeySearch:
case UIReturnKeyGoogle:
case UIReturnKeyYahoo:
action = FlutterTextInputActionSearch;
break;
case UIReturnKeyNext:
action = FlutterTextInputActionNext;
break;
case UIReturnKeyContinue:
action = FlutterTextInputActionContinue;
break;
case UIReturnKeyJoin:
action = FlutterTextInputActionJoin;
break;
case UIReturnKeyRoute:
action = FlutterTextInputActionRoute;
break;
case UIReturnKeyEmergencyCall:
action = FlutterTextInputActionEmergencyCall;
break;
}
[_textInputDelegate performAction:action withClient:_textInputClient];
return NO;
}
return YES;
}
- (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange {
NSRange selectedRange = _selectedTextRange.range;
NSRange markedTextRange = ((FlutterTextRange*)self.markedTextRange).range;
if (markedText == nil)
markedText = @"";
if (markedTextRange.length > 0) {
// Replace text in the marked range with the new text.
[self replaceRange:self.markedTextRange withText:markedText];
markedTextRange.length = markedText.length;
} else {
// Replace text in the selected range with the new text.
[self replaceRange:_selectedTextRange withText:markedText];
markedTextRange = NSMakeRange(selectedRange.location, markedText.length);
}
self.markedTextRange =
markedTextRange.length > 0 ? [FlutterTextRange rangeWithNSRange:markedTextRange] : nil;
NSUInteger selectionLocation = markedSelectedRange.location + markedTextRange.location;
selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length);
[self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange
forText:self.text]]
updateEditingState:YES];
}
- (void)unmarkText {
self.markedTextRange = nil;
[self updateEditingState];
}
- (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition
toPosition:(UITextPosition*)toPosition {
NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index;
NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index;
return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)];
}
- (NSUInteger)decrementOffsetPosition:(NSUInteger)position {
return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location;
}
- (NSUInteger)incrementOffsetPosition:(NSUInteger)position {
NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position);
return MIN(position + charRange.length, self.text.length);
}
- (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset {
NSUInteger offsetPosition = ((FlutterTextPosition*)position).index;
NSInteger newLocation = (NSInteger)offsetPosition + offset;
if (newLocation < 0 || newLocation > (NSInteger)self.text.length) {
return nil;
}
if (offset >= 0) {
for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i)
offsetPosition = [self incrementOffsetPosition:offsetPosition];
} else {
for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i)
offsetPosition = [self decrementOffsetPosition:offsetPosition];
}
return [FlutterTextPosition positionWithIndex:offsetPosition];
}
- (UITextPosition*)positionFromPosition:(UITextPosition*)position
inDirection:(UITextLayoutDirection)direction
offset:(NSInteger)offset {
// TODO(cbracken) Add RTL handling.
switch (direction) {
case UITextLayoutDirectionLeft:
case UITextLayoutDirectionUp:
return [self positionFromPosition:position offset:offset * -1];
case UITextLayoutDirectionRight:
case UITextLayoutDirectionDown:
return [self positionFromPosition:position offset:1];
}
}
- (UITextPosition*)beginningOfDocument {
return [FlutterTextPosition positionWithIndex:0];
}
- (UITextPosition*)endOfDocument {
return [FlutterTextPosition positionWithIndex:self.text.length];
}
- (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other {
NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
NSUInteger otherIndex = ((FlutterTextPosition*)other).index;
if (positionIndex < otherIndex)
return NSOrderedAscending;
if (positionIndex > otherIndex)
return NSOrderedDescending;
return NSOrderedSame;
}
- (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition {
return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index;
}
- (UITextPosition*)positionWithinRange:(UITextRange*)range
farthestInDirection:(UITextLayoutDirection)direction {
NSUInteger index;
switch (direction) {
case UITextLayoutDirectionLeft:
case UITextLayoutDirectionUp:
index = ((FlutterTextPosition*)range.start).index;
break;
case UITextLayoutDirectionRight:
case UITextLayoutDirectionDown:
index = ((FlutterTextPosition*)range.end).index;
break;
}
return [FlutterTextPosition positionWithIndex:index];
}
- (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position
inDirection:(UITextLayoutDirection)direction {
NSUInteger positionIndex = ((FlutterTextPosition*)position).index;
NSUInteger startIndex;
NSUInteger endIndex;
switch (direction) {
case UITextLayoutDirectionLeft:
case UITextLayoutDirectionUp:
startIndex = [self decrementOffsetPosition:positionIndex];
endIndex = positionIndex;
break;
case UITextLayoutDirectionRight:
case UITextLayoutDirectionDown:
startIndex = positionIndex;
endIndex = [self incrementOffsetPosition:positionIndex];
break;
}
return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)];
}
#pragma mark - UITextInput text direction handling
- (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position
inDirection:(UITextStorageDirection)direction {
// TODO(cbracken) Add RTL handling.
return UITextWritingDirectionNatural;
}
- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection
forRange:(UITextRange*)range {
// TODO(cbracken) Add RTL handling.
}
#pragma mark - UITextInput cursor, selection rect handling
// The following methods are required to support force-touch cursor positioning
// and to position the
// candidates view for multi-stage input methods (e.g., Japanese) when using a
// physical keyboard.
- (CGRect)firstRectForRange:(UITextRange*)range {
// multi-stage text is handled somewhere else.
if (_markedTextRange != nil) {
return CGRectZero;
}
NSUInteger start = ((FlutterTextPosition*)range.start).index;
NSUInteger end = ((FlutterTextPosition*)range.end).index;
[_textInputDelegate showAutocorrectionPromptRectForStart:start
end:end
withClient:_textInputClient];
// TODO(cbracken) Implement.
return CGRectZero;
}
- (CGRect)caretRectForPosition:(UITextPosition*)position {
// TODO(cbracken) Implement.
return CGRectZero;
}
- (UITextPosition*)closestPositionToPoint:(CGPoint)point {
// TODO(cbracken) Implement.
NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
return [FlutterTextPosition positionWithIndex:currentIndex];
}
- (NSArray*)selectionRectsForRange:(UITextRange*)range {
// TODO(cbracken) Implement.
return @[];
}
- (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range {
// TODO(cbracken) Implement.
return range.start;
}
- (UITextRange*)characterRangeAtPoint:(CGPoint)point {
// TODO(cbracken) Implement.
NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index;
return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)];
}
- (void)beginFloatingCursorAtPoint:(CGPoint)point {
[_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart
withClient:_textInputClient
withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
}
- (void)updateFloatingCursorAtPoint:(CGPoint)point {
[_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate
withClient:_textInputClient
withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}];
}
- (void)endFloatingCursor {
[_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd
withClient:_textInputClient
withPosition:@{@"X" : @(0), @"Y" : @(0)}];
}
#pragma mark - UIKeyInput Overrides
- (void)updateEditingState {
NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index;
NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index;
// Empty compositing range is represented by the framework's TextRange.empty.
NSInteger composingBase = -1;
NSInteger composingExtent = -1;
if (self.markedTextRange != nil) {
composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index;
composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index;
}
NSDictionary* state = @{
@"selectionBase" : @(selectionBase),
@"selectionExtent" : @(selectionExtent),
@"selectionAffinity" : @(_selectionAffinity),
@"selectionIsDirectional" : @(false),
@"composingBase" : @(composingBase),
@"composingExtent" : @(composingExtent),
@"text" : [NSString stringWithString:self.text],
};
if (_textInputClient == 0 && _autofillId != nil) {
[_textInputDelegate updateEditingClient:_textInputClient withState:state withTag:_autofillId];
} else {
[_textInputDelegate updateEditingClient:_textInputClient withState:state];
}
}
- (BOOL)hasText {
return self.text.length > 0;
}
- (void)insertText:(NSString*)text {
_selectionAffinity = _kTextAffinityDownstream;
[self replaceRange:_selectedTextRange withText:text];
}
- (void)deleteBackward {
_selectionAffinity = _kTextAffinityDownstream;
// When deleting Thai vowel, _selectedTextRange has location
// but does not have length, so we have to manually set it.
// In addition, we needed to delete only a part of grapheme cluster
// because it is the expected behavior of Thai input.
// https://github.com/flutter/flutter/issues/24203
// https://github.com/flutter/flutter/issues/21745
// https://github.com/flutter/flutter/issues/39399
//
// This is needed for correct handling of the deletion of Thai vowel input.
// TODO(cbracken): Get a good understanding of expected behavior of Thai
// input and ensure that this is the correct solution.
// https://github.com/flutter/flutter/issues/28962
if (_selectedTextRange.isEmpty && [self hasText]) {
UITextRange* oldSelectedRange = _selectedTextRange;
NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
if (oldRange.location > 0) {
NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
_selectedTextRange = [[FlutterTextRange rangeWithNSRange:newRange] copy];
[oldSelectedRange release];
}
}
if (!_selectedTextRange.isEmpty)
[self replaceRange:_selectedTextRange withText:@""];
}
- (BOOL)accessibilityElementsHidden {
// We are hiding this accessibility element.
// There are 2 accessible elements involved in text entry in 2 different parts of the view
// hierarchy. This `FlutterTextInputView` is injected at the top of key window. We use this as a
// `UITextInput` protocol to bridge text edit events between Flutter and iOS.
//
// We also create ur own custom `UIAccessibilityElements` tree with our `SemanticsObject` to
// mimic the semantics tree from Flutter. We want the text field to be represented as a
// `TextInputSemanticsObject` in that `SemanticsObject` tree rather than in this
// `FlutterTextInputView` bridge which doesn't appear above a text field from the Flutter side.
return YES;
}
@end
@interface FlutterTextInputPlugin ()
@property(nonatomic, retain) FlutterTextInputView* nonAutofillInputView;
@property(nonatomic, retain) FlutterTextInputView* nonAutofillSecureInputView;
@property(nonatomic, retain) NSMutableArray<FlutterTextInputView*>* inputViews;
@property(nonatomic, assign) FlutterTextInputView* activeView;
@end
@implementation FlutterTextInputPlugin
@synthesize textInputDelegate = _textInputDelegate;
- (instancetype)init {
self = [super init];
if (self) {
_nonAutofillInputView = [[FlutterTextInputView alloc] init];
_nonAutofillInputView.secureTextEntry = NO;
_nonAutofillSecureInputView = [[FlutterTextInputView alloc] init];
_nonAutofillSecureInputView.secureTextEntry = YES;
_inputViews = [[NSMutableArray alloc] init];
_activeView = _nonAutofillInputView;
}
return self;
}
- (void)dealloc {
[self hideTextInput];
[_nonAutofillInputView release];
[_nonAutofillSecureInputView release];
[_inputViews release];
[super dealloc];
}
- (UIView<UITextInput>*)textInputView {
return _activeView;
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* method = call.method;
id args = call.arguments;
if ([method isEqualToString:@"TextInput.show"]) {
[self showTextInput];
result(nil);
} else if ([method isEqualToString:@"TextInput.hide"]) {
[self hideTextInput];
result(nil);
} else if ([method isEqualToString:@"TextInput.setClient"]) {
[self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
result(nil);
} else if ([method isEqualToString:@"TextInput.setEditingState"]) {
[self setTextInputEditingState:args];
result(nil);
} else if ([method isEqualToString:@"TextInput.clearClient"]) {
[self clearTextInputClient];
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)showTextInput {
UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
NSAssert(keyWindow != nullptr,
@"The application must have a key window since the keyboard client "
@"must be part of the responder chain to function");
_activeView.textInputDelegate = _textInputDelegate;
if (_activeView.window != keyWindow) {
[keyWindow addSubview:_activeView];
}
[_activeView becomeFirstResponder];
}
- (void)hideTextInput {
[_activeView resignFirstResponder];
}
- (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
NSArray* fields = configuration[@"fields"];
NSString* clientUniqueId = uniqueIdFromDictionary(configuration);
bool isSecureTextEntry = [configuration[@"obscureText"] boolValue];
if (fields == nil) {
_activeView = isSecureTextEntry ? _nonAutofillSecureInputView : _nonAutofillInputView;
[FlutterTextInputPlugin setupInputView:_activeView withConfiguration:configuration];
if (_activeView.window != keyWindow) {
[keyWindow addSubview:_activeView];
}
} else {
NSAssert(clientUniqueId != nil, @"The client's unique id can't be null");
for (FlutterTextInputView* view in _inputViews) {
[view removeFromSuperview];
}
for (UIView* view in keyWindow.subviews) {
if ([view isKindOfClass:[FlutterTextInputView class]]) {
[view removeFromSuperview];
}
}
[_inputViews removeAllObjects];
for (NSDictionary* field in fields) {
FlutterTextInputView* newInputView = [[[FlutterTextInputView alloc] init] autorelease];
newInputView.textInputDelegate = _textInputDelegate;
[_inputViews addObject:newInputView];
NSString* autofillId = uniqueIdFromDictionary(field);
newInputView.autofillId = autofillId;
if ([clientUniqueId isEqualToString:autofillId]) {
_activeView = newInputView;
}
[FlutterTextInputPlugin setupInputView:newInputView withConfiguration:field];
[keyWindow addSubview:newInputView];
}
}
[_activeView setTextInputClient:client];
[_activeView reloadInputViews];
}
+ (void)setupInputView:(FlutterTextInputView*)inputView
withConfiguration:(NSDictionary*)configuration {
NSDictionary* inputType = configuration[@"inputType"];
NSString* keyboardAppearance = configuration[@"keyboardAppearance"];
NSDictionary* autofill = configuration[@"autofill"];
inputView.secureTextEntry = [configuration[@"obscureText"] boolValue];
inputView.keyboardType = ToUIKeyboardType(inputType);
inputView.returnKeyType = ToUIReturnKeyType(configuration[@"inputAction"]);
inputView.autocapitalizationType = ToUITextAutoCapitalizationType(configuration);
if (@available(iOS 11.0, *)) {
NSString* smartDashesType = configuration[@"smartDashesType"];
// This index comes from the SmartDashesType enum in the framework.
bool smartDashesIsDisabled = smartDashesType && [smartDashesType isEqualToString:@"0"];
inputView.smartDashesType =
smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes;
NSString* smartQuotesType = configuration[@"smartQuotesType"];
// This index comes from the SmartQuotesType enum in the framework.
bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"];
inputView.smartQuotesType =
smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes;
}
if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) {
inputView.keyboardAppearance = UIKeyboardAppearanceDark;
} else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) {
inputView.keyboardAppearance = UIKeyboardAppearanceLight;
} else {
inputView.keyboardAppearance = UIKeyboardAppearanceDefault;
}
NSString* autocorrect = configuration[@"autocorrect"];
inputView.autocorrectionType = autocorrect && ![autocorrect boolValue]
? UITextAutocorrectionTypeNo
: UITextAutocorrectionTypeDefault;
if (@available(iOS 10.0, *)) {
if (autofill == nil) {
inputView.textContentType = @"";
} else {
inputView.textContentType = ToUITextContentType(autofill[@"hints"]);
[inputView setTextInputState:autofill[@"editingValue"]];
// An input field needs to be visible in order to get
// autofilled when it's not the one that triggered
// autofill.
inputView.frame = CGRectMake(0, 0, 1, 1);
}
}
}
- (void)setTextInputEditingState:(NSDictionary*)state {
[_activeView setTextInputState:state];
}
- (void)clearTextInputClient {
[_activeView setTextInputClient:0];
}
@end