7 #import <Foundation/Foundation.h>
8 #import <objc/message.h>
13 #include "flutter/common/constants.h"
14 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
24 #pragma mark - TextInput channel method names
35 @"TextInputClient.updateEditingStateWithDeltas";
40 #pragma mark - TextInputConfiguration field names
41 static NSString*
const kViewId =
@"viewId";
76 typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
77 kFlutterTextAffinityUpstream,
78 kFlutterTextAffinityDownstream
81 #pragma mark - Static functions
89 if (base == nil || extent == nil) {
92 if (base.intValue == -1 && extent.intValue == -1) {
101 return hints.count > 0 ? hints[0] : nil;
107 API_AVAILABLE(macos(11.0)) {
112 if ([hint isEqualToString:
@"username"]) {
113 return NSTextContentTypeUsername;
115 if ([hint isEqualToString:
@"password"]) {
116 return NSTextContentTypePassword;
118 if ([hint isEqualToString:
@"oneTimeCode"]) {
119 return NSTextContentTypeOneTimeCode;
124 return NSTextContentTypePassword;
140 if (autofill == nil) {
147 if ([hint isEqualToString:
@"password"] || [hint isEqualToString:
@"username"]) {
171 #pragma mark - NSEvent (KeyEquivalentMarker) protocol
193 objc_setAssociatedObject(
self, &
markerKey, @
true, OBJC_ASSOCIATION_RETAIN);
197 return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
202 #pragma mark - FlutterTextInputPlugin private interface
309 - (void)setEditingState:(NSDictionary*)state;
315 - (void)updateEditState;
321 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta;
330 - (void)updateTextAndSelection;
336 - (NSString*)textAffinityString;
341 @property(readwrite, nonatomic) NSString* customRunLoopMode;
342 @property(nonatomic) NSTextInputContext* textInputContext;
346 #pragma mark - FlutterTextInputPlugin
353 self = [
super initWithFrame:NSZeroRect];
354 self.clipsToBounds = YES;
356 _delegate = delegate;
367 [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
368 [unsafeSelf handleMethodCall:call result:result];
370 _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
371 _previouslyPressedFlags = 0;
375 _editableTransform = CATransform3D();
376 _caretRect = CGRectNull;
382 if (!_currentViewController.viewLoaded) {
385 return [_currentViewController.view.window firstResponder] ==
self;
389 [_channel setMethodCallHandler:nil];
392 #pragma mark - Private
394 - (void)resignAndRemoveFromSuperview {
395 if (
self.superview != nil) {
396 [
self.window makeFirstResponder:_currentViewController.flutterView];
397 [
self removeFromSuperview];
403 NSString* method = call.
method;
405 FML_DCHECK(_currentViewController == nil);
408 errorWithCode:
@"error"
409 message:
@"Missing arguments"
410 details:
@"Missing arguments while trying to set a text input client"]);
414 if (clientID != nil) {
415 NSDictionary* config = call.
arguments[1];
417 _clientID = clientID;
418 _inputAction = config[kTextInputAction];
419 _enableDeltaModel = [config[kEnableDeltaModel] boolValue];
420 NSDictionary* inputTypeInfo = config[kTextInputType];
421 _inputType = inputTypeInfo[kTextInputTypeName];
422 _textAffinity = kFlutterTextAffinityUpstream;
424 if (@available(macOS 11.0, *)) {
428 _activeModel = std::make_unique<flutter::TextInputModel>();
430 NSObject* requestViewId = config[kViewId];
431 if ([requestViewId isKindOfClass:[NSNumber
class]]) {
432 viewId = [(NSNumber*)requestViewId longLongValue];
434 _currentViewController = [_delegate viewControllerForIdentifier:viewId];
435 FML_DCHECK(_currentViewController != nil);
438 FML_DCHECK(_currentViewController != nil);
442 if (_client == nil) {
443 [_currentViewController.view addSubview:self];
445 [
self.window makeFirstResponder:self];
448 [
self resignAndRemoveFromSuperview];
451 FML_DCHECK(_currentViewController != nil);
452 [
self resignAndRemoveFromSuperview];
454 if (_activeModel && _activeModel->composing()) {
455 _activeModel->CommitComposing();
456 _activeModel->EndComposing();
458 [_textInputContext discardMarkedText];
462 _enableDeltaModel = NO;
464 _activeModel =
nullptr;
465 _currentViewController = nil;
467 FML_DCHECK(_currentViewController != nil);
469 [
self setEditingState:state];
471 FML_DCHECK(_currentViewController != nil);
473 [
self setEditableTransform:state[kTransformKey]];
475 FML_DCHECK(_currentViewController != nil);
477 [
self updateCaretRect:rect];
484 - (void)setEditableTransform:(NSArray*)matrix {
485 CATransform3D* transform = &_editableTransform;
487 transform->m11 = [matrix[0] doubleValue];
488 transform->m12 = [matrix[1] doubleValue];
489 transform->m13 = [matrix[2] doubleValue];
490 transform->m14 = [matrix[3] doubleValue];
492 transform->m21 = [matrix[4] doubleValue];
493 transform->m22 = [matrix[5] doubleValue];
494 transform->m23 = [matrix[6] doubleValue];
495 transform->m24 = [matrix[7] doubleValue];
497 transform->m31 = [matrix[8] doubleValue];
498 transform->m32 = [matrix[9] doubleValue];
499 transform->m33 = [matrix[10] doubleValue];
500 transform->m34 = [matrix[11] doubleValue];
502 transform->m41 = [matrix[12] doubleValue];
503 transform->m42 = [matrix[13] doubleValue];
504 transform->m43 = [matrix[14] doubleValue];
505 transform->m44 = [matrix[15] doubleValue];
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]);
516 - (void)setEditingState:(NSDictionary*)state {
517 NSString* selectionAffinity = state[kSelectionAffinityKey];
518 if (selectionAffinity != nil) {
519 _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
520 ? kFlutterTextAffinityUpstream
521 : kFlutterTextAffinityDownstream;
524 NSString* text = state[kTextKey];
528 _activeModel->SetSelection(selected_range);
533 const bool wasComposing = _activeModel->composing();
534 _activeModel->SetText([text UTF8String], selected_range, composing_range);
535 if (composing_range.
collapsed() && wasComposing) {
536 [_textInputContext discardMarkedText];
538 [_client startEditing];
540 [
self updateTextAndSelection];
544 if (_activeModel ==
nullptr) {
548 NSString*
const textAffinity = [
self textAffinityString];
550 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
551 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
560 kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()] ?: [NSNull null],
564 - (void)updateEditState {
565 if (_activeModel ==
nullptr) {
569 NSDictionary* state = [
self editingState];
570 [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[ _clientID, state ]];
571 [
self updateTextAndSelection];
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;
580 NSString*
const textAffinity = [
self textAffinityString];
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),
595 NSDictionary* deltas = @{
596 @"deltas" : @[ deltaToFramework ],
599 [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod arguments:@[ _clientID, deltas ]];
600 [
self updateTextAndSelection];
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));
614 [_client updateString:text withSelection:selection];
617 [
self setSelectedRange:selection];
621 - (NSString*)textAffinityString {
626 - (BOOL)handleKeyEvent:(NSEvent*)event {
627 if (event.type == NSEventTypeKeyUp ||
628 (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
631 _previouslyPressedFlags =
event.modifierFlags;
636 _eventProducedOutput = NO;
637 BOOL res = [_textInputContext handleEvent:event];
647 bool is_navigation =
event.modifierFlags & NSEventModifierFlagFunction &&
648 event.modifierFlags & NSEventModifierFlagNumericPad;
649 bool is_navigation_in_ime = is_navigation &&
self.hasMarkedText;
651 if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) {
658 #pragma mark NSResponder
660 - (void)keyDown:(NSEvent*)event {
661 [_currentViewController keyDown:event];
664 - (void)keyUp:(NSEvent*)event {
665 [_currentViewController keyUp:event];
668 - (BOOL)performKeyEquivalent:(NSEvent*)event {
669 if ([_currentViewController isDispatchingKeyEvent:event]) {
681 [event markAsKeyEquivalent];
682 [_currentViewController keyDown:event];
686 - (void)flagsChanged:(NSEvent*)event {
687 [_currentViewController flagsChanged:event];
690 - (void)mouseDown:(NSEvent*)event {
691 [_currentViewController mouseDown:event];
694 - (void)mouseUp:(NSEvent*)event {
695 [_currentViewController mouseUp:event];
698 - (void)mouseDragged:(NSEvent*)event {
699 [_currentViewController mouseDragged:event];
702 - (void)rightMouseDown:(NSEvent*)event {
703 [_currentViewController rightMouseDown:event];
706 - (void)rightMouseUp:(NSEvent*)event {
707 [_currentViewController rightMouseUp:event];
710 - (void)rightMouseDragged:(NSEvent*)event {
711 [_currentViewController rightMouseDragged:event];
714 - (void)otherMouseDown:(NSEvent*)event {
715 [_currentViewController otherMouseDown:event];
718 - (void)otherMouseUp:(NSEvent*)event {
719 [_currentViewController otherMouseUp:event];
722 - (void)otherMouseDragged:(NSEvent*)event {
723 [_currentViewController otherMouseDragged:event];
726 - (void)mouseMoved:(NSEvent*)event {
727 [_currentViewController mouseMoved:event];
730 - (void)scrollWheel:(NSEvent*)event {
731 [_currentViewController scrollWheel:event];
734 - (NSTextInputContext*)inputContext {
735 return _textInputContext;
739 #pragma mark NSTextInputClient
741 - (void)insertTab:(
id)sender {
746 - (void)insertText:(
id)string replacementRange:(NSRange)range {
747 if (_activeModel ==
nullptr) {
751 _eventProducedOutput |=
true;
753 if (range.location != NSNotFound) {
757 long signedLength =
static_cast<long>(range.length);
758 long location = range.location;
759 long textLength = _activeModel->text_range().end();
761 size_t base = std::clamp(location, 0L, textLength);
762 size_t extent = std::clamp(location + signedLength, 0L, textLength);
765 }
else if (_activeModel->composing() &&
766 !(_activeModel->composing_range() == _activeModel->selection())) {
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();
791 replacedRange = range.location == NSNotFound
793 :
flutter::TextRange(range.location, range.location + range.length);
795 if (_enableDeltaModel) {
796 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
799 [
self updateEditState];
803 - (void)doCommandBySelector:(
SEL)selector {
804 _eventProducedOutput |= selector != NSSelectorFromString(
@"noop:");
805 if ([
self respondsToSelector:selector]) {
809 IMP imp = [
self methodForSelector:selector];
810 void (*func)(id, SEL, id) =
reinterpret_cast<void (*)(
id,
SEL,
id)
>(imp);
811 func(
self, selector, nil);
813 if (_clientID == nil) {
818 if (selector ==
@selector(insertNewline:)) {
825 NSString* name = NSStringFromSelector(selector);
826 if (_pendingSelectors == nil) {
827 _pendingSelectors = [NSMutableArray array];
829 [_pendingSelectors addObject:name];
831 if (_pendingSelectors.count == 1) {
832 __weak NSMutableArray* selectors = _pendingSelectors;
834 __weak NSNumber* clientID = _clientID;
836 CFStringRef runLoopMode =
self.customRunLoopMode != nil
838 : kCFRunLoopCommonModes;
840 CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
841 if (selectors.count > 0) {
842 [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
843 [selectors removeAllObjects];
849 - (void)insertNewline:(
id)sender {
850 if (_activeModel ==
nullptr) {
853 if (_activeModel->composing()) {
854 _activeModel->CommitComposing();
855 _activeModel->EndComposing();
859 [
self insertText:@"\n" replacementRange:self.selectedRange];
861 [_channel invokeMethod:kPerformAction arguments:@[ _clientID, _inputAction ]];
864 - (void)setMarkedText:(
id)string
865 selectedRange:(NSRange)selectedRange
866 replacementRange:(NSRange)replacementRange {
867 if (_activeModel ==
nullptr) {
870 std::string textBeforeChange = _activeModel->GetText().c_str();
871 if (!_activeModel->composing()) {
872 _activeModel->BeginComposing();
875 if (replacementRange.location != NSNotFound) {
881 _activeModel->SetComposingRange(
883 replacementRange.location + replacementRange.length),
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));
897 if (_enableDeltaModel) {
898 std::string marked_text = [rawString UTF8String];
899 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
900 selectionBeforeChange.collapsed()
901 ? composingBeforeChange
902 : selectionBeforeChange,
905 [
self updateEditState];
910 if (_activeModel ==
nullptr) {
913 _activeModel->CommitComposing();
914 _activeModel->EndComposing();
915 if (_enableDeltaModel) {
916 [
self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
918 [
self updateEditState];
922 - (NSRange)markedRange {
923 if (_activeModel ==
nullptr) {
924 return NSMakeRange(NSNotFound, 0);
927 _activeModel->composing_range().base(),
928 _activeModel->composing_range().extent() - _activeModel->composing_range().base());
931 - (BOOL)hasMarkedText {
932 return _activeModel !=
nullptr && _activeModel->composing_range().length() > 0;
935 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
936 actualRange:(NSRangePointer)actualRange {
937 if (_activeModel ==
nullptr) {
940 NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
941 if (range.location >= text.length) {
944 range.length = std::min(range.length, text.length - range.location);
945 if (actualRange != nil) {
946 *actualRange = range;
948 NSString* substring = [text substringWithRange:range];
949 return [[NSAttributedString alloc] initWithString:substring attributes:nil];
952 - (NSArray<NSString*>*)validAttributesForMarkedText {
958 - (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
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)};
966 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
967 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
969 for (
int i = 0; i < 4; i++) {
970 const CGPoint point = points[i];
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;
977 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
978 _editableTransform.m44;
982 }
else if (w != 1.0) {
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);
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)
997 NSWindow* window = fromView.window;
998 return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
1001 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
1004 return !_currentViewController.viewLoaded || CGRectEqualToRect(_caretRect, CGRectNull)
1006 : [
self screenRectFromFrameworkTransform:_caretRect];
1009 - (NSUInteger)characterIndexForPoint:(NSPoint)point {
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
typedef NS_ENUM(NSUInteger, FlutterTextAffinity)
static NSString *const kViewId
static NSString *const kUpdateEditStateWithDeltasResponseMethod
static NSString *const kTextKey
static NSString *const kMultilineInputType
static NSString *const kAutofillHints
static NSString *const kAutofillEditingValue
static NSString *const kSecureTextEntry
static NSString *const kShowMethod
static NSString *const kPerformSelectors
static NSString *const kSetEditableSizeAndTransform
static NSTextContentType GetTextContentType(NSDictionary *configuration) API_AVAILABLE(macos(11.0))
static NSString *const kSelectionBaseKey
static NSString *const kSelectionAffinityKey
static NSString *const kAssociatedAutofillFields
static NSString *const kSetEditingStateMethod
static NSString *const kComposingExtentKey
static NSString *const kAutofillId
static NSString *const kSelectionExtentKey
static NSString *const kClearClientMethod
static NSString *const kEnableDeltaModel
static NSString *const kPerformAction
static NSString * GetAutofillHint(NSDictionary *autofill)
static NSString *const kInputActionNewline
static NSString *const kTextInputChannel
static NSString *const kTextAffinityDownstream
static NSString *const kTextInputType
static NSString *const kSetClientMethod
static NSString *const kTextAffinityUpstream
static NSString *const kTransformKey
static NSString *const kAutofillProperties
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 kHideMethod
static NSString *const kTextInputTypeName
static NSString *const kSelectionIsDirectionalKey
int64_t FlutterViewIdentifier
uint64_t _previouslyPressedFlags
NSTextInputContext * _textInputContext
CATransform3D _editableTransform
std::unique_ptr< flutter::TextInputModel > _activeModel
NSMutableArray * _pendingSelectors
FlutterTextAffinity _textAffinity
__weak FlutterViewController * _currentViewController
FlutterMethodChannel * _channel
__weak id< FlutterTextInputPluginDelegate > _delegate
BOOL _eventProducedOutput
NSString * customRunLoopMode
NSDictionary * editingState()
void markAsKeyEquivalent()
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)