From d95599631e908efb89d86ca6abd102a5f6d5c161 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Wed, 29 Nov 2017 23:43:44 -0500 Subject: [PATCH 01/13] Added files to hold input method event handler class --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 10 +++++++++- .../Keyman4MacIM/KMInputMethodEventHandler.h | 15 +++++++++++++++ .../Keyman4MacIM/KMInputMethodEventHandler.m | 9 +++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 054ba75f52..fdfef81962 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ 98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FE105D1B4DE8F400525F54 /* NSWindow+SuppMethods.m */; }; 98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */; }; 98FE10641B4DEE5600525F54 /* KMInfoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98FE10621B4DEE5600525F54 /* KMInfoWindowController.xib */; }; + E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; + E21799061FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -145,6 +147,8 @@ 98FE10601B4DEE5600525F54 /* KMInfoWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KMInfoWindowController.h; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.h; sourceTree = ""; }; 98FE10611B4DEE5600525F54 /* KMInfoWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KMInfoWindowController.m; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.m; sourceTree = ""; }; 98FE10621B4DEE5600525F54 /* KMInfoWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = KMInfoWindowController.xib; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.xib; sourceTree = ""; }; + E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = ""; }; + E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -266,6 +270,7 @@ 98D6DA791A799EE700B09822 /* Frameworks */, 989C9C121A7876DE00A20425 /* Images.xcassets */, 98BDD3681BC3511200FAC7C4 /* Keyman4MacIM.entitlements */, + 98BF92401BF01CBE0002126A /* KMAboutWindow */, 983246EC1A9E8E860010B90C /* KMConfiguration */, 9897BD8E1B099A2400C3F58A /* KMDownloadKeyboard */, 98FE105F1B4DEE1100525F54 /* KMInfoWindow */, @@ -273,8 +278,9 @@ 98D6DA7D1A799FF400B09822 /* KMInputController.m */, 98A778C21A8C53BF00CF809D /* KMInputMethodAppDelegate.h */, 98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */, + E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */, + E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, 98E672961B532DE000DBDE2F /* KMKeyboardHelpWindow */, - 98BF92401BF01CBE0002126A /* KMAboutWindow */, 989C9C141A7876DE00A20425 /* MainMenu.xib */, 984B8F3E1AF1C2F400E096A8 /* OnScreenKeyboard */, 989C9C0B1A7876DE00A20425 /* Supporting Files */, @@ -521,6 +527,7 @@ 984B8F441AF1C3D900E096A8 /* OSKWindowController.m in Sources */, 9836B3711AE5F11D00780482 /* mztools.c in Sources */, 9836B3701AE5F11D00780482 /* ioapi.c in Sources */, + E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */, 98E6729F1B532F5E00DBDE2F /* KMDownloadKBWindowController.m in Sources */, 98E672A41B532F9900DBDE2F /* KMKeyboardHelpWindowController.m in Sources */, 98A778C41A8C53BF00CF809D /* KMInputMethodAppDelegate.m in Sources */, @@ -543,6 +550,7 @@ buildActionMask = 2147483647; files = ( 989C9C221A7876DE00A20425 /* Keyman4MacIMTests.m in Sources */, + E21799061FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h new file mode 100644 index 0000000000..0dfe7c2660 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h @@ -0,0 +1,15 @@ +// +// KMInputMethodEventHandler.h +// Keyman4MacIM +// +// Created by Tom Bogle on 11/22/17. +// Copyright © 2017 SIL International. All rights reserved. +// + +#import + +#ifndef KMInputMethodEventHandler_h +#define KMInputMethodEventHandler_h + + +#endif /* KMInputMethodEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m new file mode 100644 index 0000000000..811ad71263 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -0,0 +1,9 @@ +// +// KMInputMethodEventHandler.m +// Keyman4MacIM +// +// Created by Tom Bogle on 11/22/17. +// Copyright © 2017 SIL International. All rights reserved. +// +#import "KMInputController.h" +#include /* For kVK_ constants. */ From 4c7b2952f23d31021d105438f778f626f93bc49d Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Fri, 5 Jan 2018 09:52:35 -0500 Subject: [PATCH 02/13] Refactor step 1: move event handling code to new class --- .../Keyman4MacIM/KMInputController.m | 825 +---------------- .../Keyman4MacIM/KMInputMethodEventHandler.h | 6 + .../Keyman4MacIM/KMInputMethodEventHandler.m | 833 +++++++++++++++++- 3 files changed, 857 insertions(+), 807 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 2e6bb9bbc3..db2d985b35 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -7,48 +7,18 @@ // #import "KMInputController.h" +#import "KMInputMethodEventHandler.h" #include /* For kVK_ constants. */ -@interface KMInputController () -@property (nonatomic, strong) NSMutableDictionary *kbData; +//@interface KMInputController () +//@property (nonatomic, strong) NSMutableDictionary *kbData; //@property (nonatomic, strong) NSDictionary *kmModes; -@property (assign) BOOL willDeleteNullChar; -@end +//@property (assign) BOOL willDeleteNullChar; +//@end @implementation KMInputController -const CGKeyCode kProcessPendingBuffer = 0xFF; - -//_pendingBuffer contains text that is ready to be sent to the client when all delete-backs are finished. -NSMutableString* _pendingBuffer; -NSUInteger _numberOfPostedDeletesToExpect = 0; -CGKeyCode _keyCodeOfOriginalEvent; -CGEventSourceRef _sourceFromOriginalEvent = nil; -BOOL _contextOutOfDate = YES; - -// This flag indicates which mode is being used for replacing already typed text when composing characters -// (i.e., when not using deadkeys). Some apps (and some javascript-based websites, such as Google Docs) do -// not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" -// apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now -// ready to insert the composed text. -BOOL _legacyMode = NO; -// Some clients (e.g. Chrome) handle the mouse down events before we get a crack at them. For such apps that -// are able to report their current selection location (LibreOffice can't even do that!), we can do some -// checking at the start of the event processing to see if we're probably still in the same place where we -// left off previously. -BOOL _clientSelectionCanChangeUnexpectedly = YES; // REVIEW: Maybe we can get notification from these clients by handling mouseDownOnCharacterIndex. -BOOL _insertCharactersIndividually = NO; -// Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox), we want to -// try to detect it and: -// in Safari, switch to legacy mode -// in Chrome, NOT assume that it needs to re-get the context every time around (which means that if the user -// does mouse-click somewhere else, it could lead to bad behaviour). -// in Firefox, we're already in legacy mode and we do get mouse clicks, so we're already doing the best we can. -NSUInteger _failuresToRetrieveExpectedContext = NSUIntegerMax; -BOOL _forceRemoveSelectionInGoogleDocs = NO; -BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; -BOOL _cannnotTrustSelectionLength = NO; -NSRange _previousSelRange; +KMInputMethodEventHandler* _eventHandler; - (KMInputMethodAppDelegate *)AppDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; @@ -74,648 +44,20 @@ NSRange _previousSelRange; if ([self.AppDelegate debugMode]) NSLog(@"Event = %@", event); - if (event == nil || sender == nil || self.kmx == nil) + if (event == nil || sender == nil || self.kmx == nil || _eventHandler == nil) return NO; // Not sure this can ever happen. - // OSK key feedback from hardware keyboard is disabled - /*if (event.type == NSKeyDown) - [self.AppDelegate handleKeyEvent:event];*/ - - if (event.type == NSLeftMouseDown || event.type == NSLeftMouseUp ) { -// if (_clientSelectionCanChangeUnexpectedly) { -// if ([self.AppDelegate debugMode]) -// NSLog(@"WARNING: We are dealing with an app/context where we THINK we shouldn't be getting mouse events, but we just got one!"); -// _clientSelectionCanChangeUnexpectedly = NO; -// } - _contextOutOfDate = YES; - return NO; - } - else if (event.type != NSKeyDown) - return NO; // We ignore NSLeftMouseDragged events (because we'll eventually get a mouse-up). - else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { - // There are a bunch of common navigation/selection command-key combinations, but individual - // apps may implement specifc commands that also chnage the selection. There is probably no - // situation where a user could reasonably hope that any dead-keys typed before using a - // command shortcut would be remembered, so other than an insignificant performance penalty, - // the only downside to treating all commands as having the potential to change the selection - // is that some "legacy" apps can't get their context at all. - if ([self.AppDelegate debugMode]) - NSLog(@"Command key - context needs to be re-gotten."); - _contextOutOfDate = YES; - return NO; // We let the client app handle all Command-key events. - } - - if (_legacyMode && event.keyCode == kProcessPendingBuffer) - { - if ([self.AppDelegate debugMode]) - NSLog(@"Processing the special %hu code", kProcessPendingBuffer); - - NSUInteger length = [self pendingBuffer].length; - if (length > 0) { - if ([self.AppDelegate debugMode]) { - for (NSUInteger ich = 0; ich < length; ich++) - NSLog(@"Char %li: '%x'", ich, [[self pendingBuffer] characterAtIndex:ich]); - } - NSString* text; - if (length > 1 && _insertCharactersIndividually) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Using special Google Docs in Chrome logic"); - } - unichar chars[1]; - chars[0] = [[self pendingBuffer] characterAtIndex:0]; - text = [[NSString alloc] initWithCharacters:chars length:1]; - } - else { - text = [self pendingBuffer]; - } - - if ([self.AppDelegate debugMode]) - NSLog(@"Inserting text from pending buffer: \"%@\"", text); - - [sender insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _previousSelRange.location += text.length; - _previousSelRange.length = 0; - - if (length > 1 && _insertCharactersIndividually) { - // Come back for more... - [_pendingBuffer deleteCharactersInRange:NSMakeRange(0, 1)]; - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:sender afterDelay:0.1]; - } - else { - [self setPendingBuffer:@""]; - } - } - else { - if ([self.AppDelegate debugMode]) - NSLog(@"Error - expected text in pending buffer!"); - } - return YES; - } - - if (!self.willDeleteNullChar && !_contextOutOfDate && _failuresToRetrieveExpectedContext < 3) { - if ([self AppDelegate].debugMode) { - NSLog(@"Checking to see if we're in Google Docs (or some other site that can't give context)."); - } - NSUInteger bufferLength = self.contextBuffer.length; - if (bufferLength) { - NSUInteger location = [sender selectedRange].location; - - if (location != NSNotFound && location > 0) { - NSString *clientContext = [[sender attributedSubstringFromRange:NSMakeRange(0, location)] string]; - if (clientContext == nil || !clientContext.length || - [clientContext characterAtIndex:clientContext.length - 1] != - [self.contextBuffer characterAtIndex:bufferLength - 1]) { - _failuresToRetrieveExpectedContext++; - } - else { - if ([self AppDelegate].debugMode) { - NSLog(@"We got what we were expecting from the client. We can stop checking."); - } - _failuresToRetrieveExpectedContext = NSUIntegerMax; - } - } - else { - _failuresToRetrieveExpectedContext++; - } - - if (_failuresToRetrieveExpectedContext == 3) - { - if ([self AppDelegate].debugMode) { - NSLog(@"Detected Google Docs or some other editor that can't provide context. Using legacy mode."); - } - _failuresToRetrieveExpectedContext = NSUIntegerMax; - _legacyMode = YES; - if (_clientSelectionCanChangeUnexpectedly) { - _cannnotTrustSelectionLength = YES; - _clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. - // Google docs in Chrome allows only a single character at a time :-( - _insertCharactersIndividually = YES; - } - } - } - } - else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) - return NO; // We ignore any Command-key events. - - if ([self.AppDelegate debugMode]) { - if (_clientSelectionCanChangeUnexpectedly) - NSLog(@"_clientSelectionCanChangeUnexpectedly = YES"); - else - NSLog(@"_clientSelectionCanChangeUnexpectedly = NO"); - } - // The following is needed because some clients (e.g. Chrome & Terminal) handle mouse down events and we - // never get a crack at them: - // At the start of the loop before asking the Engine to process the event, unless we still have - // pending work to do based on posted deletes or the special kProcessPendingBuffer code, check to see if - // the client still reports a selection that matches the position we would expect based on the length of - // the context buffer. If not, then reset the context buffer. - if (_clientSelectionCanChangeUnexpectedly && _numberOfPostedDeletesToExpect == 0 && - (_pendingBuffer == nil || _pendingBuffer.length == 0)) { - NSRange currentSelRange = [sender selectedRange]; - - if (currentSelRange.location == NSNotFound) { - _clientSelectionCanChangeUnexpectedly = NO; - } - else if ((_previousSelRange.location != currentSelRange.location || _cannnotTrustSelectionLength || _previousSelRange.length != currentSelRange.length)) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Client selection may have changed since context was set. Resetting context..."); - NSLog(@" _previousSelRange.location = %lu", _previousSelRange.location); - NSLog(@" _previousSelRange.length = %lu", _previousSelRange.length); - NSLog(@" currentSelRange.location = %lu", currentSelRange.location); - if (_cannnotTrustSelectionLength) - NSLog(@"The following cannot be trusted and will be ignored:"); - NSLog(@" currentSelRange.length = %lu", currentSelRange.length); - } - [self updateContextBuffer:sender]; - } - } - - if (_contextOutOfDate) - [self updateContextBuffer:sender]; - - BOOL handled = NO; - BOOL deleteBackPosted = NO; - NSArray *actions = nil; - if (!self.willDeleteNullChar) { - actions = [self.kme processEvent:event]; - } - - if ([self.AppDelegate debugMode]) { - NSLog(@"sender type = %@", NSStringFromClass([sender class])); - NSLog(@"sender selection range location = %lu", [sender selectedRange].location); - NSLog(@"actions = %@", actions); - } - for (NSDictionary *action in actions) { - NSString *actionType = [[action allKeys] objectAtIndex:0]; - if ([actionType isEqualToString:Q_STR]) { - if ([self.AppDelegate debugMode]) - NSLog(@"About to start handling Q_STR action..."); - - NSString *output = [action objectForKey:actionType]; - if ([self.AppDelegate debugMode]) - NSLog(@"output = %@", output); - if (deleteBackPosted) - { - [self appendPendingBuffer:output]; - if ([self.AppDelegate debugMode]) - NSLog(@"pendingBuffer = %@", [self pendingBuffer]); - } - else { - NSUInteger nc = [self.contextBuffer deleteLastNullChars]; - // Each null in the context buffer presumably corresponds to a space we inserted when - // processing the Q_BACK, which we now need to replace with the text we're inserting. - if (nc > 0) { - if ([self.AppDelegate debugMode]) - NSLog(@"nc = %lu", nc); - NSRange selRange = [sender selectedRange]; - NSUInteger pos = selRange.location; - if (pos >= nc && pos != NSNotFound) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Replacement index = %lu", pos - nc); - NSLog(@"Replacement length = %lu", nc); - } - [sender insertText:output replacementRange:NSMakeRange(pos - nc, nc)]; - _previousSelRange.location += output.length - nc; - _previousSelRange.length = 0; - } - } - else { - // The following commented out code is the code that would make it possible to type over an - // existing selection in Word in Safari, but I don't know of a way to distinguish that case - // from other contexts in Safari (wherein this code inserts an extra leading space). -// if (_explicitlyDeleteExistingSelectionBeforeInserting) { -// NSUInteger selLength = [sender selectedRange].length; -// if (selLength > 0 && selLength != NSNotFound) { -// if ([self.AppDelegate debugMode]) { -// NSLog(@"Attempting to delete existing selection of length = %lu by replacing it with a space (which should not actually appear in the text).", selLength); -// } -// -// [sender insertText:@" " replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; -// -// if ([self.AppDelegate debugMode]) -// NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); -// -// CGEventSourceRef sourceFromEvent = CGEventCreateSourceFromEvent([event CGEvent]); -// [self postKeyPressToFrontProcess:event.keyCode from:sourceFromEvent]; -// CFRelease(sourceFromEvent); -// return YES; -// } -// } - [sender insertText:output replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _previousSelRange.location += output.length; - _previousSelRange.length = 0; - - // In Google Docs in Safari when there is an existing selection, the inserted characters - // stays selected. The following clears the selection. - if (_legacyMode && _forceRemoveSelectionInGoogleDocs) { - if ([self.AppDelegate debugMode]) - NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); - ProcessSerialNumber psn; - GetFrontProcess(&psn); - - CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, true); - //set shift and command keys down for above event - CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); - CGEventPostToPSN(&psn, event); - CFRelease(event); - - event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, false); - CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); - CGEventPostToPSN(&psn, event); - CFRelease(event); - } - } - } - - // 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 - // the state as it *will* be when all the posted/pending events have been processed. - [self.contextBuffer appendString:output]; - } - else if ([actionType isEqualToString:Q_BACK]) { - if ([self.AppDelegate debugMode]) - { - NSLog(@"About to start handling Q_BACK action..."); - NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); - } - [self.contextBuffer deleteLastNullChars]; - NSUInteger dk = [self.contextBuffer deleteLastDeadkeys]; - NSInteger n = [[action objectForKey:actionType] integerValue] - dk; - NSUInteger dc = [[self.contextBuffer lastNChars:n] deadKeyCount]; - // n is now the number of characters to delete from the context buffer - // (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) { - if ([self.AppDelegate debugMode]) - { - NSLog(@"Q_BACK"); - NSLog(@"dk = %lu", dk); - NSLog(@"n = %li", n); - NSLog(@"dc = %lu", dc); - if (_legacyMode) - NSLog(@"Using Legacy mode."); - else - NSLog(@"Using Apple IM-compliant mode."); - } - NSRange selectedRange = [sender selectedRange]; - NSInteger pos = selectedRange.location; - if (_legacyMode && self.contextBuffer != nil && (pos == 0 || pos == NSNotFound)) { - pos = self.contextBuffer.length + n; - } - if ([self.AppDelegate debugMode]) - NSLog(@"pos = %lu", pos); - - if (!_legacyMode && 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 - // comining marks on there own via insertText. One potentially negative implication - // of this is that if the script should happen to contain characters whose class is - // not known, it will skip over them and keep looking, so it could end up using a - // longer string of characters than otherwise necessary. This could result in a - // mildly jarring visual experience for the user if the app refreshes the diplay - // between the time the characters are removed and re-inserted. But presumbly this - // algorithm will eventually find either a known base character or get all the way back - // to the start of the context, so if it doesn't find a known base character, it will - // fall back to just attempting the insert with whatever it does find. I believe this - // will always work and should at least work as reliably as the old version of the code, - // 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--) { - preChar = [[sender attributedSubstringFromRange:NSMakeRange(preCharPos, nbrOfPreCharacters)] string]; - if (!preChar) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Client apparently doesn't implement attributedSubstringFromRange. Attempting to get preChar from context..."); - } - if (self.contextBuffer != nil && preCharPos < self.contextBuffer.length) { - preChar = [self.contextBuffer substringWithRange:NSMakeRange(preCharPos, 1)]; - } - if (!preChar) - break; - } - if ([self.AppDelegate debugMode]) - NSLog(@"Testing preChar: %@", preChar); - - if ([regexNonCombiningMark numberOfMatchesInString:preChar options:NSMatchingAnchored range:NSMakeRange(0, 1)] > 0) - break; - if (preCharPos == 0) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Failed to find a base character!"); - } - break; - } - if ([self.AppDelegate debugMode]) { - NSLog(@"Have not yet found a base character. nbrOfPreCharacters = %lu", nbrOfPreCharacters); - } - } - if (preChar) { - if ([self.AppDelegate debugMode]) { - NSLog(@"preChar (to insert at %lu) = \"%@\"", preCharPos, preChar); - } - [sender insertText:preChar replacementRange:NSMakeRange(preCharPos, n+nbrOfPreCharacters)]; - } - else { - if ([self.AppDelegate debugMode]) { - NSLog(@"Switching to legacy mode - client apparently doesn't implement attributedSubstringFromRange and no previous character in context buffer."); - } - _legacyMode = YES; // client apparently doesn't implement attributedSubstringFromRange. - } - } - else { - if ([self.AppDelegate debugMode]) - NSLog(@"No previous character to use for replacement - replacing range with space"); - [sender insertText:@" " replacementRange:NSMakeRange(pos - n, n)]; - [self.contextBuffer appendNullChar]; - } - } - if (_legacyMode && 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]) { - NSLog(@"Legacy mode: calling deleteBack"); - if (_cannnotTrustSelectionLength) - NSLog(@"Cannot trust client to report accurate selection length - assuming no selection."); - } - - // 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 (!_cannnotTrustSelectionLength && selectedRange.length > 0) - n++; // First delete-back will delete the existing selection. - [self deleteBack:n for:event]; - deleteBackPosted = YES; - - CFRelease(_sourceFromOriginalEvent); - _sourceFromOriginalEvent = nil; - } - } - } - else if ([actionType isEqualToString:Q_DEADKEY]) { - [self.contextBuffer deleteLastNullChars]; - NSUInteger x = [[action objectForKey:actionType] unsignedIntegerValue]; - [self.contextBuffer appendDeadkey:x]; - } - else if ([actionType isEqualToString:Q_NUL]) { - continue; - } - else if ([actionType isEqualToString:Q_RETURN]) { -// if (_legacyMode) -// _previousSelRange = [sender selectedRange]; - return YES; - } - else if ([actionType isEqualToString:Q_BEEP]) { - [[NSSound soundNamed:@"Tink"] play]; - } - - handled = YES; - } - - // Apply context changes if not handled - if (!handled) { - NSUInteger nc = [self.contextBuffer deleteLastNullChars]; - if (nc > 0) { - if ([self.AppDelegate debugMode]) - NSLog(@"Deleted %li null characters from context buffer", nc); - // This can presumably only happen if a previous event resulted in a chain of - // actions that had a Q_BACK not followed by a Q_STR (assuming that can happen). - // REVIEW: Need to test this scenario in Atom and Googe Docs in Safari - self.willDeleteNullChar = YES; - [self deleteBack:nc for:event]; - _keyCodeOfOriginalEvent = event.keyCode; - - return YES; - } - - NSString* charactersToAppend = nil; - BOOL updateEngineContext = YES; - unsigned short keyCode = event.keyCode; - switch (keyCode) { - case kVK_Delete: - if ([self.AppDelegate debugMode]) { - 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)) || - (!_legacyMode && (!self.willDeleteNullChar))) - { - // Backspace clears last "real" character from buffer, plus any surrounding deadkeys - [self.contextBuffer deleteLastDeadkeys]; - [self.contextBuffer deleteLastNChars:1]; - if (_legacyMode) { - _previousSelRange.location -= 1; - _previousSelRange.length = 0; - } - [self.contextBuffer deleteLastDeadkeys]; - } - if (_numberOfPostedDeletesToExpect > 0) { - if (--_numberOfPostedDeletesToExpect == 0) { - if ([self.AppDelegate debugMode]) - NSLog(@"Processing final posted delete-back..."); - - self.willDeleteNullChar = NO; - if (_legacyMode && _pendingBuffer != nil && _pendingBuffer.length > 0) { - if ([self.AppDelegate debugMode]) - NSLog(@"Posting special code to tell IM to insert characters from pending buffer."); - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:sender afterDelay:0.1]; - } - else { - if ([self.AppDelegate debugMode]) { - NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); - } - [self postKeyPressToFrontProcess:_keyCodeOfOriginalEvent from:_sourceFromOriginalEvent]; - _keyCodeOfOriginalEvent = 0; - CFRelease(_sourceFromOriginalEvent); - _sourceFromOriginalEvent = nil; - } - updateEngineContext = NO; - } - } - else { - self.willDeleteNullChar = NO; - } - break; - - case kVK_LeftArrow: - // Marc has suggested that it's better to just let it be dumb. Too many potential pitfalls - // trying to guess where in the context we ended up after a left arrow. -// if (_legacyMode && self.contextBuffer.length) { -// NSUInteger flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; -// if ([self.AppDelegate debugMode]) -// NSLog(@"Legacy app Left arrow. flags = 0x%02x", (int)flags); -// if ((flags ^ NSEventModifierFlagShift) == 0) { -// [self.contextBuffer deleteLastNChars:1]; -// if ([self.AppDelegate debugMode]) -// NSLog(@"Removed one character from context."); -// break; -// } -// } - /* FALLTHROUGH */ - case kVK_RightArrow: - case kVK_UpArrow: - case kVK_DownArrow: - case kVK_Home: - case kVK_End: - case kVK_PageUp: - case kVK_PageDown: - _contextOutOfDate = YES; - updateEngineContext = NO; - break; - - case kVK_Return: - case kVK_ANSI_KeypadEnter: - charactersToAppend = @"\n"; - break; - - default: - { - unichar ch = [event.characters characterAtIndex:0]; - // REVIEW: Is ch ever != keyCode? Is there ever more than one character in event.characters? - if (keyCode < 0x33 || (ch >= 0x2A && ch <= 0x39)) { // Main keys, Numpad char range, normal punctuation - charactersToAppend = event.characters; - } - else { - // Other keys - } - } - break; - } - if (charactersToAppend != nil) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Adding \"%@\" to context buffer", charactersToAppend); - } - [self.contextBuffer appendString:charactersToAppend]; - if (_legacyMode) { - _previousSelRange.location += charactersToAppend.length; - _previousSelRange.length = 0; - } - } - - if (updateEngineContext) { - [self.kme setContextBuffer:self.contextBuffer]; - } - } - - if ([self.AppDelegate debugMode]) { - NSLog(@"handledEvent: %@", handled?@"YES":@"NO"); - if (_contextOutOfDate) - NSLog(@"Context now out of date."); - else - NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); - NSLog(@"kme.contextBuffer = \"%@\"", self.kme.contextBuffer.length?[self.kme.contextBuffer codeString]:@"{empty}"); - NSRange range = [sender markedRange]; - NSLog(@"sender.markedRange.location = %lu", range.location); - NSLog(@"sender.markedRange.length = %lu", range.length); - range = [sender selectedRange]; - NSLog(@"sender.selectedRange.location = %lu", range.location); - if (_cannnotTrustSelectionLength) - NSLog(@"The following cannot be trusted and will be ignored:"); - NSLog(@"sender.selectedRange.length = %lu", range.length); - NSLog(@"***"); - } - -// Seems we can't do it this way because (at least for "legacy mode" apps, the selection range doesn't get updated -// until after we return from this method. -// if (_legacyMode) -// _previousSelRange = [sender selectedRange]; - return handled; + return [_eventHandler handleEvent:event client:sender]; } - (void)activateServer:(id)sender { [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; - - NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *clientAppId = [currApp bundleIdentifier]; - if ([self.AppDelegate debugMode]) - NSLog(@"New active app %@", clientAppId); - _previousSelRange = NSMakeRange(NSNotFound, NSNotFound); - _clientSelectionCanChangeUnexpectedly = YES; - _forceRemoveSelectionInGoogleDocs = NO; - _explicitlyDeleteExistingSelectionBeforeInserting = NO; - _cannnotTrustSelectionLength = NO; - _insertCharactersIndividually = NO; - // REVIEW: Should this list be in a info.plist file - // Use a table of known apps to decide whether or not to operate in legacy mode - // and whether or not to follow calls to setMarkedText with calls to insertText. - if ([clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId isEqual: @"org.mozilla.firefox"] || - [clientAppId isEqual: @"com.github.atom"] || - [clientAppId isEqual: @"com.collabora.libreoffice-free"] || - [clientAppId isEqual: @"com.axosoft.gitkraken"] || - [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || - [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || - [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || - [clientAppId isEqual: @"com.microsoft.Word"] - /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */) { - _legacyMode = YES; - if ([self.AppDelegate debugMode]) - NSLog(@"Using legacy mode for this app."); - } - else { - _legacyMode = NO; + + if (_eventHandler != nil) { + [_eventHandler deactivate]; } - // if ([clientAppId isEqual: @"com.google.Chrome"] || - // [clientAppId isEqual: @"com.apple.Terminal"] || - // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { - // _clientSelectionCanChangeUnexpectedly = YES; - // } - - if ([clientAppId isEqual: @"com.github.atom"]) { - // This isn't true (the context can change unexpectedly), but we can't get the context, - // so we pretend/hope it won't. - _clientSelectionCanChangeUnexpectedly = NO; - } - - // Most things in Safari work well using the normal way, but Google Docs doesn't. - if ([clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId isEqual: @"com.apple.Safari"] || - [clientAppId isEqual: @"org.mozilla.firefox"]) { - _failuresToRetrieveExpectedContext = 0; - _forceRemoveSelectionInGoogleDocs = [clientAppId isEqual: @"com.apple.Safari"]; - //_explicitlyDeleteExistingSelectionBeforeInserting = [clientAppId isEqual: @"com.apple.Safari"]; - } - else { - _failuresToRetrieveExpectedContext = NSUIntegerMax; - } - - if (_numberOfPostedDeletesToExpect > 0 || (_pendingBuffer != nil && _pendingBuffer.length > 0) || - _keyCodeOfOriginalEvent != 0 || _sourceFromOriginalEvent != nil) - { - if ([self.AppDelegate debugMode]) { - NSLog(@"ERROR: new app activated before previous app finished processing pending events!"); - NSLog(@" _numberOfPostedDeletesToExpect = %lu", _numberOfPostedDeletesToExpect); - NSLog(@" pendingBuffer = \"%@\"", _pendingBuffer == nil ? @"(NIL)" : (NSString*)[self pendingBuffer]); - NSLog(@" _keyCodeOfOriginalEvent = %hu", _keyCodeOfOriginalEvent); - } - _numberOfPostedDeletesToExpect = 0; - _pendingBuffer = nil; - _keyCodeOfOriginalEvent = 0; - if (_sourceFromOriginalEvent != nil) { - CFRelease(_sourceFromOriginalEvent); - _sourceFromOriginalEvent = nil; - } - } - - _contextOutOfDate = YES; + _eventHandler = [KMInputMethodEventHandler new]; } - (void)deactivateServer:(id)sender { @@ -724,6 +66,13 @@ NSRange _previousSelRange; NSLog(@"sender: %@", sender); NSLog(@"***"); } + // Seems like we ought to do this, but it appears there is a timing issue that + // sometimes causes the deactivate to happen AFTER the new activateServer event, + // thereby clobbering the newly created event handler. +// if (_eventHandler != nil) { +// [_eventHandler deactivate]; +// _eventHandler = nil; +// } } /* @@ -769,68 +118,10 @@ NSRange _previousSelRange; return self.AppDelegate.menu; } -- (KMEngine *)kme { - return self.AppDelegate.kme; -} - - (KMXFile *)kmx { return self.AppDelegate.kmx; } -- (NSMutableString *)contextBuffer { - if (_contextOutOfDate) { - NSException* exception = [NSException - exceptionWithName:@"InvalidOperationException" - reason:@"Attempt to access out-of-date context." - userInfo:nil]; - @throw exception; - } - return self.AppDelegate.contextBuffer; -} - -- (void)updateContextBuffer:(id)sender { - if ([self.AppDelegate debugMode]) { - NSLog(@"*** updateContextBuffer ***"); - NSLog(@"sender: %@", sender); - } - - NSRange selRange = [sender selectedRange]; - NSUInteger len = [sender length]; - if ([self.AppDelegate debugMode]) { - NSLog(@"selRange.location: %lu", (unsigned long)selRange.location); - NSLog(@"sender length: %lu", len); - } - - if (selRange.location == NSNotFound) { - // REVIEW: For now, if the client always reports its selectedRange as "not found", we're stuck assuming that any - // previous context we've built up is indeed current. This may be totally untrue, but if the client can't report - // its location, there's no point trying to check to see if it changed unexpectedly. - _clientSelectionCanChangeUnexpectedly = NO; - } - else { - len = selRange.location; - } - - NSString *preBuffer = [[sender attributedSubstringFromRange:NSMakeRange(0, len)] string]; - if ([self.AppDelegate debugMode]) { - NSLog(@"preBuffer = \"%@\"", preBuffer); - if (preBuffer.length) - NSLog(@"First character: '%x'", [preBuffer characterAtIndex:0]); - else - NSLog(@"preBuffer has a length of 0"); - } - // REVIEW: If there is ever a situation where preBuffer gets some text but the client reports its - // selectedRange as not found, we probably can't reliably assume that the current location is really - // at the end of the "preBuffer", so maybe we just need to assume no context. - [self.AppDelegate setContextBuffer:preBuffer.length?[NSMutableString stringWithString:preBuffer]:nil]; - _contextOutOfDate = NO; - if ([self.AppDelegate debugMode]) { - NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); - NSLog(@"***"); - } - _previousSelRange = selRange; -} - - (void)menuAction:(id)sender { NSMenuItem *mItem = [sender objectForKey:kIMKCommandMenuItemName]; NSInteger itag = mItem.tag; @@ -874,82 +165,4 @@ NSRange _previousSelRange; [self.AppDelegate showOSK]; } } - -// Return the pending buffer. If it is NIL create it. --(NSMutableString*)pendingBuffer; -{ - if ( _pendingBuffer == nil ) { - _pendingBuffer = [[NSMutableString alloc] init]; - } - return _pendingBuffer; -} - -// Change the pending buffer. --(void)setPendingBuffer:(NSString*)string -{ - NSMutableString* buffer = [self pendingBuffer]; - [buffer setString:string]; -} - -// Append to (creating if necessary) the pending buffer. --(void)appendPendingBuffer:(NSString*)string -{ - NSMutableString* buffer = [self pendingBuffer]; - [buffer appendString:string]; -} - -- (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); -} - -- (void)deleteBack:(NSUInteger)count for:(NSEvent *) event { - _numberOfPostedDeletesToExpect = count; - CGEventRef ev; - _sourceFromOriginalEvent = CGEventCreateSourceFromEvent([event CGEvent]); - - for (int db = 0; db < count; db++) - { - if ([self.AppDelegate debugMode]) - NSLog(@"Posting a delete (down/up) at kCGHIDEventTap."); - - ev = CGEventCreateKeyboardEvent (_sourceFromOriginalEvent, kVK_Delete, true);//delete-back down - CGEventPost(kCGHIDEventTap, ev); - CFRelease(ev); - ev = CGEventCreateKeyboardEvent (_sourceFromOriginalEvent, kVK_Delete, false); //delete-back up - CGEventPost(kCGHIDEventTap, ev); - CFRelease(ev); - } -} - -- (void)initiatePendingBufferProcessing:(id)sender { - [self postKeyPressToFrontProcess:kProcessPendingBuffer from:NULL]; -} - -- (void)postKeyPressToFrontProcess:(CGKeyCode)code from:(CGEventSourceRef) source { - ProcessSerialNumber psn; - GetFrontProcess(&psn); - - CGEventRef event = CGEventCreateKeyboardEvent(source, code, true); - CGEventPostToPSN(&psn, event); - CFRelease(event); - - if (code != kProcessPendingBuffer) { // special 0xFF code is not a real key-press, so no "up" is needed - event = CGEventCreateKeyboardEvent(source, code, false); - CGEventPostToPSN(&psn, event); - CFRelease(event); - } -} - @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h index 0dfe7c2660..eccbe57c3b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h @@ -11,5 +11,11 @@ #ifndef KMInputMethodEventHandler_h #define KMInputMethodEventHandler_h +@interface KMInputMethodEventHandler : NSObject + +- (BOOL)handleEvent:(NSEvent *)event client:(id)sender; +- (void)deactivate; + +@end #endif /* KMInputMethodEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 811ad71263..c139090651 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -5,5 +5,836 @@ // Created by Tom Bogle on 11/22/17. // Copyright © 2017 SIL International. All rights reserved. // -#import "KMInputController.h" +#import "KMInputMethodAppDelegate.h" +#import "KMInputMethodEventHandler.h" #include /* For kVK_ constants. */ + +@interface KMInputMethodEventHandler () +//@property (nonatomic, strong) NSMutableDictionary *kbData; +//@property (nonatomic, strong) NSDictionary *kmModes; +@property (assign) BOOL willDeleteNullChar; +@end + +@implementation KMInputMethodEventHandler + +const CGKeyCode kProcessPendingBuffer = 0xFF; + +//_pendingBuffer contains text that is ready to be sent to the client when all delete-backs are finished. +NSMutableString* _pendingBuffer; +NSUInteger _numberOfPostedDeletesToExpect = 0; +CGKeyCode _keyCodeOfOriginalEvent; +CGEventSourceRef _sourceFromOriginalEvent = nil; +BOOL _contextOutOfDate = YES; + +// This flag indicates which mode is being used for replacing already typed text when composing characters +// (i.e., when not using deadkeys). Some apps (and some javascript-based websites, such as Google Docs) do +// not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" +// apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now +// ready to insert the composed text. +BOOL _legacyMode = NO; +// Some clients (e.g. Chrome) handle the mouse down events before we get a crack at them. For such apps that +// are able to report their current selection location (LibreOffice can't even do that!), we can do some +// checking at the start of the event processing to see if we're probably still in the same place where we +// left off previously. +BOOL _clientSelectionCanChangeUnexpectedly = YES; // REVIEW: Maybe we can get notification from these clients by handling mouseDownOnCharacterIndex. +BOOL _insertCharactersIndividually = NO; +// Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox), we want to +// try to detect it and: +// in Safari, switch to legacy mode +// in Chrome, NOT assume that it needs to re-get the context every time around (which means that if the user +// does mouse-click somewhere else, it could lead to bad behaviour). +// in Firefox, we're already in legacy mode and we do get mouse clicks, so we're already doing the best we can. +NSUInteger _failuresToRetrieveExpectedContext = NSUIntegerMax; +BOOL _forceRemoveSelectionInGoogleDocs = NO; +BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; +BOOL _cannnotTrustSelectionLength = NO; +NSRange _previousSelRange; + +- (id)init { + NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *clientAppId = [currApp bundleIdentifier]; + if ([self.AppDelegate debugMode]) + NSLog(@"New active app %@", clientAppId); + _previousSelRange = NSMakeRange(NSNotFound, NSNotFound); + _clientSelectionCanChangeUnexpectedly = YES; + _forceRemoveSelectionInGoogleDocs = NO; + _explicitlyDeleteExistingSelectionBeforeInserting = NO; + _cannnotTrustSelectionLength = NO; + _insertCharactersIndividually = NO; + // REVIEW: Should this list be in a info.plist file + // Use a table of known apps to decide whether or not to operate in legacy mode + // and whether or not to follow calls to setMarkedText with calls to insertText. + if ([clientAppId isEqual: @"com.google.Chrome"] || + [clientAppId isEqual: @"org.mozilla.firefox"] || + [clientAppId isEqual: @"com.github.atom"] || + [clientAppId isEqual: @"com.collabora.libreoffice-free"] || + [clientAppId isEqual: @"com.axosoft.gitkraken"] || + [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || + [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || + [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || + [clientAppId isEqual: @"com.microsoft.Word"] + /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */) { + _legacyMode = YES; + if ([self.AppDelegate debugMode]) + NSLog(@"Using legacy mode for this app."); + } + else { + _legacyMode = NO; + } + + // if ([clientAppId isEqual: @"com.google.Chrome"] || + // [clientAppId isEqual: @"com.apple.Terminal"] || + // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { + // _clientSelectionCanChangeUnexpectedly = YES; + // } + + if ([clientAppId isEqual: @"com.github.atom"]) { + // This isn't true (the context can change unexpectedly), but we can't get the context, + // so we pretend/hope it won't. + _clientSelectionCanChangeUnexpectedly = NO; + } + + // Most things in Safari work well using the normal way, but Google Docs doesn't. + if ([clientAppId isEqual: @"com.google.Chrome"] || + [clientAppId isEqual: @"com.apple.Safari"] || + [clientAppId isEqual: @"org.mozilla.firefox"]) { + _failuresToRetrieveExpectedContext = 0; + _forceRemoveSelectionInGoogleDocs = [clientAppId isEqual: @"com.apple.Safari"]; + //_explicitlyDeleteExistingSelectionBeforeInserting = [clientAppId isEqual: @"com.apple.Safari"]; + } + else { + _failuresToRetrieveExpectedContext = NSUIntegerMax; + } + + _contextOutOfDate = YES; + + return self; +} + +- (void)deactivate { + if (_numberOfPostedDeletesToExpect > 0 || (_pendingBuffer != nil && _pendingBuffer.length > 0) || + _keyCodeOfOriginalEvent != 0 || _sourceFromOriginalEvent != nil) + { + if ([self.AppDelegate debugMode]) { + NSLog(@"ERROR: new app activated before previous app finished processing pending events!"); + NSLog(@" _numberOfPostedDeletesToExpect = %lu", _numberOfPostedDeletesToExpect); + NSLog(@" pendingBuffer = \"%@\"", _pendingBuffer == nil ? @"(NIL)" : (NSString*)[self pendingBuffer]); + NSLog(@" _keyCodeOfOriginalEvent = %hu", _keyCodeOfOriginalEvent); + } + _numberOfPostedDeletesToExpect = 0; + _pendingBuffer = nil; + _keyCodeOfOriginalEvent = 0; + } +} + +- (void)dealloc { + if (_sourceFromOriginalEvent != nil) { + CFRelease(_sourceFromOriginalEvent); + _sourceFromOriginalEvent = nil; + } +} + +- (KMInputMethodAppDelegate *)AppDelegate { + return (KMInputMethodAppDelegate *)[NSApp delegate]; +} + +- (KMEngine *)kme { + return self.AppDelegate.kme; +} + +- (NSMutableString *)contextBuffer { + if (_contextOutOfDate) { + NSException* exception = [NSException + exceptionWithName:@"InvalidOperationException" + reason:@"Attempt to access out-of-date context." + userInfo:nil]; + @throw exception; + } + return self.AppDelegate.contextBuffer; +} + +- (void)updateContextBuffer:(id)sender { + if ([self.AppDelegate debugMode]) { + NSLog(@"*** updateContextBuffer ***"); + NSLog(@"sender: %@", sender); + } + + NSRange selRange = [sender selectedRange]; + NSUInteger len = [sender length]; + if ([self.AppDelegate debugMode]) { + NSLog(@"selRange.location: %lu", (unsigned long)selRange.location); + NSLog(@"sender length: %lu", len); + } + + if (selRange.location == NSNotFound) { + // REVIEW: For now, if the client always reports its selectedRange as "not found", we're stuck assuming that any + // previous context we've built up is indeed current. This may be totally untrue, but if the client can't report + // its location, there's no point trying to check to see if it changed unexpectedly. + _clientSelectionCanChangeUnexpectedly = NO; + } + else { + len = selRange.location; + } + + NSString *preBuffer = [[sender attributedSubstringFromRange:NSMakeRange(0, len)] string]; + if ([self.AppDelegate debugMode]) { + NSLog(@"preBuffer = \"%@\"", preBuffer); + if (preBuffer.length) + NSLog(@"First character: '%x'", [preBuffer characterAtIndex:0]); + else + NSLog(@"preBuffer has a length of 0"); + } + // REVIEW: If there is ever a situation where preBuffer gets some text but the client reports its + // selectedRange as not found, we probably can't reliably assume that the current location is really + // at the end of the "preBuffer", so maybe we just need to assume no context. + [self.AppDelegate setContextBuffer:preBuffer.length?[NSMutableString stringWithString:preBuffer]:nil]; + _contextOutOfDate = NO; + if ([self.AppDelegate debugMode]) { + NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); + NSLog(@"***"); + } + _previousSelRange = selRange; +} + +// Return the pending buffer. If it is NIL create it. +-(NSMutableString*)pendingBuffer; +{ + if ( _pendingBuffer == nil ) { + _pendingBuffer = [[NSMutableString alloc] init]; + } + return _pendingBuffer; +} + +// Change the pending buffer. +-(void)setPendingBuffer:(NSString*)string +{ + NSMutableString* buffer = [self pendingBuffer]; + [buffer setString:string]; +} + +// Append to (creating if necessary) the pending buffer. +-(void)appendPendingBuffer:(NSString*)string +{ + NSMutableString* buffer = [self pendingBuffer]; + [buffer appendString:string]; +} + + +- (BOOL)handleEvent:(NSEvent *)event client:(id)sender { + // OSK key feedback from hardware keyboard is disabled + /*if (event.type == NSKeyDown) + [self.AppDelegate handleKeyEvent:event];*/ + + if (event.type == NSLeftMouseDown || event.type == NSLeftMouseUp ) { + // if (_clientSelectionCanChangeUnexpectedly) { + // if ([self.AppDelegate debugMode]) + // NSLog(@"WARNING: We are dealing with an app/context where we THINK we shouldn't be getting mouse events, but we just got one!"); + // _clientSelectionCanChangeUnexpectedly = NO; + // } + _contextOutOfDate = YES; + return NO; + } + else if (event.type != NSKeyDown) + return NO; // We ignore NSLeftMouseDragged events (because we'll eventually get a mouse-up). + else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { + // There are a bunch of common navigation/selection command-key combinations, but individual + // apps may implement specifc commands that also chnage the selection. There is probably no + // situation where a user could reasonably hope that any dead-keys typed before using a + // command shortcut would be remembered, so other than an insignificant performance penalty, + // the only downside to treating all commands as having the potential to change the selection + // is that some "legacy" apps can't get their context at all. + if ([self.AppDelegate debugMode]) + NSLog(@"Command key - context needs to be re-gotten."); + _contextOutOfDate = YES; + return NO; // We let the client app handle all Command-key events. + } + + if (_legacyMode && event.keyCode == kProcessPendingBuffer) + { + if ([self.AppDelegate debugMode]) + NSLog(@"Processing the special %hu code", kProcessPendingBuffer); + + NSUInteger length = [self pendingBuffer].length; + if (length > 0) { + if ([self.AppDelegate debugMode]) { + for (NSUInteger ich = 0; ich < length; ich++) + NSLog(@"Char %li: '%x'", ich, [[self pendingBuffer] characterAtIndex:ich]); + } + NSString* text; + if (length > 1 && _insertCharactersIndividually) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Using special Google Docs in Chrome logic"); + } + unichar chars[1]; + chars[0] = [[self pendingBuffer] characterAtIndex:0]; + text = [[NSString alloc] initWithCharacters:chars length:1]; + } + else { + text = [self pendingBuffer]; + } + + if ([self.AppDelegate debugMode]) + NSLog(@"Inserting text from pending buffer: \"%@\"", text); + + [sender insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + _previousSelRange.location += text.length; + _previousSelRange.length = 0; + + if (length > 1 && _insertCharactersIndividually) { + // Come back for more... + [_pendingBuffer deleteCharactersInRange:NSMakeRange(0, 1)]; + [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:sender afterDelay:0.1]; + } + else { + [self setPendingBuffer:@""]; + } + } + else { + if ([self.AppDelegate debugMode]) + NSLog(@"Error - expected text in pending buffer!"); + } + return YES; + } + + if (!self.willDeleteNullChar && !_contextOutOfDate && _failuresToRetrieveExpectedContext < 3) { + if ([self AppDelegate].debugMode) { + NSLog(@"Checking to see if we're in Google Docs (or some other site that can't give context)."); + } + NSUInteger bufferLength = self.contextBuffer.length; + if (bufferLength) { + NSUInteger location = [sender selectedRange].location; + + if (location != NSNotFound && location > 0) { + NSString *clientContext = [[sender attributedSubstringFromRange:NSMakeRange(0, location)] string]; + if (clientContext == nil || !clientContext.length || + [clientContext characterAtIndex:clientContext.length - 1] != + [self.contextBuffer characterAtIndex:bufferLength - 1]) { + _failuresToRetrieveExpectedContext++; + } + else { + if ([self AppDelegate].debugMode) { + NSLog(@"We got what we were expecting from the client. We can stop checking."); + } + _failuresToRetrieveExpectedContext = NSUIntegerMax; + } + } + else { + _failuresToRetrieveExpectedContext++; + } + + if (_failuresToRetrieveExpectedContext == 3) + { + if ([self AppDelegate].debugMode) { + NSLog(@"Detected Google Docs or some other editor that can't provide context. Using legacy mode."); + } + _failuresToRetrieveExpectedContext = NSUIntegerMax; + _legacyMode = YES; + if (_clientSelectionCanChangeUnexpectedly) { + _cannnotTrustSelectionLength = YES; + _clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. + // Google docs in Chrome allows only a single character at a time :-( + _insertCharactersIndividually = YES; + } + } + } + } + else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) + return NO; // We ignore any Command-key events. + + if ([self.AppDelegate debugMode]) { + if (_clientSelectionCanChangeUnexpectedly) + NSLog(@"_clientSelectionCanChangeUnexpectedly = YES"); + else + NSLog(@"_clientSelectionCanChangeUnexpectedly = NO"); + } + // The following is needed because some clients (e.g. Chrome & Terminal) handle mouse down events and we + // never get a crack at them: + // At the start of the loop before asking the Engine to process the event, unless we still have + // pending work to do based on posted deletes or the special kProcessPendingBuffer code, check to see if + // the client still reports a selection that matches the position we would expect based on the length of + // the context buffer. If not, then reset the context buffer. + if (_clientSelectionCanChangeUnexpectedly && _numberOfPostedDeletesToExpect == 0 && + (_pendingBuffer == nil || _pendingBuffer.length == 0)) { + NSRange currentSelRange = [sender selectedRange]; + + if (currentSelRange.location == NSNotFound) { + _clientSelectionCanChangeUnexpectedly = NO; + } + else if ((_previousSelRange.location != currentSelRange.location || _cannnotTrustSelectionLength || _previousSelRange.length != currentSelRange.length)) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Client selection may have changed since context was set. Resetting context..."); + NSLog(@" _previousSelRange.location = %lu", _previousSelRange.location); + NSLog(@" _previousSelRange.length = %lu", _previousSelRange.length); + NSLog(@" currentSelRange.location = %lu", currentSelRange.location); + if (_cannnotTrustSelectionLength) + NSLog(@"The following cannot be trusted and will be ignored:"); + NSLog(@" currentSelRange.length = %lu", currentSelRange.length); + } + [self updateContextBuffer:sender]; + } + } + + if (_contextOutOfDate) + [self updateContextBuffer:sender]; + + BOOL handled = NO; + BOOL deleteBackPosted = NO; + NSArray *actions = nil; + if (!self.willDeleteNullChar) { + actions = [self.kme processEvent:event]; + } + + if ([self.AppDelegate debugMode]) { + NSLog(@"sender type = %@", NSStringFromClass([sender class])); + NSLog(@"sender selection range location = %lu", [sender selectedRange].location); + NSLog(@"actions = %@", actions); + } + for (NSDictionary *action in actions) { + NSString *actionType = [[action allKeys] objectAtIndex:0]; + if ([actionType isEqualToString:Q_STR]) { + if ([self.AppDelegate debugMode]) + NSLog(@"About to start handling Q_STR action..."); + + NSString *output = [action objectForKey:actionType]; + if ([self.AppDelegate debugMode]) + NSLog(@"output = %@", output); + if (deleteBackPosted) + { + [self appendPendingBuffer:output]; + if ([self.AppDelegate debugMode]) + NSLog(@"pendingBuffer = %@", [self pendingBuffer]); + } + else { + NSUInteger nc = [self.contextBuffer deleteLastNullChars]; + // Each null in the context buffer presumably corresponds to a space we inserted when + // processing the Q_BACK, which we now need to replace with the text we're inserting. + if (nc > 0) { + if ([self.AppDelegate debugMode]) + NSLog(@"nc = %lu", nc); + NSRange selRange = [sender selectedRange]; + NSUInteger pos = selRange.location; + if (pos >= nc && pos != NSNotFound) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Replacement index = %lu", pos - nc); + NSLog(@"Replacement length = %lu", nc); + } + [sender insertText:output replacementRange:NSMakeRange(pos - nc, nc)]; + _previousSelRange.location += output.length - nc; + _previousSelRange.length = 0; + } + } + else { + // The following commented out code is the code that would make it possible to type over an + // existing selection in Word in Safari, but I don't know of a way to distinguish that case + // from other contexts in Safari (wherein this code inserts an extra leading space). + // if (_explicitlyDeleteExistingSelectionBeforeInserting) { + // NSUInteger selLength = [sender selectedRange].length; + // if (selLength > 0 && selLength != NSNotFound) { + // if ([self.AppDelegate debugMode]) { + // NSLog(@"Attempting to delete existing selection of length = %lu by replacing it with a space (which should not actually appear in the text).", selLength); + // } + // + // [sender insertText:@" " replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + // + // if ([self.AppDelegate debugMode]) + // NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); + // + // CGEventSourceRef sourceFromEvent = CGEventCreateSourceFromEvent([event CGEvent]); + // [self postKeyPressToFrontProcess:event.keyCode from:sourceFromEvent]; + // CFRelease(sourceFromEvent); + // return YES; + // } + // } + [sender insertText:output replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + _previousSelRange.location += output.length; + _previousSelRange.length = 0; + + // In Google Docs in Safari when there is an existing selection, the inserted characters + // stays selected. The following clears the selection. + if (_legacyMode && _forceRemoveSelectionInGoogleDocs) { + if ([self.AppDelegate debugMode]) + NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); + ProcessSerialNumber psn; + GetFrontProcess(&psn); + + CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, true); + //set shift and command keys down for above event + CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); + CGEventPostToPSN(&psn, event); + CFRelease(event); + + event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, false); + CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); + CGEventPostToPSN(&psn, event); + CFRelease(event); + } + } + } + + // 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 + // the state as it *will* be when all the posted/pending events have been processed. + [self.contextBuffer appendString:output]; + } + else if ([actionType isEqualToString:Q_BACK]) { + if ([self.AppDelegate debugMode]) + { + NSLog(@"About to start handling Q_BACK action..."); + NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); + } + [self.contextBuffer deleteLastNullChars]; + NSUInteger dk = [self.contextBuffer deleteLastDeadkeys]; + NSInteger n = [[action objectForKey:actionType] integerValue] - dk; + NSUInteger dc = [[self.contextBuffer lastNChars:n] deadKeyCount]; + // n is now the number of characters to delete from the context buffer + // (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) { + if ([self.AppDelegate debugMode]) + { + NSLog(@"Q_BACK"); + NSLog(@"dk = %lu", dk); + NSLog(@"n = %li", n); + NSLog(@"dc = %lu", dc); + if (_legacyMode) + NSLog(@"Using Legacy mode."); + else + NSLog(@"Using Apple IM-compliant mode."); + } + NSRange selectedRange = [sender selectedRange]; + NSInteger pos = selectedRange.location; + if (_legacyMode && self.contextBuffer != nil && (pos == 0 || pos == NSNotFound)) { + pos = self.contextBuffer.length + n; + } + if ([self.AppDelegate debugMode]) + NSLog(@"pos = %lu", pos); + + if (!_legacyMode && 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 + // comining marks on there own via insertText. One potentially negative implication + // of this is that if the script should happen to contain characters whose class is + // not known, it will skip over them and keep looking, so it could end up using a + // longer string of characters than otherwise necessary. This could result in a + // mildly jarring visual experience for the user if the app refreshes the diplay + // between the time the characters are removed and re-inserted. But presumbly this + // algorithm will eventually find either a known base character or get all the way back + // to the start of the context, so if it doesn't find a known base character, it will + // fall back to just attempting the insert with whatever it does find. I believe this + // will always work and should at least work as reliably as the old version of the code, + // 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--) { + preChar = [[sender attributedSubstringFromRange:NSMakeRange(preCharPos, nbrOfPreCharacters)] string]; + if (!preChar) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Client apparently doesn't implement attributedSubstringFromRange. Attempting to get preChar from context..."); + } + if (self.contextBuffer != nil && preCharPos < self.contextBuffer.length) { + preChar = [self.contextBuffer substringWithRange:NSMakeRange(preCharPos, 1)]; + } + if (!preChar) + break; + } + if ([self.AppDelegate debugMode]) + NSLog(@"Testing preChar: %@", preChar); + + if ([regexNonCombiningMark numberOfMatchesInString:preChar options:NSMatchingAnchored range:NSMakeRange(0, 1)] > 0) + break; + if (preCharPos == 0) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Failed to find a base character!"); + } + break; + } + if ([self.AppDelegate debugMode]) { + NSLog(@"Have not yet found a base character. nbrOfPreCharacters = %lu", nbrOfPreCharacters); + } + } + if (preChar) { + if ([self.AppDelegate debugMode]) { + NSLog(@"preChar (to insert at %lu) = \"%@\"", preCharPos, preChar); + } + [sender insertText:preChar replacementRange:NSMakeRange(preCharPos, n+nbrOfPreCharacters)]; + } + else { + if ([self.AppDelegate debugMode]) { + NSLog(@"Switching to legacy mode - client apparently doesn't implement attributedSubstringFromRange and no previous character in context buffer."); + } + _legacyMode = YES; // client apparently doesn't implement attributedSubstringFromRange. + } + } + else { + if ([self.AppDelegate debugMode]) + NSLog(@"No previous character to use for replacement - replacing range with space"); + [sender insertText:@" " replacementRange:NSMakeRange(pos - n, n)]; + [self.contextBuffer appendNullChar]; + } + } + if (_legacyMode && 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]) { + NSLog(@"Legacy mode: calling deleteBack"); + if (_cannnotTrustSelectionLength) + NSLog(@"Cannot trust client to report accurate selection length - assuming no selection."); + } + + // 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 (!_cannnotTrustSelectionLength && selectedRange.length > 0) + n++; // First delete-back will delete the existing selection. + [self deleteBack:n for:event]; + deleteBackPosted = YES; + + CFRelease(_sourceFromOriginalEvent); + _sourceFromOriginalEvent = nil; + } + } + } + else if ([actionType isEqualToString:Q_DEADKEY]) { + [self.contextBuffer deleteLastNullChars]; + NSUInteger x = [[action objectForKey:actionType] unsignedIntegerValue]; + [self.contextBuffer appendDeadkey:x]; + } + else if ([actionType isEqualToString:Q_NUL]) { + continue; + } + else if ([actionType isEqualToString:Q_RETURN]) { + // if (_legacyMode) + // _previousSelRange = [sender selectedRange]; + return YES; + } + else if ([actionType isEqualToString:Q_BEEP]) { + [[NSSound soundNamed:@"Tink"] play]; + } + + handled = YES; + } + + // Apply context changes if not handled + if (!handled) { + NSUInteger nc = [self.contextBuffer deleteLastNullChars]; + if (nc > 0) { + if ([self.AppDelegate debugMode]) + NSLog(@"Deleted %li null characters from context buffer", nc); + // This can presumably only happen if a previous event resulted in a chain of + // actions that had a Q_BACK not followed by a Q_STR (assuming that can happen). + // REVIEW: Need to test this scenario in Atom and Googe Docs in Safari + self.willDeleteNullChar = YES; + [self deleteBack:nc for:event]; + _keyCodeOfOriginalEvent = event.keyCode; + + return YES; + } + + NSString* charactersToAppend = nil; + BOOL updateEngineContext = YES; + unsigned short keyCode = event.keyCode; + switch (keyCode) { + case kVK_Delete: + if ([self.AppDelegate debugMode]) { + 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)) || + (!_legacyMode && (!self.willDeleteNullChar))) + { + // Backspace clears last "real" character from buffer, plus any surrounding deadkeys + [self.contextBuffer deleteLastDeadkeys]; + [self.contextBuffer deleteLastNChars:1]; + if (_legacyMode) { + _previousSelRange.location -= 1; + _previousSelRange.length = 0; + } + [self.contextBuffer deleteLastDeadkeys]; + } + if (_numberOfPostedDeletesToExpect > 0) { + if (--_numberOfPostedDeletesToExpect == 0) { + if ([self.AppDelegate debugMode]) + NSLog(@"Processing final posted delete-back..."); + + self.willDeleteNullChar = NO; + if (_legacyMode && _pendingBuffer != nil && _pendingBuffer.length > 0) { + if ([self.AppDelegate debugMode]) + NSLog(@"Posting special code to tell IM to insert characters from pending buffer."); + [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:sender afterDelay:0.1]; + } + else { + if ([self.AppDelegate debugMode]) { + NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); + } + [self postKeyPressToFrontProcess:_keyCodeOfOriginalEvent from:_sourceFromOriginalEvent]; + _keyCodeOfOriginalEvent = 0; + CFRelease(_sourceFromOriginalEvent); + _sourceFromOriginalEvent = nil; + } + updateEngineContext = NO; + } + } + else { + self.willDeleteNullChar = NO; + } + break; + + case kVK_LeftArrow: + // Marc has suggested that it's better to just let it be dumb. Too many potential pitfalls + // trying to guess where in the context we ended up after a left arrow. + // if (_legacyMode && self.contextBuffer.length) { + // NSUInteger flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; + // if ([self.AppDelegate debugMode]) + // NSLog(@"Legacy app Left arrow. flags = 0x%02x", (int)flags); + // if ((flags ^ NSEventModifierFlagShift) == 0) { + // [self.contextBuffer deleteLastNChars:1]; + // if ([self.AppDelegate debugMode]) + // NSLog(@"Removed one character from context."); + // break; + // } + // } + /* FALLTHROUGH */ + case kVK_RightArrow: + case kVK_UpArrow: + case kVK_DownArrow: + case kVK_Home: + case kVK_End: + case kVK_PageUp: + case kVK_PageDown: + _contextOutOfDate = YES; + updateEngineContext = NO; + break; + + case kVK_Return: + case kVK_ANSI_KeypadEnter: + charactersToAppend = @"\n"; + break; + + default: + { + unichar ch = [event.characters characterAtIndex:0]; + // REVIEW: Is ch ever != keyCode? Is there ever more than one character in event.characters? + if (keyCode < 0x33 || (ch >= 0x2A && ch <= 0x39)) { // Main keys, Numpad char range, normal punctuation + charactersToAppend = event.characters; + } + else { + // Other keys + } + } + break; + } + if (charactersToAppend != nil) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Adding \"%@\" to context buffer", charactersToAppend); + } + [self.contextBuffer appendString:charactersToAppend]; + if (_legacyMode) { + _previousSelRange.location += charactersToAppend.length; + _previousSelRange.length = 0; + } + } + + if (updateEngineContext) { + [self.kme setContextBuffer:self.contextBuffer]; + } + } + + if ([self.AppDelegate debugMode]) { + NSLog(@"handledEvent: %@", handled?@"YES":@"NO"); + if (_contextOutOfDate) + NSLog(@"Context now out of date."); + else + NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); + NSLog(@"kme.contextBuffer = \"%@\"", self.kme.contextBuffer.length?[self.kme.contextBuffer codeString]:@"{empty}"); + NSRange range = [sender markedRange]; + NSLog(@"sender.markedRange.location = %lu", range.location); + NSLog(@"sender.markedRange.length = %lu", range.length); + range = [sender selectedRange]; + NSLog(@"sender.selectedRange.location = %lu", range.location); + if (_cannnotTrustSelectionLength) + NSLog(@"The following cannot be trusted and will be ignored:"); + NSLog(@"sender.selectedRange.length = %lu", range.length); + NSLog(@"***"); + } + + // Seems we can't do it this way because (at least for "legacy mode" apps, the selection range doesn't get updated + // until after we return from this method. + // if (_legacyMode) + // _previousSelRange = [sender selectedRange]; + return handled; +} + +- (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); +} + +- (void)deleteBack:(NSUInteger)count for:(NSEvent *) event { + _numberOfPostedDeletesToExpect = count; + CGEventRef ev; + _sourceFromOriginalEvent = CGEventCreateSourceFromEvent([event CGEvent]); + + for (int db = 0; db < count; db++) + { + if ([self.AppDelegate debugMode]) + NSLog(@"Posting a delete (down/up) at kCGHIDEventTap."); + + ev = CGEventCreateKeyboardEvent (_sourceFromOriginalEvent, kVK_Delete, true);//delete-back down + CGEventPost(kCGHIDEventTap, ev); + CFRelease(ev); + ev = CGEventCreateKeyboardEvent (_sourceFromOriginalEvent, kVK_Delete, false); //delete-back up + CGEventPost(kCGHIDEventTap, ev); + CFRelease(ev); + } +} + +- (void)initiatePendingBufferProcessing:(id)sender { + [self postKeyPressToFrontProcess:kProcessPendingBuffer from:NULL]; +} + +- (void)postKeyPressToFrontProcess:(CGKeyCode)code from:(CGEventSourceRef) source { + ProcessSerialNumber psn; + GetFrontProcess(&psn); + + CGEventRef event = CGEventCreateKeyboardEvent(source, code, true); + CGEventPostToPSN(&psn, event); + CFRelease(event); + + if (code != kProcessPendingBuffer) { // special 0xFF code is not a real key-press, so no "up" is needed + event = CGEventCreateKeyboardEvent(source, code, false); + CGEventPostToPSN(&psn, event); + CFRelease(event); + } +} + +@end From 6176310073c98fee38b771d29b4a86bd3fbc502a Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Fri, 5 Jan 2018 15:51:08 -0500 Subject: [PATCH 03/13] Broke out browser client support into subclass There is a problem with Google Docs in Safari now - did I break something? --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 8 + .../Keyman4MacIM/KMInputController.m | 21 +- .../KMInputMethodBrowserClientEventHandler.h | 20 ++ .../KMInputMethodBrowserClientEventHandler.m | 124 ++++++++++ .../Keyman4MacIM/KMInputMethodEventHandler.h | 2 + .../Keyman4MacIM/KMInputMethodEventHandler.m | 230 +++++------------- .../KMInputMethodEventHandlerProtected.h | 37 +++ 7 files changed, 260 insertions(+), 182 deletions(-) create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index fdfef81962..e343810fb0 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 98FE10641B4DEE5600525F54 /* KMInfoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98FE10621B4DEE5600525F54 /* KMInfoWindowController.xib */; }; E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; E21799061FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; + E24C79EA1FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -149,6 +150,9 @@ 98FE10621B4DEE5600525F54 /* KMInfoWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = KMInfoWindowController.xib; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.xib; sourceTree = ""; }; E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = ""; }; E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = ""; }; + E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodBrowserClientEventHandler.m; sourceTree = ""; }; + E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodBrowserClientEventHandler.h; sourceTree = ""; }; + E24C79EC1FFFEA6B00D8E46F /* KMInputMethodEventHandlerProtected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandlerProtected.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -279,7 +283,10 @@ 98A778C21A8C53BF00CF809D /* KMInputMethodAppDelegate.h */, 98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */, E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */, + E24C79EC1FFFEA6B00D8E46F /* KMInputMethodEventHandlerProtected.h */, E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, + E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */, + E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */, 98E672961B532DE000DBDE2F /* KMKeyboardHelpWindow */, 989C9C141A7876DE00A20425 /* MainMenu.xib */, 984B8F3E1AF1C2F400E096A8 /* OnScreenKeyboard */, @@ -536,6 +543,7 @@ 98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */, 9836B3721AE5F11D00780482 /* unzip.c in Sources */, 9832472E1A9EABC70010B90C /* KMConfigColumn1CellView.m in Sources */, + E24C79EA1FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m in Sources */, 98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */, 9836B3741AE5F11D00780482 /* ZipArchive.mm in Sources */, 983247B21A9EB9690010B90C /* KMConfigColumn3CellView.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index db2d985b35..90856d0414 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -8,14 +8,9 @@ #import "KMInputController.h" #import "KMInputMethodEventHandler.h" +#import "KMInputMethodBrowserClientEventHandler.h" #include /* For kVK_ constants. */ -//@interface KMInputController () -//@property (nonatomic, strong) NSMutableDictionary *kbData; -//@property (nonatomic, strong) NSDictionary *kmModes; -//@property (assign) BOOL willDeleteNullChar; -//@end - @implementation KMInputController KMInputMethodEventHandler* _eventHandler; @@ -57,7 +52,19 @@ KMInputMethodEventHandler* _eventHandler; [_eventHandler deactivate]; } - _eventHandler = [KMInputMethodEventHandler new]; + NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *clientAppId = [currApp bundleIdentifier]; + if ([self.AppDelegate debugMode]) + NSLog(@"New active app %@", clientAppId); + + // Most things in Safari work well using the normal way, but Google Docs doesn't. + if ([clientAppId isEqual: @"com.google.Chrome"] || + [clientAppId isEqual: @"com.apple.Safari"] || + [clientAppId isEqual: @"org.mozilla.firefox"]) { + _eventHandler = [[KMInputMethodBrowserClientEventHandler alloc] initWithClient:clientAppId]; + } + else + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId]; } - (void)deactivateServer:(id)sender { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h new file mode 100644 index 0000000000..f3aef12a75 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h @@ -0,0 +1,20 @@ +// +// KMInputMethodBrowserClientEventHandler.h +// Keyman4MacIM +// This class is used for browser clients because certain javascript-based websites, such as Google Docs) do +// not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" +// apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now +// ready to insert the composed text . +// +// Created by tom on 1/5/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#ifndef KMInputMethodLegacyClientEventHandler_h +#define KMInputMethodLegacyClientEventHandler_h +#import "KMInputMethodEventHandler.h" + +@interface KMInputMethodBrowserClientEventHandler : KMInputMethodEventHandler +@end + +#endif /* KMInputMethodLegacyClientEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m new file mode 100644 index 0000000000..2d98847a1d --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -0,0 +1,124 @@ +// +// KMInputMethodBrowserClientEventHandler.m +// Keyman +// +// Created by tom on 1/5/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#import "KMInputMethodBrowserClientEventHandler.h" +#import "KMInputMethodEventHandlerProtected.h" + +@implementation KMInputMethodBrowserClientEventHandler + +// Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox), we want to +// try to detect it and: +// in Safari, switch to legacy mode// in Chrome, NOT assume that it needs to re-get the context every time around (which means that if the user +// does mouse-click somewhere else, it could lead to bad behaviour). +// in Firefox, we're already in legacy mode and we do get mouse clicks, so we're already doing the best we can. +NSUInteger _failuresToRetrieveExpectedContext; +BOOL _forceRemoveSelectionInGoogleDocs; +//BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; + +- (instancetype)initWithClient:(NSString *)clientAppId { + + self = [super initWithClient:clientAppId]; + if (self) { + _failuresToRetrieveExpectedContext = 0; + _forceRemoveSelectionInGoogleDocs = + //_explicitlyDeleteExistingSelectionBeforeInserting = + [clientAppId isEqual: @"com.apple.Safari"]; + } + return self; +} + +- (void)checkContextIn:(id) client { + if (!self.willDeleteNullChar && !self.contextOutOfDate && _failuresToRetrieveExpectedContext < 3) { + if (self.AppDelegate.debugMode) { + NSLog(@"Checking to see if we're in Google Docs (or some other site that can't give context)."); + } + NSUInteger bufferLength = self.contextBuffer.length; + if (bufferLength) { + NSUInteger location = [client selectedRange].location; + + if (location != NSNotFound && location > 0) { + NSString *clientContext = [[client attributedSubstringFromRange:NSMakeRange(0, location)] string]; + if (clientContext == nil || !clientContext.length || + [clientContext characterAtIndex:clientContext.length - 1] != + [self.contextBuffer characterAtIndex:bufferLength - 1]) { + _failuresToRetrieveExpectedContext++; + } + else { + if ([self AppDelegate].debugMode) { + NSLog(@"We got what we were expecting from the client. We can stop checking."); + } + _failuresToRetrieveExpectedContext = NSUIntegerMax; + } + } + else { + _failuresToRetrieveExpectedContext++; + } + + if (_failuresToRetrieveExpectedContext == 3) + { + if ([self AppDelegate].debugMode) { + NSLog(@"Detected Google Docs or some other editor that can't provide context. Using legacy mode."); + } + _failuresToRetrieveExpectedContext = NSUIntegerMax; + self.legacyMode = YES; + if (self.clientSelectionCanChangeUnexpectedly) { + self.cannnotTrustSelectionLength = YES; + self.clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. + // Google docs in Chrome allows only a single character at a time :-( + self.insertCharactersIndividually = YES; + } + } + } + } +} + +- (void)replaceExistingSelectionIn:(id)client with:(NSString *) text { + // The following commented out code is the code that would make it possible to type over an + // existing selection in Word in Safari, but I don't know of a way to distinguish that case + // from other contexts in Safari (wherein this code inserts an extra leading space). + // if (_explicitlyDeleteExistingSelectionBeforeInserting) { + // NSUInteger selLength = [sender selectedRange].length; + // if (selLength > 0 && selLength != NSNotFound) { + // if ([self.AppDelegate debugMode]) { + // NSLog(@"Attempting to delete existing selection of length = %lu by replacing it with a space (which should not actually appear in the text).", selLength); + // } + // + // [sender insertText:@" " replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + // + // if ([self.AppDelegate debugMode]) + // NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); + // + // CGEventSourceRef sourceFromEvent = CGEventCreateSourceFromEvent([event CGEvent]); + // [self postKeyPressToFrontProcess:event.keyCode from:sourceFromEvent]; + // CFRelease(sourceFromEvent); + // return YES; + // } + // } + [super replaceExistingSelectionIn:client with:text]; + + // In Google Docs in Safari when there is an existing selection, the inserted characters + // stays selected. The following clears the selection. + if (self.legacyMode && _forceRemoveSelectionInGoogleDocs) { + if ([self.AppDelegate debugMode]) + NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); + ProcessSerialNumber psn; + GetFrontProcess(&psn); + + CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, true); + //set shift and command keys down for above event + CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); + CGEventPostToPSN(&psn, event); + CFRelease(event); + + event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, false); + CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); + CGEventPostToPSN(&psn, event); + CFRelease(event); + } +} +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h index eccbe57c3b..790fcde534 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h @@ -7,12 +7,14 @@ // #import +#import "KMInputMethodAppDelegate.h" #ifndef KMInputMethodEventHandler_h #define KMInputMethodEventHandler_h @interface KMInputMethodEventHandler : NSObject +- (instancetype)initWithClient:(NSString *)clientAppId; - (BOOL)handleEvent:(NSEvent *)event client:(id)sender; - (void)deactivate; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index c139090651..06e82fb0be 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -3,18 +3,12 @@ // Keyman4MacIM // // Created by Tom Bogle on 11/22/17. -// Copyright © 2017 SIL International. All rights reserved. +// Copyright © 2017-2018 SIL International. All rights reserved. // -#import "KMInputMethodAppDelegate.h" #import "KMInputMethodEventHandler.h" +#import "KMInputMethodEventHandlerProtected.h" #include /* For kVK_ constants. */ -@interface KMInputMethodEventHandler () -//@property (nonatomic, strong) NSMutableDictionary *kbData; -//@property (nonatomic, strong) NSDictionary *kmModes; -@property (assign) BOOL willDeleteNullChar; -@end - @implementation KMInputMethodEventHandler const CGKeyCode kProcessPendingBuffer = 0xFF; @@ -24,90 +18,52 @@ NSMutableString* _pendingBuffer; NSUInteger _numberOfPostedDeletesToExpect = 0; CGKeyCode _keyCodeOfOriginalEvent; CGEventSourceRef _sourceFromOriginalEvent = nil; -BOOL _contextOutOfDate = YES; -// This flag indicates which mode is being used for replacing already typed text when composing characters -// (i.e., when not using deadkeys). Some apps (and some javascript-based websites, such as Google Docs) do -// not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" -// apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now -// ready to insert the composed text. -BOOL _legacyMode = NO; -// Some clients (e.g. Chrome) handle the mouse down events before we get a crack at them. For such apps that -// are able to report their current selection location (LibreOffice can't even do that!), we can do some -// checking at the start of the event processing to see if we're probably still in the same place where we -// left off previously. -BOOL _clientSelectionCanChangeUnexpectedly = YES; // REVIEW: Maybe we can get notification from these clients by handling mouseDownOnCharacterIndex. -BOOL _insertCharactersIndividually = NO; -// Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox), we want to -// try to detect it and: -// in Safari, switch to legacy mode -// in Chrome, NOT assume that it needs to re-get the context every time around (which means that if the user -// does mouse-click somewhere else, it could lead to bad behaviour). -// in Firefox, we're already in legacy mode and we do get mouse clicks, so we're already doing the best we can. -NSUInteger _failuresToRetrieveExpectedContext = NSUIntegerMax; -BOOL _forceRemoveSelectionInGoogleDocs = NO; -BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; -BOOL _cannnotTrustSelectionLength = NO; NSRange _previousSelRange; -- (id)init { - NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *clientAppId = [currApp bundleIdentifier]; - if ([self.AppDelegate debugMode]) - NSLog(@"New active app %@", clientAppId); - _previousSelRange = NSMakeRange(NSNotFound, NSNotFound); - _clientSelectionCanChangeUnexpectedly = YES; - _forceRemoveSelectionInGoogleDocs = NO; - _explicitlyDeleteExistingSelectionBeforeInserting = NO; - _cannnotTrustSelectionLength = NO; - _insertCharactersIndividually = NO; - // REVIEW: Should this list be in a info.plist file - // Use a table of known apps to decide whether or not to operate in legacy mode - // and whether or not to follow calls to setMarkedText with calls to insertText. - if ([clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId isEqual: @"org.mozilla.firefox"] || - [clientAppId isEqual: @"com.github.atom"] || - [clientAppId isEqual: @"com.collabora.libreoffice-free"] || - [clientAppId isEqual: @"com.axosoft.gitkraken"] || - [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || - [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || - [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || - [clientAppId isEqual: @"com.microsoft.Word"] - /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */) { - _legacyMode = YES; - if ([self.AppDelegate debugMode]) - NSLog(@"Using legacy mode for this app."); - } - else { - _legacyMode = NO; - } +- (instancetype)initWithClient:(NSString *)clientAppId { - // if ([clientAppId isEqual: @"com.google.Chrome"] || - // [clientAppId isEqual: @"com.apple.Terminal"] || - // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { - // _clientSelectionCanChangeUnexpectedly = YES; - // } - - if ([clientAppId isEqual: @"com.github.atom"]) { - // This isn't true (the context can change unexpectedly), but we can't get the context, - // so we pretend/hope it won't. - _clientSelectionCanChangeUnexpectedly = NO; + self = [super init]; + if (self) { + _previousSelRange = NSMakeRange(NSNotFound, NSNotFound); + _clientSelectionCanChangeUnexpectedly = YES; + _cannnotTrustSelectionLength = NO; + _insertCharactersIndividually = NO; + // REVIEW: Should this list be in a info.plist file + // Use a table of known apps to decide whether or not to operate in legacy mode + // and whether or not to follow calls to setMarkedText with calls to insertText. + if ([clientAppId isEqual: @"com.google.Chrome"] || + [clientAppId isEqual: @"org.mozilla.firefox"] || + [clientAppId isEqual: @"com.github.atom"] || + [clientAppId isEqual: @"com.collabora.libreoffice-free"] || + [clientAppId isEqual: @"com.axosoft.gitkraken"] || + [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || + [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || + [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || + [clientAppId isEqual: @"com.microsoft.Word"] + /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */) { + _legacyMode = YES; + if ([self.AppDelegate debugMode]) + NSLog(@"Using legacy mode for this app."); + } + else { + _legacyMode = NO; + } + + // if ([clientAppId isEqual: @"com.google.Chrome"] || + // [clientAppId isEqual: @"com.apple.Terminal"] || + // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { + // _clientSelectionCanChangeUnexpectedly = YES; + // } + + if ([clientAppId isEqual: @"com.github.atom"]) { + // This isn't true (the context can change unexpectedly), but we can't get the context, + // so we pretend/hope it won't. + _clientSelectionCanChangeUnexpectedly = NO; + } + + _contextOutOfDate = YES; } - - // Most things in Safari work well using the normal way, but Google Docs doesn't. - if ([clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId isEqual: @"com.apple.Safari"] || - [clientAppId isEqual: @"org.mozilla.firefox"]) { - _failuresToRetrieveExpectedContext = 0; - _forceRemoveSelectionInGoogleDocs = [clientAppId isEqual: @"com.apple.Safari"]; - //_explicitlyDeleteExistingSelectionBeforeInserting = [clientAppId isEqual: @"com.apple.Safari"]; - } - else { - _failuresToRetrieveExpectedContext = NSUIntegerMax; - } - - _contextOutOfDate = YES; - return self; } @@ -134,6 +90,16 @@ NSRange _previousSelRange; } } +- (void)checkContextIn:(id)client { + // Base implementation is no-op +} + +- (void)replaceExistingSelectionIn:(id)client with:(NSString *) text { + [client insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + _previousSelRange.location += text.length; + _previousSelRange.length = 0; +} + - (KMInputMethodAppDelegate *)AppDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } @@ -296,50 +262,7 @@ NSRange _previousSelRange; return YES; } - if (!self.willDeleteNullChar && !_contextOutOfDate && _failuresToRetrieveExpectedContext < 3) { - if ([self AppDelegate].debugMode) { - NSLog(@"Checking to see if we're in Google Docs (or some other site that can't give context)."); - } - NSUInteger bufferLength = self.contextBuffer.length; - if (bufferLength) { - NSUInteger location = [sender selectedRange].location; - - if (location != NSNotFound && location > 0) { - NSString *clientContext = [[sender attributedSubstringFromRange:NSMakeRange(0, location)] string]; - if (clientContext == nil || !clientContext.length || - [clientContext characterAtIndex:clientContext.length - 1] != - [self.contextBuffer characterAtIndex:bufferLength - 1]) { - _failuresToRetrieveExpectedContext++; - } - else { - if ([self AppDelegate].debugMode) { - NSLog(@"We got what we were expecting from the client. We can stop checking."); - } - _failuresToRetrieveExpectedContext = NSUIntegerMax; - } - } - else { - _failuresToRetrieveExpectedContext++; - } - - if (_failuresToRetrieveExpectedContext == 3) - { - if ([self AppDelegate].debugMode) { - NSLog(@"Detected Google Docs or some other editor that can't provide context. Using legacy mode."); - } - _failuresToRetrieveExpectedContext = NSUIntegerMax; - _legacyMode = YES; - if (_clientSelectionCanChangeUnexpectedly) { - _cannnotTrustSelectionLength = YES; - _clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. - // Google docs in Chrome allows only a single character at a time :-( - _insertCharactersIndividually = YES; - } - } - } - } - else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) - return NO; // We ignore any Command-key events. + [self checkContextIn:sender]; if ([self.AppDelegate debugMode]) { if (_clientSelectionCanChangeUnexpectedly) @@ -424,50 +347,7 @@ NSRange _previousSelRange; } } else { - // The following commented out code is the code that would make it possible to type over an - // existing selection in Word in Safari, but I don't know of a way to distinguish that case - // from other contexts in Safari (wherein this code inserts an extra leading space). - // if (_explicitlyDeleteExistingSelectionBeforeInserting) { - // NSUInteger selLength = [sender selectedRange].length; - // if (selLength > 0 && selLength != NSNotFound) { - // if ([self.AppDelegate debugMode]) { - // NSLog(@"Attempting to delete existing selection of length = %lu by replacing it with a space (which should not actually appear in the text).", selLength); - // } - // - // [sender insertText:@" " replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - // - // if ([self.AppDelegate debugMode]) - // NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); - // - // CGEventSourceRef sourceFromEvent = CGEventCreateSourceFromEvent([event CGEvent]); - // [self postKeyPressToFrontProcess:event.keyCode from:sourceFromEvent]; - // CFRelease(sourceFromEvent); - // return YES; - // } - // } - [sender insertText:output replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _previousSelRange.location += output.length; - _previousSelRange.length = 0; - - // In Google Docs in Safari when there is an existing selection, the inserted characters - // stays selected. The following clears the selection. - if (_legacyMode && _forceRemoveSelectionInGoogleDocs) { - if ([self.AppDelegate debugMode]) - NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); - ProcessSerialNumber psn; - GetFrontProcess(&psn); - - CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, true); - //set shift and command keys down for above event - CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); - CGEventPostToPSN(&psn, event); - CFRelease(event); - - event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, false); - CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); - CGEventPostToPSN(&psn, event); - CFRelease(event); - } + [self replaceExistingSelectionIn:sender with:output]; } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h new file mode 100644 index 0000000000..d3bb1e0e9d --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h @@ -0,0 +1,37 @@ +// +// KMInputMethodEventHandlerProtected.h +// Keyman4MacIM +// +// Created by tom on 1/5/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#ifndef KMInputMethodEventHandlerProtected_h +#define KMInputMethodEventHandlerProtected_h + +@interface KMInputMethodEventHandler () +//@property (nonatomic) NSMutableDictionary *kbData; +//@property (nonatomic) NSDictionary *kmModes; +@property (assign) BOOL willDeleteNullChar; +@property (assign) BOOL contextOutOfDate; +// This flag indicates which mode is being used for replacing already typed text when composing characters +// (i.e., when not using deadkeys). Some apps (and some javascript-based websites, such as Google Docs) do +// not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" +// apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now +// ready to insert the composed text. +@property (assign) BOOL legacyMode; +// Some clients (e.g. Chrome) handle the mouse down events before we get a crack at them. For such apps that +// are able to report their current selection location (LibreOffice can't even do that!), we can do some +// checking at the start of the event processing to see if we're probably still in the same place where we +// left off previously. +@property (assign) BOOL clientSelectionCanChangeUnexpectedly; // REVIEW: Maybe we can get notification from these clients by handling mouseDownOnCharacterIndex. +@property (assign) BOOL insertCharactersIndividually; +@property (assign) BOOL cannnotTrustSelectionLength; + +- (void)checkContextIn:(id)client; +- (void)replaceExistingSelectionIn:(id)client with:(NSString *) text; +- (KMInputMethodAppDelegate *)AppDelegate; +- (NSMutableString *)contextBuffer; +@end + +#endif /* KMInputMethodEventHandlerProtected_h */ From e716f420383667b4f779a8737a3a3921490b44e5 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Mon, 8 Jan 2018 15:12:39 -0500 Subject: [PATCH 04/13] Google Chrome pending chars logic -> BrowserClientEventHandler. Moved special logic for dealing with pending character buffer in Google Chrome into the BrowserClientEventHandler. --- .../KMInputMethodBrowserClientEventHandler.m | 39 ++++++++++-- .../Keyman4MacIM/KMInputMethodEventHandler.m | 62 ++++++++----------- .../KMInputMethodEventHandlerProtected.h | 5 ++ 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index 2d98847a1d..8cb1b3210e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -19,15 +19,21 @@ NSUInteger _failuresToRetrieveExpectedContext; BOOL _forceRemoveSelectionInGoogleDocs; //BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; +BOOL _googleChrome; +BOOL _insertCharactersIndividually; - (instancetype)initWithClient:(NSString *)clientAppId { self = [super initWithClient:clientAppId]; if (self) { + BOOL safari = [clientAppId isEqual: @"com.apple.Safari"]; + _googleChrome = [clientAppId isEqual: @"com.google.Chrome"]; _failuresToRetrieveExpectedContext = 0; - _forceRemoveSelectionInGoogleDocs = - //_explicitlyDeleteExistingSelectionBeforeInserting = - [clientAppId isEqual: @"com.apple.Safari"]; + _forceRemoveSelectionInGoogleDocs = safari; + self.insertCharactersIndividually = NO; + //_explicitlyDeleteExistingSelectionBeforeInserting = safari; + if (safari) + self.clientSelectionCanChangeUnexpectedly = NO; } return self; } @@ -70,7 +76,7 @@ BOOL _forceRemoveSelectionInGoogleDocs; self.cannnotTrustSelectionLength = YES; self.clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. // Google docs in Chrome allows only a single character at a time :-( - self.insertCharactersIndividually = YES; + _insertCharactersIndividually = _googleChrome; } } } @@ -102,7 +108,7 @@ BOOL _forceRemoveSelectionInGoogleDocs; [super replaceExistingSelectionIn:client with:text]; // In Google Docs in Safari when there is an existing selection, the inserted characters - // stays selected. The following clears the selection. + // stay selected. The following clears the selection. if (self.legacyMode && _forceRemoveSelectionInGoogleDocs) { if ([self.AppDelegate debugMode]) NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); @@ -121,4 +127,27 @@ BOOL _forceRemoveSelectionInGoogleDocs; CFRelease(event); } } + +- (void)insertPendingBufferTextIn:(id)client { + NSUInteger length = 0; + + if (!_insertCharactersIndividually) { + length = [self pendingBuffer].length; + if (length > 1) { + [super insertPendingBufferTextIn:client]; + return; + } + } + if ([self.AppDelegate debugMode]) { + NSLog(@"Using special Google Docs in Chrome logic"); + } + NSString* remainingText = [self.pendingBuffer substringFromIndex:1]; + [self.pendingBuffer deleteLastNChars:length - 1]; + + [super insertPendingBufferTextIn:client]; + + // Reset the pending buffer to contain remaining characters and issue call to come back for more... + [self setPendingBuffer:remainingText]; + [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; +} @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 06e82fb0be..1693454704 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -100,6 +100,25 @@ NSRange _previousSelRange; _previousSelRange.length = 0; } +- (void)insertPendingBufferTextIn:(id)client { + NSUInteger length = [self pendingBuffer].length; + if (!length) { + if ([self.AppDelegate debugMode]) + NSLog(@"Error - expected text in pending buffer!"); + 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:@""]; +} + - (KMInputMethodAppDelegate *)AppDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } @@ -217,48 +236,17 @@ NSRange _previousSelRange; if (_legacyMode && event.keyCode == kProcessPendingBuffer) { - if ([self.AppDelegate debugMode]) + if ([self.AppDelegate debugMode]) { NSLog(@"Processing the special %hu code", kProcessPendingBuffer); - - NSUInteger length = [self pendingBuffer].length; - if (length > 0) { - if ([self.AppDelegate debugMode]) { + + NSUInteger length = [self pendingBuffer].length; + if (length > 0) { for (NSUInteger ich = 0; ich < length; ich++) NSLog(@"Char %li: '%x'", ich, [[self pendingBuffer] characterAtIndex:ich]); } - NSString* text; - if (length > 1 && _insertCharactersIndividually) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Using special Google Docs in Chrome logic"); - } - unichar chars[1]; - chars[0] = [[self pendingBuffer] characterAtIndex:0]; - text = [[NSString alloc] initWithCharacters:chars length:1]; - } - else { - text = [self pendingBuffer]; - } - - if ([self.AppDelegate debugMode]) - NSLog(@"Inserting text from pending buffer: \"%@\"", text); - - [sender insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _previousSelRange.location += text.length; - _previousSelRange.length = 0; - - if (length > 1 && _insertCharactersIndividually) { - // Come back for more... - [_pendingBuffer deleteCharactersInRange:NSMakeRange(0, 1)]; - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:sender afterDelay:0.1]; - } - else { - [self setPendingBuffer:@""]; - } - } - else { - if ([self.AppDelegate debugMode]) - NSLog(@"Error - expected text in pending buffer!"); } + + [self insertPendingBufferTextIn:sender]; return YES; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h index d3bb1e0e9d..13b6b4761a 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h @@ -30,8 +30,13 @@ - (void)checkContextIn:(id)client; - (void)replaceExistingSelectionIn:(id)client with:(NSString *) text; +- (void)insertPendingBufferTextIn:(id)client; - (KMInputMethodAppDelegate *)AppDelegate; - (NSMutableString *)contextBuffer; +// Return the pending buffer. If it is NIL create it. +-(NSMutableString*)pendingBuffer; +-(void)setPendingBuffer:(NSString*)string; +- (void)initiatePendingBufferProcessing:(id)sender; @end #endif /* KMInputMethodEventHandlerProtected_h */ From 3ef48599f696063b171ef500d3a2482b3b5ba3dc Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Mon, 8 Jan 2018 21:35:28 -0500 Subject: [PATCH 05/13] Cmd-A in Safari, insert chars individually in Chrome Added logic to correct bug in Safari Google Docs introduced a few months back to clear selection after replacement. Refactored code and fixed bug to correctly handle Chrome's need to replace characters individually. --- .../KMInputMethodBrowserClientEventHandler.m | 39 ++++++++++++------- .../Keyman4MacIM/KMInputMethodEventHandler.m | 23 ++++++----- .../KMInputMethodEventHandlerProtected.h | 1 + 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index 8cb1b3210e..ecb63167bd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -21,6 +21,7 @@ BOOL _forceRemoveSelectionInGoogleDocs; //BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; BOOL _googleChrome; BOOL _insertCharactersIndividually; +BOOL _preserveContextForNextCmdA; - (instancetype)initWithClient:(NSString *)clientAppId { @@ -31,6 +32,7 @@ BOOL _insertCharactersIndividually; _failuresToRetrieveExpectedContext = 0; _forceRemoveSelectionInGoogleDocs = safari; self.insertCharactersIndividually = NO; + _preserveContextForNextCmdA = NO; //_explicitlyDeleteExistingSelectionBeforeInserting = safari; if (safari) self.clientSelectionCanChangeUnexpectedly = NO; @@ -38,6 +40,15 @@ BOOL _insertCharactersIndividually; return self; } +- (void)handleCommand:(NSEvent *)event { + // If Safari issued a Cmd-A to remove the selection, we know what the + // context should be, so keep it. + if ([event.characters isEqualTo:@"a"] && _preserveContextForNextCmdA) + _preserveContextForNextCmdA = NO; + else + [super handleCommand:event]; +} + - (void)checkContextIn:(id) client { if (!self.willDeleteNullChar && !self.contextOutOfDate && _failuresToRetrieveExpectedContext < 3) { if (self.AppDelegate.debugMode) { @@ -112,6 +123,7 @@ BOOL _insertCharactersIndividually; if (self.legacyMode && _forceRemoveSelectionInGoogleDocs) { if ([self.AppDelegate debugMode]) NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); + _preserveContextForNextCmdA = YES; ProcessSerialNumber psn; GetFrontProcess(&psn); @@ -129,25 +141,24 @@ BOOL _insertCharactersIndividually; } - (void)insertPendingBufferTextIn:(id)client { - NSUInteger length = 0; + NSString* remainingText = @""; if (!_insertCharactersIndividually) { - length = [self pendingBuffer].length; + NSUInteger length = [self pendingBuffer].length; if (length > 1) { - [super insertPendingBufferTextIn:client]; - return; + if ([self.AppDelegate debugMode]) + NSLog(@"Using special Google Docs in Chrome logic (length = %lu)", length); + remainingText = [self.pendingBuffer substringFromIndex:1]; + [self.pendingBuffer deleteLastNChars:length - 1]; } } - if ([self.AppDelegate debugMode]) { - NSLog(@"Using special Google Docs in Chrome logic"); - } - NSString* remainingText = [self.pendingBuffer substringFromIndex:1]; - [self.pendingBuffer deleteLastNChars:length - 1]; - + [super insertPendingBufferTextIn:client]; - - // Reset the pending buffer to contain remaining characters and issue call to come back for more... - [self setPendingBuffer:remainingText]; - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; + + if ([remainingText length] > 0) { + // Reset the pending buffer to contain remaining characters and issue call to come back for more... + [self setPendingBuffer:remainingText]; + [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; + } } @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 1693454704..4d92999eb4 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -90,6 +90,19 @@ NSRange _previousSelRange; } } +- (void)handleCommand:(NSEvent *)event { + // There are a bunch of common navigation/selection command-key combinations, but individual + // apps may implement specific commands that also change the selection. There is probably no + // situation where a user could reasonably hope that any dead-keys typed before using a + // command shortcut would be remembered, so other than an insignificant performance penalty, + // the only downside to treating all commands as having the potential to change the selection + // is that some "legacy" apps can't get their context at all. This can be overridden so that + // legacy apps can mitigate this problem as appropriate. + if ([self.AppDelegate debugMode]) + NSLog(@"Command key - context needs to be re-gotten."); + _contextOutOfDate = YES; +} + - (void)checkContextIn:(id)client { // Base implementation is no-op } @@ -222,15 +235,7 @@ NSRange _previousSelRange; else if (event.type != NSKeyDown) return NO; // We ignore NSLeftMouseDragged events (because we'll eventually get a mouse-up). else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { - // There are a bunch of common navigation/selection command-key combinations, but individual - // apps may implement specifc commands that also chnage the selection. There is probably no - // situation where a user could reasonably hope that any dead-keys typed before using a - // command shortcut would be remembered, so other than an insignificant performance penalty, - // the only downside to treating all commands as having the potential to change the selection - // is that some "legacy" apps can't get their context at all. - if ([self.AppDelegate debugMode]) - NSLog(@"Command key - context needs to be re-gotten."); - _contextOutOfDate = YES; + [self handleCommand:event]; return NO; // We let the client app handle all Command-key events. } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h index 13b6b4761a..ebf05bc7d9 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h @@ -28,6 +28,7 @@ @property (assign) BOOL insertCharactersIndividually; @property (assign) BOOL cannnotTrustSelectionLength; +- (void)handleCommand:(NSEvent *)event; - (void)checkContextIn:(id)client; - (void)replaceExistingSelectionIn:(id)client with:(NSString *) text; - (void)insertPendingBufferTextIn:(id)client; From 0f091f97052ae1f1c3935fdf6f4c9203d5ef8644 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Mon, 8 Jan 2018 22:22:57 -0500 Subject: [PATCH 06/13] Commented out code for firefox Added (and then commented out) code to have Firefox pretend not to change selection without reporting. Ths code improves some things but makes other things worse. I need to compare it to behavior before refactoring. --- .../Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index ecb63167bd..a191cf85ad 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -34,8 +34,10 @@ BOOL _preserveContextForNextCmdA; self.insertCharactersIndividually = NO; _preserveContextForNextCmdA = NO; //_explicitlyDeleteExistingSelectionBeforeInserting = safari; - if (safari) + if (safari /*|| + [clientAppId isEqual: @"org.mozilla.firefox"]*/) { self.clientSelectionCanChangeUnexpectedly = NO; + } } return self; } From 50cd075d038a24078be692d502785e1b276dfc8f Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Tue, 9 Jan 2018 15:52:14 -0500 Subject: [PATCH 07/13] Split browser-specific code into separate subclasses Safari and Chrome get their own subclasses, but I need to compare performance. --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 14 ++ .../Keyman4MacIM/KMInputController.m | 14 +- .../KMInputMethodBrowserClientEventHandler.h | 8 +- .../KMInputMethodBrowserClientEventHandler.m | 122 +++--------------- ...MethodBrowserClientEventHandlerProtected.h | 18 +++ .../KMInputMethodChromeClientEventHandler.h | 17 +++ .../KMInputMethodChromeClientEventHandler.m | 55 ++++++++ .../Keyman4MacIM/KMInputMethodEventHandler.m | 70 +++++----- .../KMInputMethodEventHandlerProtected.h | 5 +- .../KMInputMethodSafariClientEventHandler.h | 17 +++ .../KMInputMethodSafariClientEventHandler.m | 86 ++++++++++++ 11 files changed, 278 insertions(+), 148 deletions(-) create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.h create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index e343810fb0..aec06cf552 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -52,6 +52,8 @@ 98FE10641B4DEE5600525F54 /* KMInfoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98FE10621B4DEE5600525F54 /* KMInfoWindowController.xib */; }; E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; E21799061FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; + E22020F6200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E22020F5200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m */; }; + E22020F8200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E22020F7200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m */; }; E24C79EA1FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */; }; /* End PBXBuildFile section */ @@ -150,6 +152,11 @@ 98FE10621B4DEE5600525F54 /* KMInfoWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = KMInfoWindowController.xib; path = Keyman4MacIM/KMInfoWindow/KMInfoWindowController.xib; sourceTree = ""; }; E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = ""; }; E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = ""; }; + E22020F3200505EF00B74FAC /* KMInputMethodSafariClientEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodSafariClientEventHandler.h; sourceTree = ""; }; + E22020F42005069300B74FAC /* KMInputMethodChromeClientEventHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMInputMethodChromeClientEventHandler.h; sourceTree = ""; }; + E22020F5200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodSafariClientEventHandler.m; sourceTree = ""; }; + E22020F7200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodChromeClientEventHandler.m; sourceTree = ""; }; + E22020F920050D6300B74FAC /* KMInputMethodBrowserClientEventHandlerProtected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMInputMethodBrowserClientEventHandlerProtected.h; sourceTree = ""; }; E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodBrowserClientEventHandler.m; sourceTree = ""; }; E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodBrowserClientEventHandler.h; sourceTree = ""; }; E24C79EC1FFFEA6B00D8E46F /* KMInputMethodEventHandlerProtected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandlerProtected.h; sourceTree = ""; }; @@ -284,9 +291,14 @@ 98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */, E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */, E24C79EC1FFFEA6B00D8E46F /* KMInputMethodEventHandlerProtected.h */, + E22020F920050D6300B74FAC /* KMInputMethodBrowserClientEventHandlerProtected.h */, E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */, + E22020F3200505EF00B74FAC /* KMInputMethodSafariClientEventHandler.h */, + E22020F42005069300B74FAC /* KMInputMethodChromeClientEventHandler.h */, E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */, + E22020F7200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m */, + E22020F5200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m */, 98E672961B532DE000DBDE2F /* KMKeyboardHelpWindow */, 989C9C141A7876DE00A20425 /* MainMenu.xib */, 984B8F3E1AF1C2F400E096A8 /* OnScreenKeyboard */, @@ -543,8 +555,10 @@ 98FE10631B4DEE5600525F54 /* KMInfoWindowController.m in Sources */, 9836B3721AE5F11D00780482 /* unzip.c in Sources */, 9832472E1A9EABC70010B90C /* KMConfigColumn1CellView.m in Sources */, + E22020F6200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m in Sources */, E24C79EA1FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m in Sources */, 98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */, + E22020F8200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m in Sources */, 9836B3741AE5F11D00780482 /* ZipArchive.mm in Sources */, 983247B21A9EB9690010B90C /* KMConfigColumn3CellView.m in Sources */, 98BF92521BF03D4D0002126A /* KMAboutBGView.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 90856d0414..1a472b6d21 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -9,6 +9,8 @@ #import "KMInputController.h" #import "KMInputMethodEventHandler.h" #import "KMInputMethodBrowserClientEventHandler.h" +#import "KMInputMethodSafariClientEventHandler.h" +#import "KMInputMethodChromeClientEventHandler.h" #include /* For kVK_ constants. */ @implementation KMInputController @@ -58,10 +60,14 @@ KMInputMethodEventHandler* _eventHandler; NSLog(@"New active app %@", clientAppId); // Most things in Safari work well using the normal way, but Google Docs doesn't. - if ([clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId isEqual: @"com.apple.Safari"] || - [clientAppId isEqual: @"org.mozilla.firefox"]) { - _eventHandler = [[KMInputMethodBrowserClientEventHandler alloc] initWithClient:clientAppId]; + if ([clientAppId isEqual: @"com.apple.Safari"]) { + _eventHandler = [[KMInputMethodSafariClientEventHandler alloc] init]; + } + else if ([clientAppId isEqual: @"org.mozilla.firefox"]) { + _eventHandler = [[KMInputMethodBrowserClientEventHandler alloc] init]; + } + else if ([clientAppId isEqual: @"com.google.Chrome"]) { + _eventHandler = [[KMInputMethodChromeClientEventHandler alloc] init]; } else _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h index f3aef12a75..f04233322b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.h @@ -4,17 +4,17 @@ // This class is used for browser clients because certain javascript-based websites, such as Google Docs) do // not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" // apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now -// ready to insert the composed text . +// ready to insert the composed text. // // Created by tom on 1/5/18. // Copyright © 2018 SIL International. All rights reserved. // -#ifndef KMInputMethodLegacyClientEventHandler_h -#define KMInputMethodLegacyClientEventHandler_h +#ifndef KMInputMethodBrowserClientEventHandler_h +#define KMInputMethodBrowserClientEventHandler_h #import "KMInputMethodEventHandler.h" @interface KMInputMethodBrowserClientEventHandler : KMInputMethodEventHandler @end -#endif /* KMInputMethodLegacyClientEventHandler_h */ +#endif /* KMInputMethodBrowserClientEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index a191cf85ad..261f30c18b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -8,47 +8,24 @@ #import "KMInputMethodBrowserClientEventHandler.h" #import "KMInputMethodEventHandlerProtected.h" +#import "KMInputMethodBrowserClientEventHandlerProtected.h" @implementation KMInputMethodBrowserClientEventHandler // Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox), we want to -// try to detect it and: -// in Safari, switch to legacy mode// in Chrome, NOT assume that it needs to re-get the context every time around (which means that if the user -// does mouse-click somewhere else, it could lead to bad behaviour). -// in Firefox, we're already in legacy mode and we do get mouse clicks, so we're already doing the best we can. +// try to detect it so each browser can respond appropriatey. NSUInteger _failuresToRetrieveExpectedContext; -BOOL _forceRemoveSelectionInGoogleDocs; -//BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; -BOOL _googleChrome; -BOOL _insertCharactersIndividually; -BOOL _preserveContextForNextCmdA; -- (instancetype)initWithClient:(NSString *)clientAppId { - - self = [super initWithClient:clientAppId]; - if (self) { - BOOL safari = [clientAppId isEqual: @"com.apple.Safari"]; - _googleChrome = [clientAppId isEqual: @"com.google.Chrome"]; - _failuresToRetrieveExpectedContext = 0; - _forceRemoveSelectionInGoogleDocs = safari; - self.insertCharactersIndividually = NO; - _preserveContextForNextCmdA = NO; - //_explicitlyDeleteExistingSelectionBeforeInserting = safari; - if (safari /*|| - [clientAppId isEqual: @"org.mozilla.firefox"]*/) { - self.clientSelectionCanChangeUnexpectedly = NO; - } - } - return self; +- (instancetype)init { + return [self initWithLegacyMode:YES clientSelectionCanChangeUnexpectedly:YES]; } -- (void)handleCommand:(NSEvent *)event { - // If Safari issued a Cmd-A to remove the selection, we know what the - // context should be, so keep it. - if ([event.characters isEqualTo:@"a"] && _preserveContextForNextCmdA) - _preserveContextForNextCmdA = NO; - else - [super handleCommand:event]; +- (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly { + self = [super initWithLegacyMode:legacy clientSelectionCanChangeUnexpectedly: flagClientSelectionCanChangeUnexpectedly]; + if (self) { + _failuresToRetrieveExpectedContext = 0; + } + return self; } - (void)checkContextIn:(id) client { @@ -81,86 +58,19 @@ BOOL _preserveContextForNextCmdA; if (_failuresToRetrieveExpectedContext == 3) { if ([self AppDelegate].debugMode) { - NSLog(@"Detected Google Docs or some other editor that can't provide context. Using legacy mode."); + NSLog(@"Detected Google Docs or some other editor that can't provide context."); } _failuresToRetrieveExpectedContext = NSUIntegerMax; - self.legacyMode = YES; - if (self.clientSelectionCanChangeUnexpectedly) { - self.cannnotTrustSelectionLength = YES; - self.clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. - // Google docs in Chrome allows only a single character at a time :-( - _insertCharactersIndividually = _googleChrome; - } + [self setInGoogleDocs]; } } } } -- (void)replaceExistingSelectionIn:(id)client with:(NSString *) text { - // The following commented out code is the code that would make it possible to type over an - // existing selection in Word in Safari, but I don't know of a way to distinguish that case - // from other contexts in Safari (wherein this code inserts an extra leading space). - // if (_explicitlyDeleteExistingSelectionBeforeInserting) { - // NSUInteger selLength = [sender selectedRange].length; - // if (selLength > 0 && selLength != NSNotFound) { - // if ([self.AppDelegate debugMode]) { - // NSLog(@"Attempting to delete existing selection of length = %lu by replacing it with a space (which should not actually appear in the text).", selLength); - // } - // - // [sender insertText:@" " replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - // - // if ([self.AppDelegate debugMode]) - // NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); - // - // CGEventSourceRef sourceFromEvent = CGEventCreateSourceFromEvent([event CGEvent]); - // [self postKeyPressToFrontProcess:event.keyCode from:sourceFromEvent]; - // CFRelease(sourceFromEvent); - // return YES; - // } - // } - [super replaceExistingSelectionIn:client with:text]; - - // In Google Docs in Safari when there is an existing selection, the inserted characters - // stay selected. The following clears the selection. - if (self.legacyMode && _forceRemoveSelectionInGoogleDocs) { - if ([self.AppDelegate debugMode]) - NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); - _preserveContextForNextCmdA = YES; - ProcessSerialNumber psn; - GetFrontProcess(&psn); - - CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, true); - //set shift and command keys down for above event - CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); - CGEventPostToPSN(&psn, event); - CFRelease(event); - - event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, false); - CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); - CGEventPostToPSN(&psn, event); - CFRelease(event); - } -} - -- (void)insertPendingBufferTextIn:(id)client { - - NSString* remainingText = @""; - if (!_insertCharactersIndividually) { - NSUInteger length = [self pendingBuffer].length; - if (length > 1) { - if ([self.AppDelegate debugMode]) - NSLog(@"Using special Google Docs in Chrome logic (length = %lu)", length); - remainingText = [self.pendingBuffer substringFromIndex:1]; - [self.pendingBuffer deleteLastNChars:length - 1]; - } - } - - [super insertPendingBufferTextIn:client]; - - if ([remainingText length] > 0) { - // Reset the pending buffer to contain remaining characters and issue call to come back for more... - [self setPendingBuffer:remainingText]; - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; +- (void)setInGoogleDocs { + if (self.clientSelectionCanChangeUnexpectedly) { + self.cannnotTrustSelectionLength = YES; + self.clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. } } @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h new file mode 100644 index 0000000000..e1486c65cd --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h @@ -0,0 +1,18 @@ +// +// KMInputMethodBrowserClientEventHandlerProtected.h +// Keyman4MacIM +// +// Created by tom on 1/9/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#ifndef KMInputMethodEventBrowserClientHandlerProtected_h +#define KMInputMethodEventBrowserClientHandlerProtected_h + +@interface KMInputMethodBrowserClientEventHandler () + +- (void)setInGoogleDocs; + +@end + +#endif /* KMInputMethodEventBrowserClientHandlerProtected_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h new file mode 100644 index 0000000000..7c2d40eb59 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h @@ -0,0 +1,17 @@ +// +// KMInputMethodChromeClientEventHandler.h +// Keyman4MacIM +// This class is used for the Google Chrome browser to get specific behavior needed to improve behavior for some sites. +// +// Created by tom on 1/9/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#ifndef KMInputMethodChromeClientEventHandler_h +#define KMInputMethodChromeClientEventHandler_h +#import "KMInputMethodBrowserClientEventHandler.h" + +@interface KMInputMethodChromeClientEventHandler : KMInputMethodBrowserClientEventHandler +@end + +#endif /* KMInputMethodChromeClientEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m new file mode 100644 index 0000000000..93bcf7dc3c --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m @@ -0,0 +1,55 @@ +// +// KMInputMethodChromeClientEventHandler.m +// Keyman +// +// Created by tom on 1/9/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#import "KMInputMethodChromeClientEventHandler.h" +#import "KMInputMethodEventHandlerProtected.h" +#import "KMInputMethodBrowserClientEventHandlerProtected.h" + +@implementation KMInputMethodChromeClientEventHandler + +// Because Google Docs can't report its context, if we detect that we are in Google Docs, we change to +// NOT assume that it needs to re-get the context every time around (which means that if the user +// does mouse-click somewhere else, it could lead to bad behaviour). +BOOL _insertCharactersIndividually; + +- (instancetype)init { + self = [super init]; + if (self) { + _insertCharactersIndividually = NO; + } + return self; +} + +- (void)setInGoogleDocs { + [super setInGoogleDocs]; + // Google docs in Chrome allows only a single character at a time :-( + _insertCharactersIndividually = YES; +} + +- (void)insertPendingBufferTextIn:(id)client { + + NSString* remainingText = @""; + if (!_insertCharactersIndividually) { + NSUInteger length = [self pendingBuffer].length; + if (length > 1) { + if ([self.AppDelegate debugMode]) + NSLog(@"Using special Google Docs in Chrome logic (length = %lu)", length); + remainingText = [self.pendingBuffer substringFromIndex:1]; + [self.pendingBuffer deleteLastNChars:length - 1]; + } + } + + [super insertPendingBufferTextIn:client]; + + if ([remainingText length] > 0) { + // Reset the pending buffer to contain remaining characters and issue call to come back for more... + [self setPendingBuffer:remainingText]; + [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; + } +} +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 4d92999eb4..b32518e7f2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -21,50 +21,54 @@ CGEventSourceRef _sourceFromOriginalEvent = nil; NSRange _previousSelRange; -- (instancetype)initWithClient:(NSString *)clientAppId { - +// Protected initializer for use by subclasses +- (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly { self = [super init]; if (self) { _previousSelRange = NSMakeRange(NSNotFound, NSNotFound); - _clientSelectionCanChangeUnexpectedly = YES; + _clientSelectionCanChangeUnexpectedly = flagClientSelectionCanChangeUnexpectedly; _cannnotTrustSelectionLength = NO; _insertCharactersIndividually = NO; - // REVIEW: Should this list be in a info.plist file - // Use a table of known apps to decide whether or not to operate in legacy mode - // and whether or not to follow calls to setMarkedText with calls to insertText. - if ([clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId isEqual: @"org.mozilla.firefox"] || - [clientAppId isEqual: @"com.github.atom"] || + _legacyMode = NO; + _contextOutOfDate = YES; + if (legacy) { + [self switchToLegacyMode]; + } + } + return self; +} + +// This is the public initializer. +- (instancetype)initWithClient:(NSString *)clientAppId { + BOOL legacy = ([clientAppId isEqual: @"com.github.atom"] || [clientAppId isEqual: @"com.collabora.libreoffice-free"] || [clientAppId isEqual: @"com.axosoft.gitkraken"] || [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || [clientAppId isEqual: @"com.microsoft.Word"] - /*||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems */) { - _legacyMode = YES; - if ([self.AppDelegate debugMode]) - NSLog(@"Using legacy mode for this app."); - } - else { - _legacyMode = NO; - } - - // if ([clientAppId isEqual: @"com.google.Chrome"] || - // [clientAppId isEqual: @"com.apple.Terminal"] || - // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { - // _clientSelectionCanChangeUnexpectedly = YES; - // } - - if ([clientAppId isEqual: @"com.github.atom"]) { - // This isn't true (the context can change unexpectedly), but we can't get the context, - // so we pretend/hope it won't. - _clientSelectionCanChangeUnexpectedly = NO; - } - - _contextOutOfDate = YES; - } - return self; + /*||[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 + // like "disregardPossibleSelectionChanges". + // if ([clientAppId isEqual: @"com.google.Chrome"] || + // [clientAppId isEqual: @"com.apple.Terminal"] || + // [clientAppId isEqual: @"com.apple.dt.Xcode"]) { + // _clientSelectionCanChangeUnexpectedly = YES; + // } + + // 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"]); + return [self initWithLegacyMode:legacy clientSelectionCanChangeUnexpectedly:selectionCanChangeUnexpectedly]; +} + +- (void)switchToLegacyMode { + _legacyMode = YES; + if ([self.AppDelegate debugMode]) + NSLog(@"Using legacy mode for this app."); } - (void)deactivate { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h index ebf05bc7d9..54d0fa1221 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h @@ -19,7 +19,7 @@ // not properly deal with calls to insertText calls that replace a range of characters. So for these "legacy" // apps, we post one or more deletes (i.e., backspace), followed by a special code that tells us we're now // ready to insert the composed text. -@property (assign) BOOL legacyMode; +@property BOOL legacyMode; // Some clients (e.g. Chrome) handle the mouse down events before we get a crack at them. For such apps that // are able to report their current selection location (LibreOffice can't even do that!), we can do some // checking at the start of the event processing to see if we're probably still in the same place where we @@ -28,7 +28,10 @@ @property (assign) BOOL insertCharactersIndividually; @property (assign) BOOL cannnotTrustSelectionLength; +- (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. +- (void)switchToLegacyMode; - (void)checkContextIn:(id)client; - (void)replaceExistingSelectionIn:(id)client with:(NSString *) text; - (void)insertPendingBufferTextIn:(id)client; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.h new file mode 100644 index 0000000000..b65236ec83 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.h @@ -0,0 +1,17 @@ +// +// KMInputMethodSafariClientEventHandler.h +// Keyman4MacIM +// This class is used for the Safari browser to get specific behavior needed to improve behavior for some sites. +// +// Created by tom on 1/9/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#ifndef KMInputMethodSafariClientEventHandler_h +#define KMInputMethodSafariClientEventHandler_h +#import "KMInputMethodBrowserClientEventHandler.h" + +@interface KMInputMethodSafariClientEventHandler : KMInputMethodBrowserClientEventHandler +@end + +#endif /* KMInputMethodSafariClientEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m new file mode 100644 index 0000000000..1c1f4e1593 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m @@ -0,0 +1,86 @@ +// +// KMInputMethodSafariClientEventHandler.m +// Keyman +// +// Created by tom on 1/9/18. +// Copyright © 2018 SIL International. All rights reserved. +// + +#import "KMInputMethodSafariClientEventHandler.h" +#import "KMInputMethodEventHandlerProtected.h" +#import "KMInputMethodBrowserClientEventHandlerProtected.h" + +@implementation KMInputMethodSafariClientEventHandler + +// Because Google Docs can't report its context, if we detect that we are in Google Docs, we change to legacy mode. +//BOOL _explicitlyDeleteExistingSelectionBeforeInserting = NO; +BOOL _preserveContextForNextCmdA; + +- (instancetype)init { + self = [super initWithLegacyMode:NO clientSelectionCanChangeUnexpectedly:NO]; + if (self) { + _preserveContextForNextCmdA = NO; + //_explicitlyDeleteExistingSelectionBeforeInserting = YES; + } + return self; +} + +- (void)handleCommand:(NSEvent *)event { + // If Safari issued a Cmd-A to remove the selection, we know what the + // context should be, so keep it. + if ([event.characters isEqualTo:@"a"] && _preserveContextForNextCmdA) + _preserveContextForNextCmdA = NO; + else + [super handleCommand:event]; +} + +- (void)setInGoogleDocs { + [self switchToLegacyMode]; +} + +- (void)replaceExistingSelectionIn:(id)client with:(NSString *) text { + // The following commented out code is the code that would make it possible to type over an + // existing selection in Word in Safari, but I don't know of a way to distinguish that case + // from other contexts in Safari (wherein this code inserts an extra leading space). + // if (_explicitlyDeleteExistingSelectionBeforeInserting) { + // NSUInteger selLength = [sender selectedRange].length; + // if (selLength > 0 && selLength != NSNotFound) { + // if ([self.AppDelegate debugMode]) { + // NSLog(@"Attempting to delete existing selection of length = %lu by replacing it with a space (which should not actually appear in the text).", selLength); + // } + // + // [sender insertText:@" " replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + // + // if ([self.AppDelegate debugMode]) + // NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); + // + // CGEventSourceRef sourceFromEvent = CGEventCreateSourceFromEvent([event CGEvent]); + // [self postKeyPressToFrontProcess:event.keyCode from:sourceFromEvent]; + // CFRelease(sourceFromEvent); + // return YES; + // } + // } + [super replaceExistingSelectionIn:client with:text]; + + // In Google Docs (i.e., "legacy mode" when there is an existing selection, the inserted characters + // stay selected. The following clears the selection. + if (self.legacyMode) { + if ([self.AppDelegate debugMode]) + NSLog(@"Sending Command-Shift-A to clear selection in Google Docs"); + _preserveContextForNextCmdA = YES; + ProcessSerialNumber psn; + GetFrontProcess(&psn); + + CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, true); + //set shift and command keys down for above event + CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); + CGEventPostToPSN(&psn, event); + CFRelease(event); + + event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_A, false); + CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand); + CGEventPostToPSN(&psn, event); + CFRelease(event); + } +} +@end From 72cef42d25c892d386c15b33debca8405e69794f Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Thu, 11 Jan 2018 12:58:17 -0500 Subject: [PATCH 08/13] Broke up long methods Major refactoring to break long methods up by factoring out into meaningful sub-methods. Also removed small amount of unused code left over from previous refactoring step. --- .../Keyman4MacIM/KMInputMethodEventHandler.m | 499 +++++++++--------- .../KMInputMethodEventHandlerProtected.h | 1 - 2 files changed, 255 insertions(+), 245 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index b32518e7f2..46f5a2e216 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -28,7 +28,6 @@ NSRange _previousSelRange; _previousSelRange = NSMakeRange(NSNotFound, NSNotFound); _clientSelectionCanChangeUnexpectedly = flagClientSelectionCanChangeUnexpectedly; _cannnotTrustSelectionLength = NO; - _insertCharactersIndividually = NO; _legacyMode = NO; _contextOutOfDate = YES; if (legacy) { @@ -221,51 +220,9 @@ NSRange _previousSelRange; [buffer appendString:string]; } - -- (BOOL)handleEvent:(NSEvent *)event client:(id)sender { - // OSK key feedback from hardware keyboard is disabled - /*if (event.type == NSKeyDown) - [self.AppDelegate handleKeyEvent:event];*/ - - if (event.type == NSLeftMouseDown || event.type == NSLeftMouseUp ) { - // if (_clientSelectionCanChangeUnexpectedly) { - // if ([self.AppDelegate debugMode]) - // NSLog(@"WARNING: We are dealing with an app/context where we THINK we shouldn't be getting mouse events, but we just got one!"); - // _clientSelectionCanChangeUnexpectedly = NO; - // } - _contextOutOfDate = YES; - return NO; - } - else if (event.type != NSKeyDown) - return NO; // We ignore NSLeftMouseDragged events (because we'll eventually get a mouse-up). - else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { - [self handleCommand:event]; - return NO; // We let the client app handle all Command-key events. - } - - if (_legacyMode && event.keyCode == kProcessPendingBuffer) - { - 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++) - NSLog(@"Char %li: '%x'", ich, [[self pendingBuffer] characterAtIndex:ich]); - } - } - - [self insertPendingBufferTextIn:sender]; - return YES; - } - - [self checkContextIn:sender]; - +- (void)updateContextBufferIfNeeded:(id)client { if ([self.AppDelegate debugMode]) { - if (_clientSelectionCanChangeUnexpectedly) - NSLog(@"_clientSelectionCanChangeUnexpectedly = YES"); - else - NSLog(@"_clientSelectionCanChangeUnexpectedly = NO"); + NSLog(@"_clientSelectionCanChangeUnexpectedly = %@", _clientSelectionCanChangeUnexpectedly ? @"YES" : @"NO"); } // The following is needed because some clients (e.g. Chrome & Terminal) handle mouse down events and we // never get a crack at them: @@ -275,7 +232,7 @@ NSRange _previousSelRange; // the context buffer. If not, then reset the context buffer. if (_clientSelectionCanChangeUnexpectedly && _numberOfPostedDeletesToExpect == 0 && (_pendingBuffer == nil || _pendingBuffer.length == 0)) { - NSRange currentSelRange = [sender selectedRange]; + NSRange currentSelRange = [client selectedRange]; if (currentSelRange.location == NSNotFound) { _clientSelectionCanChangeUnexpectedly = NO; @@ -290,31 +247,36 @@ NSRange _previousSelRange; NSLog(@"The following cannot be trusted and will be ignored:"); NSLog(@" currentSelRange.length = %lu", currentSelRange.length); } - [self updateContextBuffer:sender]; + [self updateContextBuffer:client]; } } if (_contextOutOfDate) - [self updateContextBuffer:sender]; - - BOOL handled = NO; + [self updateContextBuffer:client]; +} + +- (BOOL) handleKeymanEngineActions:(NSEvent *)event in:(id) sender { BOOL deleteBackPosted = NO; NSArray *actions = nil; - if (!self.willDeleteNullChar) { + if (![self willDeleteNullChar]) { actions = [self.kme processEvent:event]; + if (actions.count == 0) + return NO; } + else + return NO; if ([self.AppDelegate debugMode]) { - NSLog(@"sender type = %@", NSStringFromClass([sender class])); - NSLog(@"sender selection range location = %lu", [sender selectedRange].location); NSLog(@"actions = %@", actions); } for (NSDictionary *action in actions) { NSString *actionType = [[action allKeys] objectAtIndex:0]; + if ([self.AppDelegate debugMode]) { + NSLog(@"Handling %@ action...", actionType); + NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); + } + if ([actionType isEqualToString:Q_STR]) { - if ([self.AppDelegate debugMode]) - NSLog(@"About to start handling Q_STR action..."); - NSString *output = [action objectForKey:actionType]; if ([self.AppDelegate debugMode]) NSLog(@"output = %@", output); @@ -355,11 +317,6 @@ NSRange _previousSelRange; [self.contextBuffer appendString:output]; } else if ([actionType isEqualToString:Q_BACK]) { - if ([self.AppDelegate debugMode]) - { - NSLog(@"About to start handling Q_BACK action..."); - NSLog(@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"); - } [self.contextBuffer deleteLastNullChars]; NSUInteger dk = [self.contextBuffer deleteLastDeadkeys]; NSInteger n = [[action objectForKey:actionType] integerValue] - dk; @@ -371,120 +328,7 @@ NSRange _previousSelRange; // n is now the number of characters to delete from the client. if (n > 0) { - if ([self.AppDelegate debugMode]) - { - NSLog(@"Q_BACK"); - NSLog(@"dk = %lu", dk); - NSLog(@"n = %li", n); - NSLog(@"dc = %lu", dc); - if (_legacyMode) - NSLog(@"Using Legacy mode."); - else - NSLog(@"Using Apple IM-compliant mode."); - } - NSRange selectedRange = [sender selectedRange]; - NSInteger pos = selectedRange.location; - if (_legacyMode && self.contextBuffer != nil && (pos == 0 || pos == NSNotFound)) { - pos = self.contextBuffer.length + n; - } - if ([self.AppDelegate debugMode]) - NSLog(@"pos = %lu", pos); - - if (!_legacyMode && 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 - // comining marks on there own via insertText. One potentially negative implication - // of this is that if the script should happen to contain characters whose class is - // not known, it will skip over them and keep looking, so it could end up using a - // longer string of characters than otherwise necessary. This could result in a - // mildly jarring visual experience for the user if the app refreshes the diplay - // between the time the characters are removed and re-inserted. But presumbly this - // algorithm will eventually find either a known base character or get all the way back - // to the start of the context, so if it doesn't find a known base character, it will - // fall back to just attempting the insert with whatever it does find. I believe this - // will always work and should at least work as reliably as the old version of the code, - // 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--) { - preChar = [[sender attributedSubstringFromRange:NSMakeRange(preCharPos, nbrOfPreCharacters)] string]; - if (!preChar) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Client apparently doesn't implement attributedSubstringFromRange. Attempting to get preChar from context..."); - } - if (self.contextBuffer != nil && preCharPos < self.contextBuffer.length) { - preChar = [self.contextBuffer substringWithRange:NSMakeRange(preCharPos, 1)]; - } - if (!preChar) - break; - } - if ([self.AppDelegate debugMode]) - NSLog(@"Testing preChar: %@", preChar); - - if ([regexNonCombiningMark numberOfMatchesInString:preChar options:NSMatchingAnchored range:NSMakeRange(0, 1)] > 0) - break; - if (preCharPos == 0) { - if ([self.AppDelegate debugMode]) { - NSLog(@"Failed to find a base character!"); - } - break; - } - if ([self.AppDelegate debugMode]) { - NSLog(@"Have not yet found a base character. nbrOfPreCharacters = %lu", nbrOfPreCharacters); - } - } - if (preChar) { - if ([self.AppDelegate debugMode]) { - NSLog(@"preChar (to insert at %lu) = \"%@\"", preCharPos, preChar); - } - [sender insertText:preChar replacementRange:NSMakeRange(preCharPos, n+nbrOfPreCharacters)]; - } - else { - if ([self.AppDelegate debugMode]) { - NSLog(@"Switching to legacy mode - client apparently doesn't implement attributedSubstringFromRange and no previous character in context buffer."); - } - _legacyMode = YES; // client apparently doesn't implement attributedSubstringFromRange. - } - } - else { - if ([self.AppDelegate debugMode]) - NSLog(@"No previous character to use for replacement - replacing range with space"); - [sender insertText:@" " replacementRange:NSMakeRange(pos - n, n)]; - [self.contextBuffer appendNullChar]; - } - } - if (_legacyMode && 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]) { - NSLog(@"Legacy mode: calling deleteBack"); - if (_cannnotTrustSelectionLength) - NSLog(@"Cannot trust client to report accurate selection length - assuming no selection."); - } - - // 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 (!_cannnotTrustSelectionLength && selectedRange.length > 0) - n++; // First delete-back will delete the existing selection. - [self deleteBack:n for:event]; - deleteBackPosted = YES; - - CFRelease(_sourceFromOriginalEvent); - _sourceFromOriginalEvent = nil; - } + deleteBackPosted = [self deleteBack:n in:sender for: event]; } } else if ([actionType isEqualToString:Q_DEADKEY]) { @@ -496,18 +340,111 @@ NSRange _previousSelRange; continue; } else if ([actionType isEqualToString:Q_RETURN]) { - // if (_legacyMode) - // _previousSelRange = [sender selectedRange]; return YES; } else if ([actionType isEqualToString:Q_BEEP]) { [[NSSound soundNamed:@"Tink"] play]; } - - handled = YES; + } + return YES; +} + +- (void)processUnhandledDeleteBack:(id)client updateEngineContext:(BOOL *)updateEngineContext { + if ([self.AppDelegate debugMode]) { + NSLog(@"Processing an unhandled delete-back..."); + NSLog(@"_numberOfPostedDeletesToExpect = %lu", _numberOfPostedDeletesToExpect); } - // Apply context changes if not handled + // 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)) || + (!_legacyMode && (!self.willDeleteNullChar))) + { + // Backspace clears last "real" character from buffer, plus any surrounding deadkeys + [self.contextBuffer deleteLastDeadkeys]; + [self.contextBuffer deleteLastNChars:1]; + if (_legacyMode) { + _previousSelRange.location -= 1; + _previousSelRange.length = 0; + } + [self.contextBuffer deleteLastDeadkeys]; + } + if (_numberOfPostedDeletesToExpect > 0) { + if (--_numberOfPostedDeletesToExpect == 0) { + if ([self.AppDelegate debugMode]) + NSLog(@"Processing final posted delete-back..."); + + self.willDeleteNullChar = NO; + if (_legacyMode && _pendingBuffer != nil && _pendingBuffer.length > 0) { + if ([self.AppDelegate debugMode]) + NSLog(@"Posting special code to tell IM to insert characters from pending buffer."); + [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; + } + else { + if ([self.AppDelegate debugMode]) { + NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); + } + [self postKeyPressToFrontProcess:_keyCodeOfOriginalEvent from:_sourceFromOriginalEvent]; + _keyCodeOfOriginalEvent = 0; + CFRelease(_sourceFromOriginalEvent); + _sourceFromOriginalEvent = nil; + } + *updateEngineContext = NO; + } + } + else { + self.willDeleteNullChar = NO; + } +} + +- (BOOL)handleEvent:(NSEvent *)event client:(id)sender { + // OSK key feedback from hardware keyboard is disabled + /*if (event.type == NSKeyDown) + [self.AppDelegate handleKeyEvent:event];*/ + + if (event.type == NSLeftMouseDown || event.type == NSLeftMouseUp ) { + // if (_clientSelectionCanChangeUnexpectedly) { + // if ([self.AppDelegate debugMode]) + // NSLog(@"WARNING: We are dealing with an app/context where we THINK we shouldn't be getting mouse events, but we just got one!"); + // _clientSelectionCanChangeUnexpectedly = NO; + // } + _contextOutOfDate = YES; + return NO; + } + else if (event.type != NSKeyDown) + return NO; // We ignore NSLeftMouseDragged events (because we'll eventually get a mouse-up). + else if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { + [self handleCommand:event]; + return NO; // We let the client app handle all Command-key events. + } + + if (_legacyMode && event.keyCode == kProcessPendingBuffer) + { + 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++) + NSLog(@"Char %li: '%x'", ich, [[self pendingBuffer] characterAtIndex:ich]); + } + } + + [self insertPendingBufferTextIn:sender]; + return YES; + } + + [self checkContextIn:sender]; + + [self updateContextBufferIfNeeded:sender]; + + if ([self.AppDelegate debugMode]) { + NSLog(@"sender type = %@", NSStringFromClass([sender class])); + NSLog(@"sender selection range location = %lu", [sender selectedRange].location); + } + + BOOL handled = [self handleKeymanEngineActions:event in: sender]; + if (!handled) { NSUInteger nc = [self.contextBuffer deleteLastNullChars]; if (nc > 0) { @@ -517,79 +454,25 @@ NSRange _previousSelRange; // actions that had a Q_BACK not followed by a Q_STR (assuming that can happen). // REVIEW: Need to test this scenario in Atom and Googe Docs in Safari self.willDeleteNullChar = YES; - [self deleteBack:nc for:event]; + [self postDeleteBack: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; BOOL updateEngineContext = YES; unsigned short keyCode = event.keyCode; switch (keyCode) { case kVK_Delete: - if ([self.AppDelegate debugMode]) { - 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)) || - (!_legacyMode && (!self.willDeleteNullChar))) - { - // Backspace clears last "real" character from buffer, plus any surrounding deadkeys - [self.contextBuffer deleteLastDeadkeys]; - [self.contextBuffer deleteLastNChars:1]; - if (_legacyMode) { - _previousSelRange.location -= 1; - _previousSelRange.length = 0; - } - [self.contextBuffer deleteLastDeadkeys]; - } - if (_numberOfPostedDeletesToExpect > 0) { - if (--_numberOfPostedDeletesToExpect == 0) { - if ([self.AppDelegate debugMode]) - NSLog(@"Processing final posted delete-back..."); - - self.willDeleteNullChar = NO; - if (_legacyMode && _pendingBuffer != nil && _pendingBuffer.length > 0) { - if ([self.AppDelegate debugMode]) - NSLog(@"Posting special code to tell IM to insert characters from pending buffer."); - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:sender afterDelay:0.1]; - } - else { - if ([self.AppDelegate debugMode]) { - NSLog(@"Re-posting original (unhandled) code: %d", (int)_keyCodeOfOriginalEvent); - } - [self postKeyPressToFrontProcess:_keyCodeOfOriginalEvent from:_sourceFromOriginalEvent]; - _keyCodeOfOriginalEvent = 0; - CFRelease(_sourceFromOriginalEvent); - _sourceFromOriginalEvent = nil; - } - updateEngineContext = NO; - } - } - else { - self.willDeleteNullChar = NO; - } + [self processUnhandledDeleteBack:sender updateEngineContext:&updateEngineContext]; break; case kVK_LeftArrow: - // Marc has suggested that it's better to just let it be dumb. Too many potential pitfalls - // trying to guess where in the context we ended up after a left arrow. - // if (_legacyMode && self.contextBuffer.length) { - // NSUInteger flags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask; - // if ([self.AppDelegate debugMode]) - // NSLog(@"Legacy app Left arrow. flags = 0x%02x", (int)flags); - // if ((flags ^ NSEventModifierFlagShift) == 0) { - // [self.contextBuffer deleteLastNChars:1]; - // if ([self.AppDelegate debugMode]) - // NSLog(@"Removed one character from context."); - // break; - // } - // } - /* FALLTHROUGH */ + // 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. case kVK_RightArrow: case kVK_UpArrow: case kVK_DownArrow: @@ -636,7 +519,6 @@ NSRange _previousSelRange; } if ([self.AppDelegate debugMode]) { - NSLog(@"handledEvent: %@", handled?@"YES":@"NO"); if (_contextOutOfDate) NSLog(@"Context now out of date."); else @@ -653,13 +535,142 @@ NSRange _previousSelRange; NSLog(@"***"); } - // Seems we can't do it this way because (at least for "legacy mode" apps, the selection range doesn't get updated - // until after we return from this method. + // Seems we can't do it this way because (at least for "legacy mode" apps) the selection range doesn't get + // updated until after we return from this method. // if (_legacyMode) // _previousSelRange = [sender selectedRange]; return handled; } +- (BOOL)deleteBack:(NSUInteger)n in:(id) client for:(NSEvent *) event { + if ([self.AppDelegate debugMode]) + NSLog(@"Attempting to back-delete %li characters.", n); + NSRange selectedRange = [client selectedRange]; + NSInteger pos = selectedRange.location; + if (!_legacyMode) + [self deleteBack:n at: pos in: client]; + if (_legacyMode) + return [self deleteBackLegacy:n at: pos with: selectedRange for: event]; + + return NO; +} + +- (void)deleteBack:(NSUInteger)n at:(NSUInteger) pos in:(id)client { + if ([self.AppDelegate debugMode]) { + 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 + // of this is that if the script should happen to contain characters whose class is + // not known, it will skip over them and keep looking, so it could end up using a + // longer string of characters than otherwise necessary. This could result in a + // mildly jarring visual experience for the user if the app refreshes the diplay + // between the time the characters are removed and re-inserted. But presumbly this + // algorithm will eventually find either a known base character or get all the way back + // to the start of the context, so if it doesn't find a known base character, it will + // fall back to just attempting the insert with whatever it does find. I believe this + // will always work and should at least work as reliably as the old version of the code, + // 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--) { + preChar = [[client attributedSubstringFromRange:NSMakeRange(preCharPos, nbrOfPreCharacters)] string]; + if (!preChar) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Client apparently doesn't implement attributedSubstringFromRange. Attempting to get preChar from context..."); + } + if (self.contextBuffer != nil && preCharPos < self.contextBuffer.length) { + preChar = [self.contextBuffer substringWithRange:NSMakeRange(preCharPos, 1)]; + } + if (!preChar) + break; + } + if ([self.AppDelegate debugMode]) + NSLog(@"Testing preChar: %@", preChar); + + if ([regexNonCombiningMark numberOfMatchesInString:preChar options:NSMatchingAnchored range:NSMakeRange(0, 1)] > 0) + break; + if (preCharPos == 0) { + if ([self.AppDelegate debugMode]) { + NSLog(@"Failed to find a base character!"); + } + break; + } + if ([self.AppDelegate debugMode]) { + NSLog(@"Have not yet found a base character. nbrOfPreCharacters = %lu", nbrOfPreCharacters); + } + } + if (preChar) { + if ([self.AppDelegate debugMode]) { + NSLog(@"preChar (to insert at %lu) = \"%@\"", preCharPos, preChar); + } + [client insertText:preChar replacementRange:NSMakeRange(preCharPos, n+nbrOfPreCharacters)]; + } + else { + if ([self.AppDelegate debugMode]) { + NSLog(@"Switching to legacy mode - client apparently doesn't implement attributedSubstringFromRange and no previous character in context buffer."); + } + _legacyMode = YES; // client apparently doesn't implement attributedSubstringFromRange. + } + } + else { + if ([self.AppDelegate debugMode]) + NSLog(@"No previous character to use for replacement - replacing range with space"); + [client insertText:@" " replacementRange:NSMakeRange(pos - n, n)]; + [self.contextBuffer appendNullChar]; + } + } +} + +- (BOOL)deleteBackLegacy:(NSUInteger)n at:(NSUInteger) pos with:(NSRange) selectedRange for:(NSEvent *) event { + 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]) { + NSLog(@"Legacy mode: calling postDeleteBack"); + if (_cannnotTrustSelectionLength) + NSLog(@"Cannot trust client to report accurate selection length - assuming no selection."); + } + + // 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 (!_cannnotTrustSelectionLength && selectedRange.length > 0) + n++; // First delete-back will delete the existing selection. + [self postDeleteBack:n for:event]; + + CFRelease(_sourceFromOriginalEvent); + _sourceFromOriginalEvent = nil; + return YES; + } + return NO; +} + - (void)sendEvent:(NSEvent *)event { ProcessSerialNumber psn; GetFrontProcess(&psn); @@ -676,7 +687,7 @@ NSRange _previousSelRange; CFRelease(keyUpEvent); } -- (void)deleteBack:(NSUInteger)count for:(NSEvent *) event { +- (void)postDeleteBack:(NSUInteger)count for:(NSEvent *) event { _numberOfPostedDeletesToExpect = count; CGEventRef ev; _sourceFromOriginalEvent = CGEventCreateSourceFromEvent([event CGEvent]); diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h index 54d0fa1221..73bda95e3d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandlerProtected.h @@ -25,7 +25,6 @@ // checking at the start of the event processing to see if we're probably still in the same place where we // left off previously. @property (assign) BOOL clientSelectionCanChangeUnexpectedly; // REVIEW: Maybe we can get notification from these clients by handling mouseDownOnCharacterIndex. -@property (assign) BOOL insertCharactersIndividually; @property (assign) BOOL cannnotTrustSelectionLength; - (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly; From 595b866038a7b75dbf25f51f61ac0abc13d4a2e8 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Thu, 11 Jan 2018 13:21:52 -0500 Subject: [PATCH 09/13] Fixed Chrome bug from previous refactoring step Check for special "insert characters individually" logic was backwards. --- .../Keyman4MacIM/KMInputMethodChromeClientEventHandler.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m index 93bcf7dc3c..4566b90501 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m @@ -34,7 +34,7 @@ BOOL _insertCharactersIndividually; - (void)insertPendingBufferTextIn:(id)client { NSString* remainingText = @""; - if (!_insertCharactersIndividually) { + if (_insertCharactersIndividually) { NSUInteger length = [self pendingBuffer].length; if (length > 1) { if ([self.AppDelegate debugMode]) From 78be3af7d37524cfd33b077e752a0bd6cb65f369 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Fri, 12 Jan 2018 09:30:09 -0500 Subject: [PATCH 10/13] Fixed Chrome bug: MS Word sometimes treated like Google Docs --- .../KMInputMethodBrowserClientEventHandler.m | 45 ++++++++++++++----- ...MethodBrowserClientEventHandlerProtected.h | 4 +- .../KMInputMethodChromeClientEventHandler.m | 10 +++-- .../KMInputMethodSafariClientEventHandler.m | 2 +- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index 261f30c18b..f64eac9e0f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -23,6 +23,7 @@ NSUInteger _failuresToRetrieveExpectedContext; - (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly { self = [super initWithLegacyMode:legacy clientSelectionCanChangeUnexpectedly: flagClientSelectionCanChangeUnexpectedly]; if (self) { + _couldBeInGoogleDocs = NO; _failuresToRetrieveExpectedContext = 0; } return self; @@ -38,10 +39,29 @@ NSUInteger _failuresToRetrieveExpectedContext; NSUInteger location = [client selectedRange].location; if (location != NSNotFound && location > 0) { - NSString *clientContext = [[client attributedSubstringFromRange:NSMakeRange(0, location)] string]; - if (clientContext == nil || !clientContext.length || + if ([self AppDelegate].debugMode) + NSLog(@"Trying to get context up to location %lu", location); + NSString* clientContext = nil; + if ([client respondsToSelector:@selector(attributedSubstringFromRange:)]) + clientContext = [[client attributedSubstringFromRange:NSMakeRange(0, location)] string]; + if (clientContext == nil) + { + // Client is failing to provide useful response to attributedSubstringFromRange. + // Word (in MS Live) occasionally does this, but (apparently) Google Docs doesn't. + _couldBeInGoogleDocs = NO; + [self setInSiteThatDoesNotGiveContext]; + } + else if (!clientContext.length || [clientContext characterAtIndex:clientContext.length - 1] != [self.contextBuffer characterAtIndex:bufferLength - 1]) { + if ([self AppDelegate].debugMode) { + NSLog(@"Expected context = '%@'", self.contextBuffer); + NSLog(@"Actual clientContext = '%@'", (clientContext == nil ? @"{nil}" : clientContext)); + uint32_t codepoint = [clientContext characterAtIndex:clientContext.length - 1]; + NSLog(@"Last character in clientContext = '%lu'", (unsigned long)codepoint); + codepoint = [self.contextBuffer characterAtIndex:bufferLength - 1]; + NSLog(@"Last character in contextBuffer = '%lu'", (unsigned long)codepoint); + } _failuresToRetrieveExpectedContext++; } else { @@ -52,22 +72,27 @@ NSUInteger _failuresToRetrieveExpectedContext; } } else { + if ([self AppDelegate].debugMode) { + NSLog(@"bufferLength is %lu, but location was %lu.", bufferLength, location); + } _failuresToRetrieveExpectedContext++; } if (_failuresToRetrieveExpectedContext == 3) - { - if ([self AppDelegate].debugMode) { - NSLog(@"Detected Google Docs or some other editor that can't provide context."); - } - _failuresToRetrieveExpectedContext = NSUIntegerMax; - [self setInGoogleDocs]; - } + [self setInSiteThatDoesNotGiveContext]; } } } -- (void)setInGoogleDocs { +- (void)setInSiteThatDoesNotGiveContext { + if ([self AppDelegate].debugMode) { + if (_couldBeInGoogleDocs) + NSLog(@"Detected Google Docs or some other editor that can't provide context."); + else + NSLog(@"Detected some editor that can't provide context (not Google Docs)."); + } + _failuresToRetrieveExpectedContext = NSUIntegerMax; + if (self.clientSelectionCanChangeUnexpectedly) { self.cannnotTrustSelectionLength = YES; self.clientSelectionCanChangeUnexpectedly = NO; // This isn't true (it can change unexpectedly), but we can't get the context, so we pretend/hope it won't. diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h index e1486c65cd..d28f3a21f3 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h @@ -11,7 +11,9 @@ @interface KMInputMethodBrowserClientEventHandler () -- (void)setInGoogleDocs; +@property (assign) BOOL couldBeInGoogleDocs; + +- (void)setInSiteThatDoesNotGiveContext; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m index 4566b90501..af385236c1 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m @@ -25,10 +25,12 @@ BOOL _insertCharactersIndividually; return self; } -- (void)setInGoogleDocs { - [super setInGoogleDocs]; - // Google docs in Chrome allows only a single character at a time :-( - _insertCharactersIndividually = YES; +- (void)setInSiteThatDoesNotGiveContext { + [super setInSiteThatDoesNotGiveContext]; + if ([self couldBeInGoogleDocs]) { + // Google docs in Chrome allows only a single character at a time :-( + _insertCharactersIndividually = YES; + } } - (void)insertPendingBufferTextIn:(id)client { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m index 1c1f4e1593..535303cdd8 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodSafariClientEventHandler.m @@ -34,7 +34,7 @@ BOOL _preserveContextForNextCmdA; [super handleCommand:event]; } -- (void)setInGoogleDocs { +- (void)setInSiteThatDoesNotGiveContext { [self switchToLegacyMode]; } From 0d7a519220a4e7947e5d0fd1fec1e8ca30fe2fe0 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Fri, 12 Jan 2018 11:52:44 -0500 Subject: [PATCH 11/13] Commented out Chrome-specific code (possibly temporary) --- .../Keyman4MacIM/KMInputController.m | 6 +- .../KMInputMethodBrowserClientEventHandler.m | 48 +++++---- ...MethodBrowserClientEventHandlerProtected.h | 2 +- .../KMInputMethodChromeClientEventHandler.h | 17 ++-- .../KMInputMethodChromeClientEventHandler.m | 97 ++++++++++--------- 5 files changed, 92 insertions(+), 78 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 1a472b6d21..24e5effa80 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -63,12 +63,10 @@ KMInputMethodEventHandler* _eventHandler; if ([clientAppId isEqual: @"com.apple.Safari"]) { _eventHandler = [[KMInputMethodSafariClientEventHandler alloc] init]; } - else if ([clientAppId isEqual: @"org.mozilla.firefox"]) { + else if ([clientAppId isEqual: @"org.mozilla.firefox"] || + [clientAppId isEqual: @"com.google.Chrome"]) { _eventHandler = [[KMInputMethodBrowserClientEventHandler alloc] init]; } - else if ([clientAppId isEqual: @"com.google.Chrome"]) { - _eventHandler = [[KMInputMethodChromeClientEventHandler alloc] init]; - } else _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index f64eac9e0f..a60306f80e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -23,7 +23,7 @@ NSUInteger _failuresToRetrieveExpectedContext; - (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly { self = [super initWithLegacyMode:legacy clientSelectionCanChangeUnexpectedly: flagClientSelectionCanChangeUnexpectedly]; if (self) { - _couldBeInGoogleDocs = NO; +// _couldBeInGoogleDocs = YES; _failuresToRetrieveExpectedContext = 0; } return self; @@ -48,27 +48,41 @@ NSUInteger _failuresToRetrieveExpectedContext; { // Client is failing to provide useful response to attributedSubstringFromRange. // Word (in MS Live) occasionally does this, but (apparently) Google Docs doesn't. - _couldBeInGoogleDocs = NO; + //_couldBeInGoogleDocs = NO; [self setInSiteThatDoesNotGiveContext]; } - else if (!clientContext.length || - [clientContext characterAtIndex:clientContext.length - 1] != - [self.contextBuffer characterAtIndex:bufferLength - 1]) { + else if (!clientContext.length) { if ([self AppDelegate].debugMode) { NSLog(@"Expected context = '%@'", self.contextBuffer); - NSLog(@"Actual clientContext = '%@'", (clientContext == nil ? @"{nil}" : clientContext)); - uint32_t codepoint = [clientContext characterAtIndex:clientContext.length - 1]; - NSLog(@"Last character in clientContext = '%lu'", (unsigned long)codepoint); - codepoint = [self.contextBuffer characterAtIndex:bufferLength - 1]; - NSLog(@"Last character in contextBuffer = '%lu'", (unsigned long)codepoint); + NSLog(@"Actual clientContext was empty"); } _failuresToRetrieveExpectedContext++; } - else { - if ([self AppDelegate].debugMode) { - NSLog(@"We got what we were expecting from the client. We can stop checking."); + else + { + unichar lastCodepointExpected = [self.contextBuffer characterAtIndex:bufferLength - 1]; + unichar lastCodepointInClient = [clientContext characterAtIndex:clientContext.length - 1]; + if (lastCodepointExpected != lastCodepointInClient) { + if ([self AppDelegate].debugMode) { + NSLog(@"Expected context = '%@'", self.contextBuffer); + NSLog(@"Actual clientContext = '%@'", (clientContext == nil ? @"{nil}" : clientContext)); + NSLog(@"Last character expected (in contextBuffer) = '%lu'", (unsigned long)lastCodepointExpected); + NSLog(@"Last character in clientContext = '%lu'", (unsigned long)lastCodepointInClient); + } + // MS Word converts/reports plain spaces (32) as non-breaking spaces (160). If we + // get this kind of mismatch, we don't want to count this as a definite match, but + // we also don't want to count it as a failure to match. So just wait for a more + // reliable character to test. + if (lastCodepointInClient != 160 || lastCodepointExpected != 32) { + _failuresToRetrieveExpectedContext++; + } + } + else { + if ([self AppDelegate].debugMode) { + NSLog(@"We got what we were expecting from the client. We can stop checking."); + } + _failuresToRetrieveExpectedContext = NSUIntegerMax; } - _failuresToRetrieveExpectedContext = NSUIntegerMax; } } else { @@ -86,9 +100,9 @@ NSUInteger _failuresToRetrieveExpectedContext; - (void)setInSiteThatDoesNotGiveContext { if ([self AppDelegate].debugMode) { - if (_couldBeInGoogleDocs) - NSLog(@"Detected Google Docs or some other editor that can't provide context."); - else +// if (_couldBeInGoogleDocs) +// NSLog(@"Detected Google Docs or some other editor that can't provide context."); +// else NSLog(@"Detected some editor that can't provide context (not Google Docs)."); } _failuresToRetrieveExpectedContext = NSUIntegerMax; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h index d28f3a21f3..17f2f0b599 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h @@ -11,7 +11,7 @@ @interface KMInputMethodBrowserClientEventHandler () -@property (assign) BOOL couldBeInGoogleDocs; +//@property (assign) BOOL couldBeInGoogleDocs; - (void)setInSiteThatDoesNotGiveContext; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h index 7c2d40eb59..3c59815f01 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h @@ -6,12 +6,13 @@ // Created by tom on 1/9/18. // Copyright © 2018 SIL International. All rights reserved. // +// +//#ifndef KMInputMethodChromeClientEventHandler_h +//#define KMInputMethodChromeClientEventHandler_h +//#import "KMInputMethodBrowserClientEventHandler.h" +// +//@interface KMInputMethodChromeClientEventHandler : KMInputMethodBrowserClientEventHandler +//@end +// +//#endif /* KMInputMethodChromeClientEventHandler_h */ -#ifndef KMInputMethodChromeClientEventHandler_h -#define KMInputMethodChromeClientEventHandler_h -#import "KMInputMethodBrowserClientEventHandler.h" - -@interface KMInputMethodChromeClientEventHandler : KMInputMethodBrowserClientEventHandler -@end - -#endif /* KMInputMethodChromeClientEventHandler_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m index af385236c1..44c9369d5f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m @@ -6,52 +6,53 @@ // Copyright © 2018 SIL International. All rights reserved. // -#import "KMInputMethodChromeClientEventHandler.h" -#import "KMInputMethodEventHandlerProtected.h" -#import "KMInputMethodBrowserClientEventHandlerProtected.h" +//#import "KMInputMethodChromeClientEventHandler.h" +//#import "KMInputMethodEventHandlerProtected.h" +//#import "KMInputMethodBrowserClientEventHandlerProtected.h" +// +//@implementation KMInputMethodChromeClientEventHandler +// +//// Because Google Docs can't report its context, if we detect that we are in Google Docs, we change to +//// NOT assume that it needs to re-get the context every time around (which means that if the user +//// does mouse-click somewhere else, it could lead to bad behaviour). +//BOOL _insertCharactersIndividually; +// +//- (instancetype)init { +// self = [super init]; +// if (self) { +// _insertCharactersIndividually = NO; +// } +// return self; +//} +// +//- (void)setInSiteThatDoesNotGiveContext { +// [super setInSiteThatDoesNotGiveContext]; +// if ([self couldBeInGoogleDocs]) { +// // Google docs in Chrome allows only a single character at a time :-( +// _insertCharactersIndividually = YES; +// } +//} +// +//- (void)insertPendingBufferTextIn:(id)client { +// +// NSString* remainingText = @""; +// if (_insertCharactersIndividually) { +// NSUInteger length = [self pendingBuffer].length; +// if (length > 1) { +// if ([self.AppDelegate debugMode]) +// NSLog(@"Using special Google Docs in Chrome logic (length = %lu)", length); +// remainingText = [self.pendingBuffer substringFromIndex:1]; +// [self.pendingBuffer deleteLastNChars:length - 1]; +// } +// } +// +// [super insertPendingBufferTextIn:client]; +// +// if ([remainingText length] > 0) { +// // Reset the pending buffer to contain remaining characters and issue call to come back for more... +// [self setPendingBuffer:remainingText]; +// [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; +// } +//} +//@end -@implementation KMInputMethodChromeClientEventHandler - -// Because Google Docs can't report its context, if we detect that we are in Google Docs, we change to -// NOT assume that it needs to re-get the context every time around (which means that if the user -// does mouse-click somewhere else, it could lead to bad behaviour). -BOOL _insertCharactersIndividually; - -- (instancetype)init { - self = [super init]; - if (self) { - _insertCharactersIndividually = NO; - } - return self; -} - -- (void)setInSiteThatDoesNotGiveContext { - [super setInSiteThatDoesNotGiveContext]; - if ([self couldBeInGoogleDocs]) { - // Google docs in Chrome allows only a single character at a time :-( - _insertCharactersIndividually = YES; - } -} - -- (void)insertPendingBufferTextIn:(id)client { - - NSString* remainingText = @""; - if (_insertCharactersIndividually) { - NSUInteger length = [self pendingBuffer].length; - if (length > 1) { - if ([self.AppDelegate debugMode]) - NSLog(@"Using special Google Docs in Chrome logic (length = %lu)", length); - remainingText = [self.pendingBuffer substringFromIndex:1]; - [self.pendingBuffer deleteLastNChars:length - 1]; - } - } - - [super insertPendingBufferTextIn:client]; - - if ([remainingText length] > 0) { - // Reset the pending buffer to contain remaining characters and issue call to come back for more... - [self setPendingBuffer:remainingText]; - [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; - } -} -@end From e42626d229c107f9227f004200add54ceef3adb2 Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Fri, 12 Jan 2018 14:34:20 -0500 Subject: [PATCH 12/13] Permanently removed Chrome-specific behavior. --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 6 -- .../Keyman4MacIM/KMInputController.m | 1 - .../KMInputMethodBrowserClientEventHandler.m | 13 ++--- ...MethodBrowserClientEventHandlerProtected.h | 2 - .../KMInputMethodChromeClientEventHandler.h | 18 ------ .../KMInputMethodChromeClientEventHandler.m | 58 ------------------- 6 files changed, 4 insertions(+), 94 deletions(-) delete mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h delete mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index aec06cf552..0579836d62 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -53,7 +53,6 @@ E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; E21799061FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; E22020F6200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E22020F5200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m */; }; - E22020F8200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E22020F7200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m */; }; E24C79EA1FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */; }; /* End PBXBuildFile section */ @@ -153,9 +152,7 @@ E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = ""; }; E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = ""; }; E22020F3200505EF00B74FAC /* KMInputMethodSafariClientEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodSafariClientEventHandler.h; sourceTree = ""; }; - E22020F42005069300B74FAC /* KMInputMethodChromeClientEventHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMInputMethodChromeClientEventHandler.h; sourceTree = ""; }; E22020F5200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodSafariClientEventHandler.m; sourceTree = ""; }; - E22020F7200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodChromeClientEventHandler.m; sourceTree = ""; }; E22020F920050D6300B74FAC /* KMInputMethodBrowserClientEventHandlerProtected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KMInputMethodBrowserClientEventHandlerProtected.h; sourceTree = ""; }; E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodBrowserClientEventHandler.m; sourceTree = ""; }; E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodBrowserClientEventHandler.h; sourceTree = ""; }; @@ -295,9 +292,7 @@ E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, E24C79EB1FFFCE3000D8E46F /* KMInputMethodBrowserClientEventHandler.h */, E22020F3200505EF00B74FAC /* KMInputMethodSafariClientEventHandler.h */, - E22020F42005069300B74FAC /* KMInputMethodChromeClientEventHandler.h */, E24C79E91FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m */, - E22020F7200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m */, E22020F5200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m */, 98E672961B532DE000DBDE2F /* KMKeyboardHelpWindow */, 989C9C141A7876DE00A20425 /* MainMenu.xib */, @@ -558,7 +553,6 @@ E22020F6200506F300B74FAC /* KMInputMethodSafariClientEventHandler.m in Sources */, E24C79EA1FFFCC7500D8E46F /* KMInputMethodBrowserClientEventHandler.m in Sources */, 98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */, - E22020F8200508F100B74FAC /* KMInputMethodChromeClientEventHandler.m in Sources */, 9836B3741AE5F11D00780482 /* ZipArchive.mm in Sources */, 983247B21A9EB9690010B90C /* KMConfigColumn3CellView.m in Sources */, 98BF92521BF03D4D0002126A /* KMAboutBGView.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 24e5effa80..85cb61637d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -10,7 +10,6 @@ #import "KMInputMethodEventHandler.h" #import "KMInputMethodBrowserClientEventHandler.h" #import "KMInputMethodSafariClientEventHandler.h" -#import "KMInputMethodChromeClientEventHandler.h" #include /* For kVK_ constants. */ @implementation KMInputController diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m index a60306f80e..ed18aee28e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandler.m @@ -12,8 +12,8 @@ @implementation KMInputMethodBrowserClientEventHandler -// Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox), we want to -// try to detect it so each browser can respond appropriatey. +// Because Google Docs can't report its context in any of the browsers (Safari, Chrome, Firefox) and Word sometimes +// has trouble doing this in Chrome, we want to try to detect it so each browser can respond appropriately. NSUInteger _failuresToRetrieveExpectedContext; - (instancetype)init { @@ -23,7 +23,6 @@ NSUInteger _failuresToRetrieveExpectedContext; - (instancetype)initWithLegacyMode:(BOOL)legacy clientSelectionCanChangeUnexpectedly:(BOOL) flagClientSelectionCanChangeUnexpectedly { self = [super initWithLegacyMode:legacy clientSelectionCanChangeUnexpectedly: flagClientSelectionCanChangeUnexpectedly]; if (self) { -// _couldBeInGoogleDocs = YES; _failuresToRetrieveExpectedContext = 0; } return self; @@ -47,8 +46,7 @@ NSUInteger _failuresToRetrieveExpectedContext; if (clientContext == nil) { // Client is failing to provide useful response to attributedSubstringFromRange. - // Word (in MS Live) occasionally does this, but (apparently) Google Docs doesn't. - //_couldBeInGoogleDocs = NO; + // Word (in MS Live) occasionally does this. [self setInSiteThatDoesNotGiveContext]; } else if (!clientContext.length) { @@ -100,10 +98,7 @@ NSUInteger _failuresToRetrieveExpectedContext; - (void)setInSiteThatDoesNotGiveContext { if ([self AppDelegate].debugMode) { -// if (_couldBeInGoogleDocs) -// NSLog(@"Detected Google Docs or some other editor that can't provide context."); -// else - NSLog(@"Detected some editor that can't provide context (not Google Docs)."); + NSLog(@"Detected some editor (e.g., Google Docs) that can't provide context."); } _failuresToRetrieveExpectedContext = NSUIntegerMax; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h index 17f2f0b599..faaf606546 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodBrowserClientEventHandlerProtected.h @@ -11,8 +11,6 @@ @interface KMInputMethodBrowserClientEventHandler () -//@property (assign) BOOL couldBeInGoogleDocs; - - (void)setInSiteThatDoesNotGiveContext; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h deleted file mode 100644 index 3c59815f01..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// KMInputMethodChromeClientEventHandler.h -// Keyman4MacIM -// This class is used for the Google Chrome browser to get specific behavior needed to improve behavior for some sites. -// -// Created by tom on 1/9/18. -// Copyright © 2018 SIL International. All rights reserved. -// -// -//#ifndef KMInputMethodChromeClientEventHandler_h -//#define KMInputMethodChromeClientEventHandler_h -//#import "KMInputMethodBrowserClientEventHandler.h" -// -//@interface KMInputMethodChromeClientEventHandler : KMInputMethodBrowserClientEventHandler -//@end -// -//#endif /* KMInputMethodChromeClientEventHandler_h */ - diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m deleted file mode 100644 index 44c9369d5f..0000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodChromeClientEventHandler.m +++ /dev/null @@ -1,58 +0,0 @@ -// -// KMInputMethodChromeClientEventHandler.m -// Keyman -// -// Created by tom on 1/9/18. -// Copyright © 2018 SIL International. All rights reserved. -// - -//#import "KMInputMethodChromeClientEventHandler.h" -//#import "KMInputMethodEventHandlerProtected.h" -//#import "KMInputMethodBrowserClientEventHandlerProtected.h" -// -//@implementation KMInputMethodChromeClientEventHandler -// -//// Because Google Docs can't report its context, if we detect that we are in Google Docs, we change to -//// NOT assume that it needs to re-get the context every time around (which means that if the user -//// does mouse-click somewhere else, it could lead to bad behaviour). -//BOOL _insertCharactersIndividually; -// -//- (instancetype)init { -// self = [super init]; -// if (self) { -// _insertCharactersIndividually = NO; -// } -// return self; -//} -// -//- (void)setInSiteThatDoesNotGiveContext { -// [super setInSiteThatDoesNotGiveContext]; -// if ([self couldBeInGoogleDocs]) { -// // Google docs in Chrome allows only a single character at a time :-( -// _insertCharactersIndividually = YES; -// } -//} -// -//- (void)insertPendingBufferTextIn:(id)client { -// -// NSString* remainingText = @""; -// if (_insertCharactersIndividually) { -// NSUInteger length = [self pendingBuffer].length; -// if (length > 1) { -// if ([self.AppDelegate debugMode]) -// NSLog(@"Using special Google Docs in Chrome logic (length = %lu)", length); -// remainingText = [self.pendingBuffer substringFromIndex:1]; -// [self.pendingBuffer deleteLastNChars:length - 1]; -// } -// } -// -// [super insertPendingBufferTextIn:client]; -// -// if ([remainingText length] > 0) { -// // Reset the pending buffer to contain remaining characters and issue call to come back for more... -// [self setPendingBuffer:remainingText]; -// [self performSelector:@selector(initiatePendingBufferProcessing:) withObject:client afterDelay:0.1]; -// } -//} -//@end - From c3a6b1415fa6990928116c582ce5de22469102dc Mon Sep 17 00:00:00 2001 From: Tom Bogle Date: Thu, 18 Jan 2018 14:09:50 -0500 Subject: [PATCH 13/13] Set debugMode back to NO. --- mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index b95643bd95..042e6874e8 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -63,7 +63,7 @@ typedef enum { - (id)init { self = [super init]; if (self) { - _debugMode = YES; // Disable before release + _debugMode = NO; // Disable before release [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleURLEvent:withReplyEvent:) forEventClass:kInternetEventClass