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