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