Flutter macOS Embedder
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 
6 
7 #import <Foundation/Foundation.h>
8 #import <objc/message.h>
9 
10 #include <algorithm>
11 #include <memory>
12 
13 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
20 
21 static NSString* const kTextInputChannel = @"flutter/textinput";
22 
23 #pragma mark - TextInput channel method names
24 // See https://api.flutter.dev/flutter/services/SystemChannels/textInput-constant.html
25 static NSString* const kSetClientMethod = @"TextInput.setClient";
26 static NSString* const kShowMethod = @"TextInput.show";
27 static NSString* const kHideMethod = @"TextInput.hide";
28 static NSString* const kClearClientMethod = @"TextInput.clearClient";
29 static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
30 static NSString* const kSetEditableSizeAndTransform = @"TextInput.setEditableSizeAndTransform";
31 static NSString* const kSetCaretRect = @"TextInput.setCaretRect";
32 static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState";
34  @"TextInputClient.updateEditingStateWithDeltas";
35 static NSString* const kPerformAction = @"TextInputClient.performAction";
36 static NSString* const kPerformSelectors = @"TextInputClient.performSelectors";
37 static NSString* const kMultilineInputType = @"TextInputType.multiline";
38 
39 #pragma mark - TextInputConfiguration field names
40 static NSString* const kViewId = @"viewId";
41 static NSString* const kSecureTextEntry = @"obscureText";
42 static NSString* const kTextInputAction = @"inputAction";
43 static NSString* const kEnableDeltaModel = @"enableDeltaModel";
44 static NSString* const kTextInputType = @"inputType";
45 static NSString* const kTextInputTypeName = @"name";
46 static NSString* const kSelectionBaseKey = @"selectionBase";
47 static NSString* const kSelectionExtentKey = @"selectionExtent";
48 static NSString* const kSelectionAffinityKey = @"selectionAffinity";
49 static NSString* const kSelectionIsDirectionalKey = @"selectionIsDirectional";
50 static NSString* const kComposingBaseKey = @"composingBase";
51 static NSString* const kComposingExtentKey = @"composingExtent";
52 static NSString* const kTextKey = @"text";
53 static NSString* const kTransformKey = @"transform";
54 static NSString* const kAssociatedAutofillFields = @"fields";
55 
56 // TextInputConfiguration.autofill and sub-field names
57 static NSString* const kAutofillProperties = @"autofill";
58 static NSString* const kAutofillId = @"uniqueIdentifier";
59 static NSString* const kAutofillEditingValue = @"editingValue";
60 static NSString* const kAutofillHints = @"hints";
61 
62 // TextAffinity types
63 static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream";
64 static NSString* const kTextAffinityUpstream = @"TextAffinity.upstream";
65 
66 // TextInputAction types
67 static NSString* const kInputActionNewline = @"TextInputAction.newline";
68 
69 #pragma mark - Enums
70 /**
71  * The affinity of the current cursor position. If the cursor is at a position
72  * representing a soft line break, the cursor may be drawn either at the end of
73  * the current line (upstream) or at the beginning of the next (downstream).
74  */
75 typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
76  kFlutterTextAffinityUpstream,
77  kFlutterTextAffinityDownstream
78 };
79 
80 #pragma mark - Static functions
81 
82 /*
83  * Updates a range given base and extent fields.
84  */
86  NSNumber* extent,
87  const flutter::TextRange& range) {
88  if (base == nil || extent == nil) {
89  return range;
90  }
91  if (base.intValue == -1 && extent.intValue == -1) {
92  return flutter::TextRange(0, 0);
93  }
94  return flutter::TextRange([base unsignedLongValue], [extent unsignedLongValue]);
95 }
96 
97 // Returns the autofill hint content type, if specified; otherwise nil.
98 static NSString* GetAutofillHint(NSDictionary* autofill) {
99  NSArray<NSString*>* hints = autofill[kAutofillHints];
100  return hints.count > 0 ? hints[0] : nil;
101 }
102 
103 // Returns the text content type for the specified TextInputConfiguration.
104 // NSTextContentType is only available for macOS 11.0 and later.
105 static NSTextContentType GetTextContentType(NSDictionary* configuration)
106  API_AVAILABLE(macos(11.0)) {
107  // Check autofill hints.
108  NSDictionary* autofill = configuration[kAutofillProperties];
109  if (autofill) {
110  NSString* hint = GetAutofillHint(autofill);
111  if ([hint isEqualToString:@"username"]) {
112  return NSTextContentTypeUsername;
113  }
114  if ([hint isEqualToString:@"password"]) {
115  return NSTextContentTypePassword;
116  }
117  if ([hint isEqualToString:@"oneTimeCode"]) {
118  return NSTextContentTypeOneTimeCode;
119  }
120  }
121  // If no autofill hints, guess based on other attributes.
122  if ([configuration[kSecureTextEntry] boolValue]) {
123  return NSTextContentTypePassword;
124  }
125  return nil;
126 }
127 
128 // Returns YES if configuration describes a field for which autocomplete should be enabled for
129 // the specified TextInputConfiguration. Autocomplete is enabled by default, but will be disabled
130 // if the field is password-related, or if the configuration contains no autofill settings.
131 static BOOL EnableAutocompleteForTextInputConfiguration(NSDictionary* configuration) {
132  // Disable if obscureText is set.
133  if ([configuration[kSecureTextEntry] boolValue]) {
134  return NO;
135  }
136 
137  // Disable if autofill properties are not set.
138  NSDictionary* autofill = configuration[kAutofillProperties];
139  if (autofill == nil) {
140  return NO;
141  }
142 
143  // Disable if autofill properties indicate a username/password.
144  // See: https://github.com/flutter/flutter/issues/119824
145  NSString* hint = GetAutofillHint(autofill);
146  if ([hint isEqualToString:@"password"] || [hint isEqualToString:@"username"]) {
147  return NO;
148  }
149  return YES;
150 }
151 
152 // Returns YES if configuration describes a field for which autocomplete should be enabled.
153 // Autocomplete is enabled by default, but will be disabled if the field is password-related, or if
154 // the configuration contains no autofill settings.
155 //
156 // In the case where the current field is part of an AutofillGroup, the configuration will have
157 // a fields attribute with a list of TextInputConfigurations, one for each field. In the case where
158 // any field in the group disables autocomplete, we disable it for all.
159 static BOOL EnableAutocomplete(NSDictionary* configuration) {
160  for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
162  return NO;
163  }
164  }
165 
166  // Check the top-level TextInputConfiguration.
167  return EnableAutocompleteForTextInputConfiguration(configuration);
168 }
169 
170 #pragma mark - NSEvent (KeyEquivalentMarker) protocol
171 
173 
174 // Internally marks that the event was received through performKeyEquivalent:.
175 // When text editing is active, keyboard events that have modifier keys pressed
176 // are received through performKeyEquivalent: instead of keyDown:. If such event
177 // is passed to TextInputContext but doesn't result in a text editing action it
178 // needs to be forwarded by FlutterKeyboardManager to the next responder.
179 - (void)markAsKeyEquivalent;
180 
181 // Returns YES if the event is marked as a key equivalent.
182 - (BOOL)isKeyEquivalent;
183 
184 @end
185 
186 @implementation NSEvent (KeyEquivalentMarker)
187 
188 // This field doesn't need a value because only its address is used as a unique identifier.
189 static char markerKey;
190 
191 - (void)markAsKeyEquivalent {
192  objc_setAssociatedObject(self, &markerKey, @true, OBJC_ASSOCIATION_RETAIN);
193 }
194 
195 - (BOOL)isKeyEquivalent {
196  return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
197 }
198 
199 @end
200 
201 #pragma mark - FlutterTextInputPlugin private interface
202 
203 /**
204  * Private properties of FlutterTextInputPlugin.
205  */
206 @interface FlutterTextInputPlugin () {
207  /**
208  * A text input context, representing a connection to the Cocoa text input system.
209  */
210  NSTextInputContext* _textInputContext;
211 
212  /**
213  * The channel used to communicate with Flutter.
214  */
216 
217  /**
218  * The FlutterViewController to manage input for.
219  */
221 
222  /**
223  * Used to obtain view controller on client creation
224  */
225  __weak id<FlutterTextInputPluginDelegate> _delegate;
226 
227  /**
228  * Whether the text input is shown in the view.
229  *
230  * Defaults to TRUE on startup.
231  */
232  BOOL _shown;
233 
234  /**
235  * The current state of the keyboard and pressed keys.
236  */
238 
239  /**
240  * The affinity for the current cursor position.
241  */
242  FlutterTextAffinity _textAffinity;
243 
244  /**
245  * ID of the text input client.
246  */
247  NSNumber* _clientID;
248 
249  /**
250  * Keyboard type of the client. See available options:
251  * https://api.flutter.dev/flutter/services/TextInputType-class.html
252  */
253  NSString* _inputType;
254 
255  /**
256  * An action requested by the user on the input client. See available options:
257  * https://api.flutter.dev/flutter/services/TextInputAction-class.html
258  */
259  NSString* _inputAction;
260 
261  /**
262  * Set to true if the last event fed to the input context produced a text editing command
263  * or text output. It is reset to false at the beginning of every key event, and is only
264  * used while processing this event.
265  */
267 
268  /**
269  * Whether to enable the sending of text input updates from the engine to the
270  * framework as TextEditingDeltas rather than as one TextEditingValue.
271  * For more information on the delta model, see:
272  * https://master-api.flutter.dev/flutter/services/TextInputConfiguration/enableDeltaModel.html
273  */
275 
276  /**
277  * Used to gather multiple selectors performed in one run loop turn. These
278  * will be all sent in one platform channel call so that the framework can process
279  * them in single microtask.
280  */
281  NSMutableArray* _pendingSelectors;
282 
283  /**
284  * The currently active text input model.
285  */
286  std::unique_ptr<flutter::TextInputModel> _activeModel;
287 
288  /**
289  * Transform for current the editable. Used to determine position of accent selection menu.
290  */
291  CATransform3D _editableTransform;
292 
293  /**
294  * Current position of caret in local (editable) coordinates.
295  */
296  CGRect _caretRect;
297 }
298 
299 /**
300  * Handles a Flutter system message on the text input channel.
301  */
302 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
303 
304 /**
305  * Updates the text input model with state received from the framework via the
306  * TextInput.setEditingState message.
307  */
308 - (void)setEditingState:(NSDictionary*)state;
309 
310 /**
311  * Informs the Flutter framework of changes to the text input model's state by
312  * sending the entire new state.
313  */
314 - (void)updateEditState;
315 
316 /**
317  * Informs the Flutter framework of changes to the text input model's state by
318  * sending only the difference.
319  */
320 - (void)updateEditStateWithDelta:(const flutter::TextEditingDelta)delta;
321 
322 /**
323  * Updates the stringValue and selectedRange that stored in the NSTextView interface
324  * that this plugin inherits from.
325  *
326  * If there is a FlutterTextField uses this plugin as its field editor, this method
327  * will update the stringValue and selectedRange through the API of the FlutterTextField.
328  */
329 - (void)updateTextAndSelection;
330 
331 /**
332  * Return the string representation of the current textAffinity as it should be
333  * sent over the FlutterMethodChannel.
334  */
335 - (NSString*)textAffinityString;
336 
337 /**
338  * Allow overriding run loop mode for test.
339  */
340 @property(readwrite, nonatomic) NSString* customRunLoopMode;
341 @property(nonatomic) NSTextInputContext* textInputContext;
342 
343 @end
344 
345 #pragma mark - FlutterTextInputPlugin
346 
347 @implementation FlutterTextInputPlugin
348 
349 - (instancetype)initWithDelegate:(id<FlutterTextInputPluginDelegate>)delegate {
350  // The view needs an empty frame otherwise it is visible on dark background.
351  // https://github.com/flutter/flutter/issues/118504
352  self = [super initWithFrame:NSZeroRect];
353  self.clipsToBounds = YES;
354  if (self != nil) {
355  _delegate = delegate;
356  _channel = [FlutterMethodChannel methodChannelWithName:kTextInputChannel
357  binaryMessenger:_delegate.binaryMessenger
358  codec:[FlutterJSONMethodCodec sharedInstance]];
359  _shown = FALSE;
360  // NSTextView does not support _weak reference, so this class has to
361  // use __unsafe_unretained and manage the reference by itself.
362  //
363  // Since the dealloc removes the handler, the pointer should
364  // be valid if the handler is ever called.
365  __unsafe_unretained FlutterTextInputPlugin* unsafeSelf = self;
366  [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
367  [unsafeSelf handleMethodCall:call result:result];
368  }];
369  _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
370  _previouslyPressedFlags = 0;
371 
372  // Initialize with the zero matrix which is not
373  // an affine transform.
374  _editableTransform = CATransform3D();
375  _caretRect = CGRectNull;
376  }
377  return self;
378 }
379 
380 - (BOOL)isFirstResponder {
381  if (!_currentViewController.viewLoaded) {
382  return false;
383  }
384  return [_currentViewController.view.window firstResponder] == self;
385 }
386 
387 - (void)dealloc {
388  [_channel setMethodCallHandler:nil];
389 }
390 
391 #pragma mark - Private
392 
393 - (void)resignAndRemoveFromSuperview {
394  if (self.superview != nil) {
395  [self.window makeFirstResponder:_currentViewController.flutterView];
396  [self removeFromSuperview];
397  }
398 }
399 
400 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
401  BOOL handled = YES;
402  NSString* method = call.method;
403  if ([method isEqualToString:kSetClientMethod]) {
404  FML_DCHECK(_currentViewController == nil);
405  if (!call.arguments[0] || !call.arguments[1]) {
406  result([FlutterError
407  errorWithCode:@"error"
408  message:@"Missing arguments"
409  details:@"Missing arguments while trying to set a text input client"]);
410  return;
411  }
412  NSNumber* clientID = call.arguments[0];
413  if (clientID != nil) {
414  NSDictionary* config = call.arguments[1];
415 
416  _clientID = clientID;
417  _inputAction = config[kTextInputAction];
418  _enableDeltaModel = [config[kEnableDeltaModel] boolValue];
419  NSDictionary* inputTypeInfo = config[kTextInputType];
420  _inputType = inputTypeInfo[kTextInputTypeName];
421  _textAffinity = kFlutterTextAffinityUpstream;
422  self.automaticTextCompletionEnabled = EnableAutocomplete(config);
423  if (@available(macOS 11.0, *)) {
424  self.contentType = GetTextContentType(config);
425  }
426 
427  _activeModel = std::make_unique<flutter::TextInputModel>();
428  NSNumber* viewId = config[kViewId];
429  _currentViewController = [_delegate viewControllerForIdentifier:viewId.longLongValue];
430  FML_DCHECK(_currentViewController != nil);
431  }
432  } else if ([method isEqualToString:kShowMethod]) {
433  FML_DCHECK(_currentViewController != nil);
434  // Ensure the plugin is in hierarchy. Only do this with accessibility disabled.
435  // When accessibility is enabled cocoa will reparent the plugin inside
436  // FlutterTextField in [FlutterTextField startEditing].
437  if (_client == nil) {
438  [_currentViewController.view addSubview:self];
439  }
440  [self.window makeFirstResponder:self];
441  _shown = TRUE;
442  } else if ([method isEqualToString:kHideMethod]) {
443  [self resignAndRemoveFromSuperview];
444  _shown = FALSE;
445  } else if ([method isEqualToString:kClearClientMethod]) {
446  FML_DCHECK(_currentViewController != nil);
447  [self resignAndRemoveFromSuperview];
448  // If there's an active mark region, commit it, end composing, and clear the IME's mark text.
449  if (_activeModel && _activeModel->composing()) {
450  _activeModel->CommitComposing();
451  _activeModel->EndComposing();
452  }
453  [_textInputContext discardMarkedText];
454 
455  _clientID = nil;
456  _inputAction = nil;
457  _enableDeltaModel = NO;
458  _inputType = nil;
459  _activeModel = nullptr;
460  _currentViewController = nil;
461  } else if ([method isEqualToString:kSetEditingStateMethod]) {
462  FML_DCHECK(_currentViewController != nil);
463  NSDictionary* state = call.arguments;
464  [self setEditingState:state];
465  } else if ([method isEqualToString:kSetEditableSizeAndTransform]) {
466  FML_DCHECK(_currentViewController != nil);
467  NSDictionary* state = call.arguments;
468  [self setEditableTransform:state[kTransformKey]];
469  } else if ([method isEqualToString:kSetCaretRect]) {
470  FML_DCHECK(_currentViewController != nil);
471  NSDictionary* rect = call.arguments;
472  [self updateCaretRect:rect];
473  } else {
474  handled = NO;
475  }
476  result(handled ? nil : FlutterMethodNotImplemented);
477 }
478 
479 - (void)setEditableTransform:(NSArray*)matrix {
480  CATransform3D* transform = &_editableTransform;
481 
482  transform->m11 = [matrix[0] doubleValue];
483  transform->m12 = [matrix[1] doubleValue];
484  transform->m13 = [matrix[2] doubleValue];
485  transform->m14 = [matrix[3] doubleValue];
486 
487  transform->m21 = [matrix[4] doubleValue];
488  transform->m22 = [matrix[5] doubleValue];
489  transform->m23 = [matrix[6] doubleValue];
490  transform->m24 = [matrix[7] doubleValue];
491 
492  transform->m31 = [matrix[8] doubleValue];
493  transform->m32 = [matrix[9] doubleValue];
494  transform->m33 = [matrix[10] doubleValue];
495  transform->m34 = [matrix[11] doubleValue];
496 
497  transform->m41 = [matrix[12] doubleValue];
498  transform->m42 = [matrix[13] doubleValue];
499  transform->m43 = [matrix[14] doubleValue];
500  transform->m44 = [matrix[15] doubleValue];
501 }
502 
503 - (void)updateCaretRect:(NSDictionary*)dictionary {
504  NSAssert(dictionary[@"x"] != nil && dictionary[@"y"] != nil && dictionary[@"width"] != nil &&
505  dictionary[@"height"] != nil,
506  @"Expected a dictionary representing a CGRect, got %@", dictionary);
507  _caretRect = CGRectMake([dictionary[@"x"] doubleValue], [dictionary[@"y"] doubleValue],
508  [dictionary[@"width"] doubleValue], [dictionary[@"height"] doubleValue]);
509 }
510 
511 - (void)setEditingState:(NSDictionary*)state {
512  NSString* selectionAffinity = state[kSelectionAffinityKey];
513  if (selectionAffinity != nil) {
514  _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
515  ? kFlutterTextAffinityUpstream
516  : kFlutterTextAffinityDownstream;
517  }
518 
519  NSString* text = state[kTextKey];
520 
521  flutter::TextRange selected_range = RangeFromBaseExtent(
522  state[kSelectionBaseKey], state[kSelectionExtentKey], _activeModel->selection());
523  _activeModel->SetSelection(selected_range);
524 
525  flutter::TextRange composing_range = RangeFromBaseExtent(
526  state[kComposingBaseKey], state[kComposingExtentKey], _activeModel->composing_range());
527 
528  const bool wasComposing = _activeModel->composing();
529  _activeModel->SetText([text UTF8String], selected_range, composing_range);
530  if (composing_range.collapsed() && wasComposing) {
531  [_textInputContext discardMarkedText];
532  }
533  [_client startEditing];
534 
535  [self updateTextAndSelection];
536 }
537 
538 - (NSDictionary*)editingState {
539  if (_activeModel == nullptr) {
540  return nil;
541  }
542 
543  NSString* const textAffinity = [self textAffinityString];
544 
545  int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
546  int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
547 
548  return @{
549  kSelectionBaseKey : @(_activeModel->selection().base()),
550  kSelectionExtentKey : @(_activeModel->selection().extent()),
551  kSelectionAffinityKey : textAffinity,
553  kComposingBaseKey : @(composingBase),
554  kComposingExtentKey : @(composingExtent),
555  kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()] ?: [NSNull null],
556  };
557 }
558 
559 - (void)updateEditState {
560  if (_activeModel == nullptr) {
561  return;
562  }
563 
564  NSDictionary* state = [self editingState];
565  [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[ _clientID, state ]];
566  [self updateTextAndSelection];
567 }
568 
569 - (void)updateEditStateWithDelta:(const flutter::TextEditingDelta)delta {
570  NSUInteger selectionBase = _activeModel->selection().base();
571  NSUInteger selectionExtent = _activeModel->selection().extent();
572  int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
573  int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
574 
575  NSString* const textAffinity = [self textAffinityString];
576 
577  NSDictionary* deltaToFramework = @{
578  @"oldText" : @(delta.old_text().c_str()),
579  @"deltaText" : @(delta.delta_text().c_str()),
580  @"deltaStart" : @(delta.delta_start()),
581  @"deltaEnd" : @(delta.delta_end()),
582  @"selectionBase" : @(selectionBase),
583  @"selectionExtent" : @(selectionExtent),
584  @"selectionAffinity" : textAffinity,
585  @"selectionIsDirectional" : @(false),
586  @"composingBase" : @(composingBase),
587  @"composingExtent" : @(composingExtent),
588  };
589 
590  NSDictionary* deltas = @{
591  @"deltas" : @[ deltaToFramework ],
592  };
593 
594  [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod arguments:@[ _clientID, deltas ]];
595  [self updateTextAndSelection];
596 }
597 
598 - (void)updateTextAndSelection {
599  NSAssert(_activeModel != nullptr, @"Flutter text model must not be null.");
600  NSString* text = @(_activeModel->GetText().data());
601  int start = _activeModel->selection().base();
602  int extend = _activeModel->selection().extent();
603  NSRange selection = NSMakeRange(MIN(start, extend), ABS(start - extend));
604  // There may be a native text field client if VoiceOver is on.
605  // In this case, this plugin has to update text and selection through
606  // the client in order for VoiceOver to announce the text editing
607  // properly.
608  if (_client) {
609  [_client updateString:text withSelection:selection];
610  } else {
611  self.string = text;
612  [self setSelectedRange:selection];
613  }
614 }
615 
616 - (NSString*)textAffinityString {
617  return (_textAffinity == kFlutterTextAffinityUpstream) ? kTextAffinityUpstream
619 }
620 
621 - (BOOL)handleKeyEvent:(NSEvent*)event {
622  if (event.type == NSEventTypeKeyUp ||
623  (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
624  return NO;
625  }
626  _previouslyPressedFlags = event.modifierFlags;
627  if (!_shown) {
628  return NO;
629  }
630 
631  _eventProducedOutput = NO;
632  BOOL res = [_textInputContext handleEvent:event];
633  // NSTextInputContext#handleEvent returns YES if the context handles the event. One of the reasons
634  // the event is handled is because it's a key equivalent. But a key equivalent might produce a
635  // text command (indicated by calling doCommandBySelector) or might not (for example, Cmd+Q). In
636  // the latter case, this command somehow has not been executed yet and Flutter must dispatch it to
637  // the next responder. See https://github.com/flutter/flutter/issues/106354 .
638  // The event is also not redispatched if there is IME composition active, because it might be
639  // handled by the IME. See https://github.com/flutter/flutter/issues/134699
640 
641  // both NSEventModifierFlagNumericPad and NSEventModifierFlagFunction are set for arrow keys.
642  bool is_navigation = event.modifierFlags & NSEventModifierFlagFunction &&
643  event.modifierFlags & NSEventModifierFlagNumericPad;
644  bool is_navigation_in_ime = is_navigation && self.hasMarkedText;
645 
646  if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) {
647  return NO;
648  }
649  return res;
650 }
651 
652 #pragma mark -
653 #pragma mark NSResponder
654 
655 - (void)keyDown:(NSEvent*)event {
656  [_currentViewController keyDown:event];
657 }
658 
659 - (void)keyUp:(NSEvent*)event {
660  [_currentViewController keyUp:event];
661 }
662 
663 - (BOOL)performKeyEquivalent:(NSEvent*)event {
664  if ([_currentViewController isDispatchingKeyEvent:event]) {
665  // When NSWindow is nextResponder, keyboard manager will send to it
666  // unhandled events (through [NSWindow keyDown:]). If event has both
667  // control and cmd modifiers set (i.e. cmd+control+space - emoji picker)
668  // NSWindow will then send this event as performKeyEquivalent: to first
669  // responder, which is FlutterTextInputPlugin. If that's the case, the
670  // plugin must not handle the event, otherwise the emoji picker would not
671  // work (due to first responder returning YES from performKeyEquivalent:)
672  // and there would be endless loop, because FlutterViewController will
673  // send the event back to [keyboardManager handleEvent:].
674  return NO;
675  }
676  [event markAsKeyEquivalent];
677  [_currentViewController keyDown:event];
678  return YES;
679 }
680 
681 - (void)flagsChanged:(NSEvent*)event {
682  [_currentViewController flagsChanged:event];
683 }
684 
685 - (void)mouseDown:(NSEvent*)event {
686  [_currentViewController mouseDown:event];
687 }
688 
689 - (void)mouseUp:(NSEvent*)event {
690  [_currentViewController mouseUp:event];
691 }
692 
693 - (void)mouseDragged:(NSEvent*)event {
694  [_currentViewController mouseDragged:event];
695 }
696 
697 - (void)rightMouseDown:(NSEvent*)event {
698  [_currentViewController rightMouseDown:event];
699 }
700 
701 - (void)rightMouseUp:(NSEvent*)event {
702  [_currentViewController rightMouseUp:event];
703 }
704 
705 - (void)rightMouseDragged:(NSEvent*)event {
706  [_currentViewController rightMouseDragged:event];
707 }
708 
709 - (void)otherMouseDown:(NSEvent*)event {
710  [_currentViewController otherMouseDown:event];
711 }
712 
713 - (void)otherMouseUp:(NSEvent*)event {
714  [_currentViewController otherMouseUp:event];
715 }
716 
717 - (void)otherMouseDragged:(NSEvent*)event {
718  [_currentViewController otherMouseDragged:event];
719 }
720 
721 - (void)mouseMoved:(NSEvent*)event {
722  [_currentViewController mouseMoved:event];
723 }
724 
725 - (void)scrollWheel:(NSEvent*)event {
726  [_currentViewController scrollWheel:event];
727 }
728 
729 - (NSTextInputContext*)inputContext {
730  return _textInputContext;
731 }
732 
733 #pragma mark -
734 #pragma mark NSTextInputClient
735 
736 - (void)insertTab:(id)sender {
737  // Implementing insertTab: makes AppKit send tab as command, instead of
738  // insertText with '\t'.
739 }
740 
741 - (void)insertText:(id)string replacementRange:(NSRange)range {
742  if (_activeModel == nullptr) {
743  return;
744  }
745 
746  _eventProducedOutput |= true;
747 
748  if (range.location != NSNotFound) {
749  // The selected range can actually have negative numbers, since it can start
750  // at the end of the range if the user selected the text going backwards.
751  // Cast to a signed type to determine whether or not the selection is reversed.
752  long signedLength = static_cast<long>(range.length);
753  long location = range.location;
754  long textLength = _activeModel->text_range().end();
755 
756  size_t base = std::clamp(location, 0L, textLength);
757  size_t extent = std::clamp(location + signedLength, 0L, textLength);
758 
759  _activeModel->SetSelection(flutter::TextRange(base, extent));
760  }
761 
762  flutter::TextRange oldSelection = _activeModel->selection();
763  flutter::TextRange composingBeforeChange = _activeModel->composing_range();
764  flutter::TextRange replacedRange(-1, -1);
765 
766  std::string textBeforeChange = _activeModel->GetText().c_str();
767  std::string utf8String = [string UTF8String];
768  _activeModel->AddText(utf8String);
769  if (_activeModel->composing()) {
770  replacedRange = composingBeforeChange;
771  _activeModel->CommitComposing();
772  _activeModel->EndComposing();
773  } else {
774  replacedRange = range.location == NSNotFound
775  ? flutter::TextRange(oldSelection.base(), oldSelection.extent())
776  : flutter::TextRange(range.location, range.location + range.length);
777  }
778  if (_enableDeltaModel) {
779  [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
780  utf8String)];
781  } else {
782  [self updateEditState];
783  }
784 }
785 
786 - (void)doCommandBySelector:(SEL)selector {
787  _eventProducedOutput |= selector != NSSelectorFromString(@"noop:");
788  if ([self respondsToSelector:selector]) {
789  // Note: The more obvious [self performSelector...] doesn't give ARC enough information to
790  // handle retain semantics properly. See https://stackoverflow.com/questions/7017281/ for more
791  // information.
792  IMP imp = [self methodForSelector:selector];
793  void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp);
794  func(self, selector, nil);
795  }
796  if (_clientID == nil) {
797  // The macOS may still call selector even if it is no longer a first responder.
798  return;
799  }
800 
801  if (selector == @selector(insertNewline:)) {
802  // Already handled through text insertion (multiline) or action.
803  return;
804  }
805 
806  // Group multiple selectors received within a single run loop turn so that
807  // the framework can process them in single microtask.
808  NSString* name = NSStringFromSelector(selector);
809  if (_pendingSelectors == nil) {
810  _pendingSelectors = [NSMutableArray array];
811  }
812  [_pendingSelectors addObject:name];
813 
814  if (_pendingSelectors.count == 1) {
815  __weak NSMutableArray* selectors = _pendingSelectors;
816  __weak FlutterMethodChannel* channel = _channel;
817  __weak NSNumber* clientID = _clientID;
818 
819  CFStringRef runLoopMode = self.customRunLoopMode != nil
820  ? (__bridge CFStringRef)self.customRunLoopMode
821  : kCFRunLoopCommonModes;
822 
823  CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
824  if (selectors.count > 0) {
825  [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
826  [selectors removeAllObjects];
827  }
828  });
829  }
830 }
831 
832 - (void)insertNewline:(id)sender {
833  if (_activeModel == nullptr) {
834  return;
835  }
836  if (_activeModel->composing()) {
837  _activeModel->CommitComposing();
838  _activeModel->EndComposing();
839  }
840  if ([_inputType isEqualToString:kMultilineInputType] &&
841  [_inputAction isEqualToString:kInputActionNewline]) {
842  [self insertText:@"\n" replacementRange:self.selectedRange];
843  }
844  [_channel invokeMethod:kPerformAction arguments:@[ _clientID, _inputAction ]];
845 }
846 
847 - (void)setMarkedText:(id)string
848  selectedRange:(NSRange)selectedRange
849  replacementRange:(NSRange)replacementRange {
850  if (_activeModel == nullptr) {
851  return;
852  }
853  std::string textBeforeChange = _activeModel->GetText().c_str();
854  if (!_activeModel->composing()) {
855  _activeModel->BeginComposing();
856  }
857 
858  if (replacementRange.location != NSNotFound) {
859  // According to the NSTextInputClient documentation replacementRange is
860  // computed from the beginning of the marked text. That doesn't seem to be
861  // the case, because in situations where the replacementRange is actually
862  // specified (i.e. when switching between characters equivalent after long
863  // key press) the replacementRange is provided while there is no composition.
864  _activeModel->SetComposingRange(
865  flutter::TextRange(replacementRange.location,
866  replacementRange.location + replacementRange.length),
867  0);
868  }
869 
870  flutter::TextRange composingBeforeChange = _activeModel->composing_range();
871  flutter::TextRange selectionBeforeChange = _activeModel->selection();
872 
873  // Input string may be NSString or NSAttributedString.
874  BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
875  const NSString* rawString = isAttributedString ? [string string] : string;
876  _activeModel->UpdateComposingText(
877  (const char16_t*)[rawString cStringUsingEncoding:NSUTF16StringEncoding],
878  flutter::TextRange(selectedRange.location, selectedRange.location + selectedRange.length));
879 
880  if (_enableDeltaModel) {
881  std::string marked_text = [rawString UTF8String];
882  [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
883  selectionBeforeChange.collapsed()
884  ? composingBeforeChange
885  : selectionBeforeChange,
886  marked_text)];
887  } else {
888  [self updateEditState];
889  }
890 }
891 
892 - (void)unmarkText {
893  if (_activeModel == nullptr) {
894  return;
895  }
896  _activeModel->CommitComposing();
897  _activeModel->EndComposing();
898  if (_enableDeltaModel) {
899  [self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
900  } else {
901  [self updateEditState];
902  }
903 }
904 
905 - (NSRange)markedRange {
906  if (_activeModel == nullptr) {
907  return NSMakeRange(NSNotFound, 0);
908  }
909  return NSMakeRange(
910  _activeModel->composing_range().base(),
911  _activeModel->composing_range().extent() - _activeModel->composing_range().base());
912 }
913 
914 - (BOOL)hasMarkedText {
915  return _activeModel != nullptr && _activeModel->composing_range().length() > 0;
916 }
917 
918 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
919  actualRange:(NSRangePointer)actualRange {
920  if (_activeModel == nullptr) {
921  return nil;
922  }
923  NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
924  if (range.location >= text.length) {
925  return nil;
926  }
927  range.length = std::min(range.length, text.length - range.location);
928  if (actualRange != nil) {
929  *actualRange = range;
930  }
931  NSString* substring = [text substringWithRange:range];
932  return [[NSAttributedString alloc] initWithString:substring attributes:nil];
933 }
934 
935 - (NSArray<NSString*>*)validAttributesForMarkedText {
936  return @[];
937 }
938 
939 // Returns the bounding CGRect of the transformed incomingRect, in screen
940 // coordinates.
941 - (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
942  CGPoint points[] = {
943  incomingRect.origin,
944  CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
945  CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
946  CGPointMake(incomingRect.origin.x + incomingRect.size.width,
947  incomingRect.origin.y + incomingRect.size.height)};
948 
949  CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
950  CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
951 
952  for (int i = 0; i < 4; i++) {
953  const CGPoint point = points[i];
954 
955  CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
956  _editableTransform.m41;
957  CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
958  _editableTransform.m42;
959 
960  const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
961  _editableTransform.m44;
962 
963  if (w == 0.0) {
964  return CGRectZero;
965  } else if (w != 1.0) {
966  x /= w;
967  y /= w;
968  }
969 
970  origin.x = MIN(origin.x, x);
971  origin.y = MIN(origin.y, y);
972  farthest.x = MAX(farthest.x, x);
973  farthest.y = MAX(farthest.y, y);
974  }
975 
976  const NSView* fromView = _currentViewController.flutterView;
977  const CGRect rectInWindow = [fromView
978  convertRect:CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y)
979  toView:nil];
980  NSWindow* window = fromView.window;
981  return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
982 }
983 
984 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
985  // This only determines position of caret instead of any arbitrary range, but it's enough
986  // to properly position accent selection popup
987  return !_currentViewController.viewLoaded || CGRectEqualToRect(_caretRect, CGRectNull)
988  ? CGRectZero
989  : [self screenRectFromFrameworkTransform:_caretRect];
990 }
991 
992 - (NSUInteger)characterIndexForPoint:(NSPoint)point {
993  // TODO(cbracken): Implement.
994  // Note: This function can't easily be implemented under the system-message architecture.
995  return 0;
996 }
997 
998 @end
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
typedef NS_ENUM(NSUInteger, FlutterTextAffinity)
static NSString *const kViewId
static NSString *const kUpdateEditStateWithDeltasResponseMethod
static NSString *const kTextKey
static NSString *const kMultilineInputType
static NSString *const kAutofillHints
static NSString *const kAutofillEditingValue
static NSString *const kSecureTextEntry
static NSString *const kShowMethod
static NSString *const kPerformSelectors
static NSString *const kSetEditableSizeAndTransform
static NSTextContentType GetTextContentType(NSDictionary *configuration) API_AVAILABLE(macos(11.0))
static NSString *const kSelectionBaseKey
static char markerKey
static NSString *const kSelectionAffinityKey
static NSString *const kAssociatedAutofillFields
static NSString *const kSetEditingStateMethod
static NSString *const kComposingExtentKey
static NSString *const kAutofillId
static NSString *const kSelectionExtentKey
static NSString *const kClearClientMethod
static NSString *const kEnableDeltaModel
static NSString *const kPerformAction
static NSString * GetAutofillHint(NSDictionary *autofill)
static NSString *const kInputActionNewline
static NSString *const kTextInputChannel
static NSString *const kTextAffinityDownstream
static NSString *const kTextInputType
static NSString *const kSetClientMethod
static NSString *const kTextAffinityUpstream
static NSString *const kTransformKey
static NSString *const kAutofillProperties
static NSString *const kUpdateEditStateResponseMethod
static BOOL EnableAutocompleteForTextInputConfiguration(NSDictionary *configuration)
static BOOL EnableAutocomplete(NSDictionary *configuration)
static NSString *const kSetCaretRect
static NSString *const kTextInputAction
static flutter::TextRange RangeFromBaseExtent(NSNumber *base, NSNumber *extent, const flutter::TextRange &range)
static NSString *const kComposingBaseKey
static NSString *const kHideMethod
static NSString *const kTextInputTypeName
static NSString *const kSelectionIsDirectionalKey
NSTextInputContext * _textInputContext
std::unique_ptr< flutter::TextInputModel > _activeModel
__weak FlutterViewController * _currentViewController
FlutterMethodChannel * _channel
__weak id< FlutterTextInputPluginDelegate > _delegate
size_t base() const
Definition: text_range.h:30
bool collapsed() const
Definition: text_range.h:77
size_t extent() const
Definition: text_range.h:36
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)