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