7#import <Foundation/Foundation.h>
8#import <objc/message.h>
24#pragma mark - TextInput channel method names
35 @"TextInputClient.updateEditingStateWithDeltas";
40#pragma mark - TextInputConfiguration field names
41static NSString*
const kViewId =
@"viewId";
76typedef 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;
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
173@interface NSEvent (KeyEquivalentMarker)
187@implementation NSEvent (KeyEquivalentMarker)
193 objc_setAssociatedObject(
self, &
markerKey, @
true, OBJC_ASSOCIATION_RETAIN);
197 return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
202#pragma mark - FlutterTextInputPlugin private interface
316- (void)setEditingState:(NSDictionary*)state;
322- (void)updateEditState;
328- (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta;
337- (void)updateTextAndSelection;
343- (NSString*)textAffinityString;
348@property(readwrite, nonatomic) NSString* customRunLoopMode;
349@property(nonatomic) NSTextInputContext* textInputContext;
353#pragma mark - FlutterTextInputPlugin
357- (instancetype)initWithDelegate:(
id<FlutterTextInputPluginDelegate>)delegate {
360 self = [
super initWithFrame:NSZeroRect];
361 self.clipsToBounds = YES;
363 _delegate = delegate;
374 [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
375 [unsafeSelf handleMethodCall:call result:result];
377 _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
378 _previouslyPressedFlags = 0;
382 _editableTransform = CATransform3D();
383 _caretRect = CGRectNull;
388- (
BOOL)isFirstResponder {
389 if (!_currentViewController.viewLoaded) {
392 return [_currentViewController.view.window firstResponder] ==
self;
396 [_channel setMethodCallHandler:nil];
399#pragma mark - Private
401- (void)resignAndRemoveFromSuperview {
402 if (
self.superview != nil) {
403 [
self.window makeFirstResponder:_currentViewController.flutterView];
404 [
self removeFromSuperview];
410 NSString* method = call.
method;
415 errorWithCode:
@"error"
417 details:
@"Missing arguments while trying to set a text input client"]);
421 if (clientID != nil) {
422 NSDictionary* config = call.
arguments[1];
424 _clientID = clientID;
425 _inputAction = config[kTextInputAction];
426 _enableDeltaModel = [config[kEnableDeltaModel] boolValue];
427 NSDictionary* inputTypeInfo = config[kTextInputType];
428 _inputType = inputTypeInfo[kTextInputTypeName];
429 _textAffinity = kFlutterTextAffinityUpstream;
431 if (@available(macOS 11.0, *)) {
435 _activeModel = std::make_unique<flutter::TextInputModel>();
437 NSObject* requestViewId = config[kViewId];
438 if ([requestViewId isKindOfClass:[NSNumber class]]) {
439 viewId = [(NSNumber*)requestViewId longLongValue];
441 _currentViewController = [_delegate viewControllerForIdentifier:viewId];
442 _originalViewController = _currentViewController;
443 while (!_currentViewController.view.window.canBecomeKeyWindow) {
444 NSWindow* parentWindow = [_currentViewController.view.window parentWindow];
445 if (parentWindow == nil) {
448 NSViewController* controller = parentWindow.contentViewController;
462 if (_client == nil) {
463 [_currentViewController.view addSubview:self];
465 [
self.window makeFirstResponder:self];
468 [
self resignAndRemoveFromSuperview];
472 [
self resignAndRemoveFromSuperview];
474 if (_activeModel && _activeModel->composing()) {
475 _activeModel->CommitComposing();
476 _activeModel->EndComposing();
478 [_textInputContext discardMarkedText];
482 _enableDeltaModel = NO;
484 _activeModel =
nullptr;
485 _currentViewController = nil;
486 _originalViewController = nil;
490 [
self setEditingState:state];
494 [
self setEditableTransform:state[kTransformKey]];
498 [
self updateCaretRect:rect];
505- (void)setEditableTransform:(NSArray*)matrix {
506 CATransform3D*
transform = &_editableTransform;
508 transform->m11 = [matrix[0] doubleValue];
509 transform->m12 = [matrix[1] doubleValue];
510 transform->m13 = [matrix[2] doubleValue];
511 transform->m14 = [matrix[3] doubleValue];
513 transform->m21 = [matrix[4] doubleValue];
514 transform->m22 = [matrix[5] doubleValue];
515 transform->m23 = [matrix[6] doubleValue];
516 transform->m24 = [matrix[7] doubleValue];
518 transform->m31 = [matrix[8] doubleValue];
519 transform->m32 = [matrix[9] doubleValue];
520 transform->m33 = [matrix[10] doubleValue];
521 transform->m34 = [matrix[11] doubleValue];
523 transform->m41 = [matrix[12] doubleValue];
524 transform->m42 = [matrix[13] doubleValue];
525 transform->m43 = [matrix[14] doubleValue];
526 transform->m44 = [matrix[15] doubleValue];
529- (void)updateCaretRect:(NSDictionary*)dictionary {
530 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
531 dictionary[
@"height"] != nil,
532 @"Expected a dictionary representing a CGRect, got %@", dictionary);
533 _caretRect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
534 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
537- (void)setEditingState:(NSDictionary*)state {
538 NSString* selectionAffinity = state[kSelectionAffinityKey];
539 if (selectionAffinity != nil) {
540 _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
541 ? kFlutterTextAffinityUpstream
542 : kFlutterTextAffinityDownstream;
545 NSString*
text = state[kTextKey];
549 _activeModel->SetSelection(selected_range);
554 const bool wasComposing = _activeModel->composing();
555 _activeModel->SetText([
text UTF8String], selected_range, composing_range);
556 if (composing_range.
collapsed() && wasComposing) {
557 [_textInputContext discardMarkedText];
559 [_client startEditing];
561 [
self updateTextAndSelection];
564- (NSDictionary*)editingState {
565 if (_activeModel ==
nullptr) {
569 NSString*
const textAffinity = [
self textAffinityString];
571 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
572 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
581 kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()] ?: [NSNull null],
585- (void)updateEditState {
586 if (_activeModel ==
nullptr) {
590 NSDictionary* state = [
self editingState];
591 [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[ _clientID, state ]];
592 [
self updateTextAndSelection];
595- (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta {
596 NSUInteger selectionBase = _activeModel->
selection().base();
597 NSUInteger selectionExtent = _activeModel->selection().extent();
598 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
599 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
601 NSString*
const textAffinity = [
self textAffinityString];
603 NSDictionary* deltaToFramework = @{
604 @"oldText" : @(delta.old_text().c_str()),
605 @"deltaText" : @(delta.delta_text().c_str()),
606 @"deltaStart" : @(delta.delta_start()),
607 @"deltaEnd" : @(delta.delta_end()),
608 @"selectionBase" : @(selectionBase),
609 @"selectionExtent" : @(selectionExtent),
610 @"selectionAffinity" : textAffinity,
611 @"selectionIsDirectional" : @(false),
612 @"composingBase" : @(composingBase),
613 @"composingExtent" : @(composingExtent),
616 NSDictionary* deltas = @{
617 @"deltas" : @[ deltaToFramework ],
620 [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod arguments:@[ _clientID, deltas ]];
621 [
self updateTextAndSelection];
624- (void)updateTextAndSelection {
625 NSAssert(_activeModel !=
nullptr,
@"Flutter text model must not be null.");
626 NSString*
text = @(_activeModel->GetText().data());
627 int start = _activeModel->selection().base();
628 int extend = _activeModel->selection().extent();
629 NSRange selection = NSMakeRange(MIN(start, extend), ABS(start - extend));
635 [_client updateString:text withSelection:selection];
638 [
self setSelectedRange:selection];
642- (NSString*)textAffinityString {
647- (
BOOL)handleKeyEvent:(NSEvent*)event {
648 if (event.type == NSEventTypeKeyUp ||
649 (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
652 _previouslyPressedFlags =
event.modifierFlags;
657 _eventProducedOutput = NO;
658 BOOL res = [_textInputContext handleEvent:event];
668 bool is_navigation =
event.modifierFlags & NSEventModifierFlagFunction &&
669 event.modifierFlags & NSEventModifierFlagNumericPad;
670 bool is_navigation_in_ime = is_navigation &&
self.hasMarkedText;
672 if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) {
679#pragma mark NSResponder
681- (void)keyDown:(NSEvent*)event {
682 [_currentViewController keyDown:event];
685- (void)keyUp:(NSEvent*)event {
686 [_currentViewController keyUp:event];
689- (
BOOL)performKeyEquivalent:(NSEvent*)event {
690 if ([_currentViewController isDispatchingKeyEvent:event]) {
702 [event markAsKeyEquivalent];
703 [_currentViewController keyDown:event];
707- (void)flagsChanged:(NSEvent*)event {
708 [_currentViewController flagsChanged:event];
711- (void)mouseDown:(NSEvent*)event {
712 [_currentViewController mouseDown:event];
715- (void)mouseUp:(NSEvent*)event {
716 [_currentViewController mouseUp:event];
719- (void)mouseDragged:(NSEvent*)event {
720 [_currentViewController mouseDragged:event];
723- (void)rightMouseDown:(NSEvent*)event {
724 [_currentViewController rightMouseDown:event];
727- (void)rightMouseUp:(NSEvent*)event {
728 [_currentViewController rightMouseUp:event];
731- (void)rightMouseDragged:(NSEvent*)event {
732 [_currentViewController rightMouseDragged:event];
735- (void)otherMouseDown:(NSEvent*)event {
736 [_currentViewController otherMouseDown:event];
739- (void)otherMouseUp:(NSEvent*)event {
740 [_currentViewController otherMouseUp:event];
743- (void)otherMouseDragged:(NSEvent*)event {
744 [_currentViewController otherMouseDragged:event];
747- (void)mouseMoved:(NSEvent*)event {
748 [_currentViewController mouseMoved:event];
751- (void)scrollWheel:(NSEvent*)event {
752 [_currentViewController scrollWheel:event];
755- (NSTextInputContext*)inputContext {
756 return _textInputContext;
760#pragma mark NSTextInputClient
762- (void)insertTab:(
id)sender {
767- (void)insertText:(
id)string replacementRange:(NSRange)range {
768 if (_activeModel ==
nullptr) {
772 _eventProducedOutput |=
true;
774 if (range.location != NSNotFound) {
778 long signedLength =
static_cast<long>(range.length);
780 long textLength = _activeModel->text_range().end();
783 size_t extent = std::clamp(
location + signedLength, 0L, textLength);
786 }
else if (_activeModel->composing() &&
787 !(_activeModel->composing_range() == _activeModel->selection())) {
804 std::string textBeforeChange = _activeModel->GetText().c_str();
806 BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
807 const NSString* rawString = isAttributedString ? [string string] : string;
808 std::string utf8String = rawString ? [rawString UTF8String] :
"";
809 _activeModel->AddText(utf8String);
810 if (_activeModel->composing()) {
811 replacedRange = composingBeforeChange;
812 _activeModel->CommitComposing();
813 _activeModel->EndComposing();
815 replacedRange = range.location == NSNotFound
819 if (_enableDeltaModel) {
820 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
823 [
self updateEditState];
827- (void)doCommandBySelector:(
SEL)selector {
828 _eventProducedOutput |= selector != NSSelectorFromString(
@"noop:");
829 if ([
self respondsToSelector:selector]) {
833 IMP imp = [
self methodForSelector:selector];
834 void (*func)(
id, SEL,
id) =
reinterpret_cast<void (*)(
id,
SEL,
id)
>(imp);
835 func(
self, selector, nil);
837 if (_clientID == nil) {
842 if (selector ==
@selector(insertNewline:)) {
849 NSString*
name = NSStringFromSelector(selector);
850 if (_pendingSelectors == nil) {
851 _pendingSelectors = [NSMutableArray array];
853 [_pendingSelectors addObject:name];
855 if (_pendingSelectors.count == 1) {
856 __weak NSMutableArray* selectors = _pendingSelectors;
858 __weak NSNumber* clientID = _clientID;
860 CFStringRef runLoopMode =
self.customRunLoopMode != nil
861 ? (__bridge CFStringRef)
self.customRunLoopMode
862 : kCFRunLoopCommonModes;
864 CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
865 if (selectors.count > 0) {
866 [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
867 [selectors removeAllObjects];
873- (void)insertNewline:(
id)sender {
874 if (_activeModel ==
nullptr) {
877 if (_activeModel->composing()) {
878 _activeModel->CommitComposing();
879 _activeModel->EndComposing();
883 [
self insertText:@"\n" replacementRange:self.selectedRange];
885 [_channel invokeMethod:kPerformAction arguments:@[ _clientID, _inputAction ]];
888- (void)setMarkedText:(
id)string
889 selectedRange:(NSRange)selectedRange
890 replacementRange:(NSRange)replacementRange {
891 if (_activeModel ==
nullptr) {
894 std::string textBeforeChange = _activeModel->GetText().c_str();
895 if (!_activeModel->composing()) {
896 _activeModel->BeginComposing();
899 if (replacementRange.location != NSNotFound) {
905 _activeModel->SetComposingRange(
907 replacementRange.location + replacementRange.length),
915 BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
916 const NSString* rawString = isAttributedString ? [string string] : string;
917 _activeModel->UpdateComposingText(
918 (
const char16_t*)[rawString cStringUsingEncoding:NSUTF16StringEncoding],
921 if (_enableDeltaModel) {
922 std::string marked_text = [rawString UTF8String];
923 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
924 selectionBeforeChange.collapsed()
925 ? composingBeforeChange
926 : selectionBeforeChange,
929 [
self updateEditState];
934 if (_activeModel ==
nullptr) {
937 _activeModel->CommitComposing();
938 _activeModel->EndComposing();
939 if (_enableDeltaModel) {
940 [
self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
942 [
self updateEditState];
946- (NSRange)markedRange {
947 if (_activeModel ==
nullptr) {
948 return NSMakeRange(NSNotFound, 0);
951 _activeModel->composing_range().base(),
952 _activeModel->composing_range().extent() - _activeModel->composing_range().base());
955- (
BOOL)hasMarkedText {
956 return _activeModel !=
nullptr && _activeModel->composing_range().length() > 0;
959- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
960 actualRange:(NSRangePointer)actualRange {
961 if (_activeModel ==
nullptr) {
964 NSString*
text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
965 if (range.location >=
text.length) {
968 range.length = std::min(range.length,
text.length - range.location);
969 if (actualRange != nil) {
970 *actualRange = range;
972 NSString* substring = [text substringWithRange:range];
973 return [[NSAttributedString alloc] initWithString:substring attributes:nil];
976- (NSArray<NSString*>*)validAttributesForMarkedText {
982- (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
985 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
986 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
987 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
988 incomingRect.origin.y + incomingRect.size.height)};
990 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
991 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
993 for (
int i = 0;
i < 4;
i++) {
994 const CGPoint point =
points[i];
996 CGFloat
x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.
y +
997 _editableTransform.m41;
998 CGFloat
y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.
y +
999 _editableTransform.m42;
1001 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.
y +
1002 _editableTransform.m44;
1006 }
else if (w != 1.0) {
1011 origin.x = MIN(origin.x,
x);
1012 origin.y = MIN(origin.y,
y);
1013 farthest.x = MAX(farthest.x,
x);
1014 farthest.y = MAX(farthest.y,
y);
1017 const NSView* fromView = _originalViewController.flutterView;
1018 const CGRect rectInWindow = [fromView
1019 convertRect:CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y)
1021 NSWindow*
window = fromView.window;
1022 return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
1025- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
1028 return !_originalViewController.viewLoaded || CGRectEqualToRect(_caretRect, CGRectNull)
1030 : [
self screenRectFromFrameworkTransform:_caretRect];
1033- (NSUInteger)characterIndexForPoint:(NSPoint)point {
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
FlutterMethodChannel * _channel
uint64_t _previouslyPressedFlags
NSTextInputContext * _textInputContext
CATransform3D _editableTransform
std::unique_ptr< flutter::TextInputModel > _activeModel
NSMutableArray * _pendingSelectors
FlutterTextAffinity _textAffinity
__weak FlutterViewController * _originalViewController
__weak FlutterViewController * _currentViewController
FlutterMethodChannel * _channel
__weak id< FlutterTextInputPluginDelegate > _delegate
BOOL _eventProducedOutput
void markAsKeyEquivalent()
#define FML_DCHECK(condition)
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
static NSString *const kAutofillHints
static NSString *const kAutofillEditingValue
static NSString *const kSecureTextEntry
static NSString *const kShowMethod
static FLUTTER_ASSERT_ARC const char kTextAffinityDownstream[]
typedef NS_ENUM(NSInteger, FlutterAutofillType)
static NSString *const kAssociatedAutofillFields
static NSString *const kSetEditingStateMethod
static NSString *const kAutofillId
static NSString *const kClearClientMethod
static NSString *const kEnableDeltaModel
static NSString *const kSetClientMethod
static NSString *const kAutofillProperties
static const char kTextAffinityUpstream[]
static NSString *const kHideMethod
static NSString *const kViewId
static NSString *const kUpdateEditStateWithDeltasResponseMethod
static NSString *const kTextKey
static NSString *const kMultilineInputType
static NSString *const kPerformSelectors
static NSString * GetAutofillHint(NSDictionary *autofill)
static NSString *const kSetEditableSizeAndTransform
static NSTextContentType GetTextContentType(NSDictionary *configuration) API_AVAILABLE(macos(11.0))
static NSString *const kSelectionBaseKey
static NSString *const kSelectionAffinityKey
static NSString *const kComposingExtentKey
static NSString *const kSelectionExtentKey
static NSString *const kPerformAction
static NSString *const kInputActionNewline
static NSString *const kTextInputChannel
static NSString *const kTextInputType
static NSString *const kTransformKey
static NSString *const kUpdateEditStateResponseMethod
static BOOL EnableAutocompleteForTextInputConfiguration(NSDictionary *configuration)
static BOOL EnableAutocomplete(NSDictionary *configuration)
static NSString *const kSetCaretRect
static NSString *const kTextInputAction
static flutter::TextRange RangeFromBaseExtent(NSNumber *base, NSNumber *extent, const flutter::TextRange &range)
static NSString *const kComposingBaseKey
static NSString *const kTextInputTypeName
static NSString *const kSelectionIsDirectionalKey
constexpr int64_t kFlutterImplicitViewId
std::vector< Point > points