mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-08 15:07:33 +00:00
now that the Keyman Configuration app is available to install and manage keyboard packages, delete the old configuration interface accessed through the Keyman menu on the input method
1055 lines
40 KiB
Objective-C
1055 lines
40 KiB
Objective-C
//
|
|
// KMInputMethodAppDelegate.m
|
|
// Keyman4MacIM
|
|
//
|
|
// Created by Serkan Kurt on 12/02/2015.
|
|
// Copyright (c) 2017 SIL International. All rights reserved.
|
|
//
|
|
|
|
#import "KMInputMethodAppDelegate.h"
|
|
#import "KMInputMethodLifecycle.h"
|
|
#import "KMSettingsRepository.h"
|
|
#import "KMDataRepository.h"
|
|
#import "ZipArchive.h"
|
|
#import "KMPackageReader.h"
|
|
#import "KMPackageInfo.h"
|
|
#import "PrivacyConsent.h"
|
|
#import "KMLogs.h"
|
|
#import "KMSentryHelper.h"
|
|
@import Sentry;
|
|
|
|
#if TARGET_CPU_ARM64
|
|
NSString *processorType = @"ARM";
|
|
#elif TARGET_CPU_X86_64
|
|
NSString *processorType = @"Intel";
|
|
#else
|
|
NSString *processorType = @"Unknown";
|
|
#endif
|
|
|
|
// distributed notifications
|
|
NSString *const kKeyboardsChanged = @"com.keyman.keyboards.changed";
|
|
NSString *const kAccessibilityCheckedRequest = @"com.keyman.accessibility.check.request";
|
|
NSString *const kAccessibilityRequest = @"com.keyman.accessibility.request";
|
|
|
|
@implementation NSString (VersionNumbers)
|
|
/**
|
|
* Returns a minimal version number by removing all '.0' from end of string,
|
|
* e.g. '9.0.0' -> '9', '9.1.0' -> '9.1'.
|
|
* Use to prepare version numbers for comparison with a `NSNumericSearch`, for example:
|
|
* ```
|
|
* NSString* version1 = [fileVersion minimalVersionNumberString];
|
|
* NSString* version2 = [programVersion minimalVersionNumberString];
|
|
* [version1 compare:version2 options:NSNumericSearch]
|
|
* ```
|
|
*/
|
|
- (NSString *)minimalVersionNumberString {
|
|
static NSString *const unnecessaryVersionSuffix = @".0";
|
|
NSString *minimalVersionNumber = self;
|
|
|
|
while ([minimalVersionNumber hasSuffix:unnecessaryVersionSuffix]) {
|
|
minimalVersionNumber = [minimalVersionNumber substringToIndex:minimalVersionNumber.length - unnecessaryVersionSuffix.length];
|
|
}
|
|
|
|
return minimalVersionNumber;
|
|
}
|
|
@end
|
|
|
|
@interface KMInputMethodAppDelegate ()
|
|
@property (nonatomic, strong) KMPackageReader *packageReader;
|
|
@property BOOL receivedKeyDownFromOsk;
|
|
@property NSEventModifierFlags oskEventModifiers;
|
|
@property (nonatomic, assign) BOOL sentryTestingEnabled;
|
|
@end
|
|
|
|
@implementation KMInputMethodAppDelegate
|
|
@synthesize kme = _kme;
|
|
@synthesize kmx = _kmx;
|
|
@synthesize modifierMapping = _modifierMapping;
|
|
@synthesize kvk = _kvk;
|
|
@synthesize keyboardName = _keyboardName;
|
|
@synthesize keyboardsPath = _keyboardsPath;
|
|
//@synthesize kmxFileList = _kmxFileList;
|
|
@synthesize selectedKeyboard = _selectedKeyboard;
|
|
@synthesize enabledKeyboards = _enabledKeyboards;
|
|
@synthesize contextBuffer = _contextBuffer;
|
|
|
|
id _lastServerWithOSKShowing = nil;
|
|
|
|
- (id)init {
|
|
self = [super init];
|
|
if (self) {
|
|
[self registerObservers];
|
|
|
|
// first notify user and request access to Accessibility/PostEvent permissions
|
|
// pass block as completion handler to complete init with initCompletion
|
|
[PrivacyConsent.shared requestPrivacyAccess:^void (void){
|
|
[self initCompletion];
|
|
}];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)initCompletion {
|
|
os_log_info([KMLogs startupLog], "initCompletionHandler method invoked");
|
|
|
|
self.lowLevelEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap,
|
|
kCGHeadInsertEventTap,
|
|
kCGEventTapOptionListenOnly,
|
|
CGEventMaskBit(kCGEventFlagsChanged) |
|
|
CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
CGEventMaskBit(kCGEventLeftMouseUp) |
|
|
CGEventMaskBit(kCGEventKeyDown),
|
|
(CGEventTapCallBack)eventTapFunction,
|
|
nil);
|
|
|
|
if (!self.lowLevelEventTap) {
|
|
os_log_error([KMLogs startupLog], "Unable to create lowLevelEventTap!");
|
|
}
|
|
else {
|
|
os_log([KMLogs startupLog], "Successfully created lowLevelEventTap with CGEventTapCreate.");
|
|
CFRelease(self.lowLevelEventTap);
|
|
}
|
|
|
|
self.runLoopEventSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, self.lowLevelEventTap, 0);
|
|
|
|
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
|
|
|
if (self.runLoopEventSrc && runLoop) {
|
|
CFRunLoopAddSource(runLoop, self.runLoopEventSrc, kCFRunLoopDefaultMode);
|
|
}
|
|
|
|
// start Input Method lifecycle
|
|
[KMInputMethodLifecycle.shared startLifecycle];
|
|
}
|
|
|
|
/**
|
|
* Register observers for local and distributed notifications
|
|
*/
|
|
- (void)registerObservers {
|
|
// register to receive notifications generated from KMInputMethodLifecycle
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodActivated:) name:kInputMethodActivatedNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil];
|
|
|
|
// register to receive notifications generated from Keyman Configuration App
|
|
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardsChanged:) name:kKeyboardsChanged object:nil];
|
|
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccessibilityCheckRequested:) name:kAccessibilityCheckedRequest object:nil];
|
|
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccessibilityRequested:) name:kAccessibilityRequest object:nil];
|
|
}
|
|
|
|
/**
|
|
* When packages have been installed, removed, enabled or disabled -- notification from the Keyman Configuration app
|
|
*/
|
|
- (void)handleKeyboardsChanged:(NSNotification *)notification {
|
|
os_log_debug([KMLogs configLog], "***KMInputMethodAppDelegate handleKeyboardsChanged");
|
|
[self reloadEnabledKeyboards];
|
|
}
|
|
|
|
/**
|
|
* When the state of Accessibility permission has been requested -- notification from the Keyman Configuration app
|
|
*/
|
|
- (void)handleAccessibilityCheckRequested:(NSNotification *)notification {
|
|
BOOL hasAccess = checkAccessibility();
|
|
os_log_info([KMLogs startupLog], "KMInputMethodAppDelegate handleAccessibilityCheckRequested, hasAccess: %{public}@", hasAccess?@"YES":@"NO");
|
|
}
|
|
|
|
/**
|
|
* When Accessibility permission has been requested -- notification from the Keyman Configuration app
|
|
*/
|
|
- (void)handleAccessibilityRequested:(NSNotification *)notification {
|
|
requestAccessibility();
|
|
os_log_info([KMLogs startupLog], "KMInputMethodAppDelegate handleAccessibilityRequested");
|
|
}
|
|
|
|
/**
|
|
* When the input method is activated -- notification from KMInputMethodLifecycle
|
|
*/
|
|
- (void)inputMethodActivated:(NSNotification *)notification {
|
|
|
|
// enable event tap
|
|
if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) {
|
|
os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap...");
|
|
CGEventTapEnable(self.lowLevelEventTap, YES);
|
|
}
|
|
|
|
// show OSK if necessary
|
|
os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false");
|
|
|
|
if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) {
|
|
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
|
|
[KMSentryHelper addDebugBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
|
|
[self showOSK];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When the input method is deactivated -- notification from KMInputMethodLifecycle
|
|
*/
|
|
- (void)inputMethodDeactivated:(NSNotification *)notification {
|
|
|
|
// if the OSK is visible, hide it
|
|
if ([self.oskWindow.window isVisible]) {
|
|
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK");
|
|
[KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"];
|
|
[self.oskWindow.window setIsVisible:NO];
|
|
} else {
|
|
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden");
|
|
}
|
|
[KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
|
|
|
|
// disable the event tap
|
|
if (self.lowLevelEventTap) {
|
|
os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap");
|
|
CGEventTapEnable(self.lowLevelEventTap, NO);
|
|
}
|
|
|
|
// deactive the text input client
|
|
[self.inputController deactivateClient];
|
|
}
|
|
|
|
/**
|
|
* When the user switches to a different text input client -- notification from KMInputMethodLifecycle
|
|
*/
|
|
- (void)inputMethodChangedClient:(NSNotification *)notification {
|
|
os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodChangedClient");
|
|
[self.inputController changeClients:[KMInputMethodLifecycle getRunningApplicationId]];
|
|
}
|
|
|
|
- (KeymanVersionInfo)versionInfo {
|
|
KeymanVersionInfo result;
|
|
// Get version information from Info.plist, which is filled in
|
|
// by build.sh.
|
|
NSDictionary *keymanInfo =[[[NSBundle mainBundle] infoDictionary] objectForKey:@"Keyman"];
|
|
result.sentryEnvironment = [keymanInfo objectForKey:@"SentryEnvironment"];
|
|
result.tier = [keymanInfo objectForKey:@"Tier"];
|
|
result.versionRelease = [keymanInfo objectForKey:@"VersionRelease"];
|
|
result.versionWithTag = [keymanInfo objectForKey:@"VersionWithTag"];
|
|
result.versionGitTag = [keymanInfo objectForKey:@"VersionGitTag"];
|
|
// if([result.tier isEqualToString:@"stable"]) { // #7227 disabling:
|
|
result.keymanCom = @"keyman.com";
|
|
result.helpKeymanCom = @"help.keyman.com";
|
|
result.apiKeymanCom = @"api.keyman.com";
|
|
/*}
|
|
else {
|
|
result.keymanCom = @"keyman-staging.com";
|
|
result.helpKeymanCom = @"help.keyman-staging.com";
|
|
result.apiKeymanCom = @"api.keyman-staging.com";
|
|
}*/
|
|
return result;
|
|
}
|
|
|
|
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];
|
|
|
|
[KMLogs reportLogStatus];
|
|
[self startSentry];
|
|
[self setDefaultKeymanMenuItems];
|
|
[self updateKeyboardMenuItems];
|
|
[self setPostLaunchKeymanSentryTags];
|
|
self.sentryTestingEnabled = [KMSettingsRepository.shared readForceSentryError];
|
|
}
|
|
|
|
- (void)startSentry {
|
|
KeymanVersionInfo keymanVersionInfo = [self versionInfo];
|
|
NSString *releaseName = [NSString stringWithFormat:@"%@", keymanVersionInfo.versionGitTag];
|
|
|
|
[SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
|
|
options.dsn = @"https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522";
|
|
options.releaseName = releaseName;
|
|
options.environment = keymanVersionInfo.sentryEnvironment;
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Determine whether to force sentry events.
|
|
* When true, then typing certain characters will cause Sentry events or crashes to occur.
|
|
*
|
|
* To enable, set the flag in UserDefaults
|
|
* `defaults write keyman.inputmethod.Keyman KMForceSentryError 1`
|
|
* Also, use the SIL Euro Latin keyboard and type in the Stickies app
|
|
*/
|
|
- (BOOL)canForceSentryEvents {
|
|
NSString* const testKeyboardName = @"sil_euro_latin.kmx";
|
|
NSString* const testClientApplicationId = @"com.apple.Stickies";
|
|
BOOL canForce = NO;
|
|
|
|
if (self.sentryTestingEnabled) {
|
|
NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId];
|
|
NSString * kmxFileName = [self getKmxFileName];
|
|
canForce = [kmxFileName isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId];
|
|
os_log_info([KMLogs testLog], "canForceSentryEvents, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxFileName, applicationId, canForce?@"YES":@"NO");
|
|
}
|
|
|
|
return canForce;
|
|
}
|
|
|
|
- (NSString*) getKmxFileName {
|
|
return [[self.kmx filePath] lastPathComponent];
|
|
}
|
|
|
|
- (void)setPostLaunchKeymanSentryTags {
|
|
NSString *keyboardFileName = [self.kmx.filePath lastPathComponent];
|
|
os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName);
|
|
|
|
// assign custom keyboard tags in Sentry
|
|
[KMSentryHelper addKeyboardTag:keyboardFileName];
|
|
[KMSentryHelper addHasAccessibilityTag:[PrivacyConsent.shared checkAccessibility]];
|
|
[KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
|
|
[KMSentryHelper addArchitectureTag:processorType];
|
|
}
|
|
|
|
+ (KMInputMethodAppDelegate *)AppDelegate {
|
|
return (KMInputMethodAppDelegate *)[NSApp delegate];
|
|
}
|
|
|
|
CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
|
KMInputMethodAppDelegate *appDelegate = [KMInputMethodAppDelegate AppDelegate];
|
|
if (appDelegate != nil) {
|
|
if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
|
|
// kCGEventTapDisabledByUserInput most likely means we're "sleeping", in which case we want it to stay disabled until we get the wake-up call.
|
|
if ([KMInputMethodLifecycle.shared shouldEnableEventTap]) {
|
|
// REVIEW: We might need to consider putting in some kind of counter/flag to ensure that the very next
|
|
// event is not another disable so we don't end up in an endless cycle.
|
|
os_log([KMLogs eventsLog], "Event tap disabled by %{public}@! Attempting to restart...", (type == kCGEventTapDisabledByTimeout ? @"timeout" : @"user"));
|
|
CGEventTapEnable(appDelegate.lowLevelEventTap, YES);
|
|
if (!CGEventTapIsEnabled(appDelegate.lowLevelEventTap)) {
|
|
if (appDelegate.runLoopEventSrc) { // This should always be true
|
|
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
|
if (runLoop) {
|
|
CFRunLoopRemoveSource(runLoop, appDelegate.runLoopEventSrc, kCFRunLoopDefaultMode);
|
|
}
|
|
appDelegate.runLoopEventSrc = nil;
|
|
}
|
|
appDelegate.lowLevelEventTap = nil;
|
|
}
|
|
}
|
|
return event;
|
|
}
|
|
|
|
NSEvent* sysEvent = [NSEvent eventWithCGEvent:event];
|
|
// Too many of these to be useful for most debugging sessions, but we'll keep this around to be
|
|
// un-commented when needed.
|
|
os_log_debug([KMLogs keyTraceLog], "System Event: %{public}@", sysEvent);
|
|
|
|
switch (type) {
|
|
case kCGEventFlagsChanged:
|
|
os_log_debug([KMLogs eventsLog], "eventTapFunction: system event kCGEventFlagsChanged to: 0x%X", (int) sysEvent.modifierFlags);
|
|
appDelegate.currentModifiers = sysEvent.modifierFlags;
|
|
if (appDelegate.currentModifiers & NSEventModifierFlagCommand) {
|
|
appDelegate.contextChangedByLowLevelEvent = YES;
|
|
}
|
|
break;
|
|
|
|
case kCGEventLeftMouseUp:
|
|
case kCGEventLeftMouseDown:
|
|
case kCGEventOtherMouseUp:
|
|
case kCGEventOtherMouseDown:
|
|
os_log_debug([KMLogs eventsLog], "Event tap context invalidation flagged due to event of type: %u", type);
|
|
appDelegate.contextChangedByLowLevelEvent = YES;
|
|
break;
|
|
|
|
case kCGEventKeyDown:
|
|
appDelegate.receivedKeyDownFromOsk = NO;
|
|
os_log_debug([KMLogs keyTraceLog], "** KEY DOWN Event **");
|
|
os_log_debug([KMLogs keyTraceLog], "** event tap keydown event, keyCode: 0x%X (%d)", sysEvent.keyCode, sysEvent.keyCode);
|
|
// Pass back low-level backspace events to the input method event handler
|
|
// because some non-compliant apps do not allow us to see backspace events
|
|
// that we have generated (and we need to see them, for serialization
|
|
// of events)
|
|
if(sysEvent.keyCode == kVK_Delete && appDelegate.inputController != nil) {
|
|
os_log_debug([KMLogs keyTraceLog], "** event tap handling kVK_Delete");
|
|
[appDelegate.inputController handleBackspace:sysEvent];
|
|
} else if(sysEvent.keyCode == kVK_Delete) {
|
|
os_log_debug([KMLogs eventsLog], "** event tap cannot handle kVK_Delete, appDelegate.inputController = %{public}@", appDelegate.inputController);
|
|
}
|
|
if(sysEvent.keyCode == 255) {
|
|
os_log_debug([KMLogs eventsLog], "** event tap received kKeymanEventKeyCode = 0xFF");
|
|
} else {
|
|
if ([OSKView isOskKeyDownEvent:event]) {
|
|
[KMSentryHelper addInfoBreadCrumb:@"event" message:@"processing OSK-generated keydown event"];
|
|
NSEventModifierFlags oskEventModifiers = [OSKView extractModifierFlagsFromOskEvent:event];
|
|
appDelegate.receivedKeyDownFromOsk = YES;
|
|
appDelegate.oskEventModifiers = oskEventModifiers;
|
|
os_log_debug([KMLogs eventsLog], "*** event tap received event from OSK, modifiers: 0x%lX",(unsigned long)oskEventModifiers);
|
|
}
|
|
}
|
|
|
|
switch(sysEvent.keyCode) {
|
|
case kVK_Home:
|
|
case kVK_PageUp:
|
|
case kVK_End:
|
|
case kVK_PageDown:
|
|
// While these four may not actually move the cursor, this is application-dependent,
|
|
// so we'll treat them all as a context change. This means we lose deadkeys if these
|
|
// are pressed, but that's actually probably a good thing anyway!
|
|
case kVK_LeftArrow:
|
|
case kVK_RightArrow:
|
|
case kVK_DownArrow:
|
|
case kVK_UpArrow:
|
|
// Cursor movement means that we need to recheck the context
|
|
case kVK_Escape:
|
|
// Escape key should be treated as a context reset
|
|
case kVK_F1:
|
|
case kVK_F2:
|
|
case kVK_F3:
|
|
case kVK_F4:
|
|
case kVK_F5:
|
|
case kVK_F6:
|
|
case kVK_F7:
|
|
case kVK_F8:
|
|
case kVK_F9:
|
|
case kVK_F10:
|
|
case kVK_F11:
|
|
case kVK_F12:
|
|
case kVK_F13:
|
|
case kVK_F14:
|
|
case kVK_F15:
|
|
case kVK_F16:
|
|
case kVK_F17:
|
|
case kVK_F18:
|
|
case kVK_F19:
|
|
case kVK_F20:
|
|
// Function keys are command keys, so context is invalid
|
|
case kVK_Tab:
|
|
// A tab between cells in a Word document, for instance, is a context change
|
|
case kVK_ForwardDelete:
|
|
// Text modification by the forward delete key should reset context, but
|
|
// note that normal Delete (aka Bksp) is already managed by Keyman correctly.
|
|
appDelegate.contextChangedByLowLevelEvent = YES;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return event;
|
|
}
|
|
|
|
- (NSMenu *)menu {
|
|
return _menu;
|
|
}
|
|
|
|
- (KMEngine *)kme {
|
|
if (_kme == nil) {
|
|
_kme = [[KMEngine alloc] initWithKMX:nil context:self.contextBuffer];
|
|
}
|
|
|
|
return _kme;
|
|
}
|
|
|
|
- (KMPackageReader *)packageReader {
|
|
if (_packageReader == nil) {
|
|
_packageReader = [[KMPackageReader alloc] init];
|
|
}
|
|
|
|
return _packageReader;
|
|
}
|
|
|
|
- (void)resetKmx {
|
|
_kmx = nil;
|
|
_modifierMapping = nil;
|
|
}
|
|
|
|
- (void)loadKeyboardFromKmxFile:(KMXFile *)kmx {
|
|
_kmx = kmx;
|
|
CoreKeyboardInfo *keyboardInfo = [self.kme loadKeyboardFromKmxFile:kmx];
|
|
|
|
os_log_info([KMLogs keyboardLog], "loadKeyboardFromKmxFile, keyboard info: %{public}@", keyboardInfo);
|
|
[KMSentryHelper addInfoBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [self getKmxFileName]]];
|
|
[KMSentryHelper addKeyboardTag:[self getKmxFileName]];
|
|
|
|
_modifierMapping = [[KMModifierMapping alloc] init:keyboardInfo];
|
|
|
|
os_log_info([KMLogs keyboardLog], "modifierMapping bothOptionKeysGenerateRightAlt: %d", self.modifierMapping.bothOptionKeysGenerateRightAlt);
|
|
}
|
|
|
|
- (void)setKvk:(KVKFile *)kvk {
|
|
if (kvk != nil) {
|
|
os_log_info([KMLogs lifecycleLog], "*** setKvk, kvk.associatedKeyboard: %{public}@", kvk.associatedKeyboard);
|
|
} else {
|
|
os_log_info([KMLogs lifecycleLog], "*** setKvk, setting to nil");
|
|
}
|
|
_kvk = kvk;
|
|
|
|
if (_oskWindow != nil) {
|
|
[_oskWindow resetOSK];
|
|
}
|
|
}
|
|
|
|
- (void)setKeyboardName:(NSString *)keyboardName {
|
|
_keyboardName = keyboardName;
|
|
if (_oskWindow != nil)
|
|
[_oskWindow.window setTitle:self.oskWindowTitle];
|
|
}
|
|
|
|
- (void)applyPersistedOptions {
|
|
NSDictionary *selectedPersistedOptions = [[KMSettingsRepository shared] readOptionsForSelectedKeyboard];
|
|
|
|
// TODO: pass array instead of making repeated calls
|
|
for (NSString *key in selectedPersistedOptions) {
|
|
NSString *value = [selectedPersistedOptions objectForKey:key];
|
|
os_log_info([KMLogs configLog], "persisted options found in UserDefaults for keyboard %{public}@, key: %{public}@, value: %{public}@", _selectedKeyboard, key, value);
|
|
[self.kme setCoreOptions:key withValue:value];
|
|
}
|
|
}
|
|
|
|
- (NSString *)oskWindowTitle {
|
|
if (_keyboardName == nil || !_keyboardName.length)
|
|
return [NSString stringWithFormat:@"Keyman"];
|
|
else
|
|
return [NSString stringWithFormat:@"%@ - Keyman", _keyboardName];
|
|
}
|
|
|
|
#pragma mark - Keyman Data
|
|
|
|
/**
|
|
* Returns the root folder where keyboards are stored
|
|
*/
|
|
- (NSString *)keyboardsPath {
|
|
if (_keyboardsPath == nil) {
|
|
_keyboardsPath = [KMDataRepository shared].keyman19KeyboardsDirectory.path;
|
|
}
|
|
|
|
return _keyboardsPath;
|
|
}
|
|
|
|
- (NSString *)packageFolderFromPath:(NSString *)path {
|
|
NSString *packageFolder = nil;
|
|
NSString *sourcePath = [self keyboardsPath];
|
|
NSString *mPath = [NSString stringWithString:[path stringByDeletingLastPathComponent]];
|
|
if ([mPath isEqualToString:sourcePath])
|
|
return @"Others";
|
|
|
|
while (![mPath isEqualToString:sourcePath]) {
|
|
packageFolder = [mPath lastPathComponent];
|
|
mPath = [mPath stringByDeletingLastPathComponent];
|
|
}
|
|
|
|
return packageFolder;
|
|
}
|
|
|
|
- (KMPackageInfo *)loadPackageInfo:(NSString *)path {
|
|
return [self.packageReader loadPackageInfo:path];
|
|
}
|
|
|
|
- (NSArray *)keyboardNamesFromFolder:(NSString *)packageFolder {
|
|
os_log_debug([KMLogs dataLog], "keyboardNamesFromFolder, folder = %{public}@", packageFolder);
|
|
NSMutableArray *kbNames = [[NSMutableArray alloc] initWithCapacity:0];;
|
|
for (NSString *kmxFile in [self getKmxFilesAtPath:packageFolder]) {
|
|
NSDictionary * infoDict = [KMXFile keyboardInfoFromKmxFile:kmxFile];
|
|
if (infoDict != nil) {
|
|
NSString *name = [infoDict objectForKey:kKMKeyboardNameKey];
|
|
if (name != nil && [name length]) {
|
|
os_log_debug([KMLogs configLog], "Adding keyboard name: %{public}@", name);
|
|
[kbNames addObject:name];
|
|
}
|
|
}
|
|
}
|
|
return kbNames;
|
|
}
|
|
|
|
- (NSString *)selectedKeyboard {
|
|
if (_selectedKeyboard == nil) {
|
|
_selectedKeyboard = [[KMSettingsRepository shared] readSelectedKeyboard];
|
|
}
|
|
os_log_debug([KMLogs dataLog], "selectedKeyboard = %{public}@", _selectedKeyboard);
|
|
|
|
return _selectedKeyboard;
|
|
}
|
|
|
|
- (void)setSelectedKeyboard:(NSString *)selectedKeyboard {
|
|
[[KMSettingsRepository shared] writeSelectedKeyboard:selectedKeyboard];
|
|
_selectedKeyboard = selectedKeyboard;
|
|
}
|
|
|
|
- (NSMutableArray *)enabledKeyboards {
|
|
if (!_enabledKeyboards) {
|
|
os_log_debug([KMLogs dataLog], "initializing enabledKeyboards");
|
|
_enabledKeyboards = [[KMSettingsRepository.shared readEnabledKeyboards] mutableCopy];
|
|
|
|
[KMSentryHelper addEnabledKeyboardCountTag:_enabledKeyboards.count];
|
|
}
|
|
|
|
return _enabledKeyboards;
|
|
}
|
|
|
|
/**
|
|
* Called when the enabled keyboards have been changed by the Keyman Config app.
|
|
* Reload the enabled keyboards from the UserDefaults
|
|
* Then update the Keyman menu
|
|
*/
|
|
- (void)reloadEnabledKeyboards {
|
|
os_log_debug([KMLogs configLog], "reloadEnabledKeyboards");
|
|
|
|
// read the enabled keyboards from UserDefaults
|
|
_enabledKeyboards = [[KMSettingsRepository.shared readEnabledKeyboards] mutableCopy];
|
|
|
|
// set Sentry tag for how many keyboards are enabled
|
|
[KMSentryHelper addEnabledKeyboardCountTag:_enabledKeyboards.count];
|
|
|
|
// update the keyboard menu to reflect the updated list of enabled keyboards
|
|
[self updateKeyboardMenuItems];
|
|
}
|
|
|
|
- (void)saveEnabledKeyboards {
|
|
os_log_debug([KMLogs dataLog], "saveEnabledKeyboards");
|
|
[KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards];
|
|
[self resetEnabledKeyboards];
|
|
[self updateKeyboardMenuItems];
|
|
[KMSentryHelper addEnabledKeyboardCountTag:self.enabledKeyboards.count];
|
|
}
|
|
|
|
- (void)clearEnabledKeyboards {
|
|
[KMSettingsRepository.shared clearEnabledKeyboards];
|
|
[self updateKeyboardMenuItems];
|
|
}
|
|
|
|
- (void)addEnabledKeyboard:(NSString *) partialPath {
|
|
if (![self.enabledKeyboards containsObject:partialPath]) {
|
|
os_log_debug([KMLogs keyboardLog], "addActiveKeyboard, adding '%{public}@' to list of active keyboards: ", partialPath);
|
|
[self.enabledKeyboards addObject:partialPath];
|
|
}
|
|
}
|
|
|
|
- (void)resetEnabledKeyboards {
|
|
// Remove entries with missing files
|
|
NSMutableArray *pathsToRemove = [[NSMutableArray alloc] initWithCapacity:0];
|
|
for (NSString *path in self.enabledKeyboards) {
|
|
NSString *fullPath = [KMDataRepository.shared buildFullPath:path];
|
|
os_log_debug([KMLogs dataLog], "resetEnabledKeyboards, checking fullPath: '%{public}@' for path: '%{public}@'", fullPath, path);
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:fullPath]) {
|
|
os_log_debug([KMLogs dataLog], "resetEnabledKeyboards, need to remove non-existent path: %{public}@", fullPath);
|
|
[pathsToRemove addObject:path];
|
|
}
|
|
}
|
|
|
|
BOOL found = FALSE;
|
|
if (pathsToRemove.count > 0) {
|
|
[self.enabledKeyboards removeObjectsInArray:pathsToRemove];
|
|
found = TRUE;
|
|
}
|
|
|
|
// Remove duplicate entries
|
|
NSUInteger i = 0;
|
|
while(i < [self.enabledKeyboards count]) {
|
|
NSString *item = self.enabledKeyboards[i];
|
|
NSUInteger n = [self.enabledKeyboards indexOfObject:item inRange: NSMakeRange(i+1, [self.enabledKeyboards count]-i-1)];
|
|
if(n != NSNotFound) {
|
|
[self.enabledKeyboards removeObjectAtIndex:n];
|
|
found = TRUE;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
[KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards];
|
|
}
|
|
}
|
|
|
|
- (NSMutableString *)contextBuffer {
|
|
if (_contextBuffer == nil) {
|
|
_contextBuffer = [[NSMutableString alloc] initWithString:@""];
|
|
}
|
|
|
|
return _contextBuffer;
|
|
}
|
|
|
|
- (void)clearContextBuffer {
|
|
[self.kme clearCoreContext];
|
|
}
|
|
|
|
- (void)awakeFromNib {
|
|
[self prepareStorage];
|
|
}
|
|
|
|
/**
|
|
* Prepare the app environment for all the things that need to be persisted:
|
|
* namely, the keyboard data on disk and the settings in UserDefaults
|
|
*/
|
|
- (void)prepareStorage {
|
|
SettingsState state = [KMSettingsRepository.shared determineSettingsState];
|
|
|
|
switch (state) {
|
|
case KeymanSettingsVersion17:
|
|
os_log_info([KMLogs dataLog], "prepareStorage, migration needed for Keyman 17 to current");
|
|
[KMDataRepository.shared createSharedDirectoriesIfNecessary];
|
|
[KMDataRepository.shared migrateDataFromKeyman17];
|
|
[KMSettingsRepository.shared migrateSettingsFromKeyman17];
|
|
break;
|
|
case KeymanSettingsVersion18:
|
|
os_log_info([KMLogs dataLog], "prepareStorage, migration needed for Keyman 18 to current");
|
|
[KMDataRepository.shared createSharedDirectoriesIfNecessary];
|
|
[KMDataRepository.shared migrateDataFromKeyman18];
|
|
[KMSettingsRepository.shared migrateSettingsFromKeyman18];
|
|
break;
|
|
case KeymanSettingsNotFound:
|
|
os_log_info([KMLogs dataLog], "prepareStorage, settings not found, create them");
|
|
[KMDataRepository.shared createSharedDirectoriesIfNecessary];
|
|
[KMSettingsRepository.shared createSharedSettingsIfNecessary];
|
|
break;
|
|
case KeymanSettingsVersionCurrent:
|
|
os_log_info([KMLogs dataLog], "prepareStorage: settings are current, no migration needed");
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)setDefaultKeymanMenuItems {
|
|
NSMenuItem *osk = [self.menu itemWithTag:OSK_MENUITEM_TAG];
|
|
if (osk) {
|
|
[osk setAction:@selector(menuAction:)];
|
|
}
|
|
|
|
NSMenuItem *about = [self.menu itemWithTag:ABOUT_MENUITEM_TAG];
|
|
if (about) {
|
|
[about setAction:@selector(menuAction:)];
|
|
}
|
|
}
|
|
|
|
- (void)updateKeyboardMenuItems {
|
|
self.numberOfKeyboardMenuItems = [self calculateNumberOfKeyboardMenuItems];
|
|
[self removeDynamicKeyboardMenuItems];
|
|
[self addDynamicKeyboardMenuItems];
|
|
}
|
|
|
|
- (int)calculateNumberOfKeyboardMenuItems {
|
|
os_log_debug([KMLogs uiLog], "calculateNumberOfKeyboardMenuItems, entered");
|
|
if (self.enabledKeyboards.count == 0) {
|
|
// if there are no active keyboards, then we will insert one placeholder menu item 'No Active Keyboards'
|
|
return 1;
|
|
} else {
|
|
return (int) self.enabledKeyboards.count;
|
|
}
|
|
}
|
|
|
|
- (void)removeDynamicKeyboardMenuItems {
|
|
int numberToRemove = (int) self.menu.numberOfItems - DEFAULT_KEYMAN_MENU_ITEM_COUNT;
|
|
|
|
os_log_info([KMLogs keyboardLog], "*** removeDynamicKeyboardMenuItems, self.menu.numberOfItems = %ld, number of items to remove = %d", (long)self.menu.numberOfItems, numberToRemove);
|
|
|
|
if (numberToRemove > 0) {
|
|
for (int i = 0; i < numberToRemove; i++) {
|
|
[self.menu removeItemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)addDynamicKeyboardMenuItems {
|
|
os_log_debug([KMLogs startupLog], "addDynamicKeyboardMenuItems, entered");
|
|
BOOL didSetSelectedKeyboard = NO;
|
|
NSInteger itag = KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG;
|
|
NSString *keyboardMenuName = @"";
|
|
int menuItemIndex = KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX;
|
|
|
|
os_log_debug([KMLogs configLog], "*** populateKeyboardMenuItems, number of active keyboards=%lu", self.enabledKeyboards.count);
|
|
|
|
// loop through the active keyboards list and add them to the menu
|
|
for (NSString *path in self.enabledKeyboards) {
|
|
NSString *fullPath = [KMDataRepository.shared buildFullPath:path];
|
|
os_log_debug([KMLogs dataLog], "addDynamicKeyboardMenuItems, path = '%{public}@', full path = '%{public}@'", path, fullPath);
|
|
NSDictionary *infoDict = [KMXFile keyboardInfoFromKmxFile:fullPath];
|
|
if (!infoDict) {
|
|
continue;
|
|
}
|
|
keyboardMenuName = [infoDict objectForKey:kKMKeyboardNameKey];
|
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:keyboardMenuName action:@selector(menuAction:) keyEquivalent:@""];
|
|
[item setTag:itag++];
|
|
|
|
// if this is the selected keyboard, then configure it as selected
|
|
if ([path isEqualToString:self.selectedKeyboard]) {
|
|
[self setSelectedKeyboard:path inMenuItem:item];
|
|
didSetSelectedKeyboard = YES;
|
|
}
|
|
else {
|
|
[item setState:NSOffState];
|
|
}
|
|
|
|
[self.menu insertItem:item atIndex:menuItemIndex++];
|
|
}
|
|
|
|
if (self.enabledKeyboards.count == 0) {
|
|
[self addKeyboardPlaceholderMenuItem];
|
|
} else if (!didSetSelectedKeyboard) {
|
|
[self setDefaultSelectedKeyboard];
|
|
}
|
|
}
|
|
|
|
- (void) setSelectedKeyboard:(NSString*)keyboardName inMenuItem:(NSMenuItem*) menuItem {
|
|
KVKFile *kvk = nil;
|
|
|
|
NSString *fullPath = [KMDataRepository.shared buildFullPath:keyboardName];
|
|
os_log_debug([KMLogs dataLog], "setSelectedKeyboard, keyboardName = '%{public}@', full path = '%{public}@'", keyboardName, fullPath);
|
|
[menuItem setState:NSOnState];
|
|
KMXFile *kmx = [[KMXFile alloc] initWithFilePath:fullPath];
|
|
[self loadKeyboardFromKmxFile:kmx];
|
|
NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:fullPath];
|
|
NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey];
|
|
if (kvkFilename != nil) {
|
|
NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename];
|
|
if (kvkFilePath != nil) {
|
|
kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath];
|
|
}
|
|
}
|
|
[self setKvk:kvk];
|
|
[self setKeyboardName:[kmxInfo objectForKey:kKMKeyboardNameKey]];
|
|
[self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]];
|
|
[self applyPersistedOptions];
|
|
}
|
|
|
|
// defaults to the whatever keyboard happens to be first in the list
|
|
- (void) setDefaultSelectedKeyboard {
|
|
NSMenuItem* menuItem = [self.menu itemAtIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX];
|
|
NSString *keyboardName = [self.enabledKeyboards objectAtIndex:0];
|
|
[self setSelectedKeyboard:keyboardName inMenuItem:menuItem];
|
|
[self setSelectedKeyboard:keyboardName];
|
|
[self clearContextBuffer];
|
|
|
|
[KMSentryHelper addInfoBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]];
|
|
}
|
|
|
|
- (void) addKeyboardPlaceholderMenuItem {
|
|
NSString* placeholder = NSLocalizedString(@"no-keyboard-configured-menu-placeholder", nil);
|
|
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:placeholder action:NULL keyEquivalent:@""];
|
|
[self.menu insertItem:item atIndex:KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX];
|
|
[self resetKmx];
|
|
[self setKvk:nil];
|
|
[self setKeyboardName:nil];
|
|
[self setKeyboardIcon:nil];
|
|
[self clearContextBuffer];
|
|
[self setSelectedKeyboard:@""];
|
|
}
|
|
|
|
- (void)selectKeyboardFromMenu:(NSInteger)tag {
|
|
NSMenuItem *menuItem = [self.menu itemWithTag:tag];
|
|
NSString *title = menuItem.title;
|
|
os_log_info([KMLogs keyboardLog], "Input Menu, selected Keyboards menu, itag: %lu, title: %{public}@", tag, title);
|
|
for (NSMenuItem *item in self.menu.itemArray) {
|
|
// set the state of the keyboard items in the Keyman menu based on the new selection
|
|
if (item.tag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) {
|
|
if (item.tag == tag) {
|
|
[item setState:NSOnState];
|
|
}
|
|
else {
|
|
[item setState:NSOffState];
|
|
}
|
|
}
|
|
}
|
|
|
|
NSString *path = [self.enabledKeyboards objectAtIndex:tag-KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG];
|
|
NSString *fullPath = [KMDataRepository.shared buildFullPath:path];
|
|
os_log_debug([KMLogs dataLog], "setSelectedKeyboard, keyboardName = '%{public}@', full path = '%{public}@'", path, fullPath);
|
|
|
|
KMXFile *kmx = [[KMXFile alloc] initWithFilePath:fullPath];
|
|
[self loadKeyboardFromKmxFile:kmx];
|
|
KVKFile *kvk = nil;
|
|
NSDictionary *kmxInfo = [KMXFile keyboardInfoFromKmxFile:fullPath];
|
|
NSString *kvkFilename = [kmxInfo objectForKey:kKMVisualKeyboardKey];
|
|
if (kvkFilename != nil) {
|
|
NSString *kvkFilePath = [self kvkFilePathFromFilename:kvkFilename];
|
|
if (kvkFilePath != nil)
|
|
kvk = [[KVKFile alloc] initWithFilePath:kvkFilePath];
|
|
}
|
|
[self setKvk:kvk];
|
|
NSString *keyboardName = [kmxInfo objectForKey:kKMKeyboardNameKey];
|
|
os_log_info([KMLogs keyboardLog], "Selected keyboard from menu: %{public}@", keyboardName);
|
|
[self setKeyboardName:keyboardName];
|
|
[self setKeyboardIcon:[kmxInfo objectForKey:kKMKeyboardIconKey]];
|
|
[self clearContextBuffer];
|
|
[self setSelectedKeyboard:path];
|
|
[self applyPersistedOptions];
|
|
}
|
|
|
|
- (NSArray *)getKmxFilesInKeyboardsDirectory {
|
|
return [self getKmxFilesAtPath:self.keyboardsPath];
|
|
}
|
|
|
|
- (NSArray *)getKmxFilesAtPath:(NSString *)path {
|
|
os_log_debug([KMLogs dataLog], "getKmxFilesAtPath, path: '%{public}@'", path);
|
|
NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:path];
|
|
NSMutableArray *kmxFiles = [[NSMutableArray alloc] initWithCapacity:0];
|
|
NSString *filePath;
|
|
while (filePath = (NSString *)[dirEnum nextObject]) {
|
|
NSString *extension = [[filePath pathExtension] lowercaseString];
|
|
if ([extension isEqualToString:@"kmx"]) {
|
|
[kmxFiles addObject:[path stringByAppendingPathComponent:filePath]];
|
|
os_log_debug([KMLogs dataLog], "file = %{public}@", filePath);
|
|
}
|
|
}
|
|
|
|
return kmxFiles;
|
|
}
|
|
|
|
- (NSArray *)getKvkFilesArray {
|
|
NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:self.keyboardsPath];
|
|
NSMutableArray *kvkFilesArray = [[NSMutableArray alloc] initWithCapacity:0];
|
|
NSString *filePath;
|
|
while (filePath = (NSString *)[dirEnum nextObject]) {
|
|
NSString *extension = [[filePath pathExtension] lowercaseString];
|
|
if ([extension isEqualToString:@"kvk"])
|
|
[kvkFilesArray addObject:[self.keyboardsPath stringByAppendingPathComponent:filePath]];
|
|
}
|
|
|
|
return kvkFilesArray;
|
|
}
|
|
|
|
- (NSString *)kvkFilePathFromFilename:(NSString *)kvkFilename {
|
|
NSString *kvkFilePath = nil;
|
|
NSArray *kvkFilesArray = [self getKvkFilesArray];
|
|
for (NSString *filePath in kvkFilesArray) {
|
|
if ([[filePath lastPathComponent] isEqualToString:kvkFilename]) {
|
|
kvkFilePath = filePath;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return kvkFilePath;
|
|
}
|
|
|
|
- (NSWindowController *)oskWindow {
|
|
if (!_oskWindow) {
|
|
_oskWindow = [[OSKWindowController alloc] initWithWindowNibName:@"OSKWindowController"];
|
|
|
|
os_log_debug([KMLogs oskLog], "Loaded oskWindow from Nib, isVisible: %{public}@ readShowOskOnActivate: %{public}@", [_oskWindow.window isVisible]?@"true":@"false", [KMSettingsRepository.shared readShowOskOnActivate]?@"true":@"false");
|
|
}
|
|
|
|
return _oskWindow;
|
|
}
|
|
|
|
- (void)showOSK {
|
|
[self.oskWindow prepareToShowOsk];
|
|
[[self.oskWindow window] makeKeyAndOrderFront:nil];
|
|
[[self.oskWindow window] setLevel:NSStatusWindowLevel];
|
|
[[self.oskWindow window] setTitle:self.oskWindowTitle];
|
|
[KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
|
|
}
|
|
|
|
- (void)showAboutWindow {
|
|
[self.aboutWindow.window centerInParent];
|
|
[self.aboutWindow.window makeKeyAndOrderFront:nil];
|
|
[self.aboutWindow.window setLevel:NSFloatingWindowLevel];
|
|
}
|
|
|
|
/*
|
|
* Wrappers for creating and destroying windows
|
|
* TODO: the side-effect of creating the window and its controller should
|
|
* probably be managed better.
|
|
*/
|
|
|
|
// TODO: rewrite confusing pattern, multiple methods differing only by underscore
|
|
- (NSWindowController *)aboutWindow_ {
|
|
return _aboutWindow;
|
|
}
|
|
|
|
- (NSWindowController *)aboutWindow {
|
|
if (_aboutWindow.window == nil) {
|
|
_aboutWindow = [[KMAboutWindowController alloc] initWithWindowNibName:@"KMAboutWindowController"];
|
|
[self observeCloseFor:_aboutWindow.window];
|
|
}
|
|
|
|
return _aboutWindow;
|
|
}
|
|
|
|
- (NSWindowController *)kbHelpWindow_ {
|
|
return _kbHelpWindow;
|
|
}
|
|
|
|
- (NSWindowController *)kbHelpWindow {
|
|
if (_kbHelpWindow.window == nil) {
|
|
_kbHelpWindow = [[KMKeyboardHelpWindowController alloc] initWithWindowNibName:@"KMKeyboardHelpWindowController"];
|
|
[self observeCloseFor:_kbHelpWindow.window];
|
|
}
|
|
|
|
return _kbHelpWindow;
|
|
}
|
|
|
|
/*
|
|
* Release windows after closing -- no need to keep them hanging about
|
|
*/
|
|
|
|
- (void)observeCloseFor:(NSWindow *)window {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(windowWillClose:)
|
|
name:NSWindowWillCloseNotification
|
|
object:window];
|
|
}
|
|
|
|
- (void)windowWillClose:(NSNotification *)notification {
|
|
NSWindow* window = notification.object;
|
|
if(window == _kbHelpWindow.window) {
|
|
_kbHelpWindow.window = nil;
|
|
}
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:window];
|
|
}
|
|
|
|
- (void)handleKeyEvent:(NSEvent *)event {
|
|
os_log_debug([KMLogs keyTraceLog], "handleKeyEvent: %{public}@", event);
|
|
|
|
NSArray *stackSymbols = [NSThread callStackSymbols];
|
|
os_log_debug([KMLogs keyTraceLog], "call stack: %{public}@", stackSymbols);
|
|
|
|
if (_oskWindow == nil)
|
|
return;
|
|
|
|
[_oskWindow.oskView handleKeyEvent:event];
|
|
}
|
|
|
|
- (NSEventModifierFlags) determineModifiers {
|
|
NSEventModifierFlags modifierFlags = 0;
|
|
|
|
if (self.receivedKeyDownFromOsk) {
|
|
/**
|
|
* the event was generated from the OSK, so use the oskEventModifiers in effect at time of event generation
|
|
*/
|
|
modifierFlags = self.oskEventModifiers;
|
|
os_log_debug([KMLogs eventsLog], "--- use modifiers from OSK, oskEventModifiers: 0x%lX", (unsigned long)modifierFlags);
|
|
self.oskEventModifiers = 0;
|
|
} else {
|
|
/**
|
|
* the event originated from the physical keyboard, use the modifiers on kCGEventFlagsChanged events
|
|
*/
|
|
NSEventModifierFlags originalModifiers = self.currentModifiers;
|
|
modifierFlags = [self.modifierMapping adjustModifiers:originalModifiers];
|
|
os_log_debug([KMLogs eventsLog], "--- use adjusted modifiers from current state: 0x%lX", (unsigned long)modifierFlags);
|
|
|
|
/**
|
|
* If the OSK is open, then we also need to apply its modifiers, if any, to this event originating from the physical keyboard.
|
|
* If the OSK is closed, its modifiers will be zero.
|
|
*/
|
|
NSEventModifierFlags oskModifiers = [self.oskWindow getOskEventModifierFlags];
|
|
if (oskModifiers != 0) {
|
|
os_log_debug([KMLogs eventsLog], "--- modifiers from OSK to apply to physical keyboard event: 0x%lX", (unsigned long)oskModifiers);
|
|
|
|
// combine osk modifiers with adjusted modifiers
|
|
modifierFlags = oskModifiers | modifierFlags;
|
|
os_log_debug([KMLogs eventsLog], "--- combined modifiers to apply to physical keyboard event: 0x%lX", (unsigned long)modifierFlags);
|
|
}
|
|
}
|
|
|
|
return modifierFlags;
|
|
}
|
|
|
|
extern const CGKeyCode kProcessPendingBuffer;
|
|
|
|
// This could more easily and logically be done in the input method, but this
|
|
// allows us to override the norma behavior for unit testing, where there is no
|
|
// active event loop to post to.
|
|
- (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent{
|
|
|
|
CGEventRef ev = CGEventCreateKeyboardEvent (source, virtualKey, true); //down
|
|
if (postEvent) {
|
|
os_log_info([KMLogs eventsLog], "postKeyboardEventWithSource, keycode: %d", virtualKey);
|
|
postEvent(ev);
|
|
}
|
|
CFRelease(ev);
|
|
if (virtualKey != kProcessPendingBuffer) { // special 0xFF code is not a real key-press, so no "up" is needed
|
|
ev = CGEventCreateKeyboardEvent (source, virtualKey, false); //up
|
|
if (postEvent)
|
|
postEvent(ev);
|
|
CFRelease(ev);
|
|
}
|
|
}
|
|
|
|
@end
|