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