• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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