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
74 typedef NS_ENUM(NSUInteger, FlutterTextAffinity) {
75 kFlutterTextAffinityUpstream,
76 kFlutterTextAffinityDownstream
79 #pragma mark - Static functions
87 if (base == nil || extent == nil) {
90 if (base.intValue == -1 && extent.intValue == -1) {
99 return hints.count > 0 ? hints[0] : nil;
105 API_AVAILABLE(macos(11.0)) {
110 if ([hint isEqualToString:
@"username"]) {
111 return NSTextContentTypeUsername;
113 if ([hint isEqualToString:
@"password"]) {
114 return NSTextContentTypePassword;
116 if ([hint isEqualToString:
@"oneTimeCode"]) {
117 return NSTextContentTypeOneTimeCode;
122 return NSTextContentTypePassword;
138 if (autofill == nil) {
145 if ([hint isEqualToString:
@"password"] || [hint isEqualToString:
@"username"]) {
169 #pragma mark - NSEvent (KeyEquivalentMarker) protocol
191 objc_setAssociatedObject(
self, &
markerKey, @
true, OBJC_ASSOCIATION_RETAIN);
195 return [objc_getAssociatedObject(self, &markerKey) boolValue] == YES;
200 #pragma mark - FlutterTextInputPlugin private interface
227 @property(nonatomic) BOOL shown;
232 @property(nonatomic) uint64_t previouslyPressedFlags;
242 @property(nonatomic, nonnull) NSNumber* clientID;
248 @property(nonatomic, nonnull) NSString* inputType;
254 @property(nonatomic, nonnull) NSString* inputAction;
261 @property(nonatomic) BOOL eventProducedOutput;
269 @property(nonatomic) BOOL enableDeltaModel;
276 @property(nonatomic) NSMutableArray* pendingSelectors;
287 - (void)setEditingState:(NSDictionary*)state;
293 - (void)updateEditState;
299 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta;
308 - (void)updateTextAndSelection;
314 - (NSString*)textAffinityString;
323 #pragma mark - FlutterTextInputPlugin
329 std::unique_ptr<flutter::TextInputModel> _activeModel;
345 self = [
super initWithFrame:NSZeroRect];
346 self.clipsToBounds = YES;
348 _flutterViewController = viewController;
359 [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
360 [unsafeSelf handleMethodCall:call result:result];
362 _textInputContext = [[NSTextInputContext alloc] initWithClient:unsafeSelf];
363 _previouslyPressedFlags = 0;
373 - (BOOL)isFirstResponder {
374 if (!
self.flutterViewController.viewLoaded) {
377 return [
self.flutterViewController.view.window firstResponder] ==
self;
381 [_channel setMethodCallHandler:nil];
384 #pragma mark - Private
386 - (void)resignAndRemoveFromSuperview {
387 if (
self.superview != nil) {
388 [
self.window makeFirstResponder:_flutterViewController.flutterView];
389 [
self removeFromSuperview];
395 NSString* method = call.
method;
399 errorWithCode:
@"error"
400 message:
@"Missing arguments"
401 details:
@"Missing arguments while trying to set a text input client"]);
405 if (clientID != nil) {
406 NSDictionary* config = call.
arguments[1];
408 _clientID = clientID;
409 _inputAction = config[kTextInputAction];
410 _enableDeltaModel = [config[kEnableDeltaModel] boolValue];
411 NSDictionary* inputTypeInfo = config[kTextInputType];
412 _inputType = inputTypeInfo[kTextInputTypeName];
413 self.textAffinity = kFlutterTextAffinityUpstream;
415 if (@available(macOS 11.0, *)) {
419 _activeModel = std::make_unique<flutter::TextInputModel>();
425 if (_client == nil) {
426 [_flutterViewController.view addSubview:self];
428 [
self.window makeFirstResponder:self];
431 [
self resignAndRemoveFromSuperview];
434 [
self resignAndRemoveFromSuperview];
436 if (_activeModel && _activeModel->composing()) {
437 _activeModel->CommitComposing();
438 _activeModel->EndComposing();
440 [_textInputContext discardMarkedText];
444 _enableDeltaModel = NO;
446 _activeModel =
nullptr;
449 [
self setEditingState:state];
452 [
self setEditableTransform:state[kTransformKey]];
455 [
self updateCaretRect:rect];
462 - (void)setEditableTransform:(NSArray*)matrix {
465 transform->m11 = [matrix[0] doubleValue];
466 transform->m12 = [matrix[1] doubleValue];
467 transform->m13 = [matrix[2] doubleValue];
468 transform->m14 = [matrix[3] doubleValue];
470 transform->m21 = [matrix[4] doubleValue];
471 transform->m22 = [matrix[5] doubleValue];
472 transform->m23 = [matrix[6] doubleValue];
473 transform->m24 = [matrix[7] doubleValue];
475 transform->m31 = [matrix[8] doubleValue];
476 transform->m32 = [matrix[9] doubleValue];
477 transform->m33 = [matrix[10] doubleValue];
478 transform->m34 = [matrix[11] doubleValue];
480 transform->m41 = [matrix[12] doubleValue];
481 transform->m42 = [matrix[13] doubleValue];
482 transform->m43 = [matrix[14] doubleValue];
483 transform->m44 = [matrix[15] doubleValue];
486 - (void)updateCaretRect:(NSDictionary*)dictionary {
487 NSAssert(dictionary[
@"x"] != nil && dictionary[
@"y"] != nil && dictionary[
@"width"] != nil &&
488 dictionary[
@"height"] != nil,
489 @"Expected a dictionary representing a CGRect, got %@", dictionary);
490 _caretRect = CGRectMake([dictionary[
@"x"] doubleValue], [dictionary[
@"y"] doubleValue],
491 [dictionary[
@"width"] doubleValue], [dictionary[
@"height"] doubleValue]);
494 - (void)setEditingState:(NSDictionary*)state {
495 NSString* selectionAffinity = state[kSelectionAffinityKey];
496 if (selectionAffinity != nil) {
497 _textAffinity = [selectionAffinity isEqualToString:kTextAffinityUpstream]
498 ? kFlutterTextAffinityUpstream
499 : kFlutterTextAffinityDownstream;
502 NSString* text = state[kTextKey];
506 _activeModel->SetSelection(selected_range);
511 const bool wasComposing = _activeModel->composing();
512 _activeModel->SetText([text UTF8String], selected_range, composing_range);
513 if (composing_range.
collapsed() && wasComposing) {
514 [_textInputContext discardMarkedText];
516 [_client startEditing];
518 [
self updateTextAndSelection];
521 - (NSDictionary*)editingState {
522 if (_activeModel ==
nullptr) {
526 NSString*
const textAffinity = [
self textAffinityString];
528 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
529 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
538 kTextKey : [NSString stringWithUTF8String:_activeModel->GetText().c_str()] ?: [NSNull null],
542 - (void)updateEditState {
543 if (_activeModel ==
nullptr) {
547 NSDictionary* state = [
self editingState];
548 [_channel invokeMethod:kUpdateEditStateResponseMethod arguments:@[
self.clientID, state ]];
549 [
self updateTextAndSelection];
552 - (void)updateEditStateWithDelta:(const
flutter::TextEditingDelta)delta {
553 NSUInteger selectionBase = _activeModel->selection().base();
554 NSUInteger selectionExtent = _activeModel->selection().extent();
555 int composingBase = _activeModel->composing() ? _activeModel->composing_range().base() : -1;
556 int composingExtent = _activeModel->composing() ? _activeModel->composing_range().extent() : -1;
558 NSString*
const textAffinity = [
self textAffinityString];
560 NSDictionary* deltaToFramework = @{
561 @"oldText" : @(delta.old_text().c_str()),
562 @"deltaText" : @(delta.delta_text().c_str()),
563 @"deltaStart" : @(delta.delta_start()),
564 @"deltaEnd" : @(delta.delta_end()),
565 @"selectionBase" : @(selectionBase),
566 @"selectionExtent" : @(selectionExtent),
567 @"selectionAffinity" : textAffinity,
568 @"selectionIsDirectional" : @(
false),
569 @"composingBase" : @(composingBase),
570 @"composingExtent" : @(composingExtent),
573 NSDictionary* deltas = @{
574 @"deltas" : @[ deltaToFramework ],
577 [_channel invokeMethod:kUpdateEditStateWithDeltasResponseMethod
578 arguments:@[
self.clientID, deltas ]];
579 [
self updateTextAndSelection];
582 - (void)updateTextAndSelection {
583 NSAssert(_activeModel !=
nullptr,
@"Flutter text model must not be null.");
584 NSString* text = @(_activeModel->GetText().data());
585 int start = _activeModel->selection().base();
586 int extend = _activeModel->selection().extent();
587 NSRange selection = NSMakeRange(MIN(start, extend), ABS(start - extend));
593 [_client updateString:text withSelection:selection];
596 [
self setSelectedRange:selection];
600 - (NSString*)textAffinityString {
605 - (BOOL)handleKeyEvent:(NSEvent*)event {
606 if (event.type == NSEventTypeKeyUp ||
607 (event.type == NSEventTypeFlagsChanged && event.modifierFlags < _previouslyPressedFlags)) {
610 _previouslyPressedFlags =
event.modifierFlags;
615 _eventProducedOutput = NO;
616 BOOL res = [_textInputContext handleEvent:event];
626 bool is_navigation =
event.modifierFlags & NSEventModifierFlagFunction &&
627 event.modifierFlags & NSEventModifierFlagNumericPad;
628 bool is_navigation_in_ime = is_navigation &&
self.hasMarkedText;
630 if (event.isKeyEquivalent && !is_navigation_in_ime && !_eventProducedOutput) {
637 #pragma mark NSResponder
639 - (void)keyDown:(NSEvent*)event {
640 [
self.flutterViewController keyDown:event];
643 - (void)keyUp:(NSEvent*)event {
644 [
self.flutterViewController keyUp:event];
647 - (BOOL)performKeyEquivalent:(NSEvent*)event {
648 if ([_flutterViewController isDispatchingKeyEvent:event]) {
660 [event markAsKeyEquivalent];
661 [
self.flutterViewController keyDown:event];
665 - (void)flagsChanged:(NSEvent*)event {
666 [
self.flutterViewController flagsChanged:event];
669 - (void)mouseDown:(NSEvent*)event {
670 [
self.flutterViewController mouseDown:event];
673 - (void)mouseUp:(NSEvent*)event {
674 [
self.flutterViewController mouseUp:event];
677 - (void)mouseDragged:(NSEvent*)event {
678 [
self.flutterViewController mouseDragged:event];
681 - (void)rightMouseDown:(NSEvent*)event {
682 [
self.flutterViewController rightMouseDown:event];
685 - (void)rightMouseUp:(NSEvent*)event {
686 [
self.flutterViewController rightMouseUp:event];
689 - (void)rightMouseDragged:(NSEvent*)event {
690 [
self.flutterViewController rightMouseDragged:event];
693 - (void)otherMouseDown:(NSEvent*)event {
694 [
self.flutterViewController otherMouseDown:event];
697 - (void)otherMouseUp:(NSEvent*)event {
698 [
self.flutterViewController otherMouseUp:event];
701 - (void)otherMouseDragged:(NSEvent*)event {
702 [
self.flutterViewController otherMouseDragged:event];
705 - (void)mouseMoved:(NSEvent*)event {
706 [
self.flutterViewController mouseMoved:event];
709 - (void)scrollWheel:(NSEvent*)event {
710 [
self.flutterViewController scrollWheel:event];
713 - (NSTextInputContext*)inputContext {
714 return _textInputContext;
718 #pragma mark NSTextInputClient
720 - (void)insertTab:(
id)sender {
725 - (void)insertText:(
id)string replacementRange:(NSRange)range {
726 if (_activeModel ==
nullptr) {
730 _eventProducedOutput |=
true;
732 if (range.location != NSNotFound) {
736 long signedLength =
static_cast<long>(range.length);
737 long location = range.location;
738 long textLength = _activeModel->text_range().end();
740 size_t base = std::clamp(location, 0L, textLength);
741 size_t extent = std::clamp(location + signedLength, 0L, textLength);
750 std::string textBeforeChange = _activeModel->GetText().c_str();
751 std::string utf8String = [string UTF8String];
752 _activeModel->AddText(utf8String);
753 if (_activeModel->composing()) {
754 replacedRange = composingBeforeChange;
755 _activeModel->CommitComposing();
756 _activeModel->EndComposing();
758 replacedRange = range.location == NSNotFound
760 :
flutter::TextRange(range.location, range.location + range.length);
762 if (_enableDeltaModel) {
763 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange,
766 [
self updateEditState];
770 - (void)doCommandBySelector:(
SEL)selector {
771 _eventProducedOutput |= selector != NSSelectorFromString(
@"noop:");
772 if ([
self respondsToSelector:selector]) {
776 IMP imp = [
self methodForSelector:selector];
777 void (*func)(id, SEL, id) =
reinterpret_cast<void (*)(
id,
SEL,
id)
>(imp);
778 func(
self, selector, nil);
780 if (
self.clientID == nil) {
785 if (selector ==
@selector(insertNewline:)) {
792 NSString* name = NSStringFromSelector(selector);
793 if (_pendingSelectors == nil) {
794 _pendingSelectors = [NSMutableArray array];
796 [_pendingSelectors addObject:name];
798 if (_pendingSelectors.count == 1) {
799 __weak NSMutableArray* selectors = _pendingSelectors;
801 __weak NSNumber* clientID =
self.clientID;
803 CFStringRef runLoopMode =
self.customRunLoopMode != nil
804 ? (__bridge CFStringRef)
self.customRunLoopMode
805 : kCFRunLoopCommonModes;
807 CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
808 if (selectors.count > 0) {
809 [channel invokeMethod:kPerformSelectors arguments:@[ clientID, selectors ]];
810 [selectors removeAllObjects];
816 - (void)insertNewline:(
id)sender {
817 if (_activeModel ==
nullptr) {
820 if (_activeModel->composing()) {
821 _activeModel->CommitComposing();
822 _activeModel->EndComposing();
826 [
self insertText:@"\n" replacementRange:self.selectedRange];
828 [_channel invokeMethod:kPerformAction arguments:@[
self.clientID, self.inputAction ]];
831 - (void)setMarkedText:(
id)string
832 selectedRange:(NSRange)selectedRange
833 replacementRange:(NSRange)replacementRange {
834 if (_activeModel ==
nullptr) {
837 std::string textBeforeChange = _activeModel->GetText().c_str();
838 if (!_activeModel->composing()) {
839 _activeModel->BeginComposing();
842 if (replacementRange.location != NSNotFound) {
848 _activeModel->SetComposingRange(
850 replacementRange.location + replacementRange.length),
858 BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
859 const NSString* rawString = isAttributedString ? [string string] : string;
860 _activeModel->UpdateComposingText(
861 (
const char16_t*)[rawString cStringUsingEncoding:NSUTF16StringEncoding],
862 flutter::TextRange(selectedRange.location, selectedRange.location + selectedRange.length));
864 if (_enableDeltaModel) {
865 std::string marked_text = [rawString UTF8String];
866 [
self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange,
867 selectionBeforeChange.collapsed()
868 ? composingBeforeChange
869 : selectionBeforeChange,
872 [
self updateEditState];
877 if (_activeModel ==
nullptr) {
880 _activeModel->CommitComposing();
881 _activeModel->EndComposing();
882 if (_enableDeltaModel) {
883 [
self updateEditStateWithDelta:flutter::TextEditingDelta(_activeModel->GetText().c_str())];
885 [
self updateEditState];
889 - (NSRange)markedRange {
890 if (_activeModel ==
nullptr) {
891 return NSMakeRange(NSNotFound, 0);
894 _activeModel->composing_range().base(),
895 _activeModel->composing_range().extent() - _activeModel->composing_range().base());
898 - (BOOL)hasMarkedText {
899 return _activeModel !=
nullptr && _activeModel->composing_range().length() > 0;
902 - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
903 actualRange:(NSRangePointer)actualRange {
904 if (_activeModel ==
nullptr) {
907 if (actualRange != nil) {
908 *actualRange = range;
910 NSString* text = [NSString stringWithUTF8String:_activeModel->GetText().c_str()];
911 NSString* substring = [text substringWithRange:range];
912 return [[NSAttributedString alloc] initWithString:substring attributes:nil];
915 - (NSArray<NSString*>*)validAttributesForMarkedText {
921 - (CGRect)screenRectFromFrameworkTransform:(CGRect)incomingRect {
924 CGPointMake(incomingRect.origin.x, incomingRect.origin.y + incomingRect.size.height),
925 CGPointMake(incomingRect.origin.x + incomingRect.size.width, incomingRect.origin.y),
926 CGPointMake(incomingRect.origin.x + incomingRect.size.width,
927 incomingRect.origin.y + incomingRect.size.height)};
929 CGPoint origin = CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX);
930 CGPoint farthest = CGPointMake(-CGFLOAT_MAX, -CGFLOAT_MAX);
932 for (
int i = 0; i < 4; i++) {
933 const CGPoint point = points[i];
945 }
else if (w != 1.0) {
950 origin.x = MIN(origin.x, x);
951 origin.y = MIN(origin.y, y);
952 farthest.x = MAX(farthest.x, x);
953 farthest.y = MAX(farthest.y, y);
956 const NSView* fromView =
self.flutterViewController.flutterView;
957 const CGRect rectInWindow = [fromView
958 convertRect:CGRectMake(origin.x, origin.y, farthest.x - origin.x, farthest.y - origin.y)
960 NSWindow* window = fromView.window;
961 return window ? [window convertRectToScreen:rectInWindow] : rectInWindow;
964 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
967 return !
self.flutterViewController.viewLoaded || CGRectEqualToRect(
_caretRect, CGRectNull)
969 : [
self screenRectFromFrameworkTransform:_caretRect];
972 - (NSUInteger)characterIndexForPoint:(NSPoint)point {