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