Flutter macOS Embedder
FlutterKeyboardManager.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 
6 
7 #include <cctype>
8 #include <map>
9 
15 
16 // Turn on this flag to print complete layout data when switching IMEs. The data
17 // is used in unit tests.
18 // #define DEBUG_PRINT_LAYOUT
19 
20 namespace {
23 
24 #ifdef DEBUG_PRINT_LAYOUT
25 // Prints layout entries that will be parsed by `MockLayoutData`.
26 NSString* debugFormatLayoutData(NSString* debugLayoutData,
27  uint16_t keyCode,
28  LayoutClue clue1,
29  LayoutClue clue2) {
30  return [NSString
31  stringWithFormat:@" %@%@0x%d%04x, 0x%d%04x,", debugLayoutData,
32  keyCode % 4 == 0 ? [NSString stringWithFormat:@"\n/* 0x%02x */ ", keyCode]
33  : @" ",
34  clue1.isDeadKey, clue1.character, clue2.isDeadKey, clue2.character];
35 }
36 #endif
37 
38 // Someohow this pointer type must be defined as a single type for the compiler
39 // to compile the function pointer type (due to _Nullable).
40 typedef NSResponder* _NSResponderPtr;
41 typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
42 
43 bool isEascii(const LayoutClue& clue) {
44  return clue.character < 256 && !clue.isDeadKey;
45 }
46 
47 typedef void (^VoidBlock)();
48 
49 // Someohow this pointer type must be defined as a single type for the compiler
50 // to compile the function pointer type (due to _Nullable).
51 typedef NSResponder* _NSResponderPtr;
52 typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
53 } // namespace
54 
56 
57 /**
58  * The text input plugin set by initialization.
59  */
60 @property(nonatomic, weak) id<FlutterKeyboardViewDelegate> viewDelegate;
61 
62 /**
63  * The primary responders added by addPrimaryResponder.
64  */
65 @property(nonatomic) NSMutableArray<id<FlutterKeyPrimaryResponder>>* primaryResponders;
66 
67 @property(nonatomic) NSMutableArray<NSEvent*>* pendingEvents;
68 
69 @property(nonatomic) BOOL processingEvent;
70 
71 @property(nonatomic) NSMutableDictionary<NSNumber*, NSNumber*>* layoutMap;
72 
73 @property(nonatomic, nullable) NSEvent* eventBeingDispatched;
74 
75 /**
76  * Add a primary responder, which asynchronously decides whether to handle an
77  * event.
78  */
79 - (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder;
80 
81 /**
82  * Start processing the next event if not started already.
83  *
84  * This function might initiate an async process, whose callback calls this
85  * function again.
86  */
87 - (void)processNextEvent;
88 
89 /**
90  * Implement how to process an event.
91  *
92  * The `onFinish` must be called eventually, either during this function or
93  * asynchronously later, otherwise the event queue will be stuck.
94  *
95  * This function is called by processNextEvent.
96  */
97 - (void)performProcessEvent:(NSEvent*)event onFinish:(nonnull VoidBlock)onFinish;
98 
99 /**
100  * Dispatch an event that's not hadled by the responders to text input plugin,
101  * and potentially to the next responder.
102  */
103 - (void)dispatchTextEvent:(nonnull NSEvent*)pendingEvent;
104 
105 /**
106  * Clears the current layout and build a new one based on the current layout.
107  */
108 - (void)buildLayout;
109 
110 @end
111 
112 @implementation FlutterKeyboardManager {
113  NextResponderProvider _getNextResponder;
114 }
115 
116 - (nonnull instancetype)initWithViewDelegate:(nonnull id<FlutterKeyboardViewDelegate>)viewDelegate {
117  self = [super init];
118  if (self != nil) {
119  _processingEvent = FALSE;
120  _viewDelegate = viewDelegate;
121 
122  FlutterMethodChannel* keyboardChannel =
123  [FlutterMethodChannel methodChannelWithName:@"flutter/keyboard"
124  binaryMessenger:[_viewDelegate getBinaryMessenger]
125  codec:[FlutterStandardMethodCodec sharedInstance]];
126 
127  [keyboardChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
128  [self handleKeyboardMethodCall:call result:result];
129  }];
130 
131  _primaryResponders = [[NSMutableArray alloc] init];
132 
133  __weak __typeof__(self) weakSelf = self;
134  [self addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc]
135  initWithSendEvent:^(const FlutterKeyEvent& event,
136  FlutterKeyEventCallback callback,
137  void* userData) {
138  __strong __typeof__(weakSelf) strongSelf = weakSelf;
139  [strongSelf.viewDelegate sendKeyEvent:event
140  callback:callback
141  userData:userData];
142  }]];
143 
144  [self
145  addPrimaryResponder:[[FlutterChannelKeyResponder alloc]
146  initWithChannel:[FlutterBasicMessageChannel
147  messageChannelWithName:@"flutter/keyevent"
148  binaryMessenger:[_viewDelegate
149  getBinaryMessenger]
151  sharedInstance]]]];
152 
153  _pendingEvents = [[NSMutableArray alloc] init];
154  _layoutMap = [NSMutableDictionary<NSNumber*, NSNumber*> dictionary];
155  [self buildLayout];
156  for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
157  responder.layoutMap = _layoutMap;
158  }
159 
160  [_viewDelegate subscribeToKeyboardLayoutChange:^() {
161  [weakSelf buildLayout];
162  }];
163  }
164  return self;
165 }
166 
167 - (void)handleKeyboardMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
168  if ([[call method] isEqualToString:@"getKeyboardState"]) {
169  result([self getPressedState]);
170  } else {
172  }
173 }
174 
175 - (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder {
176  [_primaryResponders addObject:responder];
177 }
178 
179 - (void)handleEvent:(nonnull NSEvent*)event {
180  // The `handleEvent` does not process the event immediately, but instead put
181  // events into a queue. Events are processed one by one by `processNextEvent`.
182 
183  // Be sure to add a handling method in propagateKeyEvent when allowing more
184  // event types here.
185  if (event.type != NSEventTypeKeyDown && event.type != NSEventTypeKeyUp &&
186  event.type != NSEventTypeFlagsChanged) {
187  return;
188  }
189 
190  [_pendingEvents addObject:event];
191  [self processNextEvent];
192 }
193 
194 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
195  return _eventBeingDispatched == event;
196 }
197 
198 #pragma mark - Private
199 
200 - (void)processNextEvent {
201  @synchronized(self) {
202  if (_processingEvent || [_pendingEvents count] == 0) {
203  return;
204  }
205  _processingEvent = TRUE;
206  }
207 
208  NSEvent* pendingEvent = [_pendingEvents firstObject];
209  [_pendingEvents removeObjectAtIndex:0];
210 
211  __weak __typeof__(self) weakSelf = self;
212  VoidBlock onFinish = ^() {
213  weakSelf.processingEvent = FALSE;
214  [weakSelf processNextEvent];
215  };
216  [self performProcessEvent:pendingEvent onFinish:onFinish];
217 }
218 
219 - (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish {
220  // Having no primary responders require extra logic, but Flutter hard-codes
221  // all primary responders, so this is a situation that Flutter will never
222  // encounter.
223  NSAssert([_primaryResponders count] >= 0, @"At least one primary responder must be added.");
224 
225  __weak __typeof__(self) weakSelf = self;
226  __block int unreplied = [_primaryResponders count];
227  __block BOOL anyHandled = false;
228 
229  FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) {
230  unreplied -= 1;
231  NSAssert(unreplied >= 0, @"More primary responders replied than possible.");
232  anyHandled = anyHandled || handled;
233  if (unreplied == 0) {
234  if (!anyHandled) {
235  [weakSelf dispatchTextEvent:event];
236  }
237  onFinish();
238  }
239  };
240 
241  for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
242  [responder handleEvent:event callback:replyCallback];
243  }
244 }
245 
246 - (void)dispatchTextEvent:(NSEvent*)event {
247  if ([_viewDelegate onTextInputKeyEvent:event]) {
248  return;
249  }
250  NSResponder* nextResponder = _viewDelegate.nextResponder;
251  if (nextResponder == nil) {
252  return;
253  }
254  NSAssert(_eventBeingDispatched == nil, @"An event is already being dispached.");
255  _eventBeingDispatched = event;
256  switch (event.type) {
257  case NSEventTypeKeyDown:
258  if ([nextResponder respondsToSelector:@selector(keyDown:)]) {
259  [nextResponder keyDown:event];
260  }
261  break;
262  case NSEventTypeKeyUp:
263  if ([nextResponder respondsToSelector:@selector(keyUp:)]) {
264  [nextResponder keyUp:event];
265  }
266  break;
267  case NSEventTypeFlagsChanged:
268  if ([nextResponder respondsToSelector:@selector(flagsChanged:)]) {
269  [nextResponder flagsChanged:event];
270  }
271  break;
272  default:
273  NSAssert(false, @"Unexpected key event type (got %lu).", event.type);
274  }
275  NSAssert(_eventBeingDispatched != nil, @"_eventBeingDispatched was cleared unexpectedly.");
276  _eventBeingDispatched = nil;
277 }
278 
279 - (void)buildLayout {
280  [_layoutMap removeAllObjects];
281 
282  std::map<uint32_t, LayoutGoal> mandatoryGoalsByChar;
283  std::map<uint32_t, LayoutGoal> usLayoutGoalsByKeyCode;
284  for (const LayoutGoal& goal : flutter::kLayoutGoals) {
285  if (goal.mandatory) {
286  mandatoryGoalsByChar[goal.keyChar] = goal;
287  } else {
288  usLayoutGoalsByKeyCode[goal.keyCode] = goal;
289  }
290  }
291 
292  // Derive key mapping for each key code based on their layout clues.
293  // Key code 0x00 - 0x32 are typewriter keys (letters, digits, and symbols.)
294  // See keyCodeToPhysicalKey.
295  const uint16_t kMaxKeyCode = 0x32;
296 #ifdef DEBUG_PRINT_LAYOUT
297  NSString* debugLayoutData = @"";
298 #endif
299  for (uint16_t keyCode = 0; keyCode <= kMaxKeyCode; keyCode += 1) {
300  std::vector<LayoutClue> thisKeyClues = {
301  [_viewDelegate lookUpLayoutForKeyCode:keyCode shift:false],
302  [_viewDelegate lookUpLayoutForKeyCode:keyCode shift:true]};
303 #ifdef DEBUG_PRINT_LAYOUT
304  debugLayoutData =
305  debugFormatLayoutData(debugLayoutData, keyCode, thisKeyClues[0], thisKeyClues[1]);
306 #endif
307  // The logical key should be the first available clue from below:
308  //
309  // - Mandatory goal, if it matches any clue. This ensures that all alnum
310  // keys can be found somewhere.
311  // - US layout, if neither clue of the key is EASCII. This ensures that
312  // there are no non-latin logical keys.
313  // - Derived on the fly from keyCode & characters.
314  for (const LayoutClue& clue : thisKeyClues) {
315  uint32_t keyChar = clue.isDeadKey ? 0 : clue.character;
316  auto matchingGoal = mandatoryGoalsByChar.find(keyChar);
317  if (matchingGoal != mandatoryGoalsByChar.end()) {
318  // Found a key that produces a mandatory char. Use it.
319  NSAssert(_layoutMap[@(keyCode)] == nil, @"Attempting to assign an assigned key code.");
320  _layoutMap[@(keyCode)] = @(keyChar);
321  mandatoryGoalsByChar.erase(matchingGoal);
322  break;
323  }
324  }
325  bool hasAnyEascii = isEascii(thisKeyClues[0]) || isEascii(thisKeyClues[1]);
326  // See if any produced char meets the requirement as a logical key.
327  auto foundUsLayoutGoal = usLayoutGoalsByKeyCode.find(keyCode);
328  if (foundUsLayoutGoal != usLayoutGoalsByKeyCode.end() && _layoutMap[@(keyCode)] == nil &&
329  !hasAnyEascii) {
330  _layoutMap[@(keyCode)] = @(foundUsLayoutGoal->second.keyChar);
331  }
332  }
333 #ifdef DEBUG_PRINT_LAYOUT
334  NSLog(@"%@", debugLayoutData);
335 #endif
336 
337  // Ensure all mandatory goals are assigned.
338  for (auto mandatoryGoalIter : mandatoryGoalsByChar) {
339  const LayoutGoal& goal = mandatoryGoalIter.second;
340  _layoutMap[@(goal.keyCode)] = @(goal.keyChar);
341  }
342 }
343 
344 - (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
345  timestamp:(NSTimeInterval)timestamp {
346  for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
347  [responder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
348  }
349 }
350 
351 /**
352  * Returns the keyboard pressed state.
353  *
354  * Returns the keyboard pressed state. The dictionary contains one entry per
355  * pressed keys, mapping from the logical key to the physical key.
356  */
357 - (nonnull NSDictionary*)getPressedState {
358  // The embedder responder is the first element in _primaryResponders.
359  FlutterEmbedderKeyResponder* embedderResponder =
360  (FlutterEmbedderKeyResponder*)_primaryResponders[0];
361  return [embedderResponder getPressedState];
362 }
363 
364 @end
+[FlutterBasicMessageChannel messageChannelWithName:binaryMessenger:codec:]
instancetype messageChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMessageCodec > *codec)
Definition: FlutterChannels.mm:82
flutter::LayoutClue
Definition: FlutterKeyboardViewDelegate.h:20
FlutterBasicMessageChannel
Definition: FlutterChannels.h:37
FlutterKeyboardViewDelegate-p
Definition: FlutterKeyboardViewDelegate.h:42
FlutterMethodChannel
Definition: FlutterChannels.h:220
FlutterMethodNotImplemented
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
FlutterKeyPrimaryResponder-p
Definition: FlutterKeyPrimaryResponder.h:18
FlutterEngine_Internal.h
-[FlutterKeyPrimaryResponder-p handleEvent:callback:]
void handleEvent:callback:(nonnull NSEvent *event,[callback] nonnull FlutterAsyncKeyCallback callback)
-[FlutterEmbedderKeyResponder getPressedState]
nonnull NSDictionary * getPressedState()
Definition: FlutterEmbedderKeyResponder.mm:797
flutter::kLayoutGoals
const std::vector< LayoutGoal > kLayoutGoals
Definition: KeyCodeMap.g.mm:248
FlutterChannelKeyResponder.h
FlutterEmbedderKeyResponder.h
-[FlutterKeyPrimaryResponder-p syncModifiersIfNeeded:timestamp:]
void syncModifiersIfNeeded:timestamp:(NSEventModifierFlags modifierFlags,[timestamp] NSTimeInterval timestamp)
FlutterKeyPrimaryResponder.h
-[FlutterMethodChannel setMethodCallHandler:]
void setMethodCallHandler:(FlutterMethodCallHandler _Nullable handler)
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterAsyncKeyCallback
void(^ FlutterAsyncKeyCallback)(BOOL handled)
Definition: FlutterKeyPrimaryResponder.h:10
flutter::LayoutGoal
Definition: KeyCodeMap_Internal.h:94
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
FlutterKeyPrimaryResponder-p::layoutMap
NSMutableDictionary< NSNumber *, NSNumber * > * layoutMap
Definition: FlutterKeyPrimaryResponder.h:46
FlutterChannelKeyResponder
Definition: FlutterChannelKeyResponder.h:20
+[FlutterMethodChannel methodChannelWithName:binaryMessenger:codec:]
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)
FlutterKeyboardManager.h
KeyCodeMap_Internal.h
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:27
FlutterEmbedderKeyResponder
Definition: FlutterEmbedderKeyResponder.h:23
FlutterStandardMethodCodec
Definition: FlutterCodecs.h:469
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()
FlutterJSONMessageCodec
Definition: FlutterCodecs.h:81