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