Flutter Engine
FlutterTextInputPlugin.mm
Go to the documentation of this file.
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 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h"
6 
7 #import <objc/message.h>
8 
9 #include <algorithm>
10 #include <memory>
11 
12 #include "flutter/shell/platform/common/text_input_model.h"
13 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
14 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h"
15 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
16 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
17 
18 static NSString* const kTextInputChannel = @"flutter/textinput";
19 
20 // See https://api.flutter.dev/flutter/services/SystemChannels/textInput-constant.html
21 static NSString* const kSetClientMethod = @"TextInput.setClient";
22 static NSString* const kShowMethod = @"TextInput.show";
23 static NSString* const kHideMethod = @"TextInput.hide";
24 static NSString* const kClearClientMethod = @"TextInput.clearClient";
25 static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
26 static NSString* const kSetEditableSizeAndTransform = @"TextInput.setEditableSizeAndTransform";
27 static NSString* const kSetCaretRect = @"TextInput.setCaretRect";
28 static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState";
29 static NSString* const kPerformAction = @"TextInputClient.performAction";
30 static NSString* const kMultilineInputType = @"TextInputType.multiline";
31 
32 static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream";
33 static NSString* const kTextAffinityUpstream = @"TextAffinity.upstream";
34 
35 static NSString* const kTextInputAction = @"inputAction";
36 static NSString* const kTextInputType = @"inputType";
37 static NSString* const kTextInputTypeName = @"name";
38 
39 static NSString* const kSelectionBaseKey = @"selectionBase";
40 static NSString* const kSelectionExtentKey = @"selectionExtent";
41 static NSString* const kSelectionAffinityKey = @"selectionAffinity";
42 static NSString* const kSelectionIsDirectionalKey = @"selectionIsDirectional";
43 static NSString* const kComposingBaseKey = @"composingBase";
44 static NSString* const kComposingExtentKey = @"composingExtent";
45 static NSString* const kTextKey = @"text";
46 static NSString* const kTransformKey = @"transform";
47 
48 /**
49  * The affinity of the current cursor position. If the cursor is at a position representing
50  * a line break, the cursor may be drawn either at the end of the current line (upstream)
51  * or at the beginning of the next (downstream).
52  */
53 typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
54  FlutterTextAffinityUpstream,
55  FlutterTextAffinityDownstream
56 };
57 
58 /*
59  * Updates a range given base and extent fields.
60  */
62  NSNumber* extent,
63  const flutter::TextRange& range) {
64  if (base == nil || extent == nil) {
65  return range;
66  }
67  if (base.intValue == -1 && extent.intValue == -1) {
68  return flutter::TextRange(0, 0);
69  }
70  return flutter::TextRange([base unsignedLongValue], [extent unsignedLongValue]);
71 }
72 
73 /**
74  * Private properties of FlutterTextInputPlugin.
75  */
76 @interface FlutterTextInputPlugin ()
77 
78 /**
79  * A text input context, representing a connection to the Cocoa text input system.
80  */
81 @property(nonatomic) NSTextInputContext* textInputContext;
82 
83 /**
84  * The channel used to communicate with Flutter.
85  */
86 @property(nonatomic) FlutterMethodChannel* channel;
87 
88 /**
89  * The FlutterViewController to manage input for.
90  */
91 @property(nonatomic, weak) FlutterViewController* flutterViewController;
92 
93 /**
94  * Whether the text input is shown in the view.
95  *
96  * Defaults to TRUE on startup.
97  */
98 @property(nonatomic) BOOL shown;
99 
100 /**
101  * The current state of the keyboard and pressed keys.
102  */
103 @property(nonatomic) uint64_t previouslyPressedFlags;
104 
105 /**
106  * The affinity for the current cursor position.
107  */
108 @property FlutterTextAffinity textAffinity;
109 
110 /**
111  * ID of the text input client.
112  */
113 @property(nonatomic, nonnull) NSNumber* clientID;
114 
115 /**
116  * Keyboard type of the client. See available options:
117  * https://api.flutter.dev/flutter/services/TextInputType-class.html
118  */
119 @property(nonatomic, nonnull) NSString* inputType;
120 
121 /**
122  * An action requested by the user on the input client. See available options:
123  * https://api.flutter.dev/flutter/services/TextInputAction-class.html
124  */
125 @property(nonatomic, nonnull) NSString* inputAction;
126 
127 /**
128  * Handles a Flutter system message on the text input channel.
129  */
130 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
131 
132 /**
133  * Updates the text input model with state received from the framework via the
134  * TextInput.setEditingState message.
135  */
136 - (void)setEditingState:(NSDictionary*)state;
137 
138 /**
139  * Informs the Flutter framework of changes to the text input model's state.
140  */
141 - (void)updateEditState;
142 
143 /**
144  * Updates the stringValue and selectedRange that stored in the NSTextView interface
145  * that this plugin inherits from.
146  *
147  * If there is a FlutterTextField uses this plugin as its field editor, this method
148  * will update the stringValue and selectedRange through the API of the FlutterTextField.
149  */
150 - (void)updateTextAndSelection;
151 
152 @end
153 
154 @implementation FlutterTextInputPlugin {
155  /**
156  * The currently active text input model.
157  */
158  std::unique_ptr<flutter::TextInputModel> _activeModel;
159 
160  /**
161  * Transform for current the editable. Used to determine position of accent selection menu.
162  */
163  CATransform3D _editableTransform;
164 
165  /**
166  * Current position of caret in local (editable) coordinates.
167  */
168  CGRect _caretRect;
169 }
170 
171 - (instancetype)initWithViewController:(FlutterViewController*)viewController {
172  // The view needs a non-zero frame.
173  self = [super initWithFrame:NSMakeRect(0, 0, 1, 1)];
174  if (self != nil) {
175  _flutterViewController = viewController;
177  binaryMessenger:viewController.engine.binaryMessenger
178  codec:[FlutterJSONMethodCodec sharedInstance]];
179  _shown = FALSE;
180  // NSTextView does not support _weak reference, so this class has to
181  // use __unsafe_unretained and manage the reference by itself.
182  //
183  // Since the dealloc removes the handler, the pointer should
184  // be valid if the handler is ever called.
185  __unsafe_unretained FlutterTextInputPlugin* unsafeSelf = self;
186  [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
187  [unsafeSelf handleMethodCall:call result:result];
188  }];
189  _textInputContext = [[NSTextInputContext alloc] initWithClient:self];
190  _previouslyPressedFlags = 0;
191 
192  _flutterViewController = viewController;
193 
194  // Initialize with the zero matrix which is not
195  // an affine transform.
196  _editableTransform = CATransform3D();
197  _caretRect = CGRectNull;
198  }
199  return self;
200 }
201 
203  if (!self.flutterViewController.viewLoaded) {
204  return false;
205  }
206  return [self.flutterViewController.view.window firstResponder] == self;
207 }
208 
209 - (void)dealloc {
210  [_channel setMethodCallHandler:nil];
211 }
212 
213 #pragma mark - Private
214 
215 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
216  BOOL handled = YES;
217  NSString* method = call.method;
218  if ([method isEqualToString:kSetClientMethod]) {
219  if (!call.arguments[0] || !call.arguments[1]) {
221  errorWithCode:@"error"
222  message:@"Missing arguments"
223  details:@"Missing arguments while trying to set a text input client"]);
224  return;
225  }
226  NSNumber* clientID = call.arguments[0];
227  if (clientID != nil) {
228  NSDictionary* config = call.arguments[1];
229 
230  _clientID = clientID;
231  _inputAction = config[kTextInputAction];
232  NSDictionary* inputTypeInfo = config[kTextInputType];
233  _inputType = inputTypeInfo[kTextInputTypeName];
234  self.textAffinity = FlutterTextAffinityUpstream;
235 
236  _activeModel = std::make_unique<flutter::TextInputModel>();
237  }
238  } else if ([method isEqualToString:kShowMethod]) {
239  _shown = TRUE;
240  [_textInputContext activate];
241  } else if ([method isEqualToString:kHideMethod]) {
242  _shown = FALSE;
243  [_textInputContext deactivate];
244  } else if ([method isEqualToString:kClearClientMethod]) {
245  _clientID = nil;
246  _inputAction = nil;
247  _inputType = nil;
248  _activeModel = nullptr;
249  } else if ([method isEqualToString:kSetEditingStateMethod]) {
250  NSDictionary* state = call.arguments;
251  [self setEditingState:state];
252 
253  // Close the loop, since the framework state could have been updated by the
254  // engine since it sent this update, and needs to now be made to match the
255  // engine's version of the state.
256  [self updateEditState];
257  } else if ([method isEqualToString:kSetEditableSizeAndTransform]) {
258  NSDictionary* state = call.arguments;
259  [self setEditableTransform:state[kTransformKey]];
260  } else if ([method isEqualToString:kSetCaretRect]) {
261  NSDictionary* rect = call.arguments;
262  [self updateCaretRect:rect];
263  } else {
264  handled = NO;
265  }
266  result(handled ? nil : FlutterMethodNotImplemented);
267 }
268 
269 - (void)setEditableTransform:(NSArray*)matrix {
270  CATransform3D* transform = &_editableTransform;
271 
272  transform->m11 = [matrix[0] doubleValue];
273  transform->m12 = [matrix[1] doubleValue];
274  transform->m13 = [matrix[2] doubleValue];
275  transform->m14 = [matrix[3] doubleValue];
276 
277  transform->m21 = [matrix[4] doubleValue];
278  transform->m22 = [matrix[5] doubleValue];
279  transform->m23 = [matrix[6] doubleValue];
280  transform->m24 = [matrix[7] doubleValue];
281 
282  transform->m31 = [matrix[8] doubleValue];
283  transform->m32 = [matrix[9] doubleValue];
284  transform->m33 = [matrix[10] doubleValue];
285  transform->m34 = [matrix[11] doubleValue];
286 
287  transform->m41 = [matrix[12] doubleValue];
288  transform->m42 = [matrix[13] doubleValue];
289  transform->m43 = [matrix[14] doubleValue];
290  transform->m44 = [matrix[15] doubleValue];
291 }
292 
293 - (void)updateCaretRect:(NSDictionary*)dictionary {
294  NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
295  dictionary[@"height"] != nil,
296  @"Expected a dictionary representing a CGRect, got %@", dictionary);
297  _caretRect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
298  [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
299 }
300 
301 - (void)setEditingState:(NSDictionary*)state {
302  NSString* selectionAffinity = state[kSelectionAffinityKey];
303  if (selectionAffinity != nil) {
304  _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
305  ? FlutterTextAffinityUpstream
306  : FlutterTextAffinityDownstream;
307  }
308 
309  NSString* text = state[kTextKey];
310  if (text != nil) {
311  _activeModel->SetText([text UTF8String]);
312  }
313 
314  flutter::TextRange selected_range = RangeFromBaseExtent(
315  state[kSelectionBaseKey], state[kSelectionExtentKey], _activeModel->selection());
316  _activeModel->SetSelection(selected_range);
317 
318  flutter::TextRange composing_range = RangeFromBaseExtent(
319  state[kComposingBaseKey], state[kComposingExtentKey], _activeModel->composing_range());
320  size_t cursor_offset = selected_range.base() - composing_range.start();
321  _activeModel->SetComposingRange(composing_range, cursor_offset);
322  [_client becomeFirstResponder];
323  [self updateTextAndSelection];
324 }
325 
326 - (void)updateEditState {
327  if (_activeModel == nullptr) {
328  return;
329  }
330 
331  NSString* const textAffinity = (self.textAffinity == FlutterTextAffinityUpstream)
334 
335  int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
336  int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
337 
338  NSDictionary* state = @{
339  kSelectionBaseKey : @(_activeModel->selection().base()),
340  kSelectionExtentKey : @(_activeModel->selection().extent()),
341  kSelectionAffinityKey : textAffinity,
343  kComposingBaseKey : @(composingBase),
344  kComposingExtentKey : @(composingExtent),
345  kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()]
346  };
347 
348  [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[ self.clientID, state ]];
349  [self updateTextAndSelection];
350 }
351 
352 - (void)updateTextAndSelection {
353  NSAssert(_activeModel != nullptr, @"Flutter text model must not be null.");
354  NSString* text = @(_activeModel->GetText().data());
355  int start = _activeModel->selection().base();
356  int extend = _activeModel->selection().extent();
357  NSRange selection = NSMakeRange(MIN(start, extend), ABS(start - extend));
358  // There may be a native text field client if VoiceOver is on.
359  // In this case, this plugin has to update text and selection through
360  // the client in order for VoiceOver to announce the text editing
361  // properly.
362  if (_client) {
363  [_client updateString:text withSelection:selection];
364  } else {
365  self.string = text;
366  [self setSelectedRange:selection];
367  }
368 }
369 
370 #pragma mark -
371 #pragma mark FlutterKeySecondaryResponder
372 
373 /**
374  * Handles key down events received from the view controller, responding YES if
375  * the event was handled.
376  *
377  * Note, the Apple docs suggest that clients should override essentially all the
378  * mouse and keyboard event-handling methods of NSResponder. However, experimentation
379  * indicates that only key events are processed by the native layer; Flutter processes
380  * mouse events. Additionally, processing both keyUp and keyDown results in duplicate
381  * processing of the same keys.
382  */
383 - (BOOL)handleKeyEvent:(NSEvent*)event {
384  if (event.type == NSEventTypeKeyUp ||
385  (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
386  return NO;
387  }
388  _previouslyPressedFlags = event.modifierFlags;
389  if (!_shown) {
390  return NO;
391  }
392  return [_textInputContext handleEvent:event];
393 }
394 
395 #pragma mark -
396 #pragma mark NSResponder
397 
398 - (void)keyDown:(NSEvent*)event {
399  [self.flutterViewController keyDown:event];
400 }
401 
402 - (void)keyUp:(NSEvent*)event {
403  [self.flutterViewController keyUp:event];
404 }
405 
406 - (BOOL)performKeyEquivalent:(NSEvent*)event {
407  return [self.flutterViewController performKeyEquivalent:event];
408 }
409 
410 - (void)flagsChanged:(NSEvent*)event {
411  [self.flutterViewController flagsChanged:event];
412 }
413 
414 - (void)mouseDown:(NSEvent*)event {
415  [self.flutterViewController mouseDown:event];
416 }
417 
418 - (void)mouseUp:(NSEvent*)event {
419  [self.flutterViewController mouseUp:event];
420 }
421 
422 - (void)mouseDragged:(NSEvent*)event {
423  [self.flutterViewController mouseDragged:event];
424 }
425 
426 - (void)rightMouseDown:(NSEvent*)event {
427  [self.flutterViewController rightMouseDown:event];
428 }
429 
430 - (void)rightMouseUp:(NSEvent*)event {
431  [self.flutterViewController rightMouseUp:event];
432 }
433 
434 - (void)rightMouseDragged:(NSEvent*)event {
435  [self.flutterViewController rightMouseDragged:event];
436 }
437 
438 - (void)otherMouseDown:(NSEvent*)event {
439  [self.flutterViewController otherMouseDown:event];
440 }
441 
442 - (void)otherMouseUp:(NSEvent*)event {
443  [self.flutterViewController otherMouseUp:event];
444 }
445 
446 - (void)otherMouseDragged:(NSEvent*)event {
447  [self.flutterViewController otherMouseDragged:event];
448 }
449 
450 - (void)mouseMoved:(NSEvent*)event {
451  [self.flutterViewController mouseMoved:event];
452 }
453 
454 - (void)scrollWheel:(NSEvent*)event {
455  [self.flutterViewController scrollWheel:event];
456 }
457 
458 #pragma mark -
459 #pragma mark NSTextInputClient
460 
461 - (void)insertText:(id)string replacementRange:(NSRange)range {
462  if (_activeModel == nullptr) {
463  return;
464  }
465 
466  if (range.location != NSNotFound) {
467  // The selected range can actually have negative numbers, since it can start
468  // at the end of the range if the user selected the text going backwards.
469  // Cast to a signed type to determine whether or not the selection is reversed.
470  long signedLength = static_cast<long>(range.length);
471  long location = range.location;
472  long textLength = _activeModel->text_range().end();
473 
474  size_t base = std::clamp(location, 0L, textLength);
475  size_t extent = std::clamp(location + signedLength, 0L, textLength);
476  _activeModel->SetSelection(flutter::TextRange(base, extent));
477  }
478 
479  _activeModel->AddText([string UTF8String]);
480  if (_activeModel->composing()) {
481  _activeModel->CommitComposing();
482  _activeModel->EndComposing();
483  }
484  [self updateEditState];
485 }
486 
487 - (void)doCommandBySelector:(SEL)selector {
488  if ([self respondsToSelector:selector]) {
489  // Note: The more obvious [self performSelector...] doesn't give ARC enough information to
490  // handle retain semantics properly. See https://stackoverflow.com/questions/7017281/ for more
491  // information.
492  IMP imp = [self methodForSelector:selector];
493  void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp);
494  func(self, selector, nil);
495  }
496 }
497 
498 - (void)insertNewline:(id)sender {
499  if (_activeModel == nullptr) {
500  return;
501  }
502  if (_activeModel->composing()) {
503  _activeModel->CommitComposing();
504  _activeModel->EndComposing();
505  }
506  if ([self.inputType isEqualToString:kMultilineInputType]) {
507  [self insertText:@"\n" replacementRange:self.selectedRange];
508  }
509  [_channel invokeMethod:kPerformAction arguments:@[ self.clientID, self.inputAction ]];
510 }
511 
512 - (void)setMarkedText:(id)string
513  selectedRange:(NSRange)selectedRange
514  replacementRange:(NSRange)replacementRange {
515  if (_activeModel == nullptr) {
516  return;
517  }
518  if (!_activeModel->composing()) {
519  _activeModel->BeginComposing();
520  }
521 
522  // Input string may be NSString or NSAttributedString.
523  BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
524  NSString* marked_text = isAttributedString ? [string string] : string;
525  _activeModel->UpdateComposingText([marked_text UTF8String]);
526 
527  [self updateEditState];
528 }
529 
530 - (void)unmarkText {
531  if (_activeModel == nullptr) {
532  return;
533  }
534  _activeModel->CommitComposing();
535  _activeModel->EndComposing();
536  [self updateEditState];
537 }
538 
539 - (NSRange)markedRange {
540  if (_activeModel == nullptr) {
541  return NSMakeRange(NSNotFound, 0);
542  }
543  return NSMakeRange(
544  _activeModel->composing_range().base(),
545  _activeModel->composing_range().extent() - _activeModel->composing_range().base());
546 }
547 
548 - (BOOL)hasMarkedText {
549  return _activeModel != nullptr && _activeModel->composing_range().length() > 0;
550 }
551 
552 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
553  actualRange:(NSRangePointer)actualRange {
554  if (_activeModel == nullptr) {
555  return nil;
556  }
557  if (actualRange != nil) {
558  *actualRange = range;
559  }
560  NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
561  NSString* substring = [text substringWithRange:range];
562  return [[NSAttributedString alloc] initWithString:substring attributes:nil];
563 }
564 
565 - (NSArray<NSString*>*)validAttributesForMarkedText {
566  return @[];
567 }
568 
569 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
570  if (!self.flutterViewController.viewLoaded) {
571  return CGRectZero;
572  }
573  // This only determines position of caret instead of any arbitrary range, but it's enough
574  // to properly position accent selection popup
575  if (CATransform3DIsAffine(_editableTransform) && !CGRectEqualToRect(_caretRect, CGRectNull)) {
576  CGRect rect =
577  CGRectApplyAffineTransform(_caretRect, CATransform3DGetAffineTransform(_editableTransform));
578 
579  // convert to window coordinates
580  rect = [self.flutterViewController.flutterView convertRect:rect toView:nil];
581 
582  // convert to screen coordinates
583  return [self.flutterViewController.flutterView.window convertRectToScreen:rect];
584  } else {
585  return CGRectZero;
586  }
587 }
588 
589 - (NSUInteger)characterIndexForPoint:(NSPoint)point {
590  // TODO: Implement.
591  // Note: This function can't easily be implemented under the system-message architecture.
592  return 0;
593 }
594 
595 @end
static NSString *const kClearClientMethod
FlutterMethodChannel * _channel
static NSString *const kSelectionIsDirectionalKey
static NSString *const kTransformKey
static NSString *const kTextInputTypeName
static NSString *const kMultilineInputType
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name, [binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger, [codec] NSObject< FlutterMethodCodec > *codec)
static NSString *const kTextInputChannel
static NSString *const kTextInputType
static NSString *const kTextAffinityUpstream
static NSString *const kComposingBaseKey
static NSString *const kSelectionAffinityKey
GAsyncResult * result
CATransform3D _editableTransform
static NSString *const kSelectionExtentKey
static NSString *const kHideMethod
static flutter::TextRange RangeFromBaseExtent(NSNumber *base, NSNumber *extent, const flutter::TextRange &range)
static NSString *const kSetEditableSizeAndTransform
static NSString *const kShowMethod
FlKeyEvent * event
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static NSString *const kPerformAction
static NSString *const kUpdateEditStateResponseMethod
size_t start() const
Definition: text_range.h:42
static NSString *const kComposingExtentKey
void(^ FlutterResult)(id _Nullable result)
size_t base() const
Definition: text_range.h:30
int BOOL
Definition: windows_types.h:37
CGRect _caretRect
static NSString *const kTextKey
static NSString *const kSetCaretRect
std::u16string text
fml::scoped_nsobject< UIViewController > _flutterViewController
int32_t id
static NSString *const kSetClientMethod
static NSString *const kSetEditingStateMethod
static NSString *const kTextInputAction
return FALSE
AtkStateType state
static NSString *const kTextAffinityDownstream
static NSString *const kSelectionBaseKey
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented