1// Copyright 2013 The Flutter Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" 6 7#import <objc/message.h> 8 9#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" 10#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputModel.h" 11#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" 12 13static NSString* const kTextInputChannel = @"flutter/textinput"; 14 15// See https://docs.flutter.io/flutter/services/SystemChannels/textInput-constant.html 16static NSString* const kSetClientMethod = @"TextInput.setClient"; 17static NSString* const kShowMethod = @"TextInput.show"; 18static NSString* const kHideMethod = @"TextInput.hide"; 19static NSString* const kClearClientMethod = @"TextInput.clearClient"; 20static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState"; 21static NSString* const kUpdateEditStateResponseMethod = @"TextInputClient.updateEditingState"; 22static NSString* const kPerformAction = @"TextInputClient.performAction"; 23static NSString* const kMultilineInputType = @"TextInputType.multiline"; 24 25/** 26 * Private properties of FlutterTextInputPlugin. 27 */ 28@interface FlutterTextInputPlugin () <NSTextInputClient> 29 30/** 31 * A text input context, representing a connection to the Cocoa text input system. 32 */ 33@property(nonatomic) NSTextInputContext* textInputContext; 34 35/** 36 * A dictionary of text input models, one per client connection, keyed 37 * by the client connection ID. 38 */ 39@property(nonatomic) NSMutableDictionary<NSNumber*, FlutterTextInputModel*>* textInputModels; 40 41/** 42 * The currently active client connection ID. 43 */ 44@property(nonatomic, nullable) NSNumber* activeClientID; 45 46/** 47 * The currently active text input model. 48 */ 49@property(nonatomic, readonly, nullable) FlutterTextInputModel* activeModel; 50 51/** 52 * The channel used to communicate with Flutter. 53 */ 54@property(nonatomic) FlutterMethodChannel* channel; 55 56/** 57 * The FlutterViewController to manage input for. 58 */ 59@property(nonatomic, weak) FlutterViewController* flutterViewController; 60 61/** 62 * Handles a Flutter system message on the text input channel. 63 */ 64- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; 65 66@end 67 68@implementation FlutterTextInputPlugin 69 70- (instancetype)initWithViewController:(FlutterViewController*)viewController { 71 self = [super init]; 72 if (self != nil) { 73 _flutterViewController = viewController; 74 _channel = [FlutterMethodChannel methodChannelWithName:kTextInputChannel 75 binaryMessenger:viewController.engine.binaryMessenger 76 codec:[FlutterJSONMethodCodec sharedInstance]]; 77 __weak FlutterTextInputPlugin* weakSelf = self; 78 [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { 79 [weakSelf handleMethodCall:call result:result]; 80 }]; 81 _textInputModels = [[NSMutableDictionary alloc] init]; 82 _textInputContext = [[NSTextInputContext alloc] initWithClient:self]; 83 } 84 return self; 85} 86 87#pragma mark - Private 88 89- (FlutterTextInputModel*)activeModel { 90 return (_activeClientID == nil) ? nil : _textInputModels[_activeClientID]; 91} 92 93- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 94 BOOL handled = YES; 95 NSString* method = call.method; 96 if ([method isEqualToString:kSetClientMethod]) { 97 if (!call.arguments[0] || !call.arguments[1]) { 98 result([FlutterError 99 errorWithCode:@"error" 100 message:@"Missing arguments" 101 details:@"Missing arguments while trying to set a text input client"]); 102 return; 103 } 104 NSNumber* clientID = call.arguments[0]; 105 if (clientID != nil && 106 (_activeClientID == nil || ![_activeClientID isEqualToNumber:clientID])) { 107 _activeClientID = clientID; 108 // TODO: Do we need to preserve state across setClient calls? 109 FlutterTextInputModel* inputModel = 110 [[FlutterTextInputModel alloc] initWithClientID:clientID configuration:call.arguments[1]]; 111 if (!inputModel) { 112 result([FlutterError errorWithCode:@"error" 113 message:@"Failed to create an input model" 114 details:@"Configuration arguments might be missing"]); 115 return; 116 } 117 _textInputModels[_activeClientID] = inputModel; 118 } 119 } else if ([method isEqualToString:kShowMethod]) { 120 [self.flutterViewController addKeyResponder:self]; 121 [_textInputContext activate]; 122 } else if ([method isEqualToString:kHideMethod]) { 123 [self.flutterViewController removeKeyResponder:self]; 124 [_textInputContext deactivate]; 125 } else if ([method isEqualToString:kClearClientMethod]) { 126 _activeClientID = nil; 127 } else if ([method isEqualToString:kSetEditingStateMethod]) { 128 NSDictionary* state = call.arguments; 129 self.activeModel.state = state; 130 } else { 131 handled = NO; 132 NSLog(@"Unhandled text input method '%@'", method); 133 } 134 result(handled ? nil : FlutterMethodNotImplemented); 135} 136 137/** 138 * Informs the Flutter framework of changes to the text input model's state. 139 */ 140- (void)updateEditState { 141 if (self.activeModel == nil) { 142 return; 143 } 144 145 [_channel invokeMethod:kUpdateEditStateResponseMethod 146 arguments:@[ _activeClientID, _textInputModels[_activeClientID].state ]]; 147} 148 149#pragma mark - 150#pragma mark NSResponder 151 152/** 153 * Note, the Apple docs suggest that clients should override essentially all the 154 * mouse and keyboard event-handling methods of NSResponder. However, experimentation 155 * indicates that only key events are processed by the native layer; Flutter processes 156 * mouse events. Additionally, processing both keyUp and keyDown results in duplicate 157 * processing of the same keys. So for now, limit processing to just keyDown. 158 */ 159- (void)keyDown:(NSEvent*)event { 160 [_textInputContext handleEvent:event]; 161} 162 163#pragma mark - 164#pragma mark NSStandardKeyBindingMethods 165 166/** 167 * Note, experimentation indicates that moveRight and moveLeft are called rather 168 * than the supposedly more RTL-friendly moveForward and moveBackward. 169 */ 170- (void)moveLeft:(nullable id)sender { 171 NSRange selection = self.activeModel.selectedRange; 172 if (selection.length == 0) { 173 if (selection.location > 0) { 174 // Move to previous location 175 self.activeModel.selectedRange = NSMakeRange(selection.location - 1, 0); 176 [self updateEditState]; 177 } 178 } else { 179 // Collapse current selection 180 self.activeModel.selectedRange = NSMakeRange(selection.location, 0); 181 [self updateEditState]; 182 } 183} 184 185- (void)moveRight:(nullable id)sender { 186 NSRange selection = self.activeModel.selectedRange; 187 if (selection.length == 0) { 188 if (selection.location < self.activeModel.text.length) { 189 // Move to next location 190 self.activeModel.selectedRange = NSMakeRange(selection.location + 1, 0); 191 [self updateEditState]; 192 } 193 } else { 194 // Collapse current selection 195 self.activeModel.selectedRange = NSMakeRange(selection.location + selection.length, 0); 196 [self updateEditState]; 197 } 198} 199 200- (void)deleteBackward:(id)sender { 201 NSRange selection = self.activeModel.selectedRange; 202 if (selection.location == 0) 203 return; 204 NSRange range = selection; 205 if (selection.length == 0) { 206 NSUInteger location = (selection.location == NSNotFound) ? self.activeModel.text.length - 1 207 : selection.location - 1; 208 range = NSMakeRange(location, 1); 209 } 210 self.activeModel.selectedRange = NSMakeRange(range.location, 0); 211 [self insertText:@"" replacementRange:range]; // Updates edit state 212} 213 214#pragma mark - 215#pragma mark NSTextInputClient 216 217- (void)insertText:(id)string replacementRange:(NSRange)range { 218 if (self.activeModel != nil) { 219 if (range.location == NSNotFound && range.length == 0) { 220 // Use selection 221 range = self.activeModel.selectedRange; 222 } 223 if (range.location > self.activeModel.text.length) 224 range.location = self.activeModel.text.length; 225 if (range.length > (self.activeModel.text.length - range.location)) 226 range.length = self.activeModel.text.length - range.location; 227 [self.activeModel.text replaceCharactersInRange:range withString:string]; 228 self.activeModel.selectedRange = NSMakeRange(range.location + ((NSString*)string).length, 0); 229 [self updateEditState]; 230 } 231} 232 233- (void)doCommandBySelector:(SEL)selector { 234 if ([self respondsToSelector:selector]) { 235 // Note: The more obvious [self performSelector...] doesn't give ARC enough information to 236 // handle retain semantics properly. See https://stackoverflow.com/questions/7017281/ for more 237 // information. 238 IMP imp = [self methodForSelector:selector]; 239 void (*func)(id, SEL, id) = reinterpret_cast<void (*)(id, SEL, id)>(imp); 240 func(self, selector, nil); 241 } 242} 243 244- (void)insertNewline:(id)sender { 245 if (self.activeModel != nil) { 246 if ([self.activeModel.inputType isEqualToString:kMultilineInputType]) { 247 [self insertText:@"\n" replacementRange:self.activeModel.selectedRange]; 248 } 249 [_channel invokeMethod:kPerformAction 250 arguments:@[ _activeClientID, self.activeModel.inputAction ]]; 251 } 252} 253 254- (void)setMarkedText:(id)string 255 selectedRange:(NSRange)selectedRange 256 replacementRange:(NSRange)replacementRange { 257 if (self.activeModel != nil) { 258 [self.activeModel.text replaceCharactersInRange:replacementRange withString:string]; 259 self.activeModel.selectedRange = selectedRange; 260 [self updateEditState]; 261 } 262} 263 264- (void)unmarkText { 265 if (self.activeModel != nil) { 266 self.activeModel.markedRange = NSMakeRange(NSNotFound, 0); 267 [self updateEditState]; 268 } 269} 270 271- (NSRange)selectedRange { 272 return (self.activeModel == nil) ? NSMakeRange(NSNotFound, 0) : self.activeModel.selectedRange; 273} 274 275- (NSRange)markedRange { 276 return (self.activeModel == nil) ? NSMakeRange(NSNotFound, 0) : self.activeModel.markedRange; 277} 278 279- (BOOL)hasMarkedText { 280 return (self.activeModel == nil) ? NO : self.activeModel.markedRange.location != NSNotFound; 281} 282 283- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range 284 actualRange:(NSRangePointer)actualRange { 285 if (self.activeModel) { 286 if (actualRange != nil) 287 *actualRange = range; 288 NSString* substring = [self.activeModel.text substringWithRange:range]; 289 return [[NSAttributedString alloc] initWithString:substring attributes:nil]; 290 } else { 291 return nil; 292 } 293} 294 295- (NSArray<NSString*>*)validAttributesForMarkedText { 296 return @[]; 297} 298 299- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { 300 // TODO: Implement. 301 // Note: This function can't easily be implemented under the system-message architecture. 302 return CGRectZero; 303} 304 305- (NSUInteger)characterIndexForPoint:(NSPoint)point { 306 // TODO: Implement. 307 // Note: This function can't easily be implemented under the system-message architecture. 308 return 0; 309} 310 311@end 312