Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
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
20namespace {
23
24#ifdef DEBUG_PRINT_LAYOUT
25// Prints layout entries that will be parsed by `MockLayoutData`.
26NSString* 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).
40typedef NSResponder* _NSResponderPtr;
41typedef _Nullable _NSResponderPtr (^NextResponderProvider)();
42
43bool isEascii(const LayoutClue& clue) {
44 return clue.character < 256 && !clue.isDeadKey;
45}
46
47typedef 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).
51typedef NSResponder* _NSResponderPtr;
52typedef _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 =
124 binaryMessenger:[_viewDelegate getBinaryMessenger]
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
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
int count
FlKeyEvent * event
GAsyncResult * result
instancetype messageChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMessageCodec > *codec)
instancetype sharedInstance()
void setMethodCallHandler:(FlutterMethodCallHandler _Nullable handler)
instancetype methodChannelWithName:binaryMessenger:codec:(NSString *name,[binaryMessenger] NSObject< FlutterBinaryMessenger > *messenger,[codec] NSObject< FlutterMethodCodec > *codec)
void(^ FlutterAsyncKeyCallback)(BOOL handled)
return FALSE
const std::vector< LayoutGoal > kLayoutGoals
int BOOL