blob: 116c94537d9be901d46b75f1f0c1f291e9ac6372 [file] [log] [blame]
// 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