1// Copyright 2013 The Flutter Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" 6#include "flutter/fml/platform/darwin/string_range_sanitization.h" 7 8#include <Foundation/Foundation.h> 9#include <UIKit/UIKit.h> 10 11static const char _kTextAffinityDownstream[] = "TextAffinity.downstream"; 12static const char _kTextAffinityUpstream[] = "TextAffinity.upstream"; 13 14static UIKeyboardType ToUIKeyboardType(NSDictionary* type) { 15 NSString* inputType = type[@"name"]; 16 if ([inputType isEqualToString:@"TextInputType.text"]) 17 return UIKeyboardTypeDefault; 18 if ([inputType isEqualToString:@"TextInputType.multiline"]) 19 return UIKeyboardTypeDefault; 20 if ([inputType isEqualToString:@"TextInputType.number"]) { 21 if ([type[@"signed"] boolValue]) 22 return UIKeyboardTypeNumbersAndPunctuation; 23 if ([type[@"decimal"] boolValue]) 24 return UIKeyboardTypeDecimalPad; 25 return UIKeyboardTypeNumberPad; 26 } 27 if ([inputType isEqualToString:@"TextInputType.phone"]) 28 return UIKeyboardTypePhonePad; 29 if ([inputType isEqualToString:@"TextInputType.emailAddress"]) 30 return UIKeyboardTypeEmailAddress; 31 if ([inputType isEqualToString:@"TextInputType.url"]) 32 return UIKeyboardTypeURL; 33 return UIKeyboardTypeDefault; 34} 35 36static UITextAutocapitalizationType ToUITextAutoCapitalizationType(NSDictionary* type) { 37 NSString* textCapitalization = type[@"textCapitalization"]; 38 if ([textCapitalization isEqualToString:@"TextCapitalization.characters"]) { 39 return UITextAutocapitalizationTypeAllCharacters; 40 } else if ([textCapitalization isEqualToString:@"TextCapitalization.sentences"]) { 41 return UITextAutocapitalizationTypeSentences; 42 } else if ([textCapitalization isEqualToString:@"TextCapitalization.words"]) { 43 return UITextAutocapitalizationTypeWords; 44 } 45 return UITextAutocapitalizationTypeNone; 46} 47 48static UIReturnKeyType ToUIReturnKeyType(NSString* inputType) { 49 // Where did the term "unspecified" come from? iOS has a "default" and Android 50 // has "unspecified." These 2 terms seem to mean the same thing but we need 51 // to pick just one. "unspecified" was chosen because "default" is often a 52 // reserved word in languages with switch statements (dart, java, etc). 53 if ([inputType isEqualToString:@"TextInputAction.unspecified"]) 54 return UIReturnKeyDefault; 55 56 if ([inputType isEqualToString:@"TextInputAction.done"]) 57 return UIReturnKeyDone; 58 59 if ([inputType isEqualToString:@"TextInputAction.go"]) 60 return UIReturnKeyGo; 61 62 if ([inputType isEqualToString:@"TextInputAction.send"]) 63 return UIReturnKeySend; 64 65 if ([inputType isEqualToString:@"TextInputAction.search"]) 66 return UIReturnKeySearch; 67 68 if ([inputType isEqualToString:@"TextInputAction.next"]) 69 return UIReturnKeyNext; 70 71 if (@available(iOS 9.0, *)) 72 if ([inputType isEqualToString:@"TextInputAction.continueAction"]) 73 return UIReturnKeyContinue; 74 75 if ([inputType isEqualToString:@"TextInputAction.join"]) 76 return UIReturnKeyJoin; 77 78 if ([inputType isEqualToString:@"TextInputAction.route"]) 79 return UIReturnKeyRoute; 80 81 if ([inputType isEqualToString:@"TextInputAction.emergencyCall"]) 82 return UIReturnKeyEmergencyCall; 83 84 if ([inputType isEqualToString:@"TextInputAction.newline"]) 85 return UIReturnKeyDefault; 86 87 // Present default key if bad input type is given. 88 return UIReturnKeyDefault; 89} 90 91#pragma mark - FlutterTextPosition 92 93@implementation FlutterTextPosition 94 95+ (instancetype)positionWithIndex:(NSUInteger)index { 96 return [[[FlutterTextPosition alloc] initWithIndex:index] autorelease]; 97} 98 99- (instancetype)initWithIndex:(NSUInteger)index { 100 self = [super init]; 101 if (self) { 102 _index = index; 103 } 104 return self; 105} 106 107@end 108 109#pragma mark - FlutterTextRange 110 111@implementation FlutterTextRange 112 113+ (instancetype)rangeWithNSRange:(NSRange)range { 114 return [[[FlutterTextRange alloc] initWithNSRange:range] autorelease]; 115} 116 117- (instancetype)initWithNSRange:(NSRange)range { 118 self = [super init]; 119 if (self) { 120 _range = range; 121 } 122 return self; 123} 124 125- (UITextPosition*)start { 126 return [FlutterTextPosition positionWithIndex:self.range.location]; 127} 128 129- (UITextPosition*)end { 130 return [FlutterTextPosition positionWithIndex:self.range.location + self.range.length]; 131} 132 133- (BOOL)isEmpty { 134 return self.range.length == 0; 135} 136 137- (id)copyWithZone:(NSZone*)zone { 138 return [[FlutterTextRange allocWithZone:zone] initWithNSRange:self.range]; 139} 140 141@end 142 143@interface FlutterTextInputView : UIView <UITextInput> 144 145// UITextInput 146@property(nonatomic, readonly) NSMutableString* text; 147@property(nonatomic, readonly) NSMutableString* markedText; 148@property(readwrite, copy) UITextRange* selectedTextRange; 149@property(nonatomic, strong) UITextRange* markedTextRange; 150@property(nonatomic, copy) NSDictionary* markedTextStyle; 151@property(nonatomic, assign) id<UITextInputDelegate> inputDelegate; 152 153// UITextInputTraits 154@property(nonatomic) UITextAutocapitalizationType autocapitalizationType; 155@property(nonatomic) UITextAutocorrectionType autocorrectionType; 156@property(nonatomic) UITextSpellCheckingType spellCheckingType; 157@property(nonatomic) BOOL enablesReturnKeyAutomatically; 158@property(nonatomic) UIKeyboardAppearance keyboardAppearance; 159@property(nonatomic) UIKeyboardType keyboardType; 160@property(nonatomic) UIReturnKeyType returnKeyType; 161@property(nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; 162 163@property(nonatomic, assign) id<FlutterTextInputDelegate> textInputDelegate; 164 165@end 166 167@implementation FlutterTextInputView { 168 int _textInputClient; 169 const char* _selectionAffinity; 170 FlutterTextRange* _selectedTextRange; 171} 172 173@synthesize tokenizer = _tokenizer; 174 175- (instancetype)init { 176 self = [super init]; 177 178 if (self) { 179 _textInputClient = 0; 180 _selectionAffinity = _kTextAffinityUpstream; 181 182 // UITextInput 183 _text = [[NSMutableString alloc] init]; 184 _markedText = [[NSMutableString alloc] init]; 185 _selectedTextRange = [[FlutterTextRange alloc] initWithNSRange:NSMakeRange(0, 0)]; 186 187 // UITextInputTraits 188 _autocapitalizationType = UITextAutocapitalizationTypeSentences; 189 _autocorrectionType = UITextAutocorrectionTypeDefault; 190 _spellCheckingType = UITextSpellCheckingTypeDefault; 191 _enablesReturnKeyAutomatically = NO; 192 _keyboardAppearance = UIKeyboardAppearanceDefault; 193 _keyboardType = UIKeyboardTypeDefault; 194 _returnKeyType = UIReturnKeyDone; 195 _secureTextEntry = NO; 196 } 197 198 return self; 199} 200 201- (void)dealloc { 202 [_text release]; 203 [_markedText release]; 204 [_markedTextRange release]; 205 [_selectedTextRange release]; 206 [_tokenizer release]; 207 [super dealloc]; 208} 209 210- (void)setTextInputClient:(int)client { 211 _textInputClient = client; 212} 213 214- (void)setTextInputState:(NSDictionary*)state { 215 NSString* newText = state[@"text"]; 216 BOOL textChanged = ![self.text isEqualToString:newText]; 217 if (textChanged) { 218 [self.inputDelegate textWillChange:self]; 219 [self.text setString:newText]; 220 } 221 222 NSInteger composingBase = [state[@"composingBase"] intValue]; 223 NSInteger composingExtent = [state[@"composingExtent"] intValue]; 224 NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent), 225 ABS(composingBase - composingExtent)) 226 forText:self.text]; 227 self.markedTextRange = 228 composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil; 229 230 NSInteger selectionBase = [state[@"selectionBase"] intValue]; 231 NSInteger selectionExtent = [state[@"selectionExtent"] intValue]; 232 NSRange selectedRange = [self clampSelection:NSMakeRange(MIN(selectionBase, selectionExtent), 233 ABS(selectionBase - selectionExtent)) 234 forText:self.text]; 235 NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range]; 236 if (selectedRange.location != oldSelectedRange.location || 237 selectedRange.length != oldSelectedRange.length) { 238 [self.inputDelegate selectionWillChange:self]; 239 [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:selectedRange] 240 updateEditingState:NO]; 241 _selectionAffinity = _kTextAffinityDownstream; 242 if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)]) 243 _selectionAffinity = _kTextAffinityUpstream; 244 [self.inputDelegate selectionDidChange:self]; 245 } 246 247 if (textChanged) { 248 [self.inputDelegate textDidChange:self]; 249 250 // For consistency with Android behavior, send an update to the framework. 251 [self updateEditingState]; 252 } 253} 254 255- (NSRange)clampSelection:(NSRange)range forText:(NSString*)text { 256 int start = MIN(MAX(range.location, 0), text.length); 257 int length = MIN(range.length, text.length - start); 258 return NSMakeRange(start, length); 259} 260 261#pragma mark - UIResponder Overrides 262 263- (BOOL)canBecomeFirstResponder { 264 return YES; 265} 266 267#pragma mark - UITextInput Overrides 268 269- (id<UITextInputTokenizer>)tokenizer { 270 if (_tokenizer == nil) { 271 _tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self]; 272 } 273 return _tokenizer; 274} 275 276- (UITextRange*)selectedTextRange { 277 return [[_selectedTextRange copy] autorelease]; 278} 279 280- (void)setSelectedTextRange:(UITextRange*)selectedTextRange { 281 [self setSelectedTextRange:selectedTextRange updateEditingState:YES]; 282} 283 284- (void)setSelectedTextRange:(UITextRange*)selectedTextRange updateEditingState:(BOOL)update { 285 if (_selectedTextRange != selectedTextRange) { 286 UITextRange* oldSelectedRange = _selectedTextRange; 287 if (self.hasText) { 288 FlutterTextRange* flutterTextRange = (FlutterTextRange*)selectedTextRange; 289 _selectedTextRange = [[FlutterTextRange 290 rangeWithNSRange:fml::RangeForCharactersInRange(self.text, flutterTextRange.range)] copy]; 291 } else { 292 _selectedTextRange = [selectedTextRange copy]; 293 } 294 [oldSelectedRange release]; 295 296 if (update) 297 [self updateEditingState]; 298 } 299} 300 301- (id)insertDictationResultPlaceholder { 302 return @""; 303} 304 305- (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)willInsertResult { 306} 307 308- (NSString*)textInRange:(UITextRange*)range { 309 NSRange textRange = ((FlutterTextRange*)range).range; 310 return [self.text substringWithRange:textRange]; 311} 312 313- (void)replaceRange:(UITextRange*)range withText:(NSString*)text { 314 NSRange replaceRange = ((FlutterTextRange*)range).range; 315 NSRange selectedRange = _selectedTextRange.range; 316 // Adjust the text selection: 317 // * reduce the length by the intersection length 318 // * adjust the location by newLength - oldLength + intersectionLength 319 NSRange intersectionRange = NSIntersectionRange(replaceRange, selectedRange); 320 if (replaceRange.location <= selectedRange.location) 321 selectedRange.location += text.length - replaceRange.length; 322 if (intersectionRange.location != NSNotFound) { 323 selectedRange.location += intersectionRange.length; 324 selectedRange.length -= intersectionRange.length; 325 } 326 327 [self.text replaceCharactersInRange:[self clampSelection:replaceRange forText:self.text] 328 withString:text]; 329 [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange 330 forText:self.text]] 331 updateEditingState:NO]; 332 333 [self updateEditingState]; 334} 335 336- (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text { 337 if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) { 338 [_textInputDelegate performAction:FlutterTextInputActionNewline withClient:_textInputClient]; 339 return YES; 340 } 341 342 if ([text isEqualToString:@"\n"]) { 343 FlutterTextInputAction action; 344 switch (self.returnKeyType) { 345 case UIReturnKeyDefault: 346 action = FlutterTextInputActionUnspecified; 347 break; 348 case UIReturnKeyDone: 349 action = FlutterTextInputActionDone; 350 break; 351 case UIReturnKeyGo: 352 action = FlutterTextInputActionGo; 353 break; 354 case UIReturnKeySend: 355 action = FlutterTextInputActionSend; 356 break; 357 case UIReturnKeySearch: 358 case UIReturnKeyGoogle: 359 case UIReturnKeyYahoo: 360 action = FlutterTextInputActionSearch; 361 break; 362 case UIReturnKeyNext: 363 action = FlutterTextInputActionNext; 364 break; 365 case UIReturnKeyContinue: 366 action = FlutterTextInputActionContinue; 367 break; 368 case UIReturnKeyJoin: 369 action = FlutterTextInputActionJoin; 370 break; 371 case UIReturnKeyRoute: 372 action = FlutterTextInputActionRoute; 373 break; 374 case UIReturnKeyEmergencyCall: 375 action = FlutterTextInputActionEmergencyCall; 376 break; 377 } 378 379 [_textInputDelegate performAction:action withClient:_textInputClient]; 380 return NO; 381 } 382 383 return YES; 384} 385 386- (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelectedRange { 387 NSRange selectedRange = _selectedTextRange.range; 388 NSRange markedTextRange = ((FlutterTextRange*)self.markedTextRange).range; 389 390 if (markedText == nil) 391 markedText = @""; 392 393 if (markedTextRange.length > 0) { 394 // Replace text in the marked range with the new text. 395 [self replaceRange:self.markedTextRange withText:markedText]; 396 markedTextRange.length = markedText.length; 397 } else { 398 // Replace text in the selected range with the new text. 399 [self replaceRange:_selectedTextRange withText:markedText]; 400 markedTextRange = NSMakeRange(selectedRange.location, markedText.length); 401 } 402 403 self.markedTextRange = 404 markedTextRange.length > 0 ? [FlutterTextRange rangeWithNSRange:markedTextRange] : nil; 405 406 NSUInteger selectionLocation = markedSelectedRange.location + markedTextRange.location; 407 selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length); 408 [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange 409 forText:self.text]] 410 updateEditingState:YES]; 411} 412 413- (void)unmarkText { 414 self.markedTextRange = nil; 415 [self updateEditingState]; 416} 417 418- (UITextRange*)textRangeFromPosition:(UITextPosition*)fromPosition 419 toPosition:(UITextPosition*)toPosition { 420 NSUInteger fromIndex = ((FlutterTextPosition*)fromPosition).index; 421 NSUInteger toIndex = ((FlutterTextPosition*)toPosition).index; 422 return [FlutterTextRange rangeWithNSRange:NSMakeRange(fromIndex, toIndex - fromIndex)]; 423} 424 425- (NSUInteger)decrementOffsetPosition:(NSUInteger)position { 426 return fml::RangeForCharacterAtIndex(self.text, MAX(0, position - 1)).location; 427} 428 429- (NSUInteger)incrementOffsetPosition:(NSUInteger)position { 430 NSRange charRange = fml::RangeForCharacterAtIndex(self.text, position); 431 return MIN(position + charRange.length, self.text.length); 432} 433 434- (UITextPosition*)positionFromPosition:(UITextPosition*)position offset:(NSInteger)offset { 435 NSUInteger offsetPosition = ((FlutterTextPosition*)position).index; 436 437 NSInteger newLocation = (NSInteger)offsetPosition + offset; 438 if (newLocation < 0 || newLocation > (NSInteger)self.text.length) { 439 return nil; 440 } 441 442 if (offset >= 0) { 443 for (NSInteger i = 0; i < offset && offsetPosition < self.text.length; ++i) 444 offsetPosition = [self incrementOffsetPosition:offsetPosition]; 445 } else { 446 for (NSInteger i = 0; i < ABS(offset) && offsetPosition > 0; ++i) 447 offsetPosition = [self decrementOffsetPosition:offsetPosition]; 448 } 449 return [FlutterTextPosition positionWithIndex:offsetPosition]; 450} 451 452- (UITextPosition*)positionFromPosition:(UITextPosition*)position 453 inDirection:(UITextLayoutDirection)direction 454 offset:(NSInteger)offset { 455 // TODO(cbracken) Add RTL handling. 456 switch (direction) { 457 case UITextLayoutDirectionLeft: 458 case UITextLayoutDirectionUp: 459 return [self positionFromPosition:position offset:offset * -1]; 460 case UITextLayoutDirectionRight: 461 case UITextLayoutDirectionDown: 462 return [self positionFromPosition:position offset:1]; 463 } 464} 465 466- (UITextPosition*)beginningOfDocument { 467 return [FlutterTextPosition positionWithIndex:0]; 468} 469 470- (UITextPosition*)endOfDocument { 471 return [FlutterTextPosition positionWithIndex:self.text.length]; 472} 473 474- (NSComparisonResult)comparePosition:(UITextPosition*)position toPosition:(UITextPosition*)other { 475 NSUInteger positionIndex = ((FlutterTextPosition*)position).index; 476 NSUInteger otherIndex = ((FlutterTextPosition*)other).index; 477 if (positionIndex < otherIndex) 478 return NSOrderedAscending; 479 if (positionIndex > otherIndex) 480 return NSOrderedDescending; 481 return NSOrderedSame; 482} 483 484- (NSInteger)offsetFromPosition:(UITextPosition*)from toPosition:(UITextPosition*)toPosition { 485 return ((FlutterTextPosition*)toPosition).index - ((FlutterTextPosition*)from).index; 486} 487 488- (UITextPosition*)positionWithinRange:(UITextRange*)range 489 farthestInDirection:(UITextLayoutDirection)direction { 490 NSUInteger index; 491 switch (direction) { 492 case UITextLayoutDirectionLeft: 493 case UITextLayoutDirectionUp: 494 index = ((FlutterTextPosition*)range.start).index; 495 break; 496 case UITextLayoutDirectionRight: 497 case UITextLayoutDirectionDown: 498 index = ((FlutterTextPosition*)range.end).index; 499 break; 500 } 501 return [FlutterTextPosition positionWithIndex:index]; 502} 503 504- (UITextRange*)characterRangeByExtendingPosition:(UITextPosition*)position 505 inDirection:(UITextLayoutDirection)direction { 506 NSUInteger positionIndex = ((FlutterTextPosition*)position).index; 507 NSUInteger startIndex; 508 NSUInteger endIndex; 509 switch (direction) { 510 case UITextLayoutDirectionLeft: 511 case UITextLayoutDirectionUp: 512 startIndex = [self decrementOffsetPosition:positionIndex]; 513 endIndex = positionIndex; 514 break; 515 case UITextLayoutDirectionRight: 516 case UITextLayoutDirectionDown: 517 startIndex = positionIndex; 518 endIndex = [self incrementOffsetPosition:positionIndex]; 519 break; 520 } 521 return [FlutterTextRange rangeWithNSRange:NSMakeRange(startIndex, endIndex - startIndex)]; 522} 523 524#pragma mark - UITextInput text direction handling 525 526- (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition*)position 527 inDirection:(UITextStorageDirection)direction { 528 // TODO(cbracken) Add RTL handling. 529 return UITextWritingDirectionNatural; 530} 531 532- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection 533 forRange:(UITextRange*)range { 534 // TODO(cbracken) Add RTL handling. 535} 536 537#pragma mark - UITextInput cursor, selection rect handling 538 539// The following methods are required to support force-touch cursor positioning 540// and to position the 541// candidates view for multi-stage input methods (e.g., Japanese) when using a 542// physical keyboard. 543 544- (CGRect)firstRectForRange:(UITextRange*)range { 545 // TODO(cbracken) Implement. 546 return CGRectZero; 547} 548 549- (CGRect)caretRectForPosition:(UITextPosition*)position { 550 // TODO(cbracken) Implement. 551 return CGRectZero; 552} 553 554- (UITextPosition*)closestPositionToPoint:(CGPoint)point { 555 // TODO(cbracken) Implement. 556 NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index; 557 return [FlutterTextPosition positionWithIndex:currentIndex]; 558} 559 560- (NSArray*)selectionRectsForRange:(UITextRange*)range { 561 // TODO(cbracken) Implement. 562 return @[]; 563} 564 565- (UITextPosition*)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange*)range { 566 // TODO(cbracken) Implement. 567 return range.start; 568} 569 570- (UITextRange*)characterRangeAtPoint:(CGPoint)point { 571 // TODO(cbracken) Implement. 572 NSUInteger currentIndex = ((FlutterTextPosition*)_selectedTextRange.start).index; 573 return [FlutterTextRange rangeWithNSRange:fml::RangeForCharacterAtIndex(self.text, currentIndex)]; 574} 575 576- (void)beginFloatingCursorAtPoint:(CGPoint)point { 577 [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart 578 withClient:_textInputClient 579 withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; 580} 581 582- (void)updateFloatingCursorAtPoint:(CGPoint)point { 583 [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate 584 withClient:_textInputClient 585 withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; 586} 587 588- (void)endFloatingCursor { 589 [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd 590 withClient:_textInputClient 591 withPosition:@{@"X" : @(0), @"Y" : @(0)}]; 592} 593 594#pragma mark - UIKeyInput Overrides 595 596- (void)updateEditingState { 597 NSUInteger selectionBase = ((FlutterTextPosition*)_selectedTextRange.start).index; 598 NSUInteger selectionExtent = ((FlutterTextPosition*)_selectedTextRange.end).index; 599 600 // Empty compositing range is represented by the framework's TextRange.empty. 601 NSInteger composingBase = -1; 602 NSInteger composingExtent = -1; 603 if (self.markedTextRange != nil) { 604 composingBase = ((FlutterTextPosition*)self.markedTextRange.start).index; 605 composingExtent = ((FlutterTextPosition*)self.markedTextRange.end).index; 606 } 607 [_textInputDelegate updateEditingClient:_textInputClient 608 withState:@{ 609 @"selectionBase" : @(selectionBase), 610 @"selectionExtent" : @(selectionExtent), 611 @"selectionAffinity" : @(_selectionAffinity), 612 @"selectionIsDirectional" : @(false), 613 @"composingBase" : @(composingBase), 614 @"composingExtent" : @(composingExtent), 615 @"text" : [NSString stringWithString:self.text], 616 }]; 617} 618 619- (BOOL)hasText { 620 return self.text.length > 0; 621} 622 623- (void)insertText:(NSString*)text { 624 _selectionAffinity = _kTextAffinityDownstream; 625 [self replaceRange:_selectedTextRange withText:text]; 626} 627 628- (void)deleteBackward { 629 _selectionAffinity = _kTextAffinityDownstream; 630 631 // When deleting Thai vowel, _selectedTextRange has location 632 // but does not have length, so we have to manually set it. 633 // In addition, we needed to delete only a part of grapheme cluster 634 // because it is the expected behavior of Thai input. 635 // https://github.com/flutter/flutter/issues/24203 636 // https://github.com/flutter/flutter/issues/21745 637 // 638 // This is needed for correct handling of the deletion of Thai vowel input. 639 // TODO(cbracken): Get a good understanding of expected behavior of Thai 640 // input and ensure that this is the correct solution. 641 // https://github.com/flutter/flutter/issues/28962 642 if (_selectedTextRange.isEmpty && [self hasText]) { 643 NSRange oldRange = ((FlutterTextRange*)_selectedTextRange).range; 644 if (oldRange.location > 0) { 645 NSRange newRange = NSMakeRange(oldRange.location - 1, 1); 646 [self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:newRange] 647 updateEditingState:false]; 648 } 649 } 650 651 if (!_selectedTextRange.isEmpty) 652 [self replaceRange:_selectedTextRange withText:@""]; 653} 654 655@end 656 657/** 658 * Hides `FlutterTextInputView` from iOS accessibility system so it 659 * does not show up twice, once where it is in the `UIView` hierarchy, 660 * and a second time as part of the `SemanticsObject` hierarchy. 661 */ 662@interface FlutterTextInputViewAccessibilityHider : UIView { 663} 664 665@end 666 667@implementation FlutterTextInputViewAccessibilityHider { 668} 669 670- (BOOL)accessibilityElementsHidden { 671 return YES; 672} 673 674@end 675 676@implementation FlutterTextInputPlugin { 677 FlutterTextInputView* _view; 678 FlutterTextInputView* _secureView; 679 FlutterTextInputView* _activeView; 680 FlutterTextInputViewAccessibilityHider* _inputHider; 681} 682 683@synthesize textInputDelegate = _textInputDelegate; 684 685- (instancetype)init { 686 self = [super init]; 687 688 if (self) { 689 _view = [[FlutterTextInputView alloc] init]; 690 _view.secureTextEntry = NO; 691 _secureView = [[FlutterTextInputView alloc] init]; 692 _secureView.secureTextEntry = YES; 693 694 _activeView = _view; 695 _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init]; 696 } 697 698 return self; 699} 700 701- (void)dealloc { 702 [self hideTextInput]; 703 [_view release]; 704 [_secureView release]; 705 [_inputHider release]; 706 707 [super dealloc]; 708} 709 710- (UIView<UITextInput>*)textInputView { 711 return _activeView; 712} 713 714- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 715 NSString* method = call.method; 716 id args = call.arguments; 717 if ([method isEqualToString:@"TextInput.show"]) { 718 [self showTextInput]; 719 result(nil); 720 } else if ([method isEqualToString:@"TextInput.hide"]) { 721 [self hideTextInput]; 722 result(nil); 723 } else if ([method isEqualToString:@"TextInput.setClient"]) { 724 [self setTextInputClient:[args[0] intValue] withConfiguration:args[1]]; 725 result(nil); 726 } else if ([method isEqualToString:@"TextInput.setEditingState"]) { 727 [self setTextInputEditingState:args]; 728 result(nil); 729 } else if ([method isEqualToString:@"TextInput.clearClient"]) { 730 [self clearTextInputClient]; 731 result(nil); 732 } else { 733 result(FlutterMethodNotImplemented); 734 } 735} 736 737- (void)showTextInput { 738 NSAssert([UIApplication sharedApplication].keyWindow != nullptr, 739 @"The application must have a key window since the keyboard client " 740 @"must be part of the responder chain to function"); 741 _activeView.textInputDelegate = _textInputDelegate; 742 [_inputHider addSubview:_activeView]; 743 [[UIApplication sharedApplication].keyWindow addSubview:_inputHider]; 744 [_activeView becomeFirstResponder]; 745} 746 747- (void)hideTextInput { 748 [_activeView resignFirstResponder]; 749 [_activeView removeFromSuperview]; 750 [_inputHider removeFromSuperview]; 751} 752 753- (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration { 754 NSDictionary* inputType = configuration[@"inputType"]; 755 NSString* keyboardAppearance = configuration[@"keyboardAppearance"]; 756 if ([configuration[@"obscureText"] boolValue]) { 757 _activeView = _secureView; 758 } else { 759 _activeView = _view; 760 } 761 762 _activeView.keyboardType = ToUIKeyboardType(inputType); 763 _activeView.returnKeyType = ToUIReturnKeyType(configuration[@"inputAction"]); 764 _activeView.autocapitalizationType = ToUITextAutoCapitalizationType(configuration); 765 if ([keyboardAppearance isEqualToString:@"Brightness.dark"]) { 766 _activeView.keyboardAppearance = UIKeyboardAppearanceDark; 767 } else if ([keyboardAppearance isEqualToString:@"Brightness.light"]) { 768 _activeView.keyboardAppearance = UIKeyboardAppearanceLight; 769 } else { 770 _activeView.keyboardAppearance = UIKeyboardAppearanceDefault; 771 } 772 NSString* autocorrect = configuration[@"autocorrect"]; 773 _activeView.autocorrectionType = autocorrect && ![autocorrect boolValue] 774 ? UITextAutocorrectionTypeNo 775 : UITextAutocorrectionTypeDefault; 776 [_activeView setTextInputClient:client]; 777 [_activeView reloadInputViews]; 778} 779 780- (void)setTextInputEditingState:(NSDictionary*)state { 781 [_activeView setTextInputState:state]; 782} 783 784- (void)clearTextInputClient { 785 [_activeView setTextInputClient:0]; 786} 787 788@end 789