• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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