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