| 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/KeyCodeMap_Internal.h"
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 {
21 using flutter::LayoutClue;
22 using flutter::LayoutGoal;
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
55 @interface FlutterKeyboardManager ()
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 [keyboardChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
127 [self handleKeyboardMethodCall:call result:result];
128 }];
129 _primaryResponders = [[NSMutableArray alloc] init];
130 [self addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc]
131 initWithSendEvent:^(const FlutterKeyEvent& event,
132 FlutterKeyEventCallback callback,
133 void* userData) {
134 [_viewDelegate sendKeyEvent:event
135 callback:callback
136 userData:userData];
137 }]];
138 [self
139 addPrimaryResponder:[[FlutterChannelKeyResponder alloc]
140 initWithChannel:[FlutterBasicMessageChannel
141 messageChannelWithName:@"flutter/keyevent"
142 binaryMessenger:[_viewDelegate
143 getBinaryMessenger]
144 codec:[FlutterJSONMessageCodec
145 sharedInstance]]]];
146 _pendingEvents = [[NSMutableArray alloc] init];
147 _layoutMap = [NSMutableDictionary<NSNumber*, NSNumber*> dictionary];
148 [self buildLayout];
149 for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
150 responder.layoutMap = _layoutMap;
151 }
152
153 __weak __typeof__(self) weakSelf = self;
154 [_viewDelegate subscribeToKeyboardLayoutChange:^() {
155 [weakSelf buildLayout];
156 }];
157 }
158 return self;
159 }
160
161 - (void)handleKeyboardMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
162 if ([[call method] isEqualToString:@"getKeyboardState"]) {
163 result([self getPressedState]);
164 } else {
165 result(FlutterMethodNotImplemented);
166 }
167 }
168
169 - (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder {
170 [_primaryResponders addObject:responder];
171 }
172
173 - (void)handleEvent:(nonnull NSEvent*)event {
174 // The `handleEvent` does not process the event immediately, but instead put
175 // events into a queue. Events are processed one by one by `processNextEvent`.
176
177 // Be sure to add a handling method in propagateKeyEvent when allowing more
178 // event types here.
179 if (event.type != NSEventTypeKeyDown && event.type != NSEventTypeKeyUp &&
180 event.type != NSEventTypeFlagsChanged) {
181 return;
182 }
183
184 [_pendingEvents addObject:event];
185 [self processNextEvent];
186 }
187
188 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
189 return _eventBeingDispatched == event;
190 }
191
192 #pragma mark - Private
193
194 - (void)processNextEvent {
195 @synchronized(self) {
196 if (_processingEvent || [_pendingEvents count] == 0) {
197 return;
198 }
199 _processingEvent = TRUE;
200 }
201
202 NSEvent* pendingEvent = [_pendingEvents firstObject];
203 [_pendingEvents removeObjectAtIndex:0];
204
205 __weak __typeof__(self) weakSelf = self;
206 VoidBlock onFinish = ^() {
207 weakSelf.processingEvent = FALSE;
208 [weakSelf processNextEvent];
209 };
210 [self performProcessEvent:pendingEvent onFinish:onFinish];
211 }
212
213 - (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish {
214 // Having no primary responders require extra logic, but Flutter hard-codes
215 // all primary responders, so this is a situation that Flutter will never
216 // encounter.
217 NSAssert([_primaryResponders count] >= 0, @"At least one primary responder must be added.");
218
219 __weak __typeof__(self) weakSelf = self;
220 __block int unreplied = [_primaryResponders count];
221 __block BOOL anyHandled = false;
222
223 FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) {
224 unreplied -= 1;
225 NSAssert(unreplied >= 0, @"More primary responders replied than possible.");
226 anyHandled = anyHandled || handled;
227 if (unreplied == 0) {
228 if (!anyHandled) {
229 [weakSelf dispatchTextEvent:event];
230 }
231 onFinish();
232 }
233 };
234
235 for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
236 [responder handleEvent:event callback:replyCallback];
237 }
238 }
239
240 - (void)dispatchTextEvent:(NSEvent*)event {
241 if ([_viewDelegate onTextInputKeyEvent:event]) {
242 return;
243 }
244 NSResponder* nextResponder = _viewDelegate.nextResponder;
245 if (nextResponder == nil) {
246 return;
247 }
248 NSAssert(_eventBeingDispatched == nil, @"An event is already being dispached.");
249 _eventBeingDispatched = event;
250 switch (event.type) {
251 case NSEventTypeKeyDown:
252 if ([nextResponder respondsToSelector:@selector(keyDown:)]) {
253 [nextResponder keyDown:event];
254 }
255 break;
256 case NSEventTypeKeyUp:
257 if ([nextResponder respondsToSelector:@selector(keyUp:)]) {
258 [nextResponder keyUp:event];
259 }
260 break;
261 case NSEventTypeFlagsChanged:
262 if ([nextResponder respondsToSelector:@selector(flagsChanged:)]) {
263 [nextResponder flagsChanged:event];
264 }
265 break;
266 default:
267 NSAssert(false, @"Unexpected key event type (got %lu).", event.type);
268 }
269 NSAssert(_eventBeingDispatched != nil, @"_eventBeingDispatched was cleared unexpectedly.");
270 _eventBeingDispatched = nil;
271 }
272
273 - (void)buildLayout {
274 [_layoutMap removeAllObjects];
275
276 std::map<uint32_t, LayoutGoal> mandatoryGoalsByChar;
277 std::map<uint32_t, LayoutGoal> usLayoutGoalsByKeyCode;
278 for (const LayoutGoal& goal : flutter::kLayoutGoals) {
279 if (goal.mandatory) {
280 mandatoryGoalsByChar[goal.keyChar] = goal;
281 } else {
282 usLayoutGoalsByKeyCode[goal.keyCode] = goal;
283 }
284 }
285
286 // Derive key mapping for each key code based on their layout clues.
287 // Key code 0x00 - 0x32 are typewriter keys (letters, digits, and symbols.)
288 // See keyCodeToPhysicalKey.
289 const uint16_t kMaxKeyCode = 0x32;
290 #ifdef DEBUG_PRINT_LAYOUT
291 NSString* debugLayoutData = @"";
292 #endif
293 for (uint16_t keyCode = 0; keyCode <= kMaxKeyCode; keyCode += 1) {
294 std::vector<LayoutClue> thisKeyClues = {
295 [_viewDelegate lookUpLayoutForKeyCode:keyCode shift:false],
296 [_viewDelegate lookUpLayoutForKeyCode:keyCode shift:true]};
297 #ifdef DEBUG_PRINT_LAYOUT
298 debugLayoutData =
299 debugFormatLayoutData(debugLayoutData, keyCode, thisKeyClues[0], thisKeyClues[1]);
300 #endif
301 // The logical key should be the first available clue from below:
302 //
303 // - Mandatory goal, if it matches any clue. This ensures that all alnum
304 // keys can be found somewhere.
305 // - US layout, if neither clue of the key is EASCII. This ensures that
306 // there are no non-latin logical keys.
307 // - Derived on the fly from keyCode & characters.
308 for (const LayoutClue& clue : thisKeyClues) {
309 uint32_t keyChar = clue.isDeadKey ? 0 : clue.character;
310 auto matchingGoal = mandatoryGoalsByChar.find(keyChar);
311 if (matchingGoal != mandatoryGoalsByChar.end()) {
312 // Found a key that produces a mandatory char. Use it.
313 NSAssert(_layoutMap[@(keyCode)] == nil, @"Attempting to assign an assigned key code.");
314 _layoutMap[@(keyCode)] = @(keyChar);
315 mandatoryGoalsByChar.erase(matchingGoal);
316 break;
317 }
318 }
319 bool hasAnyEascii = isEascii(thisKeyClues[0]) || isEascii(thisKeyClues[1]);
320 // See if any produced char meets the requirement as a logical key.
321 auto foundUsLayoutGoal = usLayoutGoalsByKeyCode.find(keyCode);
322 if (foundUsLayoutGoal != usLayoutGoalsByKeyCode.end() && _layoutMap[@(keyCode)] == nil &&
323 !hasAnyEascii) {
324 _layoutMap[@(keyCode)] = @(foundUsLayoutGoal->second.keyChar);
325 }
326 }
327 #ifdef DEBUG_PRINT_LAYOUT
328 NSLog(@"%@", debugLayoutData);
329 #endif
330
331 // Ensure all mandatory goals are assigned.
332 for (auto mandatoryGoalIter : mandatoryGoalsByChar) {
333 const LayoutGoal& goal = mandatoryGoalIter.second;
334 _layoutMap[@(goal.keyCode)] = @(goal.keyChar);
335 }
336 }
337
338 - (void)syncModifiersIfNeeded:(NSEventModifierFlags)modifierFlags
339 timestamp:(NSTimeInterval)timestamp {
340 for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) {
341 [responder syncModifiersIfNeeded:modifierFlags timestamp:timestamp];
342 }
343 }
344
345 /**
346 * Returns the keyboard pressed state.
347 *
348 * Returns the keyboard pressed state. The dictionary contains one entry per
349 * pressed keys, mapping from the logical key to the physical key.
350 */
351 - (nonnull NSDictionary*)getPressedState {
352 // The embedder responder is the first element in _primaryResponders.
353 FlutterEmbedderKeyResponder* embedderResponder =
354 (FlutterEmbedderKeyResponder*)_primaryResponders[0];
355 return [embedderResponder getPressedState];
356 }
357
358 @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:17
FlutterBasicMessageChannel
Definition: FlutterChannels.h:39
FlutterKeyboardViewDelegate-p
Definition: FlutterKeyboardViewDelegate.h:39
FlutterMethodChannel
Definition: FlutterChannels.h:222
FlutterMethodNotImplemented
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
FlutterKeyPrimaryResponder-p
Definition: FlutterKeyPrimaryResponder.h:15
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:796
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:7
flutter::LayoutGoal
Definition: KeyCodeMap_Internal.h:91
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:196
FlutterKeyPrimaryResponder-p::layoutMap
NSMutableDictionary< NSNumber *, NSNumber * > * layoutMap
Definition: FlutterKeyPrimaryResponder.h:43
FlutterChannelKeyResponder
Definition: FlutterChannelKeyResponder.h:17
+[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:24
FlutterEmbedderKeyResponder
Definition: FlutterEmbedderKeyResponder.h:24
FlutterStandardMethodCodec
Definition: FlutterCodecs.h:467
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()
FlutterJSONMessageCodec
Definition: FlutterCodecs.h:81