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