mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 16:35:33 +00:00
355 lines
13 KiB
Objective-C
355 lines
13 KiB
Objective-C
/*
|
|
* Keyman is copyright (C) SIL International. MIT License.
|
|
*
|
|
* Created by Shawn Schantz on 2022-12-12.
|
|
*
|
|
* A class to wrap an instance of a Keyman Core keyboard (a km_core_keyboard
|
|
* object) and its associated objects. A new CoreWrapper is created whenever
|
|
* Keyman for Mac switches to a new keyboard. The CoreWrapper allows Keyman
|
|
* Engine to be mostly unaware of Keyman Core.
|
|
*/
|
|
|
|
#import "CoreWrapper.h"
|
|
#import "keyman_core_api.h"
|
|
#import "keyman_core_api_consts.h"
|
|
#import "KMELogs.h"
|
|
|
|
@interface CoreWrapper()
|
|
|
|
@property (readonly) km_core_keyboard * coreKeyboard;
|
|
@property (readonly) km_core_state *coreState;
|
|
@property (weak, nonatomic, readonly) CoreHelper *coreHelper;
|
|
|
|
@end
|
|
|
|
@implementation CoreWrapper
|
|
|
|
const int CORE_ENVIRONMENT_ARRAY_LENGTH = 6;
|
|
|
|
-(instancetype)initWithHelper:(CoreHelper*)helper kmxFilePath:(nullable NSString*) path {
|
|
self = [super init];
|
|
if (self) {
|
|
_coreHelper = helper;
|
|
|
|
// if the kmxFilePath has been provided, then load the keyboard now
|
|
if (path != nil) {
|
|
[self changeKeyboardWithKmxFilePath: path];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
-(void)clearCoreContext {
|
|
[self clearContextUsingCore];
|
|
}
|
|
-(void)changeKeyboardWithKmxFilePath:(NSString*) path {
|
|
if (path != nil) {
|
|
@try {
|
|
[self loadKeyboardUsingCore: path];
|
|
[self readKeyboardAttributesUsingCore];
|
|
[self createKeyboardStateUsingCore];
|
|
}
|
|
@catch (NSException *exception) {
|
|
if (self.coreState) {
|
|
km_core_state_dispose(self.coreState);
|
|
_coreState = nil;
|
|
}
|
|
if (self.coreKeyboard) {
|
|
km_core_keyboard_dispose(self.coreKeyboard);
|
|
_coreKeyboard = nil;
|
|
}
|
|
//TODO: use NSError instead of NSException?
|
|
@throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
-(CoreKeyboardInfo*) getKeyboardInfoForKmxFile:(NSString*)kmxFile {
|
|
NSArray *keyArray = [self getKeyArray];
|
|
CoreKeyboardInfo* info = [[CoreKeyboardInfo alloc] init:kmxFile keyArray: keyArray];
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* Get the list of Keys supported by the keyboard
|
|
*/
|
|
-(NSArray*)getKeyArray {
|
|
NSMutableArray *keyArray = [[NSMutableArray alloc] initWithCapacity:0];
|
|
km_core_keyboard_key *keyList;
|
|
|
|
km_core_status result = km_core_keyboard_get_key_list(self.coreKeyboard, &keyList);
|
|
|
|
for(km_core_keyboard_key* keyPtr = keyList; (keyPtr->key) != 0; keyPtr++) {
|
|
uint16_t key = keyPtr->key;
|
|
uint32_t modifier_flag = keyPtr->modifier_flag;
|
|
|
|
if(key != 0) {
|
|
CoreKey* coreKey = [[CoreKey alloc] init: key modifiers: modifier_flag];
|
|
[keyArray addObject:coreKey];
|
|
|
|
//os_log_debug([KMELogs coreLog], "key %d modifiers 0x%X coreKey %{public}@", key, modifier_flag, coreKey);
|
|
}
|
|
}
|
|
|
|
km_core_keyboard_key_list_dispose(keyList);
|
|
|
|
os_log_debug([KMELogs coreLog], "getKeyList returning %lu keys", (unsigned long)keyArray.count);
|
|
|
|
return keyArray;
|
|
}
|
|
|
|
-(void) dealloc{
|
|
if (self.coreState) {
|
|
km_core_state_dispose(self.coreState);
|
|
}
|
|
if (self.coreKeyboard) {
|
|
km_core_keyboard_dispose(self.coreKeyboard);
|
|
}
|
|
os_log_debug([KMELogs coreLog], "CoreWrapper dealloc called");
|
|
}
|
|
|
|
-(void)loadKeyboardUsingCore:(NSString*) path {
|
|
km_core_path_name keyboardPath = [path UTF8String];
|
|
NSError* dataError = nil;
|
|
NSData *data = [NSData dataWithContentsOfFile:path options:0 error:&dataError];
|
|
|
|
if (dataError != nil) {
|
|
os_log_error([KMELogs coreLog], "loadKeyboardUsingCore, path: %{public}@\n dataError: %{public}@", path, dataError);
|
|
[NSException raise:@"LoadKeyboardException" format:@"%@", dataError];
|
|
} else {
|
|
NSUInteger dataLength = data.length;
|
|
os_log_info([KMELogs coreLog], "loadKeyboardUsingCore, path: %{public}@\n dataLength: %lu", path, dataLength);
|
|
|
|
km_core_status result = km_core_keyboard_load_from_blob(keyboardPath,
|
|
data.bytes, dataLength, &_coreKeyboard);
|
|
if (result != KM_CORE_STATUS_OK) {
|
|
NSString *message = [NSString stringWithFormat:@"Unexpected Keyman Core result: %u", result];
|
|
os_log_error([KMELogs coreLog], "loadKeyboardUsingCore, path: %{public}@\n core result: %{public}@", path, message);
|
|
[NSException raise:@"LoadKeyboardException" format:@"%@", message];
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void)readKeyboardAttributesUsingCore {
|
|
if (self.coreKeyboard != nil) {
|
|
const km_core_keyboard_attrs * keyboardAttributes;
|
|
km_core_status result = km_core_keyboard_get_attrs(self.coreKeyboard, &keyboardAttributes);
|
|
|
|
if (result==KM_CORE_STATUS_OK) {
|
|
_keyboardVersion = [self.coreHelper createNSStringFromUnicharString:keyboardAttributes->version_string];
|
|
_keyboardId = [self.coreHelper createNSStringFromUnicharString:keyboardAttributes->id];
|
|
os_log_debug([KMELogs coreLog], "readKeyboardAttributesUsingCore, keyboardVersion: %{public}@, keyboardId: %{public}@\n", _keyboardVersion, _keyboardId);
|
|
} else {
|
|
os_log_error([KMELogs coreLog], "km_core_keyboard_get_attrs() failed with result = %u\n", result);
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void)createKeyboardStateUsingCore {
|
|
km_core_status result = KM_CORE_STATUS_OK;
|
|
|
|
// TODO: create once
|
|
// create option list
|
|
km_core_option_item coreEnvironment[CORE_ENVIRONMENT_ARRAY_LENGTH] = {0};
|
|
|
|
if([CoreWrapper setupCoreEnvironment:coreEnvironment]) {
|
|
// create state using keyboard and option list
|
|
result = km_core_state_create(self.coreKeyboard, coreEnvironment, &_coreState);
|
|
|
|
if (result != KM_CORE_STATUS_OK) {
|
|
NSString *message = [NSString stringWithFormat:@"Unexpected Keyman Core result: %u", result];
|
|
[NSException raise:@"CreateKeyboardStateException" format:@"%@", message];
|
|
}
|
|
} else {
|
|
os_log_error([KMELogs coreLog], "CoreWrapper, Unable to set environment options for keyboard");
|
|
}
|
|
}
|
|
|
|
-(CoreKeyOutput*)processEvent:(nonnull NSEvent *)event {
|
|
NSEventModifierFlags modifiers = event.modifierFlags;
|
|
|
|
// process key down events only
|
|
if (event.type != NSEventTypeKeyDown) {
|
|
return nil;
|
|
}
|
|
// do not process command keys
|
|
if (modifiers & NSEventModifierFlagCommand) {
|
|
return nil;
|
|
}
|
|
|
|
return [self processMacVirtualKey:event.keyCode
|
|
withModifiers:modifiers withKeyDown:YES];
|
|
}
|
|
|
|
-(CoreKeyOutput*)processMacVirtualKey:(unsigned short)macKeyCode
|
|
withModifiers:(NSEventModifierFlags)modifiers
|
|
withKeyDown:(BOOL) isKeyDown {
|
|
CoreKeyOutput *output = nil;
|
|
uint16_t windowsKeyCode = [self.coreHelper macVirtualKeyToWindowsVirtualKey:macKeyCode];
|
|
uint32_t modifierState = [self.coreHelper macToKeymanModifier:modifiers];
|
|
uint8_t keyDown = (isKeyDown) ? 1 : 0;
|
|
|
|
if ([self processVirtualKey:windowsKeyCode
|
|
withModifier:modifierState
|
|
withKeyDown:keyDown]) {
|
|
output = [self loadOutputForLastKeyProcessed];
|
|
os_log_debug([KMELogs coreLog], "processMacVirtualKey for macKeyCode: %d / 0x%X, core output: %{public}@", macKeyCode, macKeyCode, output);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
-(BOOL)processVirtualKey:(uint16_t)keyCode
|
|
withModifier:(uint16_t)modifierState
|
|
withKeyDown:(uint8_t) isKeyDown {
|
|
|
|
km_core_status result = km_core_process_event(self.coreState, keyCode, modifierState, isKeyDown, KM_CORE_EVENT_FLAG_DEFAULT);
|
|
|
|
if (result!=KM_CORE_STATUS_OK) {
|
|
os_log_error([KMELogs coreLog], "km_core_process_event() result = %u\n", result);
|
|
}
|
|
|
|
return (result==KM_CORE_STATUS_OK);
|
|
}
|
|
|
|
-(CoreKeyOutput*)loadOutputForLastKeyProcessed {
|
|
return [self loadActionStructUsingCore];
|
|
}
|
|
|
|
-(CoreKeyOutput*)loadActionStructUsingCore {
|
|
km_core_actions * actions = km_core_state_get_actions(self.coreState);
|
|
CoreKeyOutput *output = [self createCoreKeyOutputForActionsStruct:actions];
|
|
return output;
|
|
}
|
|
|
|
-(CoreKeyOutput*) createCoreKeyOutputForActionsStruct:(km_core_actions*)actions {
|
|
NSString* text = [self.coreHelper utf32CStringToString:actions->output];
|
|
NSDictionary* options = [self convertOptionsArray:actions->persist_options];
|
|
CapsLockState capsLock = [self convertCapsLockState:actions->new_caps_lock_state];
|
|
NSString* deletedText = [self.coreHelper utf32CStringToString:actions->deleted_context];
|
|
|
|
CoreKeyOutput* coreKeyOutput = [[CoreKeyOutput alloc] init: actions->code_points_to_delete textToDelete:deletedText textToInsert:text optionsToPersist:options alert:actions->do_alert emitKeystroke:actions->emit_keystroke capsLockState:capsLock];
|
|
|
|
return coreKeyOutput;
|
|
}
|
|
|
|
-(NSDictionary*)convertOptionsArray:(km_core_option_item*)options {
|
|
NSMutableDictionary* optionsDictionary = nil;
|
|
if (options) {
|
|
optionsDictionary = [[NSMutableDictionary alloc] init];
|
|
for (; options->key != 0; ++options) {
|
|
unichar const * optionsKey = options->key;
|
|
unichar const * valueKey = options->value;
|
|
|
|
NSString *key = [self.coreHelper createNSStringFromUnicharString:optionsKey];
|
|
NSString *value = [self.coreHelper createNSStringFromUnicharString:valueKey];
|
|
[optionsDictionary setObject:value forKey:key];
|
|
}
|
|
}
|
|
return optionsDictionary;
|
|
}
|
|
|
|
-(CapsLockState)convertCapsLockState:(km_core_caps_state)capsState {
|
|
CapsLockState capsLock = Unchanged;
|
|
switch(capsState) {
|
|
case KM_CORE_CAPS_UNCHANGED:
|
|
capsLock = Unchanged;
|
|
break;
|
|
case KM_CORE_CAPS_OFF:
|
|
capsLock = Off;
|
|
break;
|
|
case KM_CORE_CAPS_ON:
|
|
capsLock = On;
|
|
break;
|
|
default:
|
|
capsLock = Unchanged;
|
|
break;
|
|
}
|
|
return capsLock;
|
|
}
|
|
|
|
-(void)clearContextUsingCore {
|
|
km_core_state_context_clear(self.coreState);
|
|
os_log_debug([KMELogs coreLog], "km_core_state_context_clear called");
|
|
}
|
|
|
|
-(void)setContextIfNeeded:(NSString*)context {
|
|
unichar const * unicharContext = [self.coreHelper createUnicharStringFromNSString:context];
|
|
km_core_status result = km_core_state_context_set_if_needed(self.coreState, unicharContext);
|
|
os_log_debug([KMELogs coreLog], "setContextIfNeeded, context=%{public}@, km_core_state_context_set_if_needed result=%d", context, result);
|
|
}
|
|
|
|
-(NSString*)contextDebug {
|
|
km_core_cu * context = km_core_state_context_debug(self.coreState, KM_CORE_DEBUG_CONTEXT_CACHED);
|
|
NSString *debugString = [self.coreHelper createNSStringFromUnicharString:context];
|
|
km_core_cu_dispose(context);
|
|
|
|
os_log_debug([KMELogs coreLog], "contextDebug = %{public}@", debugString);
|
|
return debugString;
|
|
}
|
|
|
|
//TODO: create and save as static
|
|
+(BOOL)setupCoreEnvironment:(km_core_option_item *) coreOptionArray {
|
|
coreOptionArray[0].scope = KM_CORE_OPT_ENVIRONMENT;
|
|
coreOptionArray[0].key = KM_CORE_KMX_ENV_BASELAYOUT;
|
|
coreOptionArray[0].value = u"kbdus.dll"; // const char16_t*, encoded as UTF-16
|
|
|
|
coreOptionArray[1].scope = KM_CORE_OPT_ENVIRONMENT;
|
|
coreOptionArray[1].key = KM_CORE_KMX_ENV_BASELAYOUTALT;
|
|
coreOptionArray[1].value = u"en-US"; // const char16_t*, encoded as UTF-16
|
|
|
|
coreOptionArray[2].scope = KM_CORE_OPT_ENVIRONMENT;
|
|
coreOptionArray[2].key = KM_CORE_KMX_ENV_SIMULATEALTGR;
|
|
coreOptionArray[2].value = u"0"; // const char16_t*, encoded as UTF-16
|
|
|
|
coreOptionArray[3].scope = KM_CORE_OPT_ENVIRONMENT;
|
|
coreOptionArray[3].key = KM_CORE_KMX_ENV_BASELAYOUTGIVESCTRLRALTFORRALT;
|
|
coreOptionArray[3].value = u"0"; // const char16_t*, encoded as UTF-16
|
|
|
|
coreOptionArray[4].scope = KM_CORE_OPT_ENVIRONMENT;
|
|
coreOptionArray[4].key = KM_CORE_KMX_ENV_PLATFORM;
|
|
coreOptionArray[4].value = u"mac macos macosx hardware desktop native"; // const char16_t*, encoded as UTF-16
|
|
|
|
coreOptionArray[5] = (km_core_option_item) {0};
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
-(BOOL)setOptionsForCore: (NSString *) key value:(NSString *) value {
|
|
os_log_debug([KMELogs coreLog], "setOptionsForCore, key = %@, value = %@", key, value);
|
|
|
|
// array of length 2, second item is terminating null struct
|
|
km_core_option_item option[2] = {0};
|
|
option[0].key = [self.coreHelper createUnicharStringFromNSString: key];
|
|
option[0].value = [self.coreHelper createUnicharStringFromNSString: value];
|
|
option[0].scope = KM_CORE_OPT_KEYBOARD;
|
|
|
|
km_core_status result = km_core_state_options_update(self.coreState, &option[0]);
|
|
os_log_debug([KMELogs coreLog], "setOptionsForCore, km_core_state_options_update result = %d", result);
|
|
|
|
return (result==KM_CORE_STATUS_OK);
|
|
}
|
|
|
|
-(void)readCoreOptions: (km_core_cu const *) key {
|
|
km_core_cu const * valueFromCore = nil;
|
|
km_core_status result =
|
|
km_core_state_option_lookup(self.coreState,
|
|
KM_CORE_OPT_KEYBOARD,
|
|
key,
|
|
&valueFromCore);
|
|
if (result == KM_CORE_STATUS_OK) {
|
|
if (valueFromCore) {
|
|
os_log_debug([KMELogs coreLog], "km_core_state_option_lookup successful, current value set in core= %@", [self.coreHelper createNSStringFromUnicharString:valueFromCore]);
|
|
} else {
|
|
os_log_debug([KMELogs coreLog], "km_core_state_option_lookup returned nil");
|
|
}
|
|
} else {
|
|
if (valueFromCore) {
|
|
os_log_debug([KMELogs coreLog], "km_core_state_option_lookup failed, result = %d", result);
|
|
} else {
|
|
os_log_debug([KMELogs coreLog], "km_core_state_option_lookup returned nil, result = %d", result);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
@end
|