| // 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.text"]) |
| return UIKeyboardTypeDefault; |
| if ([inputType isEqualToString:@"TextInputType.multiline"]) |
| return UIKeyboardTypeDefault; |
| 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.emailAddress"]) |
| return UIKeyboardTypeEmailAddress; |
| 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; |
| } |
| |
| #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]; |
| } |
| |
| @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]; |
| [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]; |
| } |
| |
| NSInteger composingBase = [state[@"composingBase"] intValue]; |
| NSInteger composingExtent = [state[@"composingExtent"] intValue]; |
| NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent), |
| ABS(composingBase - composingExtent)) |
| forText:self.text]; |
| self.markedTextRange = |
| composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil; |
| |
| 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) { |
| [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]; |
| |
| // 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 { |
| return YES; |
| } |
| |
| #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; |
| } |
| [_textInputDelegate updateEditingClient:_textInputClient |
| withState:@{ |
| @"selectionBase" : @(selectionBase), |
| @"selectionExtent" : @(selectionExtent), |
| @"selectionAffinity" : @(_selectionAffinity), |
| @"selectionIsDirectional" : @(false), |
| @"composingBase" : @(composingBase), |
| @"composingExtent" : @(composingExtent), |
| @"text" : [NSString stringWithString:self.text], |
| }]; |
| } |
| |
| - (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:@""]; |
| } |
| |
| @end |
| |
| /** |
| * Hides `FlutterTextInputView` from iOS accessibility system so it |
| * does not show up twice, once where it is in the `UIView` hierarchy, |
| * and a second time as part of the `SemanticsObject` hierarchy. |
| */ |
| @interface FlutterTextInputViewAccessibilityHider : UIView { |
| } |
| |
| @end |
| |
| @implementation FlutterTextInputViewAccessibilityHider { |
| } |
| |
| - (BOOL)accessibilityElementsHidden { |
| return YES; |
| } |
| |
| @end |
| |
| @implementation FlutterTextInputPlugin { |
| FlutterTextInputView* _view; |
| FlutterTextInputView* _secureView; |
| FlutterTextInputView* _activeView; |
| FlutterTextInputViewAccessibilityHider* _inputHider; |
| } |
| |
| @synthesize textInputDelegate = _textInputDelegate; |
| |
| - (instancetype)init { |
| self = [super init]; |
| |
| if (self) { |
| _view = [[FlutterTextInputView alloc] init]; |
| _view.secureTextEntry = NO; |
| _secureView = [[FlutterTextInputView alloc] init]; |
| _secureView.secureTextEntry = YES; |
| |
| _activeView = _view; |
| _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init]; |
| } |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| [self hideTextInput]; |
| [_view release]; |
| [_secureView release]; |
| [_inputHider 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 { |
| NSAssert([UIApplication sharedApplication].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; |
| [_inputHider addSubview:_activeView]; |
| [[UIApplication sharedApplication].keyWindow addSubview:_inputHider]; |
| [_activeView becomeFirstResponder]; |
| } |
| |
| - (void)hideTextInput { |
| [_activeView resignFirstResponder]; |
| [_activeView removeFromSuperview]; |
| [_inputHider removeFromSuperview]; |
| } |
| |
| - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration { |
| NSDictionary* inputType = configuration[@"inputType"]; |
| NSString* keyboardAppearance = configuration[@"keyboardAppearance"]; |
| if ([configuration[@"obscureText"] boolValue]) { |
| _activeView = _secureView; |
| } else { |
| _activeView = _view; |
| } |
| |
| _activeView.keyboardType = ToUIKeyboardType(inputType); |
| _activeView.returnKeyType = ToUIReturnKeyType(configuration[@"inputAction"]); |
| _activeView.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"]; |
| _activeView.smartDashesType = |
| smartDashesIsDisabled ? UITextSmartDashesTypeNo : UITextSmartDashesTypeYes; |
| NSString* smartQuotesType = configuration[@"smartQuotesType"]; |
| // This index comes from the SmartQuotesType enum in the framework. |
| bool smartQuotesIsDisabled = smartQuotesType && [smartQuotesType isEqualToString:@"0"]; |
| _activeView.smartQuotesType = |
| smartQuotesIsDisabled ? UITextSmartQuotesTypeNo : UITextSmartQuotesTypeYes; |
| } |
| if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) { |
| _activeView.keyboardAppearance = UIKeyboardAppearanceDark; |
| } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) { |
| _activeView.keyboardAppearance = UIKeyboardAppearanceLight; |
| } else { |
| _activeView.keyboardAppearance = UIKeyboardAppearanceDefault; |
| } |
| NSString* autocorrect = configuration[@"autocorrect"]; |
| _activeView.autocorrectionType = autocorrect && ![autocorrect boolValue] |
| ? UITextAutocorrectionTypeNo |
| : UITextAutocorrectionTypeDefault; |
| [_activeView setTextInputClient:client]; |
| [_activeView reloadInputViews]; |
| } |
| |
| - (void)setTextInputEditingState:(NSDictionary*)state { |
| [_activeView setTextInputState:state]; |
| } |
| |
| - (void)clearTextInputClient { |
| [_activeView setTextInputClient:0]; |
| } |
| |
| @end |