7 #import <Foundation/Foundation.h>
8 #import <objc/message.h>
13 #include "flutter/fml/platform/darwin/string_range_sanitization.h"
22 #pragma mark - TextInput channel method names
33 @"TextInputClient.updateEditingStateWithDeltas";
38 #pragma mark - TextInputConfiguration field names
73 typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
74 kFlutterTextAffinityUpstream,
75 kFlutterTextAffinityDownstream
78 #pragma mark - Static functions
86 if (base == nil || extent == nil) {
89 if (base.intValue == -1 && extent.intValue == -1) {
98 return hints.count > 0 ? hints[0] : nil;
104 API_AVAILABLE(macos(11.0)) {
109 if ([hint isEqualToString:
@"username"]) {
110 return NSTextContentTypeUsername;
112 if ([hint isEqualToString:
@"password"]) {
113 return NSTextContentTypePassword;
115 if ([hint isEqualToString:
@"oneTimeCode"]) {
116 return NSTextContentTypeOneTimeCode;
121 return NSTextContentTypePassword;
137 if (autofill == nil) {
144 if ([hint isEqualToString:
@"password"] || [hint isEqualToString:
@"username"]) {
168 #pragma mark - NSEvent (KeyEquivalentMarker) protocol
190 objc_setAssociatedObject(
self, &
markerKey, @
true, OBJC_ASSOCIATION_RETAIN);
194 return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
199 #pragma mark - FlutterTextInputPlugin private interface
226 @property(nonatomic) BOOL shown;
231 @property(nonatomic) uint64_t previouslyPressedFlags;
241 @property(nonatomic, nonnull) NSNumber* clientID;
247 @property(nonatomic, nonnull) NSString* inputType;
253 @property(nonatomic, nonnull) NSString* inputAction;
260 @property(nonatomic) BOOL eventProducedOutput;
268 @property(nonatomic) BOOL enableDeltaModel;
275 @property(nonatomic) NSMutableArray* pendingSelectors;
286 - (void)setEditingState:(NSDictionary*)state;
292 - (void)updateEditState;
298 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta;
307 - (void)updateTextAndSelection;
313 - (NSString*)textAffinityString;
322 #pragma mark - FlutterTextInputPlugin
328 std::unique_ptr<flutter::TextInputModel> _activeModel;
344 self = [
super initWithFrame:NSZeroRect];
345 self.clipsToBounds = YES;
347 _flutterViewController = viewController;
358 [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
359 [unsafeSelf handleMethodCall:call result:result];
361 _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
362 _previouslyPressedFlags = 0;
372 - (BOOL)isFirstResponder {
373 if (!
self.flutterViewController.viewLoaded) {
376 return [
self.flutterViewController.view.window firstResponder] ==
self;
380 [_channel setMethodCallHandler:nil];
383 #pragma mark - Private
385 - (void)resignAndRemoveFromSuperview {
386 if (
self.superview != nil) {
389 NSResponder* nextResponder = _client != nil ? _client.nextResponder :
self.nextResponder;
390 [
self.window makeFirstResponder:nextResponder];
391 [
self removeFromSuperview];
397 NSString* method = call.
method;
401 errorWithCode:
@"error"
402 message:
@"Missing arguments"
403 details:
@"Missing arguments while trying to set a text input client"]);
407 if (clientID != nil) {
408 NSDictionary* config = call.
arguments[1];
410 _clientID = clientID;
411 _inputAction = config[kTextInputAction];
412 _enableDeltaModel = [config[kEnableDeltaModel] boolValue];
413 NSDictionary* inputTypeInfo = config[kTextInputType];
414 _inputType = inputTypeInfo[kTextInputTypeName];
415 self.textAffinity = kFlutterTextAffinityUpstream;
417 if (@available(macOS 11.0, *)) {
421 _activeModel = std::make_unique<flutter::TextInputModel>();
427 if (_client == nil) {
428 [_flutterViewController.view addSubview:self];
430 [
self.window makeFirstResponder:self];
433 [
self resignAndRemoveFromSuperview];
436 [
self resignAndRemoveFromSuperview];
438 if (_activeModel && _activeModel->composing()) {
439 _activeModel->CommitComposing();
440 _activeModel->EndComposing();
442 [_textInputContext discardMarkedText];
446 _enableDeltaModel = NO;
448 _activeModel =
nullptr;
451 [
self setEditingState:state];
454 [
self setEditableTransform:state[kTransformKey]];
457 [
self updateCaretRect:rect];
464 - (void)setEditableTransform:(NSArray*)matrix {
467 transform->m11 = [matrix[0] doubleValue];
468 transform->m12 = [matrix[1] doubleValue];
469 transform->m13 = [matrix[2] doubleValue];
470 transform->m14 = [matrix[3] doubleValue];
472 transform->m21 = [matrix[4] doubleValue];
473 transform->m22 = [matrix[5] doubleValue];
474 transform->m23 = [matrix[6] doubleValue];
475 transform->m24 = [matrix[7] doubleValue];
477 transform->m31 = [matrix[8] doubleValue];
478 transform->m32 = [matrix[9] doubleValue];
479 transform->m33 = [matrix[10] doubleValue];
480 transform->m34 = [matrix[11] doubleValue];
482 transform->m41 = [matrix[12] doubleValue];
483 transform->m42 = [matrix[13] doubleValue];
484 transform->m43 = [matrix[14] doubleValue];
485 transform->m44 = [matrix[15] doubleValue];
488 - (void)updateCaretRect:(NSDictionary*)dictionary {
489 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
490 dictionary[
@"height"] != nil,
491 @"Expected a dictionary representing a CGRect, got %@", dictionary);
492 _caretRect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
493 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
496 - (void)setEditingState:(NSDictionary*)state {
497 NSString* selectionAffinity = state[kSelectionAffinityKey];
498 if (selectionAffinity != nil) {
499 _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
500 ? kFlutterTextAffinityUpstream
501 : kFlutterTextAffinityDownstream;
504 NSString* text = state[kTextKey];
508 _activeModel->SetSelection(selected_range);
513 const bool wasComposing = _activeModel->composing();
514 _activeModel->SetText([text UTF8String], selected_range, composing_range);
515 if (composing_range.
collapsed() && wasComposing) {
516 [_textInputContext discardMarkedText];
518 [_client startEditing];
520 [
self updateTextAndSelection];
523 - (NSDictionary*)editingState {
524 if (_activeModel ==
nullptr) {
528 NSString*
const textAffinity = [
self textAffinityString];
530 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
531 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
540 kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()] ?: [NSNull null],
544 - (void)updateEditState {
545 if (_activeModel ==
nullptr) {
549 NSDictionary* state = [
self editingState];
550 [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[
self.clientID, state ]];
551 [
self updateTextAndSelection];
554 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta {
555 NSUInteger selectionBase = _activeModel->selection().base();
556 NSUInteger selectionExtent = _activeModel->selection().extent();
557 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
558 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
560 NSString*
const textAffinity = [
self textAffinityString];
562 NSDictionary* deltaToFramework = @{
563 @"oldText" : @(delta.old_text().c_str()),
564 @"deltaText" : @(delta.delta_text().c_str()),
565 @"deltaStart" : @(delta.delta_start()),
566 @"deltaEnd" : @(delta.delta_end()),
567 @"selectionBase" : @(selectionBase),
568 @"selectionExtent" : @(selectionExtent),
569 @"selectionAffinity" : textAffinity,
570 @"selectionIsDirectional" : @(
false),
571 @"composingBase" : @(composingBase),
572 @"composingExtent" : @(composingExtent),
575 NSDictionary* deltas = @{
576 @"deltas" : @[ deltaToFramework ],
579 [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod
580 arguments:@[
self.clientID, deltas ]];
581 [
self updateTextAndSelection];
584 - (void)updateTextAndSelection {
585 NSAssert(_activeModel !=
nullptr,
@"Flutter text model must not be null.");
586 NSString* text = @(_activeModel->GetText().data());
587 int start = _activeModel->selection().base();
588 int extend = _activeModel->selection().extent();
589 NSRange selection = NSMakeRange(MIN(start, extend), ABS(start - extend));
595 [_client updateString:text withSelection:selection];
598 [
self setSelectedRange:selection];
602 - (NSString*)textAffinityString {
607 - (BOOL)handleKeyEvent:(NSEvent*)event {
608 if (event.type == NSEventTypeKeyUp ||
609 (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
612 _previouslyPressedFlags =
event.modifierFlags;
617 _eventProducedOutput = NO;
618 BOOL res = [_textInputContext handleEvent:event];
628 bool is_navigation =
event.modifierFlags & NSEventModifierFlagFunction &&
629 event.modifierFlags & NSEventModifierFlagNumericPad;
630 bool is_navigation_in_ime = is_navigation &&
self.hasMarkedText;
632 if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) {
639 #pragma mark NSResponder
641 - (void)keyDown:(NSEvent*)event {
642 [
self.flutterViewController keyDown:event];
645 - (void)keyUp:(NSEvent*)event {
646 [
self.flutterViewController keyUp:event];
649 - (BOOL)performKeyEquivalent:(NSEvent*)event {
650 if ([_flutterViewController isDispatchingKeyEvent:event]) {
662 [event markAsKeyEquivalent];
663 [
self.flutterViewController keyDown:event];
667 - (void)flagsChanged:(NSEvent*)event {
668 [
self.flutterViewController flagsChanged:event];
671 - (void)mouseDown:(NSEvent*)event {
672 [
self.flutterViewController mouseDown:event];
675 - (void)mouseUp:(NSEvent*)event {
676 [
self.flutterViewController mouseUp:event];
679 - (void)mouseDragged:(NSEvent*)event {
680 [
self.flutterViewController mouseDragged:event];
683 - (void)rightMouseDown:(NSEvent*)event {
684 [
self.flutterViewController rightMouseDown:event];
687 - (void)rightMouseUp:(NSEvent*)event {
688 [
self.flutterViewController rightMouseUp:event];
691 - (void)rightMouseDragged:(NSEvent*)event {
692 [
self.flutterViewController rightMouseDragged:event];
695 - (void)otherMouseDown:(NSEvent*)event {
696 [
self.flutterViewController otherMouseDown:event];
699 - (void)otherMouseUp:(NSEvent*)event {
700 [
self.flutterViewController otherMouseUp:event];
703 - (void)otherMouseDragged:(NSEvent*)event {
704 [
self.flutterViewController otherMouseDragged:event];
707 - (void)mouseMoved:(NSEvent*)event {
708 [
self.flutterViewController mouseMoved:event];
711 - (void)scrollWheel:(NSEvent*)event {
712 [
self.flutterViewController scrollWheel:event];
715 - (NSTextInputContext*)inputContext {
716 return _textInputContext;
720 #pragma mark NSTextInputClient
722 - (void)insertTab:(
id)sender {
727 - (void)insertText:(
id)string replacementRange:(NSRange)range {
728 if (_activeModel ==
nullptr) {
732 _eventProducedOutput |=
true;
734 if (range.location != NSNotFound) {
738 long signedLength =
static_cast<long>(range.length);
739 long location = range.location;
740 long textLength = _activeModel->text_range().end();
742 size_t base = std::clamp(location, 0L, textLength);
743 size_t extent = std::clamp(location + signedLength, 0L, textLength);
752 std::string textBeforeChange = _activeModel->GetText().c_str();
753 std::string utf8String = [string UTF8String];
754 _activeModel->AddText(utf8String);
755 if (_activeModel->composing()) {
756 replacedRange = composingBeforeChange;
757 _activeModel->CommitComposing();
758 _activeModel->EndComposing();
760 replacedRange = range.location == NSNotFound
762 :
flutter::TextRange(range.location, range.location + range.length);
764 if (_enableDeltaModel) {
765 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
768 [
self updateEditState];
772 - (void)doCommandBySelector:(
SEL)selector {
773 _eventProducedOutput |= selector != NSSelectorFromString(
@"noop:");
774 if ([
self respondsToSelector:selector]) {
778 IMP imp = [
self methodForSelector:selector];
779 void (*func)(id, SEL, id) =
reinterpret_cast<void (*)(
id,
SEL,
id)
>(imp);
780 func(
self, selector, nil);
783 if (selector ==
@selector(insertNewline:)) {
790 NSString* name = NSStringFromSelector(selector);
791 if (_pendingSelectors == nil) {
792 _pendingSelectors = [NSMutableArray array];
794 [_pendingSelectors addObject:name];
796 if (_pendingSelectors.count == 1) {
797 __weak NSMutableArray* selectors = _pendingSelectors;
799 __weak NSNumber* clientID =
self.clientID;
801 CFStringRef runLoopMode =
self.customRunLoopMode != nil
802 ? (__bridge CFStringRef)
self.customRunLoopMode
803 : kCFRunLoopCommonModes;
805 CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
806 if (selectors.count > 0) {
807 [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
808 [selectors removeAllObjects];
814 - (void)insertNewline:(
id)sender {
815 if (_activeModel ==
nullptr) {
818 if (_activeModel->composing()) {
819 _activeModel->CommitComposing();
820 _activeModel->EndComposing();
824 [
self insertText:@"\n" replacementRange:self.selectedRange];
826 [_channel invokeMethod:kPerformAction arguments:@[
self.clientID, self.inputAction ]];
829 - (void)setMarkedText:(
id)string
830 selectedRange:(NSRange)selectedRange
831 replacementRange:(NSRange)replacementRange {
832 if (_activeModel ==
nullptr) {
835 std::string textBeforeChange = _activeModel->GetText().c_str();
836 if (!_activeModel->composing()) {
837 _activeModel->BeginComposing();
840 if (replacementRange.location != NSNotFound) {
846 _activeModel->SetComposingRange(
848 replacementRange.location + replacementRange.length),
856 BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
857 std::string marked_text = isAttributedString ? [[string string] UTF8String] : [string UTF8String];
858 _activeModel->UpdateComposingText(marked_text);
861 long signedLength =
static_cast<long>(selectedRange.length);
862 long location = selectedRange.location + _activeModel->composing_range().base();
863 long textLength = _activeModel->text_range().
end();
865 size_t base = std::clamp(location, 0L, textLength);
866 size_t extent = std::clamp(location + signedLength, 0L, textLength);
869 if (_enableDeltaModel) {
870 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
871 selectionBeforeChange.collapsed()
872 ? composingBeforeChange
873 : selectionBeforeChange,
876 [
self updateEditState];
881 if (_activeModel ==
nullptr) {
884 _activeModel->CommitComposing();
885 _activeModel->EndComposing();
886 if (_enableDeltaModel) {
887 [
self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
889 [
self updateEditState];
893 - (NSRange)markedRange {
894 if (_activeModel ==
nullptr) {
895 return NSMakeRange(NSNotFound, 0);
898 _activeModel->composing_range().base(),
899 _activeModel->composing_range().extent() - _activeModel->composing_range().base());
902 - (BOOL)hasMarkedText {
903 return _activeModel !=
nullptr && _activeModel->composing_range().length() > 0;
906 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
907 actualRange:(NSRangePointer)actualRange {
908 if (_activeModel ==
nullptr) {
911 if (actualRange != nil) {
912 *actualRange = range;
914 NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
915 NSString* substring = [text substringWithRange:range];
916 return [[NSAttributedString alloc] initWithString:substring attributes:nil];
919 - (NSArray<NSString*>*)validAttributesForMarkedText {
925 - (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
928 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
929 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
930 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
931 incomingRect.origin.y + incomingRect.size.height)};
933 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
934 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
936 for (
int i = 0; i < 4; i++) {
937 const CGPoint point = points[i];
949 }
else if (w != 1.0) {
954 origin.x = MIN(origin.x, x);
955 origin.y = MIN(origin.y, y);
956 farthest.x = MAX(farthest.x, x);
957 farthest.y = MAX(farthest.y, y);
960 const NSView* fromView =
self.flutterViewController.flutterView;
961 const CGRect rectInWindow = [fromView
962 convertRect:CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y)
964 NSWindow* window = fromView.window;
965 return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
968 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
971 return !
self.flutterViewController.viewLoaded || CGRectEqualToRect(
_caretRect, CGRectNull)
973 : [
self screenRectFromFrameworkTransform:_caretRect];
976 - (NSUInteger)characterIndexForPoint:(NSPoint)point {