7 #import <Foundation/Foundation.h>
8 #import <objc/message.h>
13 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
23 #pragma mark - TextInput channel method names
34 @"TextInputClient.updateEditingStateWithDeltas";
39 #pragma mark - TextInputConfiguration field names
40 static NSString*
const kViewId =
@"viewId";
75 typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
76 kFlutterTextAffinityUpstream,
77 kFlutterTextAffinityDownstream
80 #pragma mark - Static functions
88 if (base == nil || extent == nil) {
91 if (base.intValue == -1 && extent.intValue == -1) {
100 return hints.count > 0 ? hints[0] : nil;
106 API_AVAILABLE(macos(11.0)) {
111 if ([hint isEqualToString:
@"username"]) {
112 return NSTextContentTypeUsername;
114 if ([hint isEqualToString:
@"password"]) {
115 return NSTextContentTypePassword;
117 if ([hint isEqualToString:
@"oneTimeCode"]) {
118 return NSTextContentTypeOneTimeCode;
123 return NSTextContentTypePassword;
139 if (autofill == nil) {
146 if ([hint isEqualToString:
@"password"] || [hint isEqualToString:
@"username"]) {
170 #pragma mark - NSEvent (KeyEquivalentMarker) protocol
192 objc_setAssociatedObject(
self, &
markerKey, @
true, OBJC_ASSOCIATION_RETAIN);
196 return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
201 #pragma mark - FlutterTextInputPlugin private interface
308 - (void)setEditingState:(NSDictionary*)state;
314 - (void)updateEditState;
320 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta;
329 - (void)updateTextAndSelection;
335 - (NSString*)textAffinityString;
340 @property(readwrite, nonatomic) NSString* customRunLoopMode;
341 @property(nonatomic) NSTextInputContext* textInputContext;
345 #pragma mark - FlutterTextInputPlugin
352 self = [
super initWithFrame:NSZeroRect];
353 self.clipsToBounds = YES;
355 _delegate = delegate;
366 [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
367 [unsafeSelf handleMethodCall:call result:result];
369 _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
370 _previouslyPressedFlags = 0;
374 _editableTransform = CATransform3D();
375 _caretRect = CGRectNull;
381 if (!_currentViewController.viewLoaded) {
384 return [_currentViewController.view.window firstResponder] ==
self;
388 [_channel setMethodCallHandler:nil];
391 #pragma mark - Private
393 - (void)resignAndRemoveFromSuperview {
394 if (
self.superview != nil) {
395 [
self.window makeFirstResponder:_currentViewController.flutterView];
396 [
self removeFromSuperview];
402 NSString* method = call.
method;
404 FML_DCHECK(_currentViewController == nil);
407 errorWithCode:
@"error"
408 message:
@"Missing arguments"
409 details:
@"Missing arguments while trying to set a text input client"]);
413 if (clientID != nil) {
414 NSDictionary* config = call.
arguments[1];
416 _clientID = clientID;
417 _inputAction = config[kTextInputAction];
418 _enableDeltaModel = [config[kEnableDeltaModel] boolValue];
419 NSDictionary* inputTypeInfo = config[kTextInputType];
420 _inputType = inputTypeInfo[kTextInputTypeName];
421 _textAffinity = kFlutterTextAffinityUpstream;
423 if (@available(macOS 11.0, *)) {
427 _activeModel = std::make_unique<flutter::TextInputModel>();
428 NSNumber* viewId = config[kViewId];
429 _currentViewController = [_delegate viewControllerForIdentifier:viewId.longLongValue];
430 FML_DCHECK(_currentViewController != nil);
433 FML_DCHECK(_currentViewController != nil);
437 if (_client == nil) {
438 [_currentViewController.view addSubview:self];
440 [
self.window makeFirstResponder:self];
443 [
self resignAndRemoveFromSuperview];
446 FML_DCHECK(_currentViewController != nil);
447 [
self resignAndRemoveFromSuperview];
449 if (_activeModel && _activeModel->composing()) {
450 _activeModel->CommitComposing();
451 _activeModel->EndComposing();
453 [_textInputContext discardMarkedText];
457 _enableDeltaModel = NO;
459 _activeModel =
nullptr;
460 _currentViewController = nil;
462 FML_DCHECK(_currentViewController != nil);
464 [
self setEditingState:state];
466 FML_DCHECK(_currentViewController != nil);
468 [
self setEditableTransform:state[kTransformKey]];
470 FML_DCHECK(_currentViewController != nil);
472 [
self updateCaretRect:rect];
479 - (void)setEditableTransform:(NSArray*)matrix {
480 CATransform3D* transform = &_editableTransform;
482 transform->m11 = [matrix[0] doubleValue];
483 transform->m12 = [matrix[1] doubleValue];
484 transform->m13 = [matrix[2] doubleValue];
485 transform->m14 = [matrix[3] doubleValue];
487 transform->m21 = [matrix[4] doubleValue];
488 transform->m22 = [matrix[5] doubleValue];
489 transform->m23 = [matrix[6] doubleValue];
490 transform->m24 = [matrix[7] doubleValue];
492 transform->m31 = [matrix[8] doubleValue];
493 transform->m32 = [matrix[9] doubleValue];
494 transform->m33 = [matrix[10] doubleValue];
495 transform->m34 = [matrix[11] doubleValue];
497 transform->m41 = [matrix[12] doubleValue];
498 transform->m42 = [matrix[13] doubleValue];
499 transform->m43 = [matrix[14] doubleValue];
500 transform->m44 = [matrix[15] doubleValue];
503 - (void)updateCaretRect:(NSDictionary*)dictionary {
504 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
505 dictionary[
@"height"] != nil,
506 @"Expected a dictionary representing a CGRect, got %@", dictionary);
507 _caretRect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
508 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
511 - (void)setEditingState:(NSDictionary*)state {
512 NSString* selectionAffinity = state[kSelectionAffinityKey];
513 if (selectionAffinity != nil) {
514 _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
515 ? kFlutterTextAffinityUpstream
516 : kFlutterTextAffinityDownstream;
519 NSString* text = state[kTextKey];
523 _activeModel->SetSelection(selected_range);
528 const bool wasComposing = _activeModel->composing();
529 _activeModel->SetText([text UTF8String], selected_range, composing_range);
530 if (composing_range.
collapsed() && wasComposing) {
531 [_textInputContext discardMarkedText];
533 [_client startEditing];
535 [
self updateTextAndSelection];
539 if (_activeModel ==
nullptr) {
543 NSString*
const textAffinity = [
self textAffinityString];
545 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
546 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
555 kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()] ?: [NSNull null],
559 - (void)updateEditState {
560 if (_activeModel ==
nullptr) {
564 NSDictionary* state = [
self editingState];
565 [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[ _clientID, state ]];
566 [
self updateTextAndSelection];
569 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta {
570 NSUInteger selectionBase = _activeModel->selection().base();
571 NSUInteger selectionExtent = _activeModel->selection().extent();
572 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
573 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
575 NSString*
const textAffinity = [
self textAffinityString];
577 NSDictionary* deltaToFramework = @{
578 @"oldText" : @(delta.old_text().c_str()),
579 @"deltaText" : @(delta.delta_text().c_str()),
580 @"deltaStart" : @(delta.delta_start()),
581 @"deltaEnd" : @(delta.delta_end()),
582 @"selectionBase" : @(selectionBase),
583 @"selectionExtent" : @(selectionExtent),
584 @"selectionAffinity" : textAffinity,
585 @"selectionIsDirectional" : @(
false),
586 @"composingBase" : @(composingBase),
587 @"composingExtent" : @(composingExtent),
590 NSDictionary* deltas = @{
591 @"deltas" : @[ deltaToFramework ],
594 [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod arguments:@[ _clientID, deltas ]];
595 [
self updateTextAndSelection];
598 - (void)updateTextAndSelection {
599 NSAssert(_activeModel !=
nullptr,
@"Flutter text model must not be null.");
600 NSString* text = @(_activeModel->GetText().data());
601 int start = _activeModel->selection().base();
602 int extend = _activeModel->selection().extent();
603 NSRange selection = NSMakeRange(MIN(start, extend), ABS(start - extend));
609 [_client updateString:text withSelection:selection];
612 [
self setSelectedRange:selection];
616 - (NSString*)textAffinityString {
621 - (BOOL)handleKeyEvent:(NSEvent*)event {
622 if (event.type == NSEventTypeKeyUp ||
623 (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
626 _previouslyPressedFlags =
event.modifierFlags;
631 _eventProducedOutput = NO;
632 BOOL res = [_textInputContext handleEvent:event];
642 bool is_navigation =
event.modifierFlags & NSEventModifierFlagFunction &&
643 event.modifierFlags & NSEventModifierFlagNumericPad;
644 bool is_navigation_in_ime = is_navigation &&
self.hasMarkedText;
646 if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) {
653 #pragma mark NSResponder
655 - (void)keyDown:(NSEvent*)event {
656 [_currentViewController keyDown:event];
659 - (void)keyUp:(NSEvent*)event {
660 [_currentViewController keyUp:event];
663 - (BOOL)performKeyEquivalent:(NSEvent*)event {
664 if ([_currentViewController isDispatchingKeyEvent:event]) {
676 [event markAsKeyEquivalent];
677 [_currentViewController keyDown:event];
681 - (void)flagsChanged:(NSEvent*)event {
682 [_currentViewController flagsChanged:event];
685 - (void)mouseDown:(NSEvent*)event {
686 [_currentViewController mouseDown:event];
689 - (void)mouseUp:(NSEvent*)event {
690 [_currentViewController mouseUp:event];
693 - (void)mouseDragged:(NSEvent*)event {
694 [_currentViewController mouseDragged:event];
697 - (void)rightMouseDown:(NSEvent*)event {
698 [_currentViewController rightMouseDown:event];
701 - (void)rightMouseUp:(NSEvent*)event {
702 [_currentViewController rightMouseUp:event];
705 - (void)rightMouseDragged:(NSEvent*)event {
706 [_currentViewController rightMouseDragged:event];
709 - (void)otherMouseDown:(NSEvent*)event {
710 [_currentViewController otherMouseDown:event];
713 - (void)otherMouseUp:(NSEvent*)event {
714 [_currentViewController otherMouseUp:event];
717 - (void)otherMouseDragged:(NSEvent*)event {
718 [_currentViewController otherMouseDragged:event];
721 - (void)mouseMoved:(NSEvent*)event {
722 [_currentViewController mouseMoved:event];
725 - (void)scrollWheel:(NSEvent*)event {
726 [_currentViewController scrollWheel:event];
729 - (NSTextInputContext*)inputContext {
730 return _textInputContext;
734 #pragma mark NSTextInputClient
736 - (void)insertTab:(
id)sender {
741 - (void)insertText:(
id)string replacementRange:(NSRange)range {
742 if (_activeModel ==
nullptr) {
746 _eventProducedOutput |=
true;
748 if (range.location != NSNotFound) {
752 long signedLength =
static_cast<long>(range.length);
753 long location = range.location;
754 long textLength = _activeModel->text_range().end();
756 size_t base = std::clamp(location, 0L, textLength);
757 size_t extent = std::clamp(location + signedLength, 0L, textLength);
766 std::string textBeforeChange = _activeModel->GetText().c_str();
767 std::string utf8String = [string UTF8String];
768 _activeModel->AddText(utf8String);
769 if (_activeModel->composing()) {
770 replacedRange = composingBeforeChange;
771 _activeModel->CommitComposing();
772 _activeModel->EndComposing();
774 replacedRange = range.location == NSNotFound
776 :
flutter::TextRange(range.location, range.location + range.length);
778 if (_enableDeltaModel) {
779 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
782 [
self updateEditState];
786 - (void)doCommandBySelector:(
SEL)selector {
787 _eventProducedOutput |= selector != NSSelectorFromString(
@"noop:");
788 if ([
self respondsToSelector:selector]) {
792 IMP imp = [
self methodForSelector:selector];
793 void (*func)(id, SEL, id) =
reinterpret_cast<void (*)(
id,
SEL,
id)
>(imp);
794 func(
self, selector, nil);
796 if (_clientID == nil) {
801 if (selector ==
@selector(insertNewline:)) {
808 NSString* name = NSStringFromSelector(selector);
809 if (_pendingSelectors == nil) {
810 _pendingSelectors = [NSMutableArray array];
812 [_pendingSelectors addObject:name];
814 if (_pendingSelectors.count == 1) {
815 __weak NSMutableArray* selectors = _pendingSelectors;
817 __weak NSNumber* clientID = _clientID;
819 CFStringRef runLoopMode =
self.customRunLoopMode != nil
821 : kCFRunLoopCommonModes;
823 CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
824 if (selectors.count > 0) {
825 [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
826 [selectors removeAllObjects];
832 - (void)insertNewline:(
id)sender {
833 if (_activeModel ==
nullptr) {
836 if (_activeModel->composing()) {
837 _activeModel->CommitComposing();
838 _activeModel->EndComposing();
842 [
self insertText:@"\n" replacementRange:self.selectedRange];
844 [_channel invokeMethod:kPerformAction arguments:@[ _clientID, _inputAction ]];
847 - (void)setMarkedText:(
id)string
848 selectedRange:(NSRange)selectedRange
849 replacementRange:(NSRange)replacementRange {
850 if (_activeModel ==
nullptr) {
853 std::string textBeforeChange = _activeModel->GetText().c_str();
854 if (!_activeModel->composing()) {
855 _activeModel->BeginComposing();
858 if (replacementRange.location != NSNotFound) {
864 _activeModel->SetComposingRange(
866 replacementRange.location + replacementRange.length),
874 BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
875 const NSString* rawString = isAttributedString ? [string string] : string;
876 _activeModel->UpdateComposingText(
877 (
const char16_t*)[rawString cStringUsingEncoding:NSUTF16StringEncoding],
878 flutter::TextRange(selectedRange.location, selectedRange.location + selectedRange.length));
880 if (_enableDeltaModel) {
881 std::string marked_text = [rawString UTF8String];
882 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
883 selectionBeforeChange.collapsed()
884 ? composingBeforeChange
885 : selectionBeforeChange,
888 [
self updateEditState];
893 if (_activeModel ==
nullptr) {
896 _activeModel->CommitComposing();
897 _activeModel->EndComposing();
898 if (_enableDeltaModel) {
899 [
self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
901 [
self updateEditState];
905 - (NSRange)markedRange {
906 if (_activeModel ==
nullptr) {
907 return NSMakeRange(NSNotFound, 0);
910 _activeModel->composing_range().base(),
911 _activeModel->composing_range().extent() - _activeModel->composing_range().base());
914 - (BOOL)hasMarkedText {
915 return _activeModel !=
nullptr && _activeModel->composing_range().length() > 0;
918 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
919 actualRange:(NSRangePointer)actualRange {
920 if (_activeModel ==
nullptr) {
923 NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
924 if (range.location >= text.length) {
927 range.length = std::min(range.length, text.length - range.location);
928 if (actualRange != nil) {
929 *actualRange = range;
931 NSString* substring = [text substringWithRange:range];
932 return [[NSAttributedString alloc] initWithString:substring attributes:nil];
935 - (NSArray<NSString*>*)validAttributesForMarkedText {
941 - (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
944 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
945 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
946 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
947 incomingRect.origin.y + incomingRect.size.height)};
949 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
950 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
952 for (
int i = 0; i < 4; i++) {
953 const CGPoint point = points[i];
955 CGFloat x = _editableTransform.m11 * point.x + _editableTransform.m21 * point.y +
956 _editableTransform.m41;
957 CGFloat y = _editableTransform.m12 * point.x + _editableTransform.m22 * point.y +
958 _editableTransform.m42;
960 const CGFloat w = _editableTransform.m14 * point.x + _editableTransform.m24 * point.y +
961 _editableTransform.m44;
965 }
else if (w != 1.0) {
970 origin.x = MIN(origin.x, x);
971 origin.y = MIN(origin.y, y);
972 farthest.x = MAX(farthest.x, x);
973 farthest.y = MAX(farthest.y, y);
976 const NSView* fromView = _currentViewController.flutterView;
977 const CGRect rectInWindow = [fromView
978 convertRect:CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y)
980 NSWindow* window = fromView.window;
981 return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
984 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
987 return !_currentViewController.viewLoaded || CGRectEqualToRect(_caretRect, CGRectNull)
989 : [
self screenRectFromFrameworkTransform:_caretRect];
992 - (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
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)