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