| Flutter macOS Embedder |

FlutterTextInputPlugin.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h"
6
7 #import <Foundation/Foundation.h>
8 #import <objc/message.h>
9
10 #include <algorithm>
11 #include <memory>
12
13 #include "flutter/common/constants.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
15 #include "flutter/shell/platform/common/text_editing_delta.h"
16 #include "flutter/shell/platform/common/text_input_model.h"
17 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
18 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
19 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
20 #import "flutter/shell/platform/darwin/macos/framework/Source/NSView+ClipsToBounds.h"
21
22 static 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
26 static NSString* const kSetClientMethod = @"TextInput.setClient";
27 static NSString* const kShowMethod = @"TextInput.show";
28 static NSString* const kHideMethod = @"TextInput.hide";
29 static NSString* const kClearClientMethod = @"TextInput.clearClient";
30 static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
31 static NSString* const kSetEditableSizeAndTransform = @"TextInput.setEditableSizeAndTransform";
32 static NSString* const kSetCaretRect = @"TextInput.setCaretRect";
33 static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState";
34 static NSString* const kUpdateEditStateWithDeltasResponseMethod =
35 @"TextInputClient.updateEditingStateWithDeltas";
36 static NSString* const kPerformAction = @"TextInputClient.performAction";
37 static NSString* const kPerformSelectors = @"TextInputClient.performSelectors";
38 static NSString* const kMultilineInputType = @"TextInputType.multiline";
39
40 #pragma mark - TextInputConfiguration field names
41 static NSString* const kViewId = @"viewId";
42 static NSString* const kSecureTextEntry = @"obscureText";
43 static NSString* const kTextInputAction = @"inputAction";
44 static NSString* const kEnableDeltaModel = @"enableDeltaModel";
45 static NSString* const kTextInputType = @"inputType";
46 static NSString* const kTextInputTypeName = @"name";
47 static NSString* const kSelectionBaseKey = @"selectionBase";
48 static NSString* const kSelectionExtentKey = @"selectionExtent";
49 static NSString* const kSelectionAffinityKey = @"selectionAffinity";
50 static NSString* const kSelectionIsDirectionalKey = @"selectionIsDirectional";
51 static NSString* const kComposingBaseKey = @"composingBase";
52 static NSString* const kComposingExtentKey = @"composingExtent";
53 static NSString* const kTextKey = @"text";
54 static NSString* const kTransformKey = @"transform";
55 static NSString* const kAssociatedAutofillFields = @"fields";
56
57 // TextInputConfiguration.autofill and sub-field names
58 static NSString* const kAutofillProperties = @"autofill";
59 static NSString* const kAutofillId = @"uniqueIdentifier";
60 static NSString* const kAutofillEditingValue = @"editingValue";
61 static NSString* const kAutofillHints = @"hints";
62
63 // TextAffinity types
64 static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream";
65 static NSString* const kTextAffinityUpstream = @"TextAffinity.upstream";
66
67 // TextInputAction types
68 static 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 */
76 typedef 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 */
86 static flutter::TextRange RangeFromBaseExtent(NSNumber* base,
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.
99 static 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.
106 static 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.
132 static 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.
160 static BOOL EnableAutocomplete(NSDictionary* configuration) {
161 for (NSDictionary* field in configuration[kAssociatedAutofillFields]) {
162 if (!EnableAutocompleteForTextInputConfiguration(field)) {
163 return NO;
164 }
165 }
166
167 // Check the top-level TextInputConfiguration.
168 return EnableAutocompleteForTextInputConfiguration(configuration);
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.
183 - (BOOL)isKeyEquivalent;
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.
190 static char markerKey;
191
192 - (void)markAsKeyEquivalent {
193 objc_setAssociatedObject(self, &markerKey, @true, OBJC_ASSOCIATION_RETAIN);
194 }
195
196 - (BOOL)isKeyEquivalent {
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 */
216 FlutterMethodChannel* _channel;
217
218 /**
219 * The FlutterViewController to manage input for.
220 */
221 __weak FlutterViewController* _currentViewController;
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 */
233 BOOL _shown;
234
235 /**
236 * The current state of the keyboard and pressed keys.
237 */
238 uint64_t _previouslyPressedFlags;
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 */
267 BOOL _eventProducedOutput;
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 */
275 BOOL _enableDeltaModel;
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 */
297 CGRect _caretRect;
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;
357 _channel = [FlutterMethodChannel methodChannelWithName:kTextInputChannel
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>();
429 FlutterViewIdentifier viewId = flutter::kFlutterImplicitViewId;
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
526 flutter::TextRange selected_range = RangeFromBaseExtent(
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,
557 kSelectionIsDirectionalKey : @NO,
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
623 : kTextAffinityDownstream;
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 std::string utf8String = [string UTF8String];
785 _activeModel->AddText(utf8String);
786 if (_activeModel->composing()) {
787 replacedRange = composingBeforeChange;
788 _activeModel->CommitComposing();
789 _activeModel->EndComposing();
790 } else {
791 replacedRange = range.location == NSNotFound
792 ? flutter::TextRange(oldSelection.base(), oldSelection.extent())
793 : flutter::TextRange(range.location, range.location + range.length);
794 }
795 if (_enableDeltaModel) {
796 [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
797 utf8String)];
798 } else {
799 [self updateEditState];
800 }
801 }
802
803 - (void)doCommandBySelector:(SEL)selector {
804 _eventProducedOutput |= selector != NSSelectorFromString(@"noop:");
805 if ([self respondsToSelector:selector]) {
806 // Note: The more obvious [self performSelector...] doesn't give ARC enough information to
807 // handle retain semantics properly. See https://stackoverflow.com/questions/7017281/ for more
808 // information.
809 IMP imp = [self methodForSelector:selector];
810 void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp);
811 func(self, selector, nil);
812 }
813 if (_clientID == nil) {
814 // The macOS may still call selector even if it is no longer a first responder.
815 return;
816 }
817
818 if (selector == @selector(insertNewline:)) {
819 // Already handled through text insertion (multiline) or action.
820 return;
821 }
822
823 // Group multiple selectors received within a single run loop turn so that
824 // the framework can process them in single microtask.
825 NSString* name = NSStringFromSelector(selector);
826 if (_pendingSelectors == nil) {
827 _pendingSelectors = [NSMutableArray array];
828 }
829 [_pendingSelectors addObject:name];
830
831 if (_pendingSelectors.count == 1) {
832 __weak NSMutableArray* selectors = _pendingSelectors;
833 __weak FlutterMethodChannel* channel = _channel;
834 __weak NSNumber* clientID = _clientID;
835
836 CFStringRef runLoopMode = self.customRunLoopMode != nil
837 ? (__bridge CFStringRef)self.customRunLoopMode
838 : kCFRunLoopCommonModes;
839
840 CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
841 if (selectors.count > 0) {
842 [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
843 [selectors removeAllObjects];
844 }
845 });
846 }
847 }
848
849 - (void)insertNewline:(id)sender {
850 if (_activeModel == nullptr) {
851 return;
852 }
853 if (_activeModel->composing()) {
854 _activeModel->CommitComposing();
855 _activeModel->EndComposing();
856 }
857 if ([_inputType isEqualToString:kMultilineInputType] &&
858 [_inputAction isEqualToString:kInputActionNewline]) {
859 [self insertText:@"\n" replacementRange:self.selectedRange];
860 }
861 [_channel invokeMethod:kPerformAction arguments:@[ _clientID, _inputAction ]];
862 }
863
864 - (void)setMarkedText:(id)string
865 selectedRange:(NSRange)selectedRange
866 replacementRange:(NSRange)replacementRange {
867 if (_activeModel == nullptr) {
868 return;
869 }
870 std::string textBeforeChange = _activeModel->GetText().c_str();
871 if (!_activeModel->composing()) {
872 _activeModel->BeginComposing();
873 }
874
875 if (replacementRange.location != NSNotFound) {
876 // According to the NSTextInputClient documentation replacementRange is
877 // computed from the beginning of the marked text. That doesn't seem to be
878 // the case, because in situations where the replacementRange is actually
879 // specified (i.e. when switching between characters equivalent after long
880 // key press) the replacementRange is provided while there is no composition.
881 _activeModel->SetComposingRange(
882 flutter::TextRange(replacementRange.location,
883 replacementRange.location + replacementRange.length),
884 0);
885 }
886
887 flutter::TextRange composingBeforeChange = _activeModel->composing_range();
888 flutter::TextRange selectionBeforeChange = _activeModel->selection();
889
890 // Input string may be NSString or NSAttributedString.
891 BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
892 const NSString* rawString = isAttributedString ? [string string] : string;
893 _activeModel->UpdateComposingText(
894 (const char16_t*)[rawString cStringUsingEncoding:NSUTF16StringEncoding],
895 flutter::TextRange(selectedRange.location, selectedRange.location + selectedRange.length));
896
897 if (_enableDeltaModel) {
898 std::string marked_text = [rawString UTF8String];
899 [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
900 selectionBeforeChange.collapsed()
901 ? composingBeforeChange
902 : selectionBeforeChange,
903 marked_text)];
904 } else {
905 [self updateEditState];
906 }
907 }
908
909 - (void)unmarkText {
910 if (_activeModel == nullptr) {
911 return;
912 }
913 _activeModel->CommitComposing();
914 _activeModel->EndComposing();
915 if (_enableDeltaModel) {
916 [self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
917 } else {
918 [self updateEditState];
919 }
920 }
921
922 - (NSRange)markedRange {
923 if (_activeModel == nullptr) {
924 return NSMakeRange(NSNotFound, 0);
925 }
926 return NSMakeRange(
927 _activeModel->composing_range().base(),
928 _activeModel->composing_range().extent() - _activeModel->composing_range().base());
929 }
930
931 - (BOOL)hasMarkedText {
932 return _activeModel != nullptr && _activeModel->composing_range().length() > 0;
933 }
934
935 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
936 actualRange:(NSRangePointer)actualRange {
937 if (_activeModel == nullptr) {
938 return nil;
939 }
940 NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
941 if (range.location >= text.length) {
942 return nil;
943 }
944 range.length = std::min(range.length, text.length - range.location);
945 if (actualRange != nil) {
946 *actualRange = range;
947 }
948 NSString* substring = [text substringWithRange:range];
949 return [[NSAttributedString alloc] initWithString:substring attributes:nil];
950 }
951
952 - (NSArray<NSString*>*)validAttributesForMarkedText {
953 return @[];
954 }
955
956 // Returns the bounding CGRect of the transformed incomingRect, in screen
957 // coordinates.
958 - (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
959 CGPoint points[] = {
960 incomingRect.origin,
961 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
962 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
963 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
964 incomingRect.origin.y + incomingRect.size.height)};
965
966 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
967 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
968
969 for (int i = 0; i < 4; i++) {
970 const CGPoint point = points[i];
971
972 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
973 _editableTransform.m41;
974 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
975 _editableTransform.m42;
976
977 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
978 _editableTransform.m44;
979
980 if (w == 0.0) {
981 return CGRectZero;
982 } else if (w != 1.0) {
983 x /= w;
984 y /= w;
985 }
986
987 origin.x = MIN(origin.x, x);
988 origin.y = MIN(origin.y, y);
989 farthest.x = MAX(farthest.x, x);
990 farthest.y = MAX(farthest.y, y);
991 }
992
993 const NSView* fromView = _currentViewController.flutterView;
994 const CGRect rectInWindow = [fromView
995 convertRect:CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y)
996 toView:nil];
997 NSWindow* window = fromView.window;
998 return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
999 }
1000
1001 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
1002 // This only determines position of caret instead of any arbitrary range, but it's enough
1003 // to properly position accent selection popup
1004 return !_currentViewController.viewLoaded || CGRectEqualToRect(_caretRect, CGRectNull)
1005 ? CGRectZero
1006 : [self screenRectFromFrameworkTransform:_caretRect];
1007 }
1008
1009 - (NSUInteger)characterIndexForPoint:(NSPoint)point {
1010 // TODO(cbracken): Implement.
1011 // Note: This function can't easily be implemented under the system-message architecture.
1012 return 0;
1013 }
1014
1015 @end
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
FlutterMethodNotImplemented
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
FlutterCodecs.h
FlutterTextInputPlugin.h
NS_ENUM
typedef NS_ENUM(NSUInteger, FlutterTextAffinity)
Definition: FlutterTextInputPlugin.mm:76
kViewId
static NSString *const kViewId
Definition: FlutterTextInputPlugin.mm:41
kUpdateEditStateWithDeltasResponseMethod
static NSString *const kUpdateEditStateWithDeltasResponseMethod
Definition: FlutterTextInputPlugin.mm:34
kTextKey
static NSString *const kTextKey
Definition: FlutterTextInputPlugin.mm:53
kMultilineInputType
static NSString *const kMultilineInputType
Definition: FlutterTextInputPlugin.mm:38
kAutofillHints
static NSString *const kAutofillHints
Definition: FlutterTextInputPlugin.mm:61
kAutofillEditingValue
static NSString *const kAutofillEditingValue
Definition: FlutterTextInputPlugin.mm:60
kSecureTextEntry
static NSString *const kSecureTextEntry
Definition: FlutterTextInputPlugin.mm:42
kShowMethod
static NSString *const kShowMethod
Definition: FlutterTextInputPlugin.mm:27
kPerformSelectors
static NSString *const kPerformSelectors
Definition: FlutterTextInputPlugin.mm:37
kSetEditableSizeAndTransform
static NSString *const kSetEditableSizeAndTransform
Definition: FlutterTextInputPlugin.mm:31
GetTextContentType
static NSTextContentType GetTextContentType(NSDictionary *configuration) API_AVAILABLE(macos(11.0))
Definition: FlutterTextInputPlugin.mm:106
kSelectionBaseKey
static NSString *const kSelectionBaseKey
Definition: FlutterTextInputPlugin.mm:47
markerKey
static char markerKey
Definition: FlutterTextInputPlugin.mm:190
kSelectionAffinityKey
static NSString *const kSelectionAffinityKey
Definition: FlutterTextInputPlugin.mm:49
kAssociatedAutofillFields
static NSString *const kAssociatedAutofillFields
Definition: FlutterTextInputPlugin.mm:55
kSetEditingStateMethod
static NSString *const kSetEditingStateMethod
Definition: FlutterTextInputPlugin.mm:30
kComposingExtentKey
static NSString *const kComposingExtentKey
Definition: FlutterTextInputPlugin.mm:52
kAutofillId
static NSString *const kAutofillId
Definition: FlutterTextInputPlugin.mm:59
kSelectionExtentKey
static NSString *const kSelectionExtentKey
Definition: FlutterTextInputPlugin.mm:48
kClearClientMethod
static NSString *const kClearClientMethod
Definition: FlutterTextInputPlugin.mm:29
kEnableDeltaModel
static NSString *const kEnableDeltaModel
Definition: FlutterTextInputPlugin.mm:44
kPerformAction
static NSString *const kPerformAction
Definition: FlutterTextInputPlugin.mm:36
GetAutofillHint
static NSString * GetAutofillHint(NSDictionary *autofill)
Definition: FlutterTextInputPlugin.mm:99
kInputActionNewline
static NSString *const kInputActionNewline
Definition: FlutterTextInputPlugin.mm:68
kTextInputChannel
static NSString *const kTextInputChannel
Definition: FlutterTextInputPlugin.mm:22
kTextAffinityDownstream
static NSString *const kTextAffinityDownstream
Definition: FlutterTextInputPlugin.mm:64
kTextInputType
static NSString *const kTextInputType
Definition: FlutterTextInputPlugin.mm:45
kSetClientMethod
static NSString *const kSetClientMethod
Definition: FlutterTextInputPlugin.mm:26
kTextAffinityUpstream
static NSString *const kTextAffinityUpstream
Definition: FlutterTextInputPlugin.mm:65
kTransformKey
static NSString *const kTransformKey
Definition: FlutterTextInputPlugin.mm:54
kAutofillProperties
static NSString *const kAutofillProperties
Definition: FlutterTextInputPlugin.mm:58
kUpdateEditStateResponseMethod
static NSString *const kUpdateEditStateResponseMethod
Definition: FlutterTextInputPlugin.mm:33
EnableAutocompleteForTextInputConfiguration
static BOOL EnableAutocompleteForTextInputConfiguration(NSDictionary *configuration)
Definition: FlutterTextInputPlugin.mm:132
EnableAutocomplete
static BOOL EnableAutocomplete(NSDictionary *configuration)
Definition: FlutterTextInputPlugin.mm:160
kSetCaretRect
static NSString *const kSetCaretRect
Definition: FlutterTextInputPlugin.mm:32
kTextInputAction
static NSString *const kTextInputAction
Definition: FlutterTextInputPlugin.mm:43
RangeFromBaseExtent
static flutter::TextRange RangeFromBaseExtent(NSNumber *base, NSNumber *extent, const flutter::TextRange &range)
Definition: FlutterTextInputPlugin.mm:86
kComposingBaseKey
static NSString *const kComposingBaseKey
Definition: FlutterTextInputPlugin.mm:51
kHideMethod
static NSString *const kHideMethod
Definition: FlutterTextInputPlugin.mm:28
kTextInputTypeName
static NSString *const kTextInputTypeName
Definition: FlutterTextInputPlugin.mm:46
kSelectionIsDirectionalKey
static NSString *const kSelectionIsDirectionalKey
Definition: FlutterTextInputPlugin.mm:50
FlutterTextInputSemanticsObject.h
FlutterViewIdentifier
int64_t FlutterViewIdentifier
Definition: FlutterViewController.h:21
FlutterViewController_Internal.h
NSView+ClipsToBounds.h
FlutterTextInputPlugin()::_previouslyPressedFlags
uint64_t _previouslyPressedFlags
Definition: FlutterTextInputPlugin.mm:238
FlutterTextInputPlugin()::_textInputContext
NSTextInputContext * _textInputContext
Definition: FlutterTextInputPlugin.mm:211
FlutterTextInputPlugin()::_shown
BOOL _shown
Definition: FlutterTextInputPlugin.mm:233
FlutterTextInputPlugin()::_editableTransform
CATransform3D _editableTransform
Definition: FlutterTextInputPlugin.mm:292
FlutterTextInputPlugin()::_clientID
NSNumber * _clientID
Definition: FlutterTextInputPlugin.mm:248
FlutterTextInputPlugin()::_activeModel
std::unique_ptr< flutter::TextInputModel > _activeModel
Definition: FlutterTextInputPlugin.mm:287
FlutterTextInputPlugin()::_pendingSelectors
NSMutableArray * _pendingSelectors
Definition: FlutterTextInputPlugin.mm:282
FlutterTextInputPlugin()::_textAffinity
FlutterTextAffinity _textAffinity
Definition: FlutterTextInputPlugin.mm:243
FlutterTextInputPlugin()::_inputAction
NSString * _inputAction
Definition: FlutterTextInputPlugin.mm:260
FlutterTextInputPlugin()::_currentViewController
__weak FlutterViewController * _currentViewController
Definition: FlutterTextInputPlugin.mm:221
FlutterTextInputPlugin()::_caretRect
CGRect _caretRect
Definition: FlutterTextInputPlugin.mm:297
FlutterTextInputPlugin()::_channel
FlutterMethodChannel * _channel
Definition: FlutterTextInputPlugin.mm:216
FlutterTextInputPlugin()::_delegate
__weak id< FlutterTextInputPluginDelegate > _delegate
Definition: FlutterTextInputPlugin.mm:226
FlutterTextInputPlugin()::_enableDeltaModel
BOOL _enableDeltaModel
Definition: FlutterTextInputPlugin.mm:275
FlutterTextInputPlugin()::_eventProducedOutput
BOOL _eventProducedOutput
Definition: FlutterTextInputPlugin.mm:267
FlutterTextInputPlugin()::_inputType
NSString * _inputType
Definition: FlutterTextInputPlugin.mm:254
FlutterTextInputPlugin(TestMethods)::customRunLoopMode
NSString * customRunLoopMode
Definition: FlutterTextInputPlugin.h:91
-[FlutterTextInputPlugin(TestMethods) editingState]
NSDictionary * editingState()
NSEvent(KeyEquivalentMarker)
Definition: FlutterTextInputPlugin.mm:173
-[NSEvent(KeyEquivalentMarker) markAsKeyEquivalent]
void markAsKeyEquivalent()
Definition: FlutterTextInputPlugin.mm:192
-[NSEvent(KeyEquivalentMarker) isKeyEquivalent]
BOOL isKeyEquivalent()
Definition: FlutterTextInputPlugin.mm:196
flutter::TextRange
Definition: text_range.h:19
flutter::TextRange::start
size_t start() const
Definition: text_range.h:42
flutter::TextRange::base
size_t base() const
Definition: text_range.h:30
flutter::TextRange::collapsed
bool collapsed() const
Definition: text_range.h:77
flutter::TextRange::extent
size_t extent() const
Definition: text_range.h:36
FlutterError
Definition: FlutterCodecs.h:247
FlutterJSONMethodCodec
Definition: FlutterCodecs.h:455
FlutterMethodCall
Definition: FlutterCodecs.h:221
FlutterMethodCall::method
NSString * method
Definition: FlutterCodecs.h:233
FlutterMethodCall::arguments
id arguments
Definition: FlutterCodecs.h:238
FlutterMethodChannel
Definition: FlutterChannels.h:221
+[FlutterMethodChannel methodChannelWithName:binaryMessenger:codec:]
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:42
-[FlutterTextInputPlugin isFirstResponder]
BOOL isFirstResponder()
Definition: FlutterTextInputPlugin.mm:381
FlutterViewController
Definition: FlutterViewController.h:73
flutter
Definition: AccessibilityBridgeMac.h:16
FlutterTextInputPluginDelegate-p
Definition: FlutterTextInputPlugin.h:18
text_editing_delta.h
text_input_model.h