From 256ba2ef1e7745ca39de4df08dc73046c9be174a Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 8 Aug 2019 19:36:27 +1000 Subject: [PATCH] [mac] Support Adobe products in legacy mode (and VS Code) --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 2 +- .../Keyman4MacIM/KMInputController.h | 2 +- .../Keyman4MacIM/KMInputController.m | 15 +- .../Keyman4MacIM/KMInputMethodAppDelegate.h | 2 +- .../Keyman4MacIM/KMInputMethodAppDelegate.m | 157 ++++++++++-------- .../Keyman4MacIM/KMInputMethodEventHandler.h | 3 +- .../Keyman4MacIM/KMInputMethodEventHandler.m | 151 ++++++++++------- .../KMInputMethodEventHandlerProtected.h | 3 + 8 files changed, 203 insertions(+), 132 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index ca81506249..817517383d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -390,9 +390,9 @@ 98A778C21A8C53BF00CF809D /* KMInputMethodAppDelegate.h */, 98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */, E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */, + E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, E24C79EC1FFFEA6B00D8E46F /* KMInputMethodEventHandlerProtected.h */, E22020F920050D6300B74FAC /* KMInputMethodBrowserClientEventHandlerProtected.h */, - E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */, E22020F3200505EF00B74FAC /* KMInputMethodSafariClientEventHandler.h */, E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h index c9d24102ea..5a31221350 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h @@ -9,10 +9,10 @@ #import #import #import -#import "KMInputMethodAppDelegate.h" @interface KMInputController : IMKInputController - (void)menuAction:(id)sender; +- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 201d411fc4..88b529e5a4 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -57,6 +57,16 @@ NSMutableArray *servers; return [_eventHandler handleEvent:event client:sender]; } +// Passthrough from the app delegate low level event hook +// to the input method event handler for Delete Back. +- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event { + if(_eventHandler != nil) { + return [_eventHandler handleDeleteBackLowLevel:event]; + } + + return NO; +} + - (void)activateServer:(id)sender { @synchronized(servers) { [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; @@ -84,7 +94,9 @@ NSMutableArray *servers; _eventHandler = [[KMInputMethodBrowserClientEventHandler alloc] init]; } else { - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId]; + // We cache the client for use with events sourced from the low level tap + // where we don't necessarily have any access to the current client. + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:sender]; } } } @@ -123,6 +135,7 @@ NSMutableArray *servers; } } + /* - (NSDictionary *)modes:(id)sender { if ([self.AppDelegate debugMode]) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index a7a2d63f34..ddd21dc522 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -72,7 +72,7 @@ extern NSString *const kWebSite; @property (nonatomic, strong) NSImage *keyboardIcon; @property (nonatomic, strong) NSAlert *downloadInfoView; @property (nonatomic, strong) NSProgressIndicator *progressIndicator; -@property (nonatomic, weak) IMKInputController *inputController; +@property (nonatomic, weak) KMInputController *inputController; @property (nonatomic, strong) NSWindowController *configWindow; @property (nonatomic, strong) NSWindowController *downloadKBWindow; @property (nonatomic, strong) KMAboutWindowController *aboutWindow; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 3d157512a1..89ecff808b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -81,20 +81,28 @@ id _lastServerWithOSKShowing = nil; andSelector:@selector(handleURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; - - self.lowLevelEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, CGEventMaskBit(kCGEventFlagsChanged) | CGEventMaskBit(kCGEventLeftMouseDown) | CGEventMaskBit(kCGEventLeftMouseUp), (CGEventTapCallBack)eventTapFunction, nil); - + + self.lowLevelEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionListenOnly, + CGEventMaskBit(kCGEventFlagsChanged) | + CGEventMaskBit(kCGEventLeftMouseDown) | + CGEventMaskBit(kCGEventLeftMouseUp) | + CGEventMaskBit(kCGEventKeyDown), + (CGEventTapCallBack)eventTapFunction, + nil); + if (!self.lowLevelEventTap) { NSLog(@"Can't tap into low level events!"); } else { CFRelease(self.lowLevelEventTap); } - + self.runLoopEventSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, self.lowLevelEventTap, 0); - + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - + if (self.runLoopEventSrc && runLoop) { CFRunLoopAddSource(runLoop, self.runLoopEventSrc, kCFRunLoopDefaultMode); } @@ -119,7 +127,7 @@ id _lastServerWithOSKShowing = nil; #endif - (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { - + [self processURL:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]]; } @@ -129,14 +137,14 @@ id _lastServerWithOSKShowing = nil; NSURL *url = [NSURL URLWithString:urlStr]; if (self.debugMode) NSLog(@"url = %@", url); - + if ([url.lastPathComponent isEqualToString:@"download"]) { if (_connection != nil) { if (self.debugMode) NSLog(@"Already downloading a keyboard."); return; } - + NSURL *downloadUrl; NSArray *params = [[url query] componentsSeparatedByString:@"&"]; for (NSString *value in params) { @@ -154,11 +162,11 @@ id _lastServerWithOSKShowing = nil; downloadUrl = [NSURL URLWithString:urlString]; } } - + if (downloadUrl && _downloadFilename) { if (_infoWindow.window != nil) [_infoWindow close]; - + [self.downloadInfoView setInformativeText:self.downloadFilename]; if (self.configWindow.window != nil) { [self.configWindow.window makeKeyAndOrderFront:nil]; @@ -251,13 +259,13 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef } 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. //if (appDelegate.debugMode) // NSLog(@"System Event: %@", sysEvent); - + switch (type) { case kCGEventFlagsChanged: appDelegate.currentModifierFlags = sysEvent.modifierFlags; @@ -265,13 +273,24 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef appDelegate.contextChangingEventDetected = YES; } break; - + case kCGEventLeftMouseUp: case kCGEventLeftMouseDown: case kCGEventOtherMouseUp: case kCGEventOtherMouseDown: appDelegate.contextChangingEventDetected = YES; break; + + case kCGEventKeyDown: + // Pass back delete events through to the input method event handler + // because some 'legacy' apps don't allow us to see back delete events + // that we have synthesized (and we need to see them, for serialization + // of events) + if(sysEvent.keyCode == kVK_Delete && appDelegate.inputController != nil) { + [appDelegate.inputController handleDeleteBackLowLevel:sysEvent]; + } + break; + default: break; } @@ -288,7 +307,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef _kme = [[KMEngine alloc] initWithKMX:nil contextBuffer:self.contextBuffer]; [_kme setDebugMode:self.debugMode]; } - + return _kme; } @@ -400,7 +419,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [fm createDirectoryAtPath:_keyboardsPath withIntermediateDirectories:YES attributes:nil error:nil]; } } - + return _keyboardsPath; } @@ -427,11 +446,11 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef } } } - + if (others != nil) [_kmxFileList addObject:others]; } - + return _kmxFileList; } @@ -447,7 +466,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef x++; } } - + return nil; } @@ -462,15 +481,15 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef x++; continue; } - + if (index >= x && index <= (x+pArray.count)) { packagePath = [[pArray objectAtIndex:0] stringByDeletingLastPathComponent]; break; } - + x += (pArray.count+1); } - + return packagePath; } @@ -487,7 +506,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef } } } - + return index; } @@ -497,12 +516,12 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSString *mPath = [NSString stringWithString:[path stringByDeletingLastPathComponent]]; if ([mPath isEqualToString:sourcePath]) return @"Others"; - + while (![mPath isEqualToString:sourcePath]) { packageFolder = [mPath lastPathComponent]; mPath = [mPath stringByDeletingLastPathComponent]; } - + return packageFolder; } @@ -517,17 +536,17 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef hasInfo = YES; continue; } - + if (hasInfo && [line startsWith:@"Name="]) { NSString *value = [[[line substringFromIndex:5] componentsSeparatedByString:@","] objectAtIndex:0]; packageName = [NSString stringWithString:[value stringByReplacingOccurrencesOfString:@"\"" withString:@""]]; break; } } - + if (packageName == nil) packageName = packageFolder; - + return packageName; } @@ -552,7 +571,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSMutableArray *files = [NSMutableArray arrayWithCapacity:0]; NSMutableArray *fonts = [NSMutableArray arrayWithCapacity:0]; NSMutableArray *kbs = [NSMutableArray arrayWithCapacity:0]; - + @try { NSString *fileContents = [[NSString stringWithContentsOfFile:infoFile encoding:NSUTF8StringEncoding error:NULL] stringByReplacingOccurrencesOfString:@"\r" withString:@""]; NSArray *lines = [fileContents componentsSeparatedByString:@"\n"]; @@ -560,7 +579,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef for (NSString *line in lines) { if (!line.length) continue; - + if ([line startsWith:kPackage]) { contentType = ctPackage; continue; @@ -585,14 +604,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef contentType = ctFiles; continue; } - + switch (contentType) { case ctPackage: { if ([line startsWith:kReadMeFile]) [infoDict setObject:[line substringFromIndex:kReadMeFile.length+1] forKey:kReadMeFile]; else if ([line startsWith:kGraphicFile]) [infoDict setObject:[line substringFromIndex:kGraphicFile.length+1] forKey:kGraphicFile]; - + break; } case ctButtons: @@ -607,7 +626,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; [infoDict setObject:@[v1, v2] forKey:kWelcome]; } - + break; } case ctInfo: { @@ -646,14 +665,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; [infoDict setObject:@[v1, v2] forKey:kWebSite]; } - + break; } case ctFiles: { NSUInteger x = [line rangeOfString:@"="].location; if (x == NSNotFound) continue; - + NSString *s = [line substringFromIndex:x+2]; if ([s startsWith:kFile]) { NSArray *vs = [s componentsSeparatedByString:@"\","]; @@ -673,7 +692,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSString *v2 = [[vs objectAtIndex:1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; [kbs addObject:@[v1, v2]]; } - + break; } default: @@ -685,14 +704,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSLog(@"Error = %@", e.description); return nil; } - + if (files.count) [infoDict setValue:files forKey:kFile]; if (fonts.count) [infoDict setValue:fonts forKey:kFont]; if (kbs.count) [infoDict setValue:kbs forKey:kKeyboard]; - + return infoDict.count?[NSDictionary dictionaryWithDictionary:infoDict]:nil; } @@ -701,7 +720,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; _selectedKeyboard = [userData objectForKey:kKMSelectedKeyboardKey]; } - + return _selectedKeyboard; } @@ -719,7 +738,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (!_activeKeyboards) _activeKeyboards = [[NSMutableArray alloc] initWithCapacity:0]; } - + return _activeKeyboards; } @@ -744,7 +763,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (![[NSFileManager defaultManager] fileExistsAtPath:path]) [pathsToRemove addObject:path]; } - + if (pathsToRemove.count > 0) { [self.activeKeyboards removeObjectsInArray:pathsToRemove]; NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; @@ -757,7 +776,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (_contextBuffer == nil) { _contextBuffer = [[NSMutableString alloc] initWithString:@""]; } - + return _contextBuffer; } @@ -770,15 +789,15 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef - (void)awakeFromNib { [self setKeyboardsSubMenu]; - + NSMenuItem *config = [self.menu itemWithTag:2]; if (config) [config setAction:@selector(menuAction:)]; - + NSMenuItem *osk = [self.menu itemWithTag:3]; if (osk) [osk setAction:@selector(menuAction:)]; - + NSMenuItem *about = [self.menu itemWithTag:4]; if (about) [about setAction:@selector(menuAction:)]; @@ -822,7 +841,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [keyboards.submenu addItem:item]; } - + if (keyboards.submenu.numberOfItems == 0) { NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"(None)" action:NULL keyEquivalent:@""]; [keyboards.submenu addItem:item]; @@ -868,7 +887,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if ([extension isEqualToString:@"kmx"]) [kmxFiles addObject:[path stringByAppendingPathComponent:filePath]]; } - + return kmxFiles; } @@ -881,7 +900,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if ([extension isEqualToString:@"kvk"]) [kvkFiles addObject:[self.keyboardsPath stringByAppendingPathComponent:filePath]]; } - + return kvkFiles; } @@ -894,14 +913,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef break; } } - + return kvkFilePath; } - (NSWindowController *)oskWindow { if (!_oskWindow) _oskWindow = [[OSKWindowController alloc] initWithWindowNibName:@"OSKWindowController"]; - + return _oskWindow; } @@ -943,7 +962,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (_aboutWindow.window == nil) { _aboutWindow = [[KMAboutWindowController alloc] initWithWindowNibName:@"KMAboutWindowController"]; } - + return _aboutWindow; } @@ -955,7 +974,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (_infoWindow.window == nil) { _infoWindow = [[KMInfoWindowController alloc] initWithWindowNibName:@"KMInfoWindowController"]; } - + return _infoWindow; } @@ -967,7 +986,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (_kbHelpWindow.window == nil) { _kbHelpWindow = [[KMKeyboardHelpWindowController alloc] initWithWindowNibName:@"KMKeyboardHelpWindowController"]; } - + return _kbHelpWindow; } @@ -979,7 +998,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (_downloadKBWindow.window == nil) { _downloadKBWindow = [[KMDownloadKBWindowController alloc] initWithWindowNibName:@"KMDownloadKBWindowController"]; } - + return _downloadKBWindow; } @@ -1003,11 +1022,11 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self.infoWindow.window makeKeyAndOrderFront:nil]; [self.infoWindow.window setLevel:NSFloatingWindowLevel]; } - + NSString *packagePath = [self.keyboardsPath stringByAppendingPathComponent:[self.downloadFilename stringByDeletingPathExtension]]; [self.infoWindow setPackagePath:packagePath]; } - + _downloadInfoView = nil; _connection = nil; _downloadFilename = nil; @@ -1024,7 +1043,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [_downloadInfoView setAlertStyle:NSInformationalAlertStyle]; [_downloadInfoView setAccessoryView:self.progressIndicator]; } - + return _downloadInfoView; } @@ -1036,7 +1055,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [_progressIndicator setMaxValue:100]; [_progressIndicator setDoubleValue:0]; } - + return _progressIndicator; } @@ -1095,7 +1114,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef - (void)handleKeyEvent:(NSEvent *)event { if (_oskWindow == nil) return; - + [_oskWindow.oskView handleKeyEvent:event]; } @@ -1105,12 +1124,12 @@ extern const CGKeyCode kProcessPendingBuffer; // 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) postEvent(ev); CFRelease(ev); - if (virtualKey != kProcessPendingBuffer) { // special 0xFF code is not a real key-press, so no "up" is needed + 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); @@ -1133,7 +1152,7 @@ extern const CGKeyCode kProcessPendingBuffer; didUnzip = [za UnzipFileTo:destFolder overWrite:YES]; [za UnzipCloseFile]; } - + if (didUnzip) { if (self.debugMode) NSLog(@"Unzipped file: %@", filePath); @@ -1151,7 +1170,7 @@ extern const CGKeyCode kProcessPendingBuffer; NSLog(@"Failed to unzip file: %@", filePath); } } - + return didUnzip; } @@ -1159,14 +1178,14 @@ extern const CGKeyCode kProcessPendingBuffer; if (_fontsPath == nil) { BOOL isDir; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); - + if (paths.count == 1) { NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"Fonts"]; if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] && isDir) _fontsPath = [NSString stringWithString:path]; } } - + return _fontsPath; } @@ -1174,17 +1193,17 @@ extern const CGKeyCode kProcessPendingBuffer; NSString *fontsPath = self.fontsPath; if (fontsPath == nil) return; - + NSArray *fonts = [self FontFilesAtPath:path]; for (NSString *srcPath in fonts) { NSString *destPath = [fontsPath stringByAppendingPathComponent:[srcPath lastPathComponent]]; NSError *error; if ([[NSFileManager defaultManager] fileExistsAtPath:destPath]) [[NSFileManager defaultManager] removeItemAtPath:destPath error:&error]; - + if (error == nil) [[NSFileManager defaultManager] copyItemAtPath:srcPath toPath:destPath error:&error]; - + if (error != nil) NSLog(@"Error = %@", error); } @@ -1199,7 +1218,7 @@ extern const CGKeyCode kProcessPendingBuffer; if ([extension isEqualToString:@"ttf"] || [extension isEqualToString:@"otf"]) [fontFiles addObject:[path stringByAppendingPathComponent:filePath]]; } - + return fontFiles; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h index 74328af088..10fa4338a0 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h @@ -14,8 +14,9 @@ @interface KMInputMethodEventHandler : NSObject -- (instancetype)initWithClient:(NSString *)clientAppId; +- (instancetype)initWithClient:(NSString *)clientAppId client:(id)sender; - (BOOL)handleEvent:(NSEvent *)event client:(id)sender; +- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event; - (void)deactivate; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 2e259331ee..5d4d3d68d2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -43,9 +43,10 @@ NSRange _previousSelRange; } // This is the public initializer. -- (instancetype)initWithClient:(NSString *)clientAppId { +- (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { + senderForDeleteBack = sender; // TODO: Pages and Keynote (and possibly lots of other undiscovered apps that are otherwise compliant - // with Apple's IM faramework) have a problem in that if the user selects a different font (or other + // with Apple's IM framework) have a problem in that if the user selects a different font (or other // formatting) and then types a sequence that causes characters to be added to the document and then // subsequently replaced, the replacement causes the formatting decision to be forgotten. This can be // "fixed" by treating them as legacy apps, but it causes other problems. @@ -58,10 +59,15 @@ NSRange _previousSelRange; [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || [clientAppId isEqual: @"com.microsoft.Word"] || [clientAppId isEqual: @"org.openoffice.script"] || + [clientAppId isEqual: @"com.adobe.illustrator"] || [clientAppId isEqual: @"com.adobe.InDesign"] || + [clientAppId isEqual: @"com.adobe.Photoshop"] || + [clientAppId isEqual: @"com.adobe.AfterEffects"] || + [clientAppId isEqual: @"com.microsoft.VSCode"] || + [clientAppId isEqual: @"com.google.Chrome"] || [clientAppId isEqual: @"com.Keyman.test.legacyInput"] /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */); - + // We used to default to NO, so these were the obvious exceptions. But then we realized that // in any app, command keys can change the selection, so now we default to YES, and only have // a few situations where we pretend it can't. This flag should probably be renamed to something @@ -71,7 +77,7 @@ NSRange _previousSelRange; // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { // _clientSelectionCanChangeUnexpectedly = YES; // } - + // In Xcode, if Keyman is the active IM and is in "debugMode" and "English plus Spanish" is the current keyboard and you type "Crashlytics force now", it will force a simulated crash to test reporting to fabric.io. if ([self.AppDelegate debugMode] && [clientAppId isEqual: @"com.apple.dt.Xcode"]) { NSLog(@"Crashlytics - Preparing to detect Easter egg."); @@ -79,7 +85,7 @@ NSRange _previousSelRange; } else _easterEggForCrashlytics = nil; - + // For the Atom editor, this isn't really true (the context CAN change unexpectedly), but we can't get // the context, so we pretend/hope it won't. BOOL selectionCanChangeUnexpectedly = (![clientAppId isEqual: @"com.github.atom"]); @@ -148,14 +154,14 @@ NSRange _previousSelRange; return; } NSString* text = [self pendingBuffer]; - + if ([self.AppDelegate debugMode]) NSLog(@"Inserting text from pending buffer: \"%@\"", text); - + [client insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; _previousSelRange.location += text.length; _previousSelRange.length = 0; - + [self setPendingBuffer:@""]; } @@ -220,9 +226,9 @@ NSRange _previousSelRange; NSLog(@"*** updateContextBuffer ***"); NSLog(@"sender: %@", sender); } - + NSRange selRange = [self getSelectionRangefromClient: sender]; - + // Since client can't tell us the actual current position, we assume the previously known location in the context. // (It won't matter anyway if the client also fails to report its context.) NSUInteger len = (_clientCanProvideSelectionInfo != Yes) ? _previousSelRange.length : selRange.location; @@ -243,7 +249,7 @@ NSRange _previousSelRange; -(NSString *)getLimitedContextFrom:(id)sender at:(NSUInteger) len { if (![sender respondsToSelector:@selector(attributedSubstringFromRange:)]) return nil; - + NSUInteger start = 0; if (len > kMaxContext) { if ([self.AppDelegate debugMode]) @@ -251,7 +257,7 @@ NSRange _previousSelRange; start = len - kMaxContext; len = kMaxContext; } - + NSString *preBuffer = [[sender attributedSubstringFromRange:NSMakeRange(start, len)] string]; if ([self.AppDelegate debugMode]) { NSLog(@"preBuffer = \"%@\"", preBuffer ? preBuffer : @"nil"); @@ -260,7 +266,7 @@ NSRange _previousSelRange; else NSLog(@"preBuffer has a length of 0"); } - + return preBuffer; } @@ -301,7 +307,7 @@ NSRange _previousSelRange; _contextOutOfDate = YES; self.AppDelegate.contextChangingEventDetected = NO; } - + if (_contextOutOfDate) [self updateContextBuffer:client]; } @@ -347,7 +353,7 @@ NSRange _previousSelRange; } else return NO; - + if ([self.AppDelegate debugMode]) { NSLog(@"actions = %@", actions); } @@ -357,7 +363,7 @@ NSRange _previousSelRange; NSLog(@"Handling %@ action...", actionType); NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); } - + if ([actionType isEqualToString:Q_STR]) { NSString *output = [action objectForKey:actionType]; if ([self.AppDelegate debugMode]) @@ -390,7 +396,7 @@ NSRange _previousSelRange; [self replaceExistingSelectionIn:sender with:output]; } } - + // Even if the characters to insert are pending, we want to append them to the context buffer now. // Waiting until they are inserted would probably be safe, but on the off-chance that the engine // generates additional actions beyond this current one, we want to be sure that the context reflects @@ -406,7 +412,7 @@ NSRange _previousSelRange; // (which could include deadkeys the client doesn't know about). [self.contextBuffer deleteLastNChars:n]; n -= dc; - + // n is now the number of characters to delete from the client. if (n > 0) { deleteBackPosted = [self deleteBack:n in:sender for: event]; @@ -441,7 +447,7 @@ NSRange _previousSelRange; NSLog(@"Processing an unhandled delete-back..."); NSLog(@"_numberOfPostedDeletesToExpect = %lu", _numberOfPostedDeletesToExpect); } - + // If we have pending characters to insert following the delete-back, then // the context buffer has already been properly set to reflect the deletions. if ((_legacyMode && (_pendingBuffer == nil || _pendingBuffer.length == 0)) || @@ -460,7 +466,7 @@ NSRange _previousSelRange; if (--_numberOfPostedDeletesToExpect == 0) { if ([self.AppDelegate debugMode]) NSLog(@"Processing final posted delete-back..."); - + self.willDeleteNullChar = NO; if (_legacyMode) { if (_pendingBuffer != nil && _pendingBuffer.length > 0) { @@ -491,6 +497,29 @@ NSRange _previousSelRange; } } +// handleDeleteBackLowLevel: handles the situation for Delete Back for +// some 'legacy' mode apps such as Adobe apps, because when we inject +// the Delete Back, it is passed through to the app but we never see it +// in our normal handleEvent function. However, other apps, such as Word, +// show the event both in the low level tap and in the normal IM +// handleEvent. Thus, we set a flag after processing a Delete Back here so +// that we don't accidentally process it twice. Note that a Delete Back that +// we handle here should never be passed on to Keyman Engine for transform, +// as it will be part of the output from the transform. +- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event { + ignoreNextDeleteBackHighLevel = NO; + if(event.keyCode == kVK_Delete && _legacyMode && [self pendingBuffer].length > 0) { + BOOL updateEngineContext = YES; + if ([self.AppDelegate debugMode]) { + NSLog(@"legacy: delete-back received, processing"); + } + [self processUnhandledDeleteBack:self.senderForDeleteBack updateEngineContext: &updateEngineContext]; + ignoreNextDeleteBackHighLevel = YES; + } + + return ignoreNextDeleteBackHighLevel; +} + - (BOOL)handleEvent:(NSEvent *)event client:(id)sender { // OSK key feedback from hardware keyboard is disabled /*if (event.type == NSKeyDown) @@ -510,7 +539,7 @@ NSRange _previousSelRange; { if ([self.AppDelegate debugMode]) { NSLog(@"Processing the special %hu code", kProcessPendingBuffer); - + NSUInteger length = [self pendingBuffer].length; if (length > 0) { for (NSUInteger ich = 0; ich < length; ich++) @@ -521,19 +550,26 @@ NSRange _previousSelRange; [self insertPendingBufferTextIn:sender]; return YES; } - + + if(event.keyCode == kVK_Delete && _legacyMode && ignoreNextDeleteBackHighLevel) { + // This event was sent by Keyman and we should just + // pass it through to the app. handleDeleteBackLowLevel + // already did anything we need with it. + return NO; // We'll let the client app accept the Delete Back + } + [self checkContextIn:sender]; [self updateContextBufferIfNeeded:sender]; - + if ([self.AppDelegate debugMode]) { NSLog(@"sender type = %@", NSStringFromClass([sender class])); if (_clientCanProvideSelectionInfo == Yes) NSLog(@"sender selection range location = %lu", [self getSelectionRangefromClient:sender].location); } - + BOOL handled = [self handleKeymanEngineActions:event in: sender]; - + if (!handled) { NSUInteger nc = [self.contextBuffer deleteLastNullChars]; if (nc > 0) { @@ -544,10 +580,10 @@ NSRange _previousSelRange; self.willDeleteNullChar = YES; [self postDeleteBacks:nc for:event]; _keyCodeOfOriginalEvent = event.keyCode; - + return YES; } - + // For other events that the Keyman engine does not have rules, just apply context changes // and let client handle the event NSString* charactersToAppend = nil; @@ -555,9 +591,9 @@ NSRange _previousSelRange; unsigned short keyCode = event.keyCode; switch (keyCode) { case kVK_Delete: - [self processUnhandledDeleteBack:sender updateEngineContext:&updateEngineContext]; + [self processUnhandledDeleteBack: sender updateEngineContext: &updateEngineContext]; break; - + case kVK_LeftArrow: // I had started some code to try to guess where in the context we ended up after a left arrow, // but too many potential pitfalls. Marc says it's better to just let it be dumb. @@ -571,12 +607,12 @@ NSRange _previousSelRange; _contextOutOfDate = YES; updateEngineContext = NO; break; - + case kVK_Return: case kVK_ANSI_KeypadEnter: charactersToAppend = @"\n"; break; - + default: { // NOTE: Although ch is usually the same as keyCode, when the option key is depressed (and @@ -602,12 +638,12 @@ NSRange _previousSelRange; _previousSelRange.length = 0; } } - + if (updateEngineContext) { [self.kme setContextBuffer:self.contextBuffer]; } } - + if ([self.AppDelegate debugMode]) { if (_contextOutOfDate) NSLog(@"Context now out of date."); @@ -620,7 +656,7 @@ NSRange _previousSelRange; } NSLog(@"***"); } - + // Note: Although this would seem to be the obvious place to set _previousSelRange (in legacy mode), we can't // because the selection range doesn't get updated until after we return from this method. return handled; @@ -635,7 +671,7 @@ NSRange _previousSelRange; [self deleteBack:n at: pos in: client]; if (_legacyMode) return [self deleteBackLegacy:n at: pos with: selectedRange for: event]; - + return NO; } @@ -644,13 +680,13 @@ NSRange _previousSelRange; NSLog(@"Using Apple IM-compliant mode."); NSLog(@"pos = %lu", pos); } - + if (pos >= n && pos != NSNotFound) { NSInteger preCharPos = pos - (n+1); if ((preCharPos) >= 0) { NSUInteger nbrOfPreCharacters; NSString *preChar = nil; - + // This regex will look back through the context until it finds a *known* base // character because some (non-legacy) apps (e.g., Mail) do not properly handle sending // combining marks on their own via insertText. One potentially negative implication @@ -666,7 +702,7 @@ NSRange _previousSelRange; // which always used just a single character regardless of its class. NSError *error = NULL; NSRegularExpression *regexNonCombiningMark = [NSRegularExpression regularExpressionWithPattern:@"\\P{M}" options:NSRegularExpressionCaseInsensitive error:&error]; - + for (nbrOfPreCharacters = 1; YES; nbrOfPreCharacters++, preCharPos--) { if ([client respondsToSelector:@selector(attributedSubstringFromRange:)]) preChar = [[client attributedSubstringFromRange:NSMakeRange(preCharPos, nbrOfPreCharacters)] string]; @@ -682,7 +718,7 @@ NSRange _previousSelRange; } if ([self.AppDelegate debugMode]) NSLog(@"Testing preChar: %@", preChar); - + if ([regexNonCombiningMark numberOfMatchesInString:preChar options:NSMatchingAnchored range:NSMakeRange(0, 1)] > 0) break; if (preCharPos == 0) { @@ -721,12 +757,12 @@ NSRange _previousSelRange; if (self.contextBuffer != nil && (pos == 0 || pos == NSNotFound)) { pos = self.contextBuffer.length + n; } - + if ([self.AppDelegate debugMode]) { NSLog(@"Using Legacy mode."); NSLog(@"pos = %lu", pos); } - + if (pos >= n) { // n is now the number of delete-backs we need to post (plus one more if there is selected text) if ([self.AppDelegate debugMode]) { @@ -734,21 +770,20 @@ NSRange _previousSelRange; if (_clientCanProvideSelectionInfo == No || _clientCanProvideSelectionInfo == Unreliable) NSLog(@"Cannot trust client to report accurate selection length - assuming no selection."); } - + + if (_pendingBuffer != nil && [[self pendingBuffer] length] > 0) { + // We shouldn't be sending out characters before the corresponding Delete Back events are received + // if this does happen, that's unexpected... + NSLog(@"Legacy mode: ERROR: did not find expected Delete Back event"); + } + // Note: If pos is "not found", most likely the client can't accurately report the location. This might be // dangerous, but for now let's go ahead and attempt to delete the characters we think should be there. - if (_pendingBuffer != nil && [[self pendingBuffer] length] > 0) { - NSException* exception = [NSException - exceptionWithName:@"InvalidOperationException" - reason:@"Cannot process subsequent Q_BACK after Q_STR" - userInfo:nil]; - @throw exception; - } - + if (_clientCanProvideSelectionInfo == Yes && selectedRange.length > 0) n++; // First delete-back will delete the existing selection. [self postDeleteBacks:n for:event]; - + CFRelease(_sourceFromOriginalEvent); _sourceFromOriginalEvent = nil; return YES; @@ -759,14 +794,14 @@ NSRange _previousSelRange; - (void)sendEvent:(NSEvent *)event { ProcessSerialNumber psn; GetFrontProcess(&psn); - + CGEventSourceRef source = CGEventCreateSourceFromEvent([event CGEvent]); CGEventRef keyDownEvent = CGEventCreateKeyboardEvent(source, event.keyCode, true); CGEventRef keyUpEvent = CGEventCreateKeyboardEvent(source, event.keyCode, false); - + CGEventPostToPSN(&psn, keyDownEvent); CGEventPostToPSN(&psn, keyUpEvent); - + CFRelease(source); CFRelease(keyDownEvent); CFRelease(keyUpEvent); @@ -774,9 +809,9 @@ NSRange _previousSelRange; - (void)postDeleteBacks:(NSUInteger)count for:(NSEvent *) event { _numberOfPostedDeletesToExpect = count; - + _sourceFromOriginalEvent = CGEventCreateSourceFromEvent([event CGEvent]); - + for (int db = 0; db < count; db++) { if ([self.AppDelegate debugMode]) { @@ -795,7 +830,7 @@ NSRange _previousSelRange; - (void)postKeyPressToFrontProcess:(CGKeyCode)code from:(CGEventSourceRef) source { ProcessSerialNumber psn; GetFrontProcess(&psn); - + if ([self.AppDelegate debugMode]) { if (code == kProcessPendingBuffer) { NSLog(@"Posting code to tell Keyman to process characters in pending buffer."); @@ -804,7 +839,7 @@ NSRange _previousSelRange; NSLog(@"Posting a keypress (down/up) to the 'front process' for the %hu key.", code); } } - + [self.AppDelegate postKeyboardEventWithSource:source code:code postCallback:^(CGEventRef eventToPost) { CGEventPostToPSN((ProcessSerialNumberPtr)&psn, eventToPost); }]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h index 5ce208947e..2023262b7c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h @@ -34,6 +34,9 @@ typedef NS_ENUM(NSInteger, ClientCapability) { @property (assign) BOOL clientSelectionCanChangeUnexpectedly; // REVIEW: Maybe we can get notification from these clients by handling mouseDownOnCharacterIndex. @property (assign) ClientCapability clientCanProvideSelectionInfo; +@property id senderForDeleteBack; +@property BOOL ignoreNextDeleteBackHighLevel; + - (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly; - (void)handleCommand:(NSEvent *)event; // This just sets the legacyMode property to true and spits out a debug message to that effect.