| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h" |
| |
| #include <cctype> |
| #include <map> |
| |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/KeyCodeMap_Internal.h" |
| |
| // Turn on this flag to print complete layout data when switching IMEs. The data |
| // is used in unit tests. |
| // #define DEBUG_PRINT_LAYOUT |
| |
| namespace { |
| using flutter::LayoutClue; |
| using flutter::LayoutGoal; |
| |
| #ifdef DEBUG_PRINT_LAYOUT |
| // Prints layout entries that will be parsed by `MockLayoutData`. |
| NSString* debugFormatLayoutData(NSString* debugLayoutData, |
| uint16_t keyCode, |
| LayoutClue clue1, |
| LayoutClue clue2) { |
| return [NSString |
| stringWithFormat:@" %@%@0x%d%04x, 0x%d%04x,", debugLayoutData, |
| keyCode % 4 == 0 ? [NSString stringWithFormat:@"\n/* 0x%02x */ ", keyCode] |
| : @" ", |
| clue1.isDeadKey, clue1.character, clue2.isDeadKey, clue2.character]; |
| } |
| #endif |
| |
| // Someohow this pointer type must be defined as a single type for the compiler |
| // to compile the function pointer type (due to _Nullable). |
| typedef NSResponder* _NSResponderPtr; |
| typedef _Nullable _NSResponderPtr (^NextResponderProvider)(); |
| |
| bool isEascii(const LayoutClue& clue) { |
| return clue.character < 256 && !clue.isDeadKey; |
| } |
| |
| typedef void (^VoidBlock)(); |
| |
| // Someohow this pointer type must be defined as a single type for the compiler |
| // to compile the function pointer type (due to _Nullable). |
| typedef NSResponder* _NSResponderPtr; |
| typedef _Nullable _NSResponderPtr (^NextResponderProvider)(); |
| } // namespace |
| |
| @interface FlutterKeyboardManager () |
| |
| /** |
| * The text input plugin set by initialization. |
| */ |
| @property(nonatomic) id<FlutterKeyboardViewDelegate> viewDelegate; |
| |
| /** |
| * The primary responders added by addPrimaryResponder. |
| */ |
| @property(nonatomic) NSMutableArray<id<FlutterKeyPrimaryResponder>>* primaryResponders; |
| |
| @property(nonatomic) NSMutableArray<NSEvent*>* pendingEvents; |
| |
| @property(nonatomic) BOOL processingEvent; |
| |
| @property(nonatomic) NSMutableDictionary<NSNumber*, NSNumber*>* layoutMap; |
| |
| @property(nonatomic, nullable) NSEvent* eventBeingDispatched; |
| |
| /** |
| * Add a primary responder, which asynchronously decides whether to handle an |
| * event. |
| */ |
| - (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder; |
| |
| /** |
| * Start processing the next event if not started already. |
| * |
| * This function might initiate an async process, whose callback calls this |
| * function again. |
| */ |
| - (void)processNextEvent; |
| |
| /** |
| * Implement how to process an event. |
| * |
| * The `onFinish` must be called eventually, either during this function or |
| * asynchronously later, otherwise the event queue will be stuck. |
| * |
| * This function is called by processNextEvent. |
| */ |
| - (void)performProcessEvent:(NSEvent*)event onFinish:(nonnull VoidBlock)onFinish; |
| |
| /** |
| * Dispatch an event that's not hadled by the responders to text input plugin, |
| * and potentially to the next responder. |
| */ |
| - (void)dispatchTextEvent:(nonnull NSEvent*)pendingEvent; |
| |
| /** |
| * Clears the current layout and build a new one based on the current layout. |
| */ |
| - (void)buildLayout; |
| |
| @end |
| |
| @implementation FlutterKeyboardManager { |
| NextResponderProvider _getNextResponder; |
| } |
| |
| - (nonnull instancetype)initWithViewDelegate:(nonnull id<FlutterKeyboardViewDelegate>)viewDelegate { |
| self = [super init]; |
| if (self != nil) { |
| _processingEvent = FALSE; |
| _viewDelegate = viewDelegate; |
| |
| _primaryResponders = [[NSMutableArray alloc] init]; |
| [self addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] |
| initWithSendEvent:^(const FlutterKeyEvent& event, |
| FlutterKeyEventCallback callback, |
| void* userData) { |
| [_viewDelegate sendKeyEvent:event |
| callback:callback |
| userData:userData]; |
| }]]; |
| [self |
| addPrimaryResponder:[[FlutterChannelKeyResponder alloc] |
| initWithChannel:[FlutterBasicMessageChannel |
| messageChannelWithName:@"flutter/keyevent" |
| binaryMessenger:[_viewDelegate |
| getBinaryMessenger] |
| codec:[FlutterJSONMessageCodec |
| sharedInstance]]]]; |
| _pendingEvents = [[NSMutableArray alloc] init]; |
| _layoutMap = [NSMutableDictionary<NSNumber*, NSNumber*> dictionary]; |
| [self buildLayout]; |
| for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) { |
| responder.layoutMap = _layoutMap; |
| } |
| |
| __weak __typeof__(self) weakSelf = self; |
| [_viewDelegate subscribeToKeyboardLayoutChange:^() { |
| [weakSelf buildLayout]; |
| }]; |
| } |
| return self; |
| } |
| |
| - (void)addPrimaryResponder:(nonnull id<FlutterKeyPrimaryResponder>)responder { |
| [_primaryResponders addObject:responder]; |
| } |
| |
| - (void)handleEvent:(nonnull NSEvent*)event { |
| // The `handleEvent` does not process the event immediately, but instead put |
| // events into a queue. Events are processed one by one by `processNextEvent`. |
| |
| // Be sure to add a handling method in propagateKeyEvent when allowing more |
| // event types here. |
| if (event.type != NSEventTypeKeyDown && event.type != NSEventTypeKeyUp && |
| event.type != NSEventTypeFlagsChanged) { |
| return; |
| } |
| |
| [_pendingEvents addObject:event]; |
| [self processNextEvent]; |
| } |
| |
| - (BOOL)isDispatchingKeyEvent:(NSEvent*)event { |
| return _eventBeingDispatched == event; |
| } |
| |
| #pragma mark - Private |
| |
| - (void)processNextEvent { |
| @synchronized(self) { |
| if (_processingEvent || [_pendingEvents count] == 0) { |
| return; |
| } |
| _processingEvent = TRUE; |
| } |
| |
| NSEvent* pendingEvent = [_pendingEvents firstObject]; |
| [_pendingEvents removeObjectAtIndex:0]; |
| |
| __weak __typeof__(self) weakSelf = self; |
| VoidBlock onFinish = ^() { |
| weakSelf.processingEvent = FALSE; |
| [weakSelf processNextEvent]; |
| }; |
| [self performProcessEvent:pendingEvent onFinish:onFinish]; |
| } |
| |
| - (void)performProcessEvent:(NSEvent*)event onFinish:(VoidBlock)onFinish { |
| // Having no primary responders require extra logic, but Flutter hard-codes |
| // all primary responders, so this is a situation that Flutter will never |
| // encounter. |
| NSAssert([_primaryResponders count] >= 0, @"At least one primary responder must be added."); |
| |
| __weak __typeof__(self) weakSelf = self; |
| __block int unreplied = [_primaryResponders count]; |
| __block BOOL anyHandled = false; |
| |
| FlutterAsyncKeyCallback replyCallback = ^(BOOL handled) { |
| unreplied -= 1; |
| NSAssert(unreplied >= 0, @"More primary responders replied than possible."); |
| anyHandled = anyHandled || handled; |
| if (unreplied == 0) { |
| if (!anyHandled) { |
| [weakSelf dispatchTextEvent:event]; |
| } |
| onFinish(); |
| } |
| }; |
| |
| for (id<FlutterKeyPrimaryResponder> responder in _primaryResponders) { |
| [responder handleEvent:event callback:replyCallback]; |
| } |
| } |
| |
| - (void)dispatchTextEvent:(NSEvent*)event { |
| if ([_viewDelegate onTextInputKeyEvent:event]) { |
| return; |
| } |
| NSResponder* nextResponder = _viewDelegate.nextResponder; |
| if (nextResponder == nil) { |
| return; |
| } |
| NSAssert(_eventBeingDispatched == nil, @"An event is already being dispached."); |
| _eventBeingDispatched = event; |
| switch (event.type) { |
| case NSEventTypeKeyDown: |
| if ([nextResponder respondsToSelector:@selector(keyDown:)]) { |
| [nextResponder keyDown:event]; |
| } |
| break; |
| case NSEventTypeKeyUp: |
| if ([nextResponder respondsToSelector:@selector(keyUp:)]) { |
| [nextResponder keyUp:event]; |
| } |
| break; |
| case NSEventTypeFlagsChanged: |
| if ([nextResponder respondsToSelector:@selector(flagsChanged:)]) { |
| [nextResponder flagsChanged:event]; |
| } |
| break; |
| default: |
| NSAssert(false, @"Unexpected key event type (got %lu).", event.type); |
| } |
| NSAssert(_eventBeingDispatched != nil, @"_eventBeingDispatched was cleared unexpectedly."); |
| _eventBeingDispatched = nil; |
| } |
| |
| - (void)buildLayout { |
| [_layoutMap removeAllObjects]; |
| |
| std::map<uint32_t, LayoutGoal> mandatoryGoalsByChar; |
| std::map<uint32_t, LayoutGoal> usLayoutGoalsByKeyCode; |
| for (const LayoutGoal& goal : flutter::layoutGoals) { |
| if (goal.mandatory) { |
| mandatoryGoalsByChar[goal.keyChar] = goal; |
| } else { |
| usLayoutGoalsByKeyCode[goal.keyCode] = goal; |
| } |
| } |
| |
| // Derive key mapping for each key code based on their layout clues. |
| // Key code 0x00 - 0x32 are typewriter keys (letters, digits, and symbols.) |
| // See keyCodeToPhysicalKey. |
| const uint16_t kMaxKeyCode = 0x32; |
| #ifdef DEBUG_PRINT_LAYOUT |
| NSString* debugLayoutData = @""; |
| #endif |
| for (uint16_t keyCode = 0; keyCode <= kMaxKeyCode; keyCode += 1) { |
| std::vector<LayoutClue> thisKeyClues = { |
| [_viewDelegate lookUpLayoutForKeyCode:keyCode shift:false], |
| [_viewDelegate lookUpLayoutForKeyCode:keyCode shift:true]}; |
| #ifdef DEBUG_PRINT_LAYOUT |
| debugLayoutData = |
| debugFormatLayoutData(debugLayoutData, keyCode, thisKeyClues[0], thisKeyClues[1]); |
| #endif |
| // The logical key should be the first available clue from below: |
| // |
| // - Mandatory goal, if it matches any clue. This ensures that all alnum |
| // keys can be found somewhere. |
| // - US layout, if neither clue of the key is EASCII. This ensures that |
| // there are no non-latin logical keys. |
| // - Derived on the fly from keyCode & characters. |
| for (const LayoutClue& clue : thisKeyClues) { |
| uint32_t keyChar = clue.isDeadKey ? 0 : clue.character; |
| auto matchingGoal = mandatoryGoalsByChar.find(keyChar); |
| if (matchingGoal != mandatoryGoalsByChar.end()) { |
| // Found a key that produces a mandatory char. Use it. |
| NSAssert(_layoutMap[@(keyCode)] == nil, @"Attempting to assign an assigned key code."); |
| _layoutMap[@(keyCode)] = @(keyChar); |
| mandatoryGoalsByChar.erase(matchingGoal); |
| break; |
| } |
| } |
| bool hasAnyEascii = isEascii(thisKeyClues[0]) || isEascii(thisKeyClues[1]); |
| // See if any produced char meets the requirement as a logical key. |
| auto foundUsLayoutGoal = usLayoutGoalsByKeyCode.find(keyCode); |
| if (foundUsLayoutGoal != usLayoutGoalsByKeyCode.end() && _layoutMap[@(keyCode)] == nil && |
| !hasAnyEascii) { |
| _layoutMap[@(keyCode)] = @(foundUsLayoutGoal->second.keyChar); |
| } |
| } |
| #ifdef DEBUG_PRINT_LAYOUT |
| NSLog(@"%@", debugLayoutData); |
| #endif |
| |
| // Ensure all mandatory goals are assigned. |
| for (auto mandatoryGoalIter : mandatoryGoalsByChar) { |
| const LayoutGoal& goal = mandatoryGoalIter.second; |
| _layoutMap[@(goal.keyCode)] = @(goal.keyChar); |
| } |
| } |
| |
| @end |