From ada74039e048e32684002d7293059cc00cb77e85 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 3 Nov 2023 08:43:37 +0700 Subject: [PATCH 01/10] consuming actions struct from core --- .../Keyman4MacIM/KMInputMethodEventHandler.m | 140 ++++++++++++----- .../project.pbxproj | 12 ++ .../KeymanEngine4Mac/CoreWrapper/CoreHelper.h | 6 +- .../KeymanEngine4Mac/CoreWrapper/CoreHelper.m | 50 ++++-- .../CoreWrapper/CoreKeyOutput.h | 38 +++++ .../CoreWrapper/CoreKeyOutput.m | 61 ++++++++ .../CoreWrapper/CoreWrapper.h | 5 +- .../CoreWrapper/CoreWrapper.m | 84 ++++++++-- .../KeymanEngine4Mac/KME/KMEngine.h | 3 +- .../KeymanEngine4Mac/KME/KMEngine.m | 12 +- .../KeymanEngine4Mac/KeymanEngine4Mac.h | 1 + .../KeymanEngine4MacTests/CoreWrapperTests.m | 12 +- .../KeymanEngine4MacTests/KMEngineTests.m | 145 +++++++++--------- .../KeymanEngineTestsStaticHelperMethods.h | 1 + .../KeymanEngineTestsStaticHelperMethods.m | 6 + .../armenian_mnemonic.kmx | Bin 0 -> 32814 bytes 16 files changed, 414 insertions(+), 162 deletions(-) create mode 100644 mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h create mode 100644 mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.m create mode 100644 mac/KeymanEngine4Mac/KeymanEngine4MacTests/armenian_mnemonic.kmx diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index c83c3ed75a4..07d6ead3328 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -287,39 +287,32 @@ - (void)updateContextBufferIfNeeded:(id)client { } - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender { - NSArray *actions = nil; + CoreKeyOutput *output = nil; - // TODO: remove willDeleteNullChar with core code in place - if ([self willDeleteNullChar]) { - [self.appDelegate logDebugMessage:@"willDeleteNullChar = true"]; - return NO; - } - else { - actions = [self processEventWithKeymanEngine:event in:sender]; - if (actions.count == 0) { - [self checkEventForSentryEasterEgg:event]; - return NO; - } + output = [self processEventWithKeymanEngine:event in:sender]; + if (output == nil) { + [self checkEventForSentryEasterEgg:event]; + return NO; } - return [self applyKeymanCoreActions:actions event:event client:sender]; + return [self applyKeymanCoreActions:output event:event client:sender]; } -- (NSArray*) processEventWithKeymanEngine:(NSEvent *)event in:(id) sender { - NSArray* actions = nil; +- (CoreKeyOutput*) processEventWithKeymanEngine:(NSEvent *)event in:(id) sender { + CoreKeyOutput* coreKeyOutput = nil; if (self.appDelegate.lowLevelEventTap != nil) { NSEvent *eventWithOriginalModifierFlags = [NSEvent keyEventWithType:event.type location:event.locationInWindow modifierFlags:self.appDelegate.currentModifierFlags timestamp:event.timestamp windowNumber:event.windowNumber context:event.context characters:event.characters charactersIgnoringModifiers:event.charactersIgnoringModifiers isARepeat:event.isARepeat keyCode:event.keyCode]; - actions = [self.kme processEvent:eventWithOriginalModifierFlags]; + coreKeyOutput = [self.kme processEvent:eventWithOriginalModifierFlags]; [self.appDelegate logDebugMessage:@"processEventWithKeymanEngine, using AppDelegate.currentModifierFlags %lu, instead of event.modifiers = %lu", (unsigned long)self.appDelegate.currentModifierFlags, (unsigned long)event.modifierFlags]; } else { // Depending on the client app and the keyboard, using the passed-in event as it is should work okay. // Keyboards that depend on chirality support will not work. And command-key actions that change the // context might go undetected in some apps, resulting in errant behavior for subsequent typing. - actions = [self.kme processEvent:event]; + coreKeyOutput = [self.kme processEvent:event]; } - [self.appDelegate logDebugMessage:@"processEventWithKeymanEngine, actions = %@", actions]; - return actions; + [self.appDelegate logDebugMessage:@"processEventWithKeymanEngine, coreKeyOutput = %@", coreKeyOutput]; + return coreKeyOutput; } - (void)checkEventForSentryEasterEgg:(NSEvent *)event { @@ -732,23 +725,92 @@ -(NSString*)readContext:(NSEvent *)event forClient:(id) client { * -return NO if this is the only action (besides End) * -return YES after generating a backspace event */ --(BOOL)applyKeymanCoreActions:(NSArray*)actions event: (NSEvent*)event client:(id) client { - [self.appDelegate logDebugMessage:@"applyKeymanCoreActions invoked, actions.count = %lu ", (unsigned long)actions.count]; +-(BOOL)applyKeymanCoreActions:(CoreKeyOutput*)output event: (NSEvent*)event client:(id) client { + [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: output = %@ ", output]; + + BOOL handledEvent = NO; + + // TODO: queue up the text to insert if we are emitting the keystroke - KMCoreActionHandler *actionHandler = [[KMCoreActionHandler alloc] initWithActions:(NSArray*)actions keyCode: event.keyCode]; - KMActionHandlerResult *result = actionHandler.handleActions; + handledEvent = [self applyKeyOutputToTextInputClient:output keyDownEvent:event client:client]; + [self applyNonTextualOutput:output]; + + // if we are told to emit the keystroke, then be sure to say that we have not handled the event + if ((handledEvent) && (output.emitKeystroke)) { + [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: emit keystroke is forcing event as not handled"]; + handledEvent = NO; + } + + return handledEvent; +} - for (KMActionOperation *operation in [result.operations objectEnumerator]) { - if (operation.isForCompositeAction) { - [self executeCompositeOperation:operation keyDownEvent:event client:client]; +-(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonnull NSEvent *)event client:(id) client { + BOOL handledEvent = YES; + + if (output.isInsertOnlyScenario) { + [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, insert only scenario"]; + [self insertAndReplaceTextForOutput:output client:client]; + } else if (output.isDeleteOnlyScenario) { + // if we have a single delete because the backspace key was pressed, + // let it pass through by returning NO in the handleEvent call + if ((event.keyCode == kVK_Delete) && (output.codePointsToDeleteBeforeInsert == 1)) { + [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only *pass through* scenario"]; + handledEvent = NO; } else { - [self executeSimpleOperation:operation keyDownEvent:event]; + [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only scenario"]; + [self sendEvents:event forOutput:output]; + } + } else if (output.isDeleteAndInsertScenario) { + if (self.apiCompliance.mustBackspaceUsingEvents) { + [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with events"]; + [self sendEvents:event forOutput:output]; + } else { + [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with insert API"]; + // directly insert text and handle backspaces by using replace + [self insertAndReplaceTextForOutput:output client:client]; } } + + return handledEvent; +} + +-(void)applyNonTextualOutput:(CoreKeyOutput*)output { + + if(output.optionsToPersist.count>0) { + for(NSString *key in output.optionsToPersist) { + NSString *value = [output.optionsToPersist objectForKey:key]; + if(key && value) { + [self.appDelegate logDebugMessage:@"applyNonTextualOutput calling writePersistedOptions, key: %@, value: %@", key, value]; + [self.appDelegate writePersistedOptions:key withValue:value]; + } + else { + [self.appDelegate logDebugMessage:@"applyNonTextualOutput, invalid values in optionsToPersist, not writing to UserDefaults, key: %@, value: %@", key, value]; + } + } + + switch(output.capsLockState) { + case On: + //TODO: handle this + break; + case Off: + //TODO: handle this + break; + case Unchanged: + // do nothing + break; + } + + if (output.alert) { + NSBeep(); + } + } +} - return result.handledEvent; +-(void)insertAndReplaceTextForOutput:(CoreKeyOutput*)output client:(id) client { + [self insertAndReplaceText:output.textToInsert deleteCount:output.codePointsToDeleteBeforeInsert client:client]; } + /** * If we need to do something in response to a single action, then do it here. * Most of the interesting work is done in executeCompositeOperation, and @@ -784,6 +846,7 @@ -(void)executeSimpleOperation:(KMActionOperation*)operation keyDownEvent:(nonnul } } +/* -(void)executeCompositeOperation:(KMActionOperation*)operation keyDownEvent:(nonnull NSEvent *)event client:(id) client { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, composite operation: %@", operation]; @@ -792,11 +855,11 @@ -(void)executeCompositeOperation:(KMActionOperation*)operation keyDownEvent:(non [self insertAndReplaceTextForOperation:operation client:client]; } else if (operation.isBackspaceOnlyScenario) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, backspace only scenario"]; - [self sendEventsForOperation: event actionOperation:operation]; + [self sendEventsForOutput:event actionOperation:operation]; } else if (operation.isTextAndBackspaceScenario) { if (self.apiCompliance.mustBackspaceUsingEvents) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, text and backspace scenario with events"]; - [self sendEventsForOperation: event actionOperation:operation]; + [self sendEventsForOutput:event actionOperation:operation]; } else { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, text and backspace scenario with insert API"]; // directly insert text and handle backspaces by using replace @@ -804,9 +867,10 @@ -(void)executeCompositeOperation:(KMActionOperation*)operation keyDownEvent:(non } } } +*/ -(void)insertAndReplaceTextForOperation:(KMActionOperation*)operation client:(id) client { - [self insertAndReplaceText:operation.textToInsert backspaceCount:operation.backspaceCount client:client]; + [self insertAndReplaceText:operation.textToInsert deleteCount:operation.backspaceCount client:client]; } /** @@ -814,7 +878,7 @@ -(void)insertAndReplaceTextForOperation:(KMActionOperation*)operation client:(id * Because this method depends on the selectedRange API which is not implemented correctly for some client applications, * this method can only be used if approved by TextApiCompliance */ --(void)insertAndReplaceText:(NSString *)text backspaceCount:(int) replacementCount client:(id) client { +-(void)insertAndReplaceText:(NSString *)text deleteCount:(int) replacementCount client:(id) client { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler insertAndReplaceText: %@ replacementCount: %d", text, replacementCount]; int actualReplacementCount = 0; @@ -850,17 +914,17 @@ -(void)insertAndReplaceText:(NSString *)text backspaceCount:(int) replacementCou } --(void)sendEventsForOperation: (NSEvent *)event actionOperation:(KMActionOperation*)operation { - if (operation.hasBackspaces) { - for (int i = 0; i < operation.backspaceCount; i++) +-(void)sendEvents:(NSEvent *)event forOutput:(CoreKeyOutput*)output { + if (output.hasCodePointsToDelete) { + for (int i = 0; i < output.codePointsToDeleteBeforeInsert; i++) { self.generatedBackspaceCount++; [self.keySender sendBackspaceforSourceEvent:event]; } } - if (operation.hasTextToInsert) { - self.queuedText = operation.textToInsert; + if (output.hasTextToInsert) { + self.queuedText = output.textToInsert; [self.keySender sendKeymanKeyCodeForEvent:event]; } } @@ -868,7 +932,7 @@ -(void)sendEventsForOperation: (NSEvent *)event actionOperation:(KMActionOperati -(void)insertQueuedText: (NSEvent *)event client:(id) client { if (self.queuedText.length> 0) { [self.appDelegate logDebugMessage:@"insertQueuedText, inserting %@", self.queuedText]; - [self insertAndReplaceText:self.queuedText backspaceCount:0 client:client]; + [self insertAndReplaceText:self.queuedText deleteCount:0 client:client]; } else { [self.appDelegate logDebugMessage:@"handleQueuedText called but no text to insert"]; } diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj index a7d64298d07..952d2d1b31f 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ 299F9E2C29C46AFC0053E903 /* CoreTestStaticHelperMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = 299F9E2929C46AFC0053E903 /* CoreTestStaticHelperMethods.m */; }; 29A12C292AC51D3F00327D46 /* libsicuuc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 29A205D82AC2E446000750C0 /* libsicuuc.a */; }; 29A12C2A2AC51D6100327D46 /* libsicui18n.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 29A205DA2AC2E46A000750C0 /* libsicui18n.a */; }; + 29A1971E2AF091E600512A37 /* CoreKeyOutput.h in Headers */ = {isa = PBXBuildFile; fileRef = 29A1971C2AF091E600512A37 /* CoreKeyOutput.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 29A1971F2AF091E600512A37 /* CoreKeyOutput.m in Sources */ = {isa = PBXBuildFile; fileRef = 29A1971D2AF091E600512A37 /* CoreKeyOutput.m */; }; + 29A658C82AF394560038DCFE /* armenian_mnemonic.kmx in Resources */ = {isa = PBXBuildFile; fileRef = 29A658C72AF394560038DCFE /* armenian_mnemonic.kmx */; }; 378568D122FCCF0A00B481B5 /* sil_ipa.kmx in Resources */ = {isa = PBXBuildFile; fileRef = 378568D022FCCF0A00B481B5 /* sil_ipa.kmx */; }; 378568D322FCD93300B481B5 /* indexoffset1892.kmx in Resources */ = {isa = PBXBuildFile; fileRef = 378568D222FCD93300B481B5 /* indexoffset1892.kmx */; }; 980053351B37C9B50088DDDD /* NFont.h in Headers */ = {isa = PBXBuildFile; fileRef = 980053311B37C9B50088DDDD /* NFont.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -93,8 +96,11 @@ 299F9E2729C46AFB0053E903 /* CoreHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreHelperTests.m; sourceTree = ""; }; 299F9E2829C46AFB0053E903 /* CoreWrapperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreWrapperTests.m; sourceTree = ""; }; 299F9E2929C46AFC0053E903 /* CoreTestStaticHelperMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreTestStaticHelperMethods.m; sourceTree = ""; }; + 29A1971C2AF091E600512A37 /* CoreKeyOutput.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreKeyOutput.h; sourceTree = ""; }; + 29A1971D2AF091E600512A37 /* CoreKeyOutput.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CoreKeyOutput.m; sourceTree = ""; }; 29A205D82AC2E446000750C0 /* libsicuuc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsicuuc.a; path = "../../core/build/mac-arm64/release/subprojects/icu/source/common/libsicuuc.a"; sourceTree = ""; }; 29A205DA2AC2E46A000750C0 /* libsicui18n.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsicui18n.a; path = "../../core/build/mac-arm64/release/subprojects/icu/source/i18n/libsicui18n.a"; sourceTree = ""; }; + 29A658C72AF394560038DCFE /* armenian_mnemonic.kmx */ = {isa = PBXFileReference; lastKnownFileType = file; path = armenian_mnemonic.kmx; sourceTree = ""; }; 378568D022FCCF0A00B481B5 /* sil_ipa.kmx */ = {isa = PBXFileReference; lastKnownFileType = file; path = sil_ipa.kmx; sourceTree = ""; }; 378568D222FCD93300B481B5 /* indexoffset1892.kmx */ = {isa = PBXFileReference; lastKnownFileType = file; path = indexoffset1892.kmx; sourceTree = ""; }; 980053311B37C9B50088DDDD /* NFont.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NFont.h; sourceTree = ""; }; @@ -178,6 +184,8 @@ 2993C32229B1D92E00FB5E95 /* CoreAction.m */, 2993C32329B1D92E00FB5E95 /* ActionArrayOptimizer.m */, 2993C32929B1D92E00FB5E95 /* ActionArrayOptimizer.h */, + 29A1971C2AF091E600512A37 /* CoreKeyOutput.h */, + 29A1971D2AF091E600512A37 /* CoreKeyOutput.m */, ); path = CoreWrapper; sourceTree = ""; @@ -269,6 +277,7 @@ 984C2AE21A79C98D0023F89D /* Supporting Files */ = { isa = PBXGroup; children = ( + 29A658C72AF394560038DCFE /* armenian_mnemonic.kmx */, 299D0B1A29CAB2F500767276 /* el_nuer.kmx */, 378568D222FCD93300B481B5 /* indexoffset1892.kmx */, 378568D022FCCF0A00B481B5 /* sil_ipa.kmx */, @@ -324,6 +333,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 29A1971E2AF091E600512A37 /* CoreKeyOutput.h in Headers */, 984C2B091A79CA4F0023F89D /* KMBinaryFileFormat.h in Headers */, 984C2B121A79CA4F0023F89D /* KMEngine.h in Headers */, 984C2B141A79CA4F0023F89D /* KMXFile.h in Headers */, @@ -444,6 +454,7 @@ 378568D322FCD93300B481B5 /* indexoffset1892.kmx in Resources */, 299D0B1B29CAB2F500767276 /* el_nuer.kmx in Resources */, E2537D9A201641DA0069FEA6 /* TestMacEngine.kmx in Resources */, + 29A658C82AF394560038DCFE /* armenian_mnemonic.kmx in Resources */, E23380F12112C01500B90591 /* CipherMusicUnicode.kmx in Resources */, 378568D122FCCF0A00B481B5 /* sil_ipa.kmx in Resources */, E224323F20D37E97006940A1 /* PlatformTest.kmx in Resources */, @@ -503,6 +514,7 @@ 984C2B0F1A79CA4F0023F89D /* KMCompKey.m in Sources */, 984C2B131A79CA4F0023F89D /* KMEngine.m in Sources */, 984C2B171A79CA4F0023F89D /* KVKFile.m in Sources */, + 29A1971F2AF091E600512A37 /* CoreKeyOutput.m in Sources */, 981880741BC1EA5800A1FBA5 /* KeyView.m in Sources */, 981880761BC1EA5800A1FBA5 /* OSKKey.m in Sources */, 2993C32F29B1D92E00FB5E95 /* CoreHelper.m in Sources */, diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h index 5829e002174..7f89ecd0094 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.h @@ -25,9 +25,9 @@ extern UInt32 VirtualKeyMap[0x80]; -(instancetype)initWithDebugMode:(BOOL)debugMode; -(void)logDebugMessage:(NSString *)format, ...; -(unsigned short) macVirtualKeyToWindowsVirtualKey:(unsigned short) keyCode; --(UInt32)macToKeymanModifier:(NSEventModifierFlags)modifiers; --(NSArray*)optimizeActionArray:(NSArray*)actionArray; --(NSString*)utf32ValueToString:(UInt32)scalarValue; +-(UTF32Char)macToKeymanModifier:(NSEventModifierFlags)modifiers; +-(NSString*)utf32ValueToString:(UTF32Char)scalarValue; +-(NSString*)utf32CStringToString:(UTF32Char*)utf32CString; @end NS_ASSUME_NONNULL_END diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m index 9ff8b64d879..80d30c1b402 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m @@ -56,6 +56,18 @@ -(unsigned long long) unicharStringLength:(unichar const *)string { return length; } +/** + * Return the string length (number of unicode scalar, or UTF32, values) excluding the terminating null. + */ +-(unsigned long) scalarValueStringLength:(UTF32Char const *)string { + unsigned long length = 0lu; + if(NULL == string) return length; + + while('\0' != string[length]) + length++; + + return length; +} -(instancetype)initWithDebugMode:(BOOL)debugMode { self = [super init]; @@ -84,15 +96,8 @@ -(unsigned short) macVirtualKeyToWindowsVirtualKey:(unsigned short) keyCode { } } --(NSArray*)optimizeActionArray:(NSArray*)actionArray { - [self logDebugMessage:@"unoptimized actions array: %@", actionArray]; - NSArray* optimizedActionArray = [self.optimizer optimize:actionArray]; - [self logDebugMessage:@"optimized actions array: %@", optimizedActionArray]; - return optimizedActionArray; -} - --(UInt32)macToKeymanModifier:(NSEventModifierFlags)modifiers { - UInt32 keymanModifiers = 0; +-(UTF32Char)macToKeymanModifier:(NSEventModifierFlags)modifiers { + UTF32Char keymanModifiers = 0; if ([self isShiftKey:modifiers]) { keymanModifiers |= KM_CORE_MODIFIER_SHIFT; } @@ -158,11 +163,11 @@ -(BOOL)isOptionKey:(NSEventModifierFlags) modifiers { return (modifiers & NSEventModifierFlagOption) == NSEventModifierFlagOption; } -/* - Converts a UTF-32 unicode scalar value to an NSString. - Use this method to convert values from Keyman Core of type km_core_usv, equivalent to uint32_t +/** + * Converts a UTF-32 unicode scalar value to an NSString. + * Use this method to convert values from Keyman Core of type km_core_usv, equivalent to uint32_t */ --(NSString*)utf32ValueToString:(UInt32)scalarValue { +-(NSString*)utf32ValueToString:(UTF32Char)scalarValue { NSData * characterData = [[NSData alloc] initWithBytes:&scalarValue length:sizeof(scalarValue)]; NSString *characterString=[[NSString alloc] initWithBytes:[characterData bytes] length:[characterData length] encoding:NSUTF32LittleEndianStringEncoding]; @@ -170,6 +175,25 @@ -(NSString*)utf32ValueToString:(UInt32)scalarValue { return characterString; } +/** + * Converts a null-terminated string of UTF-32 unicode scalar values to an NSString. + */ +-(NSString*)utf32CStringToString:(UTF32Char*)utf32CString { + NSString *characterString = nil; + long utf32StringLength = [self scalarValueStringLength:utf32CString]; + + if (utf32StringLength > 0) { + characterString=[[NSString alloc] initWithBytes:utf32CString length:utf32StringLength*4 encoding:NSUTF32LittleEndianStringEncoding]; + /* + NSData * characterData = [[NSData alloc] initWithBytes:utf32CString length:utf32StringLength*4]; + [self logDebugMessage:@"utf32ValueToString data: '%@', string: %@", characterData, characterString]; + */ + } + + return characterString; +} + + // TODO: alphabetize instead of using whatever this order is // Creates a VK map to convert Mac VK codes to Windows VK codes diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h new file mode 100644 index 00000000000..8b7938fa57a --- /dev/null +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h @@ -0,0 +1,38 @@ +/** + * Keyman is copyright (C) SIL International. MIT License. + * + * CoreKeyOutput.h + * KeymanEngine4Mac + * + * Created by Shawn Schantz on 2023-10-31. + * + * Description... + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef enum { + Unchanged, + Off, + On +} CapsLockState; + +@interface CoreKeyOutput : NSObject +@property (nonatomic, readonly) NSUInteger codePointsToDeleteBeforeInsert; +@property (strong, nonatomic, readonly) NSString *textToInsert; +@property (strong, nonatomic, readonly) NSDictionary *optionsToPersist; +@property (nonatomic, readonly) BOOL alert; +@property (nonatomic, readonly) BOOL emitKeystroke; +@property (nonatomic, readonly) CapsLockState capsLockState; +-(instancetype)init:(NSUInteger)codePointsToDelete textToInsert:(NSString*)text optionsToPersist:(NSDictionary*)options alert:(BOOL)alert emitKeystroke:(BOOL)emit capsLockState:(CapsLockState)capsLock; +-(NSString *)description; +-(BOOL)hasCodePointsToDelete; +-(BOOL)hasTextToInsert; +-(BOOL)isDeleteOnlyScenario; +-(BOOL)isInsertOnlyScenario; +-(BOOL)isDeleteAndInsertScenario; +@end + +NS_ASSUME_NONNULL_END diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.m new file mode 100644 index 00000000000..be765c70cde --- /dev/null +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.m @@ -0,0 +1,61 @@ +/** + * Keyman is copyright (C) SIL International. MIT License. + * + * CoreKeyOutput.m KeymanEngine4Mac + * + * Created by Shawn Schantz on 2023-10-31. + * + * A value object representing one or more actions returned by Keyman Core. A + * struct describing the actions is returned by Core when processing a key down + * event. These actions are then applied by the platform code to the text client + * to produce the desired output corresponding to the user's keystroke. + * + */ + +#import "CoreKeyOutput.h" + +@implementation CoreKeyOutput + +-(instancetype)init:(NSUInteger)codePointsToDelete textToInsert:(NSString*)text optionsToPersist:(NSDictionary*)options alert:(BOOL)alert emitKeystroke:(BOOL)emit capsLockState:(CapsLockState)capsLock { + self = [super init]; + if (self) { + self->_codePointsToDeleteBeforeInsert = codePointsToDelete; + self->_textToInsert = text; + self->_optionsToPersist = options; + self->_alert = alert; + self->_emitKeystroke = emit; + self->_capsLockState = capsLock; + } + return self; +} + +-(NSString *)description +{ + NSString* str = @"teststring"; + NSData* data = [self.textToInsert dataUsingEncoding:NSUTF16LittleEndianStringEncoding]; + + return [[NSString alloc] initWithFormat: @"codePointsToDeleteBeforeInsert: %li, textToInsert: '%@', optionsToPersist: %@, alert: %d, emitKeystroke: %d, capsLockState: %d ", self.codePointsToDeleteBeforeInsert, data, self.optionsToPersist, self.alert, self.emitKeystroke, self.capsLockState]; +} + +-(BOOL)hasCodePointsToDelete { + return self.codePointsToDeleteBeforeInsert > 0; +} + +-(BOOL)hasTextToInsert { + return self.textToInsert.length > 0; +} + +-(BOOL)isDeleteOnlyScenario { + return (self.hasCodePointsToDelete && !self.hasTextToInsert); +} + +-(BOOL)isInsertOnlyScenario { + return (!self.hasCodePointsToDelete && self.hasTextToInsert); +} + +-(BOOL)isDeleteAndInsertScenario { + return (self.hasCodePointsToDelete && self.hasTextToInsert); +} + + +@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.h index 358dfe74290..a3111d425eb 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.h @@ -11,6 +11,7 @@ #import #import "CoreHelper.h" #import "CoreAction.h" +#import "CoreKeyOutput.h" NS_ASSUME_NONNULL_BEGIN @@ -22,8 +23,8 @@ NS_ASSUME_NONNULL_BEGIN -(instancetype)initWithHelper:(CoreHelper*)helper kmxFilePath:(nullable NSString*)path; -(BOOL)setOptionsForCore: (NSString *) key value:(NSString *) value; -(void)changeKeyboardWithKmxFilePath:(NSString*)path; --(NSArray*)processEvent:(nonnull NSEvent *)event; --(NSArray*)processMacVirtualKey:(unsigned short)macKeyCode +-(CoreKeyOutput*)processEvent:(nonnull NSEvent *)event; +-(CoreKeyOutput*)processMacVirtualKey:(unsigned short)macKeyCode withModifiers:(NSEventModifierFlags)modifierState withKeyDown:(BOOL)isKeyDown; -(void)setContextIfNeeded:(NSString*)context; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m index 41f298a75d4..fe236dd55ae 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m @@ -121,7 +121,7 @@ -(void)createKeyboardStateUsingCore { } } --(NSArray*)processEvent:(nonnull NSEvent *)event { +-(CoreKeyOutput*)processEvent:(nonnull NSEvent *)event { NSEventModifierFlags modifiers = event.modifierFlags; // process key down events only @@ -133,17 +133,14 @@ -(NSArray*)processEvent:(nonnull NSEvent *)event { return nil; } - NSArray* actions = [self processMacVirtualKey:event.keyCode - withModifiers:modifiers - withKeyDown:YES]; - - return [self.coreHelper optimizeActionArray:actions]; + return [self processMacVirtualKey:event.keyCode + withModifiers:modifiers withKeyDown:YES]; } --(NSArray*)processMacVirtualKey:(unsigned short)macKeyCode +-(CoreKeyOutput*)processMacVirtualKey:(unsigned short)macKeyCode withModifiers:(NSEventModifierFlags)modifiers withKeyDown:(BOOL) isKeyDown { - NSArray *actions = nil; + CoreKeyOutput *output = nil; uint16_t windowsKeyCode = [self.coreHelper macVirtualKeyToWindowsVirtualKey:macKeyCode]; uint32_t modifierState = [self.coreHelper macToKeymanModifier:modifiers]; uint8_t keyDown = (isKeyDown) ? 1 : 0; @@ -151,9 +148,9 @@ -(NSArray*)processMacVirtualKey:(unsigned short)macKeyCode if ([self processVirtualKey:windowsKeyCode withModifier:modifierState withKeyDown:keyDown]) { - actions = [self loadActionsUsingCore]; + output = [self loadOutputForLastKeyProcessed]; } - return actions; + return output; } -(BOOL)processVirtualKey:(uint16_t)keyCode @@ -169,8 +166,71 @@ -(BOOL)processVirtualKey:(uint16_t)keyCode return (result==KM_CORE_STATUS_OK); } --(NSArray*)loadActionsForLastKeyProcessed { - return [self loadActionsUsingCore]; +-(CoreKeyOutput*)loadOutputForLastKeyProcessed { + return [self loadActionStructUsingCore]; +} + +-(CoreKeyOutput*)loadActionStructUsingCore { + [self.coreHelper logDebugMessage:@"CoreWrapper loadActionStructUsingCore"]; + km_core_actions * actions = km_core_state_get_actions(self.coreState); + CoreKeyOutput *output = [self createCoreKeyOutputForActionsStruct:actions]; + + km_core_status result = km_core_actions_dispose(actions); + [self.coreHelper logDebugMessage:@"km_core_actions_dispose() result = %u\n", result]; + return output; +} + +-(CoreKeyOutput*) createCoreKeyOutputForActionsStruct:(km_core_actions*)actions { + NSString* text = [self.coreHelper utf32CStringToString:actions->output]; + NSDictionary* options = [self convertOptionsArray:actions->persist_options]; + CapsLockState capsLock = [self convertCapsLockState:actions->new_caps_lock_state]; + + CoreKeyOutput* coreKeyOutput = [[CoreKeyOutput alloc] init: actions->code_points_to_delete textToInsert:text optionsToPersist:options alert:actions->do_alert emitKeystroke:actions->emit_keystroke capsLockState:capsLock]; + + return coreKeyOutput; +} + +-(NSDictionary*)convertOptionsArray:(km_core_option_item*)options { + NSDictionary* optionsDictionary = nil; + unichar const * optionsKey = options->key; + unichar const * valueKey = options->value; + optionsDictionary = [[NSDictionary alloc] init]; + NSString *key = [self.coreHelper createNSStringFromUnicharString:optionsKey]; + NSString *value = [self.coreHelper createNSStringFromUnicharString:valueKey]; + [optionsDictionary insertValue:value inPropertyWithKey:key]; +/* + if (options) { + optionsDictionary = [[NSDictionary alloc] init]; + for (; options->key != 0; ++options) { + unichar const * optionsKey = options->key; + unichar const * valueKey = options->value; + + NSString *key = [self.coreHelper createNSStringFromUnicharString:optionsKey]; + NSString *value = [self.coreHelper createNSStringFromUnicharString:valueKey]; + [optionsDictionary insertValue:value inPropertyWithKey:key]; + } + } + */ + return optionsDictionary; +} + +-(CapsLockState)convertCapsLockState:(km_core_caps_state)capsState { + CapsLockState capsLock = Unchanged; + switch(capsState) { + case KM_CORE_CAPS_UNCHANGED: + capsLock = Unchanged; + break; + case KM_CORE_CAPS_OFF: + capsLock = Off; + break; + case KM_CORE_CAPS_ON: + capsLock = On; + break; + default: + capsLock = Unchanged; + break; + } + return capsLock; } -(NSArray*)loadActionsUsingCore { diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h index 470d8e73b85..4ec0c595cbc 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.h @@ -11,6 +11,7 @@ #import #import "KMXFile.h" +#import "CoreKeyOutput.h" //#define USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE @@ -26,7 +27,7 @@ - (void)setCoreContext:(NSString *)context; - (void)setCoreOptions:(NSString *)key withValue:(NSString *)value; -- (NSArray *)processEvent:(NSEvent *)event; +- (CoreKeyOutput *)processEvent:(NSEvent *)event; - (void)setUseVerboseLogging:(BOOL)useVerboseLogging; @end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m index 6af3e5f0064..16c1b0c7046 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMEngine.m @@ -111,19 +111,13 @@ - (void)setCoreOptions:(NSString *)key withValue:(NSString *)value { } /* - * Returns an NSArray of CoreAction objects. Returns nil if no actions result. + * Returns a CoreKeyOutput object detailing the results of the keystroke */ -- (NSArray *)processEvent:(NSEvent *)event { +- (CoreKeyOutput *)processEvent:(NSEvent *)event { if (!self.kmx) return nil; - // CoreWrapper returns an array of CoreAction objects - NSArray *coreActions = [self.coreWrapper processEvent:event]; - if ([coreActions count] == 0) { - return nil; - } else { - return coreActions; - } + return [self.coreWrapper processEvent:event]; } - (void) processPossibleEasterEggCharacterFrom:(NSString *)characters { diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h index 161d843b898..773f8fa827f 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h @@ -29,6 +29,7 @@ FOUNDATION_EXPORT const unsigned char KeymanEngine4MacVersionString[]; #import #import #import +#import #import "KMkeyViewProtocol.h" #import "OSKView.h" #import "TimerTarget.h" diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreWrapperTests.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreWrapperTests.m index 22267e09780..d3e35bf4649 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreWrapperTests.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreWrapperTests.m @@ -58,15 +58,9 @@ - (void)testprocessEvent_lowercaseA_returnsExpectedCharacterForKmx { NSString *kmxPath = [CoreTestStaticHelperMethods getKmxFilePathTestMacEngine]; CoreWrapper *core = [[CoreWrapper alloc] initWithHelper: [CoreTestStaticHelperMethods helper] kmxFilePath:kmxPath]; - // expecting two actions: character action with 'Ç' and end action - NSArray *actions = [core processMacVirtualKey:MVK_A withModifiers:0 withKeyDown:YES]; - XCTAssert(actions.count == 2, @"Expected 2 actions"); - CoreAction *charAction = actions[0]; - XCTAssert(charAction.actionType==CharacterAction, @"Expected CharacterAction"); - NSString *content = charAction.content; - XCTAssert([content isEqualToString:@"\u00C7"], @"Expected capital C cedille (U+00C7)"); - CoreAction *endAction = actions[1]; - XCTAssert(endAction.actionType==EndAction, @"Expected EndAction"); + // expecting character with 'Ç' + CoreKeyOutput *coreOutput = [core processMacVirtualKey:MVK_A withModifiers:0 withKeyDown:YES]; + XCTAssert([coreOutput.textToInsert isEqualToString:@"\u00C7"], @"Expected capital C cedille (U+00C7)"); } - (void)testgetContextAsString_ContextContainsEmojis_ReturnsSameContext { diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m index d2075c0542b..dbd6c8ff131 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m @@ -33,8 +33,8 @@ - (void)tearDown { - (void)testinitWithKMX_NilKmx_ProcessEventReturnsNil { KMEngine *engine = [[KMEngine alloc] initWithKMX:nil context:@"" verboseLogging:YES]; NSEvent *event = [[NSEvent alloc] init]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions == nil, @"Expected processEvent to return nil for nil kmx"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output == nil, @"Expected processEvent to return nil for nil kmx"); } - (void)testinitWithKMX_ValidKmxEmptyContext_InitializedWithEmptyContext { @@ -68,40 +68,39 @@ - (void)testsetCoreContextIfNeeded_EmptyContext_InitialContextUpdated { } */ -- (void)testprocessEvent_eventForCommandKey_ReturnsNilActionsArray { +- (void)testprocessEvent_eventForCommandKey_ReturnsNilOutput { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagCommand timestamp:0 windowNumber:0 context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:kVK_ANSI_A]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions == nil, @"Expected nil array of actions"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output == nil, @"expected nil output from core"); } -- (void)testprocessEvent_eventWithoutKeycode_ReturnsNilActionsArray { +- (void)testprocessEvent_eventWithoutKeycode_ReturnsNilOutput { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved location:NSMakePoint(29, 21) modifierFlags:NSEventModifierFlagShift timestamp:0 windowNumber:0 context:nil eventNumber:23 clickCount:0 pressure:0]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions == nil, @"Expected nil array of actions"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output == nil, @"nil CoreKeyOutput"); } -// TODO: +// TODO: shouldn't this return emitKeystroke = YES? - (void)testprocessEvent_eventForUnmappedKey_ReturnsNoActions { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"z" charactersIgnoringModifiers:@"z" isARepeat:NO keyCode:kVK_ANSI_Z]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions == nil, @"Expected nil array of actions"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output == nil, @"Expected nil array of actions"); } - (void)testprocessEvent_eventForLowercaseA_ReturnsCharacterActionWithExpectedCharacterBasedOnKmx { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileTestMacEngine]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:kVK_ANSI_A]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected 1 action"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction"); - XCTAssert([action.content isEqualToString:@"\u00C7"], @"Expected capital C cedille (U+00C7)"); + CoreKeyOutput *output = [engine processEvent:event]; + NSLog(@"output = %@", output); + XCTAssert(output.hasTextToInsert, @"output has text to insert"); + XCTAssert([output.textToInsert isEqualToString:@"\u00C7"], @"Expected capital C cedille (U+00C7)"); } // TODO: fails with core, investigate @@ -142,17 +141,15 @@ -(void)testprocessEvent_eventForCtrlShiftNumeralWithCipherMusicKmx_ReturnsQstrAc } */ -- (void)testprocessEvent_eventsForOpenCurlyBraceWithCipherMusicKmx_ReturnsCharacterActionForStartSlide { +- (void)testprocessEvent_eventsForOpenCurlyBraceWithCipherMusicKmx_ReturnsCharacterForStartSlide { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; UTF32Char expectedUtf32Char = 0x1D177; NSString * expectedStartSlideSurrogatePair = [[NSString alloc] initWithBytes:&expectedUtf32Char length:4 encoding:NSUTF32LittleEndianStringEncoding]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagShift timestamp:0 windowNumber:0 context:nil characters:@"{" charactersIgnoringModifiers:@"[" isARepeat:NO keyCode:kVK_ANSI_LeftBracket]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected 1 action"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction"); - XCTAssert([action.content isEqualToString:expectedStartSlideSurrogatePair], @"Output incorrect"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.hasTextToInsert, @"expected hasTextToInsert"); + XCTAssert([output.textToInsert isEqualToString:expectedStartSlideSurrogatePair], @"output = %@", expectedStartSlideSurrogatePair); } /* @@ -214,8 +211,8 @@ - (void)testprocessEvent_eventForShiftNumeralsWithoutRulesInCipherMusicKmx_Retur break; } NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagShift timestamp:0 windowNumber:0 context:nil characters:chars charactersIgnoringModifiers:unmodifiedChars isARepeat:NO keyCode:ansiCode]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 0, @"Expected no matching action in keyboard"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.emitKeystroke, @"expected emit keycode"); } } @@ -224,17 +221,16 @@ - (void)testprocessEvent_eventForPeriodWithCipherMusicKmx_ReturnsNoAction { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"." charactersIgnoringModifiers:@"." isARepeat:NO keyCode:kVK_ANSI_Period]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 0, @"Expected no actions"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(!output.hasTextToInsert, @"expected no text to insert"); } -// TODO: fails with core, returns CharacterAction -- (void)testprocessEvent_eventForCtrl8WithCipherMusicKmx_ReturnsNoAction { +- (void)testprocessEvent_eventForCtrl8WithCipherMusicKmx_ReturnsEmit { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForCipherMusicTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:NSEventModifierFlagControl timestamp:0 windowNumber:0 context:nil characters:@"8" charactersIgnoringModifiers:@"8" isARepeat:NO keyCode:kVK_ANSI_8]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 0, @"Expected no actions"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.emitKeystroke, @"emitKeystroke == YES"); } + (void)fillInNamesAndModifiersForAllChiralCombinations { @@ -287,11 +283,9 @@ - (void)testprocessEvent_eventForAWithModifiers_ReturnsCharacterActionWithExpect // NOTE: 'a' happens to be keyCode 0 (see initVirtualKeyMapping in CoreHelper) NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:modifiers[i] timestamp:0 windowNumber:0 context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:0]; NSString * keyCombination = [names[i] stringByAppendingFormat:@" %@", charactersIgnoringModifiers]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected 1 action for %@", keyCombination); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction %@", keyCombination); - NSString *output = [action content]; + CoreKeyOutput *coreKeyOutput = [engine processEvent:event]; + XCTAssert(coreKeyOutput.hasTextToInsert, @"hasTextToInsert for %@", keyCombination); + NSString *output = coreKeyOutput.textToInsert; NSLog(@"output = %@", output); switch (modifiers[i]) { case LEFT_SHIFT_FLAG: @@ -360,11 +354,8 @@ - (void)testprocessEvent_eventForSWithModifiers_ReturnsCharacterActionWithExpect // NOTE: 's' happens to be keyCode 1 (see initVirtualKeyMapping in CoreHelper) NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:modifiers[i] timestamp:0 windowNumber:0 context:nil characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:1]; NSString * keyCombination = [names[i] stringByAppendingFormat:@" %@", charactersIgnoringModifiers]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected 1 action for %@", keyCombination); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction for %@", keyCombination); - NSString *output = [action content]; + CoreKeyOutput *coreKeyOutput = [engine processEvent:event]; + NSString *output = coreKeyOutput.textToInsert; NSLog(@"output = %@", output); switch (modifiers[i]) { case LEFT_SHIFT_FLAG: @@ -422,11 +413,8 @@ - (NSString *)checkPlatform_getOutputForKeystroke: (NSString*) character modifie KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSString *lcChar = [character lowercaseString]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:flag timestamp:0 windowNumber:0 context:nil characters:character charactersIgnoringModifiers:lcChar isARepeat:NO keyCode:code]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected 1 action"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction"); - return [action content]; + CoreKeyOutput *output = [engine processEvent:event]; + return output.textToInsert; } // TODO: rewrite for combined character actions @@ -524,14 +512,9 @@ - (void)testEngine_ipaKeyboardAction_DoesNotCrash_Issue1892 { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForIndexOffsetTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"z" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:kVK_ANSI_A]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 2, @"Expected 2 actions"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacterBackspace], @"Expected CharacterAction"); - - action = actions[1]; - XCTAssert([action isCharacter], @"Expected CharacterAction"); - XCTAssert([action.content isEqualToString:@"Z"], @"Expected output to be 'Z'."); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.hasCodePointsToDelete, @"output hasCodePointsToDelete == YES"); + XCTAssert([output.textToInsert isEqualToString:@"Z"], @"Expected output to be 'Z'."); } /* @@ -553,21 +536,39 @@ - (void)testCoreProcessEvent_eventForFWithElNuerKmx_ReturnsCorrectCharacter { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"f" charactersIgnoringModifiers:@"f" isARepeat:NO keyCode:kVK_ANSI_F]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected one action"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction"); - XCTAssert([action.content isEqualToString:@"ɣ"], @"Expected output to be 'ɣ'."); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert([output.textToInsert isEqualToString:@"ɣ"], @"Expected output to be 'ɣ'."); + XCTAssert(!output.hasCodePointsToDelete, @"expected to delete nothing"); + XCTAssert(!output.emitKeystroke, @"expected to emit nothing"); } -- (void)testCoreProcessEvent_eventDeleteWithElNuerKmx_EmptiesContextReturnsDelete { +- (void)testArmenianMnemonic_triggerPersistOptions_ReturnsOptions { + KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForArmenianMnemonicTests]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"√" verboseLogging:YES]; + NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"w" charactersIgnoringModifiers:@"w" isARepeat:NO keyCode:kVK_ANSI_W]; + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(!output.hasCodePointsToDelete, @"expected to delete nothing"); +} + +- (void)testCoreProcessEvent_backspaceElNuerEmptyContext_PassesThrough { + KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; + KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"" verboseLogging:YES]; + NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\b" charactersIgnoringModifiers:@"\b" isARepeat:NO keyCode:kVK_Delete]; + CoreKeyOutput *output = [engine processEvent:event]; + NSLog(@"output: %@", output); + XCTAssert(!output.hasTextToInsert, @"expected to insert nothing"); + XCTAssert(!output.hasCodePointsToDelete, @"expected to delete nothing"); + XCTAssert(output.emitKeystroke, @"expected to emit key"); +} + +- (void)testCoreProcessEvent_backspaceElNuerWithContext_EmptiesContextReturnsDelete { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\b" charactersIgnoringModifiers:@"\b" isARepeat:NO keyCode:kVK_Delete]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected one action"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacterBackspace], @"Expected CharacterBackspace action"); + CoreKeyOutput *output = [engine processEvent:event]; + NSLog(@"output: %@", output); + XCTAssert(output.codePointsToDeleteBeforeInsert == 1, @"Expected output to delete one code point"); + XCTAssert(!output.hasTextToInsert, @"expected to insert nothing"); NSString *context = engine.getCoreContext; XCTAssert([context isEqualToString:@""], @"Context should be empty."); } @@ -589,10 +590,8 @@ - (void)testCoreProcessEvent_eventReturnWithElNuerKmx_ContextUnchangedReturnsRet KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\n" charactersIgnoringModifiers:@"\n" isARepeat:NO keyCode:kVK_Return]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 2, @"Expected one action"); - CoreAction *action = actions[1]; - XCTAssert(action.actionType == EmitKeystrokeAction, @"Expected EmitKeystrokeAction"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.emitKeystroke, @"Expected emitKeystroke==YES"); NSString *context = engine.getCoreContext; XCTAssert([context isEqualToString:@"ɣ"], @"Context should be unchanged."); } @@ -613,10 +612,8 @@ - (void)testCoreProcessEvent_eventTabWithElNuerKmx_ContextUnchangedReturnsTab { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForElNuerTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"ɣ" verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"\t" charactersIgnoringModifiers:@"\t" isARepeat:NO keyCode:kVK_Tab]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 2, @"Expected two actions"); - CoreAction *action = actions[1]; - XCTAssert(action.actionType == EmitKeystrokeAction, @"Expected EmitKeystrokeAction"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.emitKeystroke, @"Expected emitKeystroke==YES"); NSString *context = engine.getCoreContext; XCTAssert([context isEqualToString:@"ɣ"], @"Context should be unchanged."); } @@ -638,10 +635,8 @@ - (void)testCoreProcessEvent_eventSingleQuoteWithElNuerKmx_ReturnsDiacritic { NSString *context = @"ɛ"; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:context verboseLogging:YES]; NSEvent *event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"'" charactersIgnoringModifiers:@"'" isARepeat:NO keyCode:kVK_ANSI_Quote]; - NSArray *actions = [engine processEvent:event]; - XCTAssert(actions.count == 1, @"Expected one action"); - CoreAction *action = actions[0]; - XCTAssert([action isCharacter], @"Expected CharacterAction"); + CoreKeyOutput *output = [engine processEvent:event]; + XCTAssert(output.hasTextToInsert, @"returns text to insert"); context = engine.getCoreContext; XCTAssert([context isEqualToString:@"\u025B\u0308"], @"Context updated with diacritic."); } diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.h b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.h index aa7ce01a2b0..6d8e08ab5cb 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.h @@ -26,6 +26,7 @@ + (KMXFile *)getKmxFileForSilIpaTests; + (KMXFile *)getKmxFileForIndexOffsetTests; + (KMXFile *)getKmxFileForElNuerTests; ++ (KMXFile *)getKmxFileForArmenianMnemonicTests; @end #endif /* KeymanEngineTestsStaticHelperMethods_h */ diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.m index 73adc45b80b..65576e80d53 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KeymanEngineTestsStaticHelperMethods.m @@ -48,4 +48,10 @@ + (KMXFile *)getKmxFileForElNuerTests { return kmxFile; } ++ (KMXFile *)getKmxFileForArmenianMnemonicTests { + NSString *path = [[NSBundle bundleForClass:[KeymanEngineTestsStaticHelperMethods class]] pathForResource:@"armenian_mnemonic.kmx" ofType:nil]; + KMXFile *kmxFile = [[KMXFile alloc] initWithFilePath:path]; + return kmxFile; +} + @end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/armenian_mnemonic.kmx b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/armenian_mnemonic.kmx new file mode 100644 index 0000000000000000000000000000000000000000..7a8acd950ebdaa77302daf9d382e3bfcc1b72bcf GIT binary patch literal 32814 zcmds=acq>=neIOvhB}n8u1hKFo4V5pOC0J@$8jC%QrC5;>pBi`iIepuln|G=#36*G z4s{4+U6)eU%H<-2B7{(cST2`kk;SrHmZdb;LI@#*P-I0kVp)U`iV#8wp~xHgBDl|U zUcWxid=u2HbpN_DI%m%BH_v=CbH4X{GxiMl>5lg2lBr4Z+#erqPH>vQ4yV6HD?)2P zOTL&S_i*CSPY2&llJygl9+vjNnclg7p;Fy?6#Vq{Ft#2se+PH|m}j2FqcQ^wsg zUM=IbGVYi0RvGV*@opLKm+>JPACvJ38K05yu#7Lr_==3L%lNj8M`c{(JA(5dX*Yp1m`K8|tun7=9G+cF-J@u-aN%D8B9+|DE!PnPji8JEgZEpToPd+OZ6(R$NlnNl z$q#Y+*RhqA*oNKDGZ?=D$1T2^{2aIahj8z2;8CpznPqT~w3YYoYdI?*} z-`1SuHEi3T(_`4;8_AovkLAf@VcTy7?)jYC;-h=xb7o(|T)l>DJMQZhJfA=Rywme< zkGvE2_%h~GdMR4v5x7dl9%i{L0CcnT|ZErmn9?hG0Zp+8^gZT>Fo{x)h|*` zU&o%ns|;Qvi}4!8p08}LW>(^yB;U={&&lkPLgLbKgQW__dvKEUI>$A znZLm~Noq1b#5vyX=b6KCE7Og0lGJAEaE|^rnYVCG zlAmUNf-`-8W?|-)%y%+aefx(7M!2LnRES*$sdx3vR@C|e{Ae=uE@;GEYHl% z@bSK!c|1Jd?}zLB*{;tl$aH3^GrgJSyk|TcM}%*MXUzM4JNY%9>*Ay|_aDhOa^J}P zDfv?}BR3<517Pj}oL|nJc;v(*6}gIBG0wAb?#KCSx$@jUCChTJbiy`B9uV;9Q-3B3py`cpSI7U*nqKb?M9hSNZ#G=d=B19rd5bs;t6m zz+9~1Y~u7Jj{HisLVU%D*VQJCc{Cr-mZ!ORe_Msd)8q%@`^>&K&9V3Qr}yq~>ad*IY zdBCjOX$A&G;7MpNh${w9L3>KvRB$O;hq!6r8ED7cH6%$HxE!t39quCwo{7f0=ZW__ z3p^XG$y9Ka;3~A|#8rc9(f-|C1L^}j5AEN?)q@+++T7tj=7XEjo)y;uUVzpvu8oYg zP}~dPMQDF?*MOr8cqtlJBv0H&o!~CC?*?OTId}yc-?{L_+)D6jv?qfx*8}cFs};8v zydLd4;`+cF&}zhO1P`D+A?_XU7Bs%1^2Gby3Vs)jtC}ae?ckkgLlijCy$9Zn=Ji($ zw+Flr&FhbDKX?$$>yPdr_%NE+UoqzX2>2M9*B{++@DQ5U8r=!-DKxJ&y3^pZXkKe{ z=fJ~gml8Yiem?|%gmz5a1!mB^{&?HV;Hzj}e{>&%ucQ6gZwq$=d<)HMlDXU95i~zG z(R~WOgXVQgcNbjL!gsrNqMHaVM)TU`W1bA2isrRTR|1}f=C#Y)P6wBvdF|3Y1kR%2 zuEL2f2cCuYU&U2`E782hnVSQyM)Mk{s{zkN`;pwX4qT7+2X_tlodbA2ny-aoxF&E5 zny&@AR&X1duLZh=;1|$*Ezos<7o+)Fpj!g&MDw*kw+y@-&DR25H+UtQuLZhQ;2tzz z3v_G1Yteiy(5(aaq4`>%>j!T{8%pd%w+Z|X+LbYvz-=x_>nA`jHdTn3VtZ%W`gIWTrIdE z<(k0@Q*IG>S<0;duSvP};7uvF1-v8Wc7qS3++pzhDR&C|LCSpuzKrH|zo2AbCu-A(XqG_OgzPr#p|`TVgCM!^$ta8Wk}JU!*g!4)Z21+GiEMsRD&wS$+W zTo-s%%JqW#Q*HpfE#-EC_omz+_-M)vfzPDeF!*B1T?OAvxe+j50CZl8!KEoz2A-L6 zmEamQKW=dDYr%DBete;u2W~*~V+ma&xCzaVS9HzbRy2Q&;(l8IUWn$$FuHbd2ip6| z*a>bCcnO-n4l=hC+?{f(!Ru0P19)@Fy$jxza{IuCQtlY|WXhcdpHI0<;A<&&3p|=~ zMR?(zf~L=78n`^=W`V0xZZ5bnx%9e z_e zhE|=}iS9c1CYrzBC~3rd7w{+Ic;yIq6b)C06LWXKd}HB`xgzi+G=Cq)Trs#*9CKyh znc|qM1lNjVt{&Vhj=47QB5}-hf>(%Rt_Qqc9CI7NTf{N99lTo{bNj)E#W8mrd`cX1 z=fEF{W9~Bex;W-;gYSrAZX({OO-1v$Vs1J(D~`DeaJ4w*>cI2GG1m%yK^${Sz{|xk zw+g&g9CQ8Pcf>Kb4g8)s=JtXQiev65_=Gs-&VWA@$J|Bm$Ksf~3I0?ZbIDIpGiW+5 zlfk8E{{ES{Y2b2k+}f(71hxYPfcc9Xt(RhOsW|4!z%#{hdzIi?am>|&o5eBL2JR5Y+#+x%n*Z*Gxnq_)$5Wm|F+#6USUXcq1Bq+#Anr0&f+^+z#*_aopYk@DXv$ zy$?Puj=2xO7sN4l1$;vsbDx0kieqllU*q>iX!`hOfOF!Qn+>iJ$L-AnH;H3z0k}gP zb4$V9;+R_vUMG&Z4dBhAXw>my2U=7Pv|rw>KBuD2}-naJx9>7K6LQF}D)jD~`E7@PIhxwt{zxV{Q+4 zP#kkdz(eAgI}IKd$J_<*RdLMS0FQ`c?k>0(za-RoDFM$A$6OgWhsGc0dE)kFf-A%^ zHyb<$4L^2<6LVGI8gb0kg6q)uBL`2+%>y@xW3Cb0god};;lx}sxK$i;3&0D}@YZuY z*ADIw$J`?D5;VNc8qX~SFB8XH7q}Y@Z#%|wE5NJ7F}E7L1`RKpjyTviLAADFGbH~A_#4&dc{E;~3E`zU& zW9~NijyUEfzKEaS(0pGqHyxZ6$6N)tS{!qA;Q8X1YX!d`j=3e^<>HuI1zszTxqk3F z;+We8eoq{8d%*|AF?SR^B#yZg;4^4=8#7*~XTj&iF?R`kO&qs(3p^^0xuV55o}%gY zrh&`FF*gfbC63#h3vLw0TnkwobBn=U;+R_r?iI&eA9z3V=f1-6vyq=fa}FE*92}8$6N=vQyg>M z;2v?ztpjfq$J}P{c5%$@0`C{c+#&FBam<|rpA*O2dGKX%%v}TD635(a@F*JJF7m|O z9dNQFI_8SNlhE+CCY+cn22T;k+*EKW8eZndbJM^x#4%R}E=R-5+;}bvo+*yGS>V}d zJY4d``>h05iDRxBT#JT7M>sJz7u+b0xfXD{IOZ0EyTmcK65K0}xjyiKIOev3cZy?f z4|q@I{4pt#(Y6z4sfS9=DNW>;+R_p-YAZ_&EW0g znA-*3FOIoG;N#+$I|)7~j=A&T%i@^32EHwhxl!=MpXblZ6!3I$%$0*H#BqC7;5u>4 zHG*5kG1m@WB96H(@G5c4^@97wF*g9-CXTtC;JxCQ8w4K}$J`M3j5y|o!576bcNKh7 z9CIUJe91VkUW&mb;+QK1m!Zv<`g;gGOB{1^z;ng%ejC6o;+R_qUM!BeW#Ep@K$lm?Evo)$J_z%5pm4D4?Zo9xeve>#4&dTd_x>_pMdX*V{Xzbm@BmTGA}d0 zIdRO*2G@w=_U3_`#4)!3+#!y+rQmLH%&i8m6UW>J@Mdw$y$jwYj=6o{L*ke_20kf{ zxwGK&;+VSxz9x>jTi{V~%oTOvy&js*%QSGgIOb-7tHg18bHR<`m}>#Ii(_svxJw*! zE5W_unCk-%h+}Rmc&9k#_J9wFV{Q<91noz%e~*HXi(~E#_?$Ry?;`lJIOeW^KSpbk z?Og-k5Xan2@NG2zeJr>43HYu!<|h3D^@pap8Q`2a=4OLy#BqD`z)j+qTLA75$J|nI zw>aijgV%{;ZUcC;IOg63?-Iw{KJX!N%pC)t6vy0I@Og2}T>@Vd$J{OOs5s_|mZSdA zek}7m4O}jcxmn;UaopZqaHBZpTEOk%m|G0)635(1aIZM#`oII?nA-~8DUP{4;6ZWB z9RUxCW9~F~SR8W~z*ogFcLO{kj=8(w;#c$Mr373ij=3DTQXIEe1FjdxTobrW9CIDu zPI1h2gL}j=w+_5P9CI7No6(wOzik0;6UW>>@Bwk$-VpetIOa})&!GK8ws#i%fjH(a zfv*AQZ z4Zb6exrwjgJu8~e6?4p;+We4-Y$;09pK$)t+L+sfcJ@GZa;Vs z%|Bn@_6~xF#4&dcd`TR0H^6tqF*o^jtR*zvUIn;P9CLHP)oA{C1-DlNo-2;ICUA>5 z<~qQO#WB|nUMY^bb>Kd6%=LpeqCG3~vI+c-IOaBkx1#yyAKczH@OE*`?Et@r=AVBs zw+p;S9CLfY`_cUK59SVl4~k>%5cmk%`0vb-I|@D_j=3}755;kN7r`HkW9}ySQ*q4U zAO0ni(fmG`D+NC!j=7oOIpUbB1viLet{J>g9CM4n%fvCa0=z~XbL+vI#4)!8yh9vw zyTJ#f7cIOeLrb>f(71haFN$OCD)^>2=0?ED-{j9rF}PG5b7kO} z;<&v^aIHAz>cP$8m}>(s631L8c!fCTdcf<&F}D%CMI3Y6!MnvVw;z019COFPr^GRL z4*Zcg<}QP;i(~FK_>MT{Ca%W&4K%MI=B9(Q;+U%dSBYb;8eE6wpMNkn58NP*xkhjk znt%SmTr;>;9CHi63(@@Z59Zpz%fvCa8r(0Axh>#b;+Pu*zb}rtv)~WJF?R`kMI3Xt zz@La?ZUj7v=AXMTcL$7r7(VWpD*{hK^UqzFD+W&y$J|tKDVl%o!rU})xj5!#fvdzZ zHy7L}j=2_ayEx_+gS*5rw-Vedj=4VYfH>y1f_I8zZVz}+9CJs&L*ke_4IUQ9+y(Ge zam?KSkBDRLF1WZSe_l$!W#X92fh)yvdo|#Cam+P=+r%-~0qzvXTsOE!9CPcy8^tlV z8N6K_bGyL%#W8mXd|VuJC&A~$F?Sw(SsZiMz_-OQHwvEkX8ycP0Z$jlTsgQx9Jf~m zt`o;xBe+!@bM4^8;+R_k?n3j=T}m48dI?^E=AXOJtpu+|^Unk5dceJCLy4W})`Hig z-Mjadv_M7Y<%CavH{o2qUq)2k% z+QBfMmgU8&(7zS>-q8Ol^lL)@X6SoD|IZ0eheH3apg2x%^FPCQf9SuS<%K=3Bf%UC{l8{;@jt@& z@i6{l=nsUxIo$hK!niN=e;4{qq5pm8H-`Rqq2Cbt--f$fqy5HXC~pAU+0+#Fwf+fBA8`)=F2cOd4^T|^E^`m^F*HEDtRN% z`~}Pp^2`*Nt~~PqOl_W-2=kjf^97in=9yxcN5XUV=RO%GmuGm*;yhCdAh*&kDWGDYutDr*7h$x`}h@CeEpwIHzvnoVtl~>L$*qn>eR#;+(pPbLu9}shc>b zZsMG};ib)~n>eR#d``(YvWNcB(9aJ2w?bbT`o}^)C-gSU4`Q4@%WmQxCeE^( zILmJ0EW3%b>?Y2#n>fpE;w-y~v+O3$vYR-|ZaB*}r*1f>-ww0jMlbQ|80PEo&{v24 ziO|=C{yU+!Pqd#5#Zs`9m9L>HL#`8k|{m|Ekeo45tA&egnW9zHK*m~PfeAK?5 z3XL6eJi|R#XzVED8ScwMW5*@WaPJlxJ5qUu`@7KCvCA{8fI?$OGtaOl3XL7#Ji{s} zGQcG^bG5-(AcrmGrTM`+}pgy_PdR2 z&vQQWYe9|ec{i~Z)YzVP6Kg?@?Rht`7S!0DcN1$tjqQ0iyeu@f=RFf^L5=NsH?bDf z*q(P2Ye9|ec{i~Z)YzVP6Kg?@?Rht`7S!0DcN1$tjqQ0iu@=%jt zTA{I5W6$uFqtMuq&@(&|78+I`6IO>d`c61zS^q*9TW>YX{gq#{YOH46#F|xOHR~qU ztQxCXH?d~bSk1bLHLJ!d)D53sp|P6vOsrWoRAT$s<8@n6KhtDRj8X-vuapV|VxXsmuc6YE!v)vp^~78eo%IUo}?0ZesnavHEor>sO7{ubWuEYFK?_t3w;j3w3J!{4ln@DU7XuK8&q@E{v^j z4`b`w!q|GNQ`R2uG1jRXt5Y|zPSseQx`}nF#_H5ftWz~sr*2}Mso6tWMp;I#pwJ>L%8y8mm(`u};-kow|v2 zs>bTnO{`NjtQxY_kB#gY%Hv}GF;tBmL*2wGdh_2?$nqZ(EX+3LqeF9x>C;+DO# ztZLk_j;w0juxhME+^|OM=;MY*A3N5#;jzZ5#to~+YQzm|#45rKE5i1_8}5JG+itkG zZBM%4^RqqahI`WXn;Y&o+e>b^mu#Q7;XbiF;D(nr-)=bHoH=rW|FM`S{9`MeUJCus z!YY1Q`pz)6-e#YV&S$^S*#7WLT-R!B54nl!T8-^9H*sC7vAyUfzJ{o={pu#ZhN!VU z?Is={)Y!gv6ORvStQy?J$4npvEf3O*}rRu^Mv28Y(naRi5Exp|LviOguiQ z;oc_Oez%eBdCqx$EvT_ta1(1mjn#siSPN>b7Tm;IP-C^=hL?rLYQZzH7SvcRxQVr( z#%jS$tOYez3vOa9sIgjb6Kg?@)qLXhn+DJbeZYlV*_~~Hysd5rNTgLYX!q|GN zYwq{_x>jR#?IzZ>8mntJv98rvUAu{Ot;VX@4WD13vAXt5tZOw^*KT-OXsoV16YE-y z)wP>g*J`Y;-Nd?9V|DE&*0ma|Yd5j3)mRm~iFK{U>e@}LYc*EaZem@lvAT8>>spOf zv71=eYOJo^#JX0)>LXhn+DN~>@Eod`8SVk~?c!I3y0%`w9q~+90csWm^IFJT|8^K# zZ)<}0l)om_*qU$?*Mu5d6K>*~P-APtOwS8#2eT>FF6 zwMF>*8}ZvrKPusHJeR0O(I#Xlo%uQTpYHXdkiR(;_tur@qovE=^|3YKy%rkMFsj+qDCayCzw$9wdb*9GFnVY!I)Yv+66W5sMvw_Jq(nLGWRQv7W>JQn?y>5qcx50B9L%cQ{9jLLYcN5ov8e0c$;yO@c_3I|quNtdgH?e-zSpB+*^{d9}*G;Tn zHCDfFcv)zyemxWGSB=%Ln^?bUtbX0Z`c-4cRyXn3s>Y73ZsM_3jU8Lv#AB-(TPtqj zT2aHQAzS^}=

jZ1=jkoa_Z%_*<&ghsc+^Gs`^NtG=_iE0ea!bCF+Vjg|F!Wvz2$i)|59GQY&=hIc^>0q{x3rQzl?4Fq47NZgoK{Q`ZCj`&efta6|m(Pyp=`GLKgqZ(A$bWO}@#n_#^b-<#zCOf! zabEu6@jSid`C1Y4{A-G(;cxr#@&DC$o_<2`eBFroFNXZAvB&esc%FVj_`B774T<^E zkgpiaKRTYLpAf!9&exNee=y`{kLABLo~NIHzY)pP1e{AsvSGX&FRars;yv(vK^AR7 z=rQ1nZtBE8;;pT(kL%Ig{lzU7TeqH+Vv$CJ%(zkO?-}j&My#BLp^LgF>FSqwO)=Qr|eU6`ZUNom;-Ii}Z YeSJEUzW&d#-aeg8-)HJ@@Sn#23+|MhN&o-= literal 0 HcmV?d00001 From 75a99a940fc9378dfb135af408cbb23515b17bd4 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Tue, 21 Nov 2023 10:22:02 +0700 Subject: [PATCH 02/10] formatting changes --- .../Keyman4MacIM/KMInputMethodEventHandler.m | 21 +++++++------------ .../CoreWrapper/CoreWrapper.m | 13 +++++++----- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 07d6ead3328..6c2c7d1cd95 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -291,8 +291,8 @@ - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender { output = [self processEventWithKeymanEngine:event in:sender]; if (output == nil) { - [self checkEventForSentryEasterEgg:event]; - return NO; + [self checkEventForSentryEasterEgg:event]; + return NO; } return [self applyKeymanCoreActions:output event:event client:sender]; @@ -309,7 +309,7 @@ - (CoreKeyOutput*) processEventWithKeymanEngine:(NSEvent *)event in:(id) sender // Depending on the client app and the keyboard, using the passed-in event as it is should work okay. // Keyboards that depend on chirality support will not work. And command-key actions that change the // context might go undetected in some apps, resulting in errant behavior for subsequent typing. - coreKeyOutput = [self.kme processEvent:event]; + coreKeyOutput = [self.kme processEvent:event]; } [self.appDelegate logDebugMessage:@"processEventWithKeymanEngine, coreKeyOutput = %@", coreKeyOutput]; return coreKeyOutput; @@ -737,8 +737,8 @@ -(BOOL)applyKeymanCoreActions:(CoreKeyOutput*)output event: (NSEvent*)event clie // if we are told to emit the keystroke, then be sure to say that we have not handled the event if ((handledEvent) && (output.emitKeystroke)) { - [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: emit keystroke is forcing event as not handled"]; - handledEvent = NO; + [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: emit keystroke is forcing event as not handled"]; + handledEvent = NO; } return handledEvent; @@ -763,7 +763,7 @@ -(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonn } else if (output.isDeleteAndInsertScenario) { if (self.apiCompliance.mustBackspaceUsingEvents) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with events"]; - [self sendEvents:event forOutput:output]; + [self sendEvents:event forOutput:output]; } else { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with insert API"]; // directly insert text and handle backspaces by using replace @@ -775,7 +775,6 @@ -(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonn } -(void)applyNonTextualOutput:(CoreKeyOutput*)output { - if(output.optionsToPersist.count>0) { for(NSString *key in output.optionsToPersist) { NSString *value = [output.optionsToPersist objectForKey:key]; @@ -810,12 +809,12 @@ -(void)insertAndReplaceTextForOutput:(CoreKeyOutput*)output client:(id) client { [self insertAndReplaceText:output.textToInsert deleteCount:output.codePointsToDeleteBeforeInsert client:client]; } - /** * If we need to do something in response to a single action, then do it here. * Most of the interesting work is done in executeCompositeOperation, and * much of the other work is done when updating the context. */ +/* -(void)executeSimpleOperation:(KMActionOperation*)operation keyDownEvent:(nonnull NSEvent *)event { CoreAction *action = operation.action; if (action != nil) { @@ -846,7 +845,6 @@ -(void)executeSimpleOperation:(KMActionOperation*)operation keyDownEvent:(nonnul } } -/* -(void)executeCompositeOperation:(KMActionOperation*)operation keyDownEvent:(nonnull NSEvent *)event client:(id) client { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, composite operation: %@", operation]; @@ -869,10 +867,6 @@ -(void)executeCompositeOperation:(KMActionOperation*)operation keyDownEvent:(non } */ --(void)insertAndReplaceTextForOperation:(KMActionOperation*)operation client:(id) client { - [self insertAndReplaceText:operation.textToInsert deleteCount:operation.backspaceCount client:client]; -} - /** * This directly inserts text and applies backspaces for the operation by replacing existing text with the new text. * Because this method depends on the selectedRange API which is not implemented correctly for some client applications, @@ -913,7 +907,6 @@ -(void)insertAndReplaceText:(NSString *)text deleteCount:(int) replacementCount } } - -(void)sendEvents:(NSEvent *)event forOutput:(CoreKeyOutput*)output { if (output.hasCodePointsToDelete) { for (int i = 0; i < output.codePointsToDeleteBeforeInsert; i++) diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m index fe236dd55ae..1cd37e8e2d8 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m @@ -193,11 +193,14 @@ -(CoreKeyOutput*) createCoreKeyOutputForActionsStruct:(km_core_actions*)actions -(NSDictionary*)convertOptionsArray:(km_core_option_item*)options { NSDictionary* optionsDictionary = nil; unichar const * optionsKey = options->key; - unichar const * valueKey = options->value; - optionsDictionary = [[NSDictionary alloc] init]; - NSString *key = [self.coreHelper createNSStringFromUnicharString:optionsKey]; - NSString *value = [self.coreHelper createNSStringFromUnicharString:valueKey]; - [optionsDictionary insertValue:value inPropertyWithKey:key]; + unichar const * optionsValue = options->value; + if ((optionsKey != nil) && (optionsValue != nil)) { + optionsDictionary = [[NSDictionary alloc] init]; + NSString *key = [self.coreHelper createNSStringFromUnicharString:optionsKey]; + NSString *value = [self.coreHelper createNSStringFromUnicharString:optionsValue]; + //[optionsDictionary insertValue:value inPropertyWithKey:key]; + [optionsDictionary insertValue:@"one" inPropertyWithKey:@"1"]; + } /* if (options) { optionsDictionary = [[NSDictionary alloc] init]; From d20f6368275776a8e37bc7b82ba546a15dd88f93 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Wed, 22 Nov 2023 10:35:59 +0700 Subject: [PATCH 03/10] disable persist options unit test, enforce emit keystroke returned from keyman core --- .../Keyman4MacIM/KMInputMethodEventHandler.m | 124 +++++------------- .../xcschemes/KeymanEngine4Mac.xcscheme | 3 + .../CoreWrapper/CoreWrapper.m | 3 +- .../KeymanEngine4MacTests/KMEngineTests.m | 1 + 4 files changed, 39 insertions(+), 92 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 6c2c7d1cd95..9c450b1bc69 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -716,28 +716,18 @@ -(NSString*)readContext:(NSEvent *)event forClient:(id) client { } /** - * Returns YES if we have applied an event to the client. - * If NO, then (passthrough case) we are instructing the OS to apply the original event. - * If any of the returned actions result in a change to the client, then return YES. - * Changes to the client may be executed with insertText or by generating a KeyDown event. - * For a CharacterAction, return YES after applying the character to the client. - * For a BackspaceAction: - * -return NO if this is the only action (besides End) - * -return YES after generating a backspace event + * Returns NO if we have not applied an event to the client or if Keyman Core determines that we should emit the keystroke */ -(BOOL)applyKeymanCoreActions:(CoreKeyOutput*)output event: (NSEvent*)event client:(id) client { [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: output = %@ ", output]; - BOOL handledEvent = NO; - - // TODO: queue up the text to insert if we are emitting the keystroke - - handledEvent = [self applyKeyOutputToTextInputClient:output keyDownEvent:event client:client]; + BOOL handledEvent = [self applyKeyOutputToTextInputClient:output keyDownEvent:event client:client]; [self applyNonTextualOutput:output]; - // if we are told to emit the keystroke, then be sure to say that we have not handled the event - if ((handledEvent) && (output.emitKeystroke)) { - [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: emit keystroke is forcing event as not handled"]; + // if output from Keyman Core indicates to emit the keystroke, + // then return NO, so that the OS still handles the event + if (output.emitKeystroke) { + [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: emit keystroke true, returning false for handledEvent"]; handledEvent = NO; } @@ -751,15 +741,18 @@ -(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonn [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, insert only scenario"]; [self insertAndReplaceTextForOutput:output client:client]; } else if (output.isDeleteOnlyScenario) { + /* + // TODO: is pass through scenario handled by emit? if so delete this conditional // if we have a single delete because the backspace key was pressed, // let it pass through by returning NO in the handleEvent call if ((event.keyCode == kVK_Delete) && (output.codePointsToDeleteBeforeInsert == 1)) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only *pass through* scenario"]; handledEvent = NO; } else { + */ [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only scenario"]; [self sendEvents:event forOutput:output]; - } + //} } else if (output.isDeleteAndInsertScenario) { if (self.apiCompliance.mustBackspaceUsingEvents) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with events"]; @@ -775,9 +768,19 @@ -(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonn } -(void)applyNonTextualOutput:(CoreKeyOutput*)output { - if(output.optionsToPersist.count>0) { - for(NSString *key in output.optionsToPersist) { - NSString *value = [output.optionsToPersist objectForKey:key]; + [self persistOptions:output.optionsToPersist]; + + [self applyCapsLock:output.capsLockState]; + + if (output.alert) { + NSBeep(); + } +} + +-(void) persistOptions:(NSDictionary*)options{ + if(options.count>0) { + for(NSString *key in options) { + NSString *value = [options objectForKey:key]; if(key && value) { [self.appDelegate logDebugMessage:@"applyNonTextualOutput calling writePersistedOptions, key: %@, value: %@", key, value]; [self.appDelegate writePersistedOptions:key withValue:value]; @@ -786,86 +789,26 @@ -(void)applyNonTextualOutput:(CoreKeyOutput*)output { [self.appDelegate logDebugMessage:@"applyNonTextualOutput, invalid values in optionsToPersist, not writing to UserDefaults, key: %@, value: %@", key, value]; } } - - switch(output.capsLockState) { - case On: - //TODO: handle this - break; - case Off: - //TODO: handle this - break; - case Unchanged: - // do nothing - break; - } - - if (output.alert) { - NSBeep(); - } } } --(void)insertAndReplaceTextForOutput:(CoreKeyOutput*)output client:(id) client { - [self insertAndReplaceText:output.textToInsert deleteCount:output.codePointsToDeleteBeforeInsert client:client]; -} - -/** - * If we need to do something in response to a single action, then do it here. - * Most of the interesting work is done in executeCompositeOperation, and - * much of the other work is done when updating the context. - */ -/* --(void)executeSimpleOperation:(KMActionOperation*)operation keyDownEvent:(nonnull NSEvent *)event { - CoreAction *action = operation.action; - if (action != nil) { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeSimpleOperation: %@", operation]; - } - switch(action.actionType) { - case AlertAction: - NSBeep(); +-(void) applyCapsLock:(CapsLockState)capsLockState { + switch(capsLockState) { + case On: + //TODO: handle this break; - case PersistOptionAction: - if(action.key && action.value) { - [self.appDelegate logDebugMessage:@"PersistOptionAction calling writePersistedOptions, key: %@, value: %@", action.key, action.value]; - [self.appDelegate writePersistedOptions:action.key withValue:action.value]; - } - else { - [self.appDelegate logDebugMessage:@"Invalid values for PersistOptionAction, not writing to UserDefaults, key: %@, value: %@", action.key, action.value]; - } + case Off: + //TODO: handle this break; - case InvalidateContextAction: - self.contextChanged = YES; - break; - case CapsLockAction: { - //TODO: handle this somewhere - break; - } - default: + case Unchanged: + // do nothing break; } } --(void)executeCompositeOperation:(KMActionOperation*)operation keyDownEvent:(nonnull NSEvent *)event client:(id) client { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, composite operation: %@", operation]; - - if (operation.isTextOnlyScenario) { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, text only scenario"]; - [self insertAndReplaceTextForOperation:operation client:client]; - } else if (operation.isBackspaceOnlyScenario) { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, backspace only scenario"]; - [self sendEventsForOutput:event actionOperation:operation]; - } else if (operation.isTextAndBackspaceScenario) { - if (self.apiCompliance.mustBackspaceUsingEvents) { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, text and backspace scenario with events"]; - [self sendEventsForOutput:event actionOperation:operation]; - } else { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler executeCompositeOperation, text and backspace scenario with insert API"]; - // directly insert text and handle backspaces by using replace - [self insertAndReplaceTextForOperation:operation client:client]; - } - } +-(void)insertAndReplaceTextForOutput:(CoreKeyOutput*)output client:(id) client { + [self insertAndReplaceText:output.textToInsert deleteCount:output.codePointsToDeleteBeforeInsert client:client]; } -*/ /** * This directly inserts text and applies backspaces for the operation by replacing existing text with the new text. @@ -901,6 +844,7 @@ -(void)insertAndReplaceText:(NSString *)text deleteCount:(int) replacementCount } else { replacementRange = notFoundRange; } + [client insertText:text replacementRange:replacementRange]; if (self.apiCompliance.isComplianceUncertain) { [self.apiCompliance testComplianceAfterInsert:client]; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/xcshareddata/xcschemes/KeymanEngine4Mac.xcscheme b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/xcshareddata/xcschemes/KeymanEngine4Mac.xcscheme index 3668b806f5e..2d3bcdda67d 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/xcshareddata/xcschemes/KeymanEngine4Mac.xcscheme +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/xcshareddata/xcschemes/KeymanEngine4Mac.xcscheme @@ -47,6 +47,9 @@ ReferencedContainer = "container:KeymanEngine4Mac.xcodeproj"> + + diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m index 1cd37e8e2d8..3f573346845 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreWrapper.m @@ -198,8 +198,7 @@ -(NSDictionary*)convertOptionsArray:(km_core_option_item*)options { optionsDictionary = [[NSDictionary alloc] init]; NSString *key = [self.coreHelper createNSStringFromUnicharString:optionsKey]; NSString *value = [self.coreHelper createNSStringFromUnicharString:optionsValue]; - //[optionsDictionary insertValue:value inPropertyWithKey:key]; - [optionsDictionary insertValue:@"one" inPropertyWithKey:@"1"]; + [optionsDictionary insertValue:value inPropertyWithKey:key]; } /* if (options) { diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m index dbd6c8ff131..144938b0177 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/KMEngineTests.m @@ -542,6 +542,7 @@ - (void)testCoreProcessEvent_eventForFWithElNuerKmx_ReturnsCorrectCharacter { XCTAssert(!output.emitKeystroke, @"expected to emit nothing"); } +// TODO: enable after resolving issue getting options from Keyman Core - (void)testArmenianMnemonic_triggerPersistOptions_ReturnsOptions { KMXFile *kmxFile = [KeymanEngineTestsStaticHelperMethods getKmxFileForArmenianMnemonicTests]; KMEngine *engine = [[KMEngine alloc] initWithKMX:kmxFile context:@"√" verboseLogging:YES]; From c966a62087cce63c40fb25b0ba9e0a66b0ee8507 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Wed, 22 Nov 2023 12:29:21 +0700 Subject: [PATCH 04/10] remove single backspace code and rely on emit from core --- .../Keyman4MacIM/KMInputMethodEventHandler.m | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 9c450b1bc69..4d5b1df5072 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -741,18 +741,8 @@ -(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonn [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, insert only scenario"]; [self insertAndReplaceTextForOutput:output client:client]; } else if (output.isDeleteOnlyScenario) { - /* - // TODO: is pass through scenario handled by emit? if so delete this conditional - // if we have a single delete because the backspace key was pressed, - // let it pass through by returning NO in the handleEvent call - if ((event.keyCode == kVK_Delete) && (output.codePointsToDeleteBeforeInsert == 1)) { - [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only *pass through* scenario"]; - handledEvent = NO; - } else { - */ [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only scenario"]; [self sendEvents:event forOutput:output]; - //} } else if (output.isDeleteAndInsertScenario) { if (self.apiCompliance.mustBackspaceUsingEvents) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with events"]; @@ -807,7 +797,7 @@ -(void) applyCapsLock:(CapsLockState)capsLockState { } -(void)insertAndReplaceTextForOutput:(CoreKeyOutput*)output client:(id) client { - [self insertAndReplaceText:output.textToInsert deleteCount:output.codePointsToDeleteBeforeInsert client:client]; + [self insertAndReplaceText:output.textToInsert deleteCount:(int)output.codePointsToDeleteBeforeInsert client:client]; } /** From ad34af7f74ceef215d24e906534d0af93e2026cb Mon Sep 17 00:00:00 2001 From: sgschantz Date: Wed, 22 Nov 2023 13:43:12 +0700 Subject: [PATCH 05/10] remove action array optimizer and handler obsoleted by core improvements --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 12 - .../Keyman4MacIM/KMCoreActionHandler.h | 45 --- .../Keyman4MacIM/KMCoreActionHandler.m | 319 ------------------ .../Keyman4MacIM/KMInputMethodEventHandler.m | 1 - .../KeymanTests/CoreActionHandlerTests.m | 43 --- .../project.pbxproj | 8 - .../CoreWrapper/ActionArrayOptimizer.h | 20 -- .../CoreWrapper/ActionArrayOptimizer.m | 61 ---- .../KeymanEngine4Mac/CoreWrapper/CoreHelper.m | 3 - .../KeymanEngine4Mac/KeymanEngine4Mac.h | 1 - .../KeymanEngine4MacTests/CoreHelperTests.m | 52 --- 11 files changed, 565 deletions(-) delete mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.h delete mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.m delete mode 100644 mac/Keyman4MacIM/KeymanTests/CoreActionHandlerTests.m delete mode 100644 mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.h delete mode 100644 mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 609a5acfded..8574f5c6daf 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -18,16 +18,13 @@ 297A501928DF4D360074EB1B /* PrivacyConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 297A501628DF4D360074EB1B /* PrivacyConsent.m */; }; 298D09F72A1F4533006B9DFE /* TextApiCompliance.m in Sources */ = {isa = PBXBuildFile; fileRef = 298D09F52A1F4533006B9DFE /* TextApiCompliance.m */; }; 2992F41C2A28479400E08929 /* KeySender.m in Sources */ = {isa = PBXBuildFile; fileRef = 299ABD6F29ECE75B00AA5948 /* KeySender.m */; }; - 2992F41D2A2847A900E08929 /* KMCoreActionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 2996D63929F2719F00DF4D56 /* KMCoreActionHandler.m */; }; 2992F41E2A2847AF00E08929 /* PrivacyConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 297A501628DF4D360074EB1B /* PrivacyConsent.m */; }; 2992F41F2A2847C900E08929 /* TextApiCompliance.m in Sources */ = {isa = PBXBuildFile; fileRef = 298D09F52A1F4533006B9DFE /* TextApiCompliance.m */; }; 2992F4202A28482800E08929 /* PrivacyWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 297A501228DF4D360074EB1B /* PrivacyWindowController.m */; }; - 2996D63A29F2719F00DF4D56 /* KMCoreActionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 2996D63929F2719F00DF4D56 /* KMCoreActionHandler.m */; }; 2999C69929D3E4CE006366F5 /* InputMethodTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2999C69829D3E4CE006366F5 /* InputMethodTests.m */; }; 299ABD7129ECE75B00AA5948 /* KeySender.m in Sources */ = {isa = PBXBuildFile; fileRef = 299ABD6F29ECE75B00AA5948 /* KeySender.m */; }; 299B5E2029126CA20070841D /* keyman-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 299B5E1E29126CA20070841D /* keyman-icon@2x.png */; }; 299B5E2129126CA20070841D /* keyman-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 299B5E1F29126CA20070841D /* keyman-icon.png */; }; - 29A2D94E2A44FC4500FABFDB /* CoreActionHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29A2D94D2A44FC4500FABFDB /* CoreActionHandlerTests.m */; }; 29B42A572728339900EDD5D3 /* KMInfoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A592728339900EDD5D3 /* KMInfoWindowController.xib */; }; 29B42A602728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A622728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib */; }; 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; }; @@ -195,14 +192,11 @@ 298566E52980E584004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 298D09F52A1F4533006B9DFE /* TextApiCompliance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextApiCompliance.m; sourceTree = ""; }; 298D09F62A1F4533006B9DFE /* TextApiCompliance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextApiCompliance.h; sourceTree = ""; }; - 2996D63829F2719F00DF4D56 /* KMCoreActionHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMCoreActionHandler.h; sourceTree = ""; }; - 2996D63929F2719F00DF4D56 /* KMCoreActionHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMCoreActionHandler.m; sourceTree = ""; }; 2999C69829D3E4CE006366F5 /* InputMethodTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InputMethodTests.m; sourceTree = ""; }; 299ABD6F29ECE75B00AA5948 /* KeySender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KeySender.m; sourceTree = ""; }; 299ABD7029ECE75B00AA5948 /* KeySender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeySender.h; sourceTree = ""; }; 299B5E1E29126CA20070841D /* keyman-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "keyman-icon@2x.png"; sourceTree = ""; }; 299B5E1F29126CA20070841D /* keyman-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "keyman-icon.png"; sourceTree = ""; }; - 29A2D94D2A44FC4500FABFDB /* CoreActionHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CoreActionHandlerTests.m; sourceTree = ""; }; 29A4730527E9D6D500DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/KMAboutWindowController.strings; sourceTree = ""; }; 29A4730627E9D6D500DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/preferences.strings; sourceTree = ""; }; 29A4730727E9D6D600DE07C7 /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/KMInfoWindowController.strings; sourceTree = ""; }; @@ -572,8 +566,6 @@ 98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */, E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */, E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */, - 2996D63829F2719F00DF4D56 /* KMCoreActionHandler.h */, - 2996D63929F2719F00DF4D56 /* KMCoreActionHandler.m */, 298D09F62A1F4533006B9DFE /* TextApiCompliance.h */, 298D09F52A1F4533006B9DFE /* TextApiCompliance.m */, E24C79EC1FFFEA6B00D8E46F /* KMInputMethodEventHandlerProtected.h */, @@ -708,7 +700,6 @@ E211769E20E18C0B00F8065D /* AppleCompliantTestClient.h */, E211769F20E18C5200F8065D /* AppleCompliantTestClient.m */, 2999C69829D3E4CE006366F5 /* InputMethodTests.m */, - 29A2D94D2A44FC4500FABFDB /* CoreActionHandlerTests.m */, ); path = KeymanTests; sourceTree = ""; @@ -1011,7 +1002,6 @@ 297A501928DF4D360074EB1B /* PrivacyConsent.m in Sources */, 98FE105E1B4DE8F400525F54 /* NSWindow+SuppMethods.m in Sources */, 9836B3741AE5F11D00780482 /* ZipArchive.mm in Sources */, - 2996D63A29F2719F00DF4D56 /* KMCoreActionHandler.m in Sources */, 983247B21A9EB9690010B90C /* KMConfigColumn3CellView.m in Sources */, 290BC75E274F3FD7005CD1C3 /* KMKeyboardInfo.m in Sources */, 297A501728DF4D360074EB1B /* PrivacyWindowController.m in Sources */, @@ -1028,9 +1018,7 @@ 2992F4202A28482800E08929 /* PrivacyWindowController.m in Sources */, 2992F41F2A2847C900E08929 /* TextApiCompliance.m in Sources */, 2992F41E2A2847AF00E08929 /* PrivacyConsent.m in Sources */, - 2992F41D2A2847A900E08929 /* KMCoreActionHandler.m in Sources */, 2992F41C2A28479400E08929 /* KeySender.m in Sources */, - 29A2D94E2A44FC4500FABFDB /* CoreActionHandlerTests.m in Sources */, 2999C69929D3E4CE006366F5 /* InputMethodTests.m in Sources */, E211769D20E182DD00F8065D /* NoContextTestClient.m in Sources */, E2D5529D20DD8E0200D77CFB /* LegacyTestClient.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.h deleted file mode 100644 index ffef9ba1250..00000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Keyman is copyright (C) SIL International. MIT License. - * - * KMCoreActionHandler.h - * Keyman - * - * Created by Shawn Schantz on 2023-04-21. - * - */ - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface KMActionOperation : NSObject --(instancetype)initForSimpleAction:(CoreAction*)action; --(instancetype)initForCompositeAction:(NSString*)textToInsert backspaceCount:(int)backspaces; --(BOOL)hasTextToInsert; --(BOOL)hasBackspaces; --(BOOL)isBackspaceOnlyScenario; --(BOOL)isTextOnlyScenario; --(BOOL)isTextAndBackspaceScenario; -@property (readonly) CoreAction *action; -@property (readonly) NSString *textToInsert; -@property (readonly) int backspaceCount; -@property (readonly) BOOL isForSimpleAction; -@property (readonly) BOOL isForCompositeAction; -@end - -@interface KMActionHandlerResult : NSObject -@property (readonly) BOOL handledEvent; -@property (readonly) NSArray *operations; -@property (readonly) NSString *textToInsert; -@property (readonly) int backspaceCount; - --(instancetype)initForActions:(NSArray*)actions handledEvent:(BOOL)handledEvent backspaceCount:(int) backspaces textToInsert:(NSString*) text; -@end - -@interface KMCoreActionHandler : NSObject --(instancetype)initWithActions:(NSArray*)actions keyCode: (unsigned short)keyCode; --(KMActionHandlerResult*)handleActions; -@end - -NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.m deleted file mode 100644 index 41a66350f49..00000000000 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMCoreActionHandler.m +++ /dev/null @@ -1,319 +0,0 @@ -/** - * Keyman is copyright (C) SIL International. MIT License. - * - * KMCoreActionHandler.m - * Keyman - * - * Created by Shawn Schantz on 2023-04-21. - * - * Processes an NSArray of CoreAction objects and determines how they should be - * applied to the client application. Creates a KMActionHandlerResult object to instruct - * Keyman how to apply the changes to the client to conform to the CoreAction - * objects. - * - * To apply the changes in the correct order in the client application, the - * pattern in which the actions occur are analyzed and classified with the - * ActionPattern enum. Some patterns require a different strategy to preserve - * the order. - */ - -#import "KMInputMethodEventHandler.h" -#import "KMCoreActionHandler.h" - -@interface KMCoreActionHandler () - -typedef enum { - BackspacesOnly, - CharactersOnly, - BackspaceBeforeCharacter, - CharacterBeforeBackspace, - None -} ClientTextOutputPattern; - -@property (readonly) NSArray *actions; -@property (readonly) unsigned short keyCode; /* device-independent key number */ -@property (readonly) ClientTextOutputPattern pattern; -@property int backspaceCount; -@property BOOL useEvents; -@property BOOL emitKeystroke; -@end - -@implementation KMCoreActionHandler - --(KMInputMethodAppDelegate *)appDelegate { - return (KMInputMethodAppDelegate *)[NSApp delegate]; -} - --(instancetype)initWithActions:(NSArray*)actions keyCode: (unsigned short)keyCode { - self = [super init]; - if (self) { - _actions = actions; - _keyCode = keyCode; - _backspaceCount = 0; - _emitKeystroke = NO; - } - return self; -} - -/* - * Loop through the actions and identify which pattern of which actions of type - * CharacterAction and CharacterBackspaceAction occur. We use this pattern to - * determine how to apply changes to the client application. - */ --(ClientTextOutputPattern)analyzeActions:(NSArray*)actions { - BOOL containsCharacters = NO; - BOOL containsBackspaces = NO; - BOOL backspaceFirst = NO; - BOOL characterFirst = NO; - // TODO: adjust backspace count for code points with more than one code unit? - // TODO: backspace event and replacement range must be tested for surrogate pairs - - // first, loop through the actions to summarize the contents - // only looking at characters and backspaces of characters - for (CoreAction *action in [actions objectEnumerator]) { - if (action.isCharacter) { - containsCharacters = YES; - if (!containsBackspaces) { - characterFirst = YES; - } - } else if (action.isCharacterBackspace) { - containsBackspaces = YES; - self.backspaceCount++; - if (!containsCharacters) { - backspaceFirst = YES; - } - } - } - - [self.appDelegate logDebugMessage:@"analyzeActions, containsCharacters = %d, containsBackspaces = %d ", containsCharacters, containsBackspaces]; - - ClientTextOutputPattern actionPattern = None; - // second, asssign an actionPattern type based on what we found - if (containsCharacters) { - if (containsBackspaces) { - // contains both characters and backspaces, now determine order - if (characterFirst) { - actionPattern = CharacterBeforeBackspace; - } else if (backspaceFirst) { - actionPattern = BackspaceBeforeCharacter; - } - } else { - actionPattern = CharactersOnly; - } - } else if (containsBackspaces) { - actionPattern = BackspacesOnly; - } - - return actionPattern; -} - --(BOOL)isSinglePassThroughBackspace { - BOOL isPassThrough = - (self.keyCode == kVK_Delete) - && (self.pattern == BackspacesOnly) - && (self.backspaceCount == 1); - return isPassThrough; -} - -/* - * Returns result object to instruct Input Method how to apply changes to client text. - */ --(KMActionHandlerResult*)handleActions { - KMActionHandlerResult *result = nil; - - [self.appDelegate logDebugMessage:@"handleActions invoked, actions.count = %lu ", (unsigned long)self.actions.count]; - - // examine the pattern of actions for required text inserted and deleted - _pattern = [self analyzeActions:self.actions]; - - // use the ClientTextOutputPattern to create the result object - switch (self.pattern) { - case CharactersOnly: - result = [self buildResultForCharactersOnly]; - break; - case BackspacesOnly: - if ([self isSinglePassThroughBackspace]) { - result = [self buildResultForSinglePassThroughBackspaceNoText]; - } else { - result = [self buildResultForMultipleBackspacesNoText]; - } - break; - case BackspaceBeforeCharacter: - result = [self buildResultForBackspacesBeforeText]; - break; - case None: - result = [self buildResultForNoCharactersOrBackspaces]; - break; - default: - NSLog(@"KMCoreActionHandler handleActions(), Unimplemented pattern***"); - } - - return result; -} - -/** - * The event is marked as handled and the result includes the new text string to insert in the client - */ --(KMActionHandlerResult*)buildResultForCharactersOnly { - [self.appDelegate logDebugMessage:@"buildResultForCharactersOnly"]; - return [[KMActionHandlerResult alloc] initForActions:self.actions handledEvent:YES backspaceCount:0 textToInsert:[self collectOutputText]]; -} - -/** - * Mark the event as NOT handled, we simply let the original backspace event pass through to the client without manipulating the client text - */ --(KMActionHandlerResult*)buildResultForSinglePassThroughBackspaceNoText { - [self.appDelegate logDebugMessage:@"buildResultForSinglePassThroughBackspaceNoText"]; - return [[KMActionHandlerResult alloc] initForActions:self.actions handledEvent:NO backspaceCount:0 textToInsert:@""]; -} - -/** - * Multiple backspaces with no text will be handled by generating events - */ --(KMActionHandlerResult*)buildResultForMultipleBackspacesNoText { - [self.appDelegate logDebugMessage:@"buildResultForMultipleBackspacesNoText"]; - return [[KMActionHandlerResult alloc] initForActions:self.actions handledEvent:YES backspaceCount:self.backspaceCount textToInsert:@""]; -} - -/** - * For backspaces needed before text, we will insert after backspaces are completed - */ --(KMActionHandlerResult*)buildResultForBackspacesBeforeText { - [self.appDelegate logDebugMessage:@"buildResultForBackspacesBeforeText"]; - return [[KMActionHandlerResult alloc] initForActions:self.actions handledEvent:YES backspaceCount:self.backspaceCount textToInsert:[self collectOutputText]]; -} - -/** - * For case with no actions of type CharacterAction or CharacterBackspaceAction - */ --(KMActionHandlerResult*)buildResultForNoCharactersOrBackspaces { - [self.appDelegate logDebugMessage:@"buildResultForNoCharactersOrBackspaces"]; - return [[KMActionHandlerResult alloc] initForActions:self.actions handledEvent:NO backspaceCount:0 textToInsert:@""]; -} - - --(NSString*)collectOutputText { - NSMutableString *output = [[NSMutableString alloc]init]; - for (CoreAction *action in [self.actions objectEnumerator]) { - if (action.actionType==CharacterAction) { - [output appendString:action.content]; - } - } - return output; -} - -@end - -@implementation KMActionHandlerResult --(instancetype)initForActions:(NSArray*)actions handledEvent:(BOOL)handledEvent backspaceCount:(int)backspaces textToInsert:(NSString*)text { - self = [super init]; - if (self) { - _handledEvent = handledEvent; - _backspaceCount = backspaces; - _textToInsert = text; - _operations = [self populateOperationListForActions:actions]; - } - return self; -} - --(NSArray*) populateOperationListForActions:(NSArray*)actions { - NSMutableArray* operationsList = [NSMutableArray arrayWithCapacity:actions.count]; - BOOL addedCompositeOperation = NO; - - for (CoreAction *action in [actions objectEnumerator]) - { - switch(action.actionType) { - case CharacterAction: - case CharacterBackspaceAction: - /** - * only create the composite operation if we are handling the event, and, then, only if we haven't already created one yet - */ - if(self.handledEvent) { - if(!addedCompositeOperation) { - KMActionOperation *operation = [[KMActionOperation alloc] initForCompositeAction:self.textToInsert backspaceCount:self.backspaceCount]; - [operationsList addObject:operation]; - addedCompositeOperation = YES; - } - } else { - /** - * if we are not handling the event (letting backspace passthrough), then just make an operation for the action - * so that it can be used to update the context - */ - KMActionOperation *operation = [[KMActionOperation alloc] initForSimpleAction:action]; - [operationsList addObject:operation]; - } - break; - case MarkerAction: - case MarkerBackspaceAction: - case AlertAction: - case PersistOptionAction: - case EmitKeystrokeAction: - case InvalidateContextAction: - case CapsLockAction: { - KMActionOperation *operation = [[KMActionOperation alloc] initForSimpleAction:action]; - [operationsList addObject:operation]; - break; - } - case EndAction: - // this should never happen as it has been removed during optimization - break; - } - } - - return [operationsList copy]; -} - -@end - -@implementation KMActionOperation - --(instancetype)initForSimpleAction:(CoreAction*)action { - self = [super init]; - if (self) { - _action = action; - _textToInsert = @""; - _backspaceCount = 0; - _isForSimpleAction = YES; - _isForCompositeAction = NO; - } - return self; -} - --(instancetype)initForCompositeAction:(NSString*)textToInsert backspaceCount:(int)backspaces { - self = [super init]; - if (self) { - _action = nil; - _textToInsert = textToInsert; - _backspaceCount = backspaces; - _isForSimpleAction = NO; - _isForCompositeAction = YES; - } - return self; -} - --(NSString *)description -{ - return [[NSString alloc] initWithFormat: @"%@ %@ textToInsert: %@ backspaces: %d", self.isForSimpleAction?@"simpleAction":@"compositeAction", self.action, self.textToInsert, self.backspaceCount]; -} - --(BOOL)hasTextToInsert { - return (self.textToInsert.length > 0); -} - --(BOOL)hasBackspaces { - return (self.backspaceCount > 0); -} - --(BOOL)isBackspaceOnlyScenario { - return (self.hasBackspaces && !self.hasTextToInsert); -} - --(BOOL)isTextOnlyScenario { - return (!self.hasBackspaces && self.hasTextToInsert); -} - --(BOOL)isTextAndBackspaceScenario { - return (self.hasBackspaces && self.hasTextToInsert); -} - -@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 4d5b1df5072..eab1508ebdd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -10,7 +10,6 @@ #import #import /* For kVK_ constants. */ #import "KeySender.h" -#import "KMCoreActionHandler.h" #import "TextApiCompliance.h" @import Sentry; diff --git a/mac/Keyman4MacIM/KeymanTests/CoreActionHandlerTests.m b/mac/Keyman4MacIM/KeymanTests/CoreActionHandlerTests.m deleted file mode 100644 index 52b4cc13f8a..00000000000 --- a/mac/Keyman4MacIM/KeymanTests/CoreActionHandlerTests.m +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Keyman is copyright (C) SIL International. MIT License. - * - * CoreActionHandlerTests.m - * KeymanTests - * - * Created by Shawn Schantz on 2023-06-22. - * - * Description... - */ - -#import -#import "KMCoreActionHandler.h" - -//#import - -@interface CoreActionHandlerTests : XCTestCase - -@end - -@implementation CoreActionHandlerTests - -- (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -- (void)testExample { - - NSArray *actions = nil; - unsigned int keycode = 0; - - KMCoreActionHandler *actionHandler = [[KMCoreActionHandler alloc] initWithActions:actions keyCode: keycode]; - - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - NSLog(@"*** KMCoreActionHandler test is unimplemented."); -} - -@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj index 952d2d1b31f..0a46301b7f5 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac.xcodeproj/project.pbxproj @@ -8,11 +8,9 @@ /* Begin PBXBuildFile section */ 2993C32C29B1D92E00FB5E95 /* CoreAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2993C32229B1D92E00FB5E95 /* CoreAction.m */; }; - 2993C32D29B1D92E00FB5E95 /* ActionArrayOptimizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2993C32329B1D92E00FB5E95 /* ActionArrayOptimizer.m */; }; 2993C32E29B1D92E00FB5E95 /* CoreWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 2993C32429B1D92E00FB5E95 /* CoreWrapper.m */; }; 2993C32F29B1D92E00FB5E95 /* CoreHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 2993C32529B1D92E00FB5E95 /* CoreHelper.m */; }; 2993C33229B1D92E00FB5E95 /* CoreWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 2993C32829B1D92E00FB5E95 /* CoreWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 2993C33329B1D92E00FB5E95 /* ActionArrayOptimizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 2993C32929B1D92E00FB5E95 /* ActionArrayOptimizer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2993C33429B1D92E00FB5E95 /* CoreAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2993C32A29B1D92E00FB5E95 /* CoreAction.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2993C33529B1D92E00FB5E95 /* CoreHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 2993C32B29B1D92E00FB5E95 /* CoreHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2993C33829B1DD6900FB5E95 /* libkeymancore.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2993C33729B1DD6900FB5E95 /* libkeymancore.a */; }; @@ -83,11 +81,9 @@ /* Begin PBXFileReference section */ 2993C32229B1D92E00FB5E95 /* CoreAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreAction.m; sourceTree = ""; }; - 2993C32329B1D92E00FB5E95 /* ActionArrayOptimizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ActionArrayOptimizer.m; sourceTree = ""; }; 2993C32429B1D92E00FB5E95 /* CoreWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreWrapper.m; sourceTree = ""; }; 2993C32529B1D92E00FB5E95 /* CoreHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CoreHelper.m; sourceTree = ""; }; 2993C32829B1D92E00FB5E95 /* CoreWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreWrapper.h; sourceTree = ""; }; - 2993C32929B1D92E00FB5E95 /* ActionArrayOptimizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActionArrayOptimizer.h; sourceTree = ""; }; 2993C32A29B1D92E00FB5E95 /* CoreAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreAction.h; sourceTree = ""; }; 2993C32B29B1D92E00FB5E95 /* CoreHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreHelper.h; sourceTree = ""; }; 2993C33729B1DD6900FB5E95 /* libkeymancore.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libkeymancore.a; path = ../../core/build/mac/debug/libkeymancore.a; sourceTree = ""; }; @@ -182,8 +178,6 @@ 2993C32429B1D92E00FB5E95 /* CoreWrapper.m */, 2993C32A29B1D92E00FB5E95 /* CoreAction.h */, 2993C32229B1D92E00FB5E95 /* CoreAction.m */, - 2993C32329B1D92E00FB5E95 /* ActionArrayOptimizer.m */, - 2993C32929B1D92E00FB5E95 /* ActionArrayOptimizer.h */, 29A1971C2AF091E600512A37 /* CoreKeyOutput.h */, 29A1971D2AF091E600512A37 /* CoreKeyOutput.m */, ); @@ -354,7 +348,6 @@ 981880711BC1EA5800A1FBA5 /* KeyLabelCell.h in Headers */, 981880731BC1EA5800A1FBA5 /* KeyView.h in Headers */, 2993C33529B1D92E00FB5E95 /* CoreHelper.h in Headers */, - 2993C33329B1D92E00FB5E95 /* ActionArrayOptimizer.h in Headers */, 2993C33229B1D92E00FB5E95 /* CoreWrapper.h in Headers */, 9818806F1BC1EA5800A1FBA5 /* KeyLabel.h in Headers */, 981880751BC1EA5800A1FBA5 /* OSKKey.h in Headers */, @@ -507,7 +500,6 @@ 9818807C1BC1EE9700A1FBA5 /* TimerTarget.m in Sources */, 980053381B37C9B50088DDDD /* NKey.m in Sources */, 981880701BC1EA5800A1FBA5 /* KeyLabel.m in Sources */, - 2993C32D29B1D92E00FB5E95 /* ActionArrayOptimizer.m in Sources */, 981880721BC1EA5800A1FBA5 /* KeyLabelCell.m in Sources */, 981880781BC1EA5800A1FBA5 /* OSKView.m in Sources */, 2993C32E29B1D92E00FB5E95 /* CoreWrapper.m in Sources */, diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.h deleted file mode 100644 index b79f2519c8d..00000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Keyman is copyright (C) SIL International. MIT License. - * - * ActionArrayOptimizer.h - * Keyman - * - * Created by Shawn Schantz on 2023-02-23. - * - */ - -#import -#import "CoreAction.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface ActionArrayOptimizer : NSObject --(NSArray*)optimize:(NSArray*)actionArray; -@end - -NS_ASSUME_NONNULL_END diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.m deleted file mode 100644 index 115db71ff6f..00000000000 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/ActionArrayOptimizer.m +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Keyman is copyright (C) SIL International. MIT License. - * - * ActionArrayOptimizer.m - * Keyman - * - * Created by Shawn Schantz on 2023-02-23. - * - * Manipulates an array of CoreAction objects created in response to the processing - * of a key and reduces them to the minimal representation. For example, a character - * that causes a re-ordering of the context may cause the character to be deleted. - * Instead of emitting it twice and deleting it once, just emit it once. - */ - -#import "ActionArrayOptimizer.h" - -@implementation ActionArrayOptimizer - -/* - * This optimizes the array of CoreAction objects to best simplify - * output by the Input Method based on its use of Keyman Core. - */ --(NSArray*)optimize:(NSArray*)actionArray { - - NSMutableArray *optimizedArray = [[NSMutableArray alloc] init]; - - /** - loop through actions in order - if we find an EndAction, do not save to optimizedArray - if we find a CharacterBackspaceAction then remove the last action (as - long as the type of backspace matches the type of the last action) and - do not save the BackspaceAction to optimizedArray - otherwise, simply copy the action to the optimizedArray - */ - for (CoreAction *action in [actionArray objectEnumerator]) - { - if ([self checkForUnnecessaryAction:action]) { - continue; - } else if (optimizedArray.count > 0) { - CoreAction *lastAction = optimizedArray.lastObject; - if ((action.isCharacterBackspace) && (lastAction.isCharacter)) { - [optimizedArray removeLastObject]; - continue; - } else if ((action.isMarkerBackspace) && (lastAction.isMarker)) { - [optimizedArray removeLastObject]; - continue; - } - } - - [optimizedArray insertObject:action atIndex:optimizedArray.count]; - } - - return optimizedArray; -} - -/* returns YES if the action is unnecessary and should be removed */ --(BOOL)checkForUnnecessaryAction:(CoreAction*)action { - return action.actionType == EndAction; -} - -@end diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m index 80d30c1b402..47ae04a83c0 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m @@ -17,7 +17,6 @@ #import "CoreHelper.h" #import "CoreAction.h" #import "keyman_core_api.h" -#import "ActionArrayOptimizer.h" #import "MacVKCodes.h" #import "WindowsVKCodes.h" @@ -25,7 +24,6 @@ UInt32 VirtualKeyMap[VIRTUAL_KEY_ARRAY_SIZE]; @interface CoreHelper() -@property (strong, nonatomic, readonly) ActionArrayOptimizer *optimizer; @end @@ -73,7 +71,6 @@ -(instancetype)initWithDebugMode:(BOOL)debugMode { self = [super init]; if (self) { _debugMode = debugMode; - _optimizer = [[ActionArrayOptimizer alloc] init]; [self initVirtualKeyMapping]; } return self; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h index 773f8fa827f..eef0225413b 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KeymanEngine4Mac.h @@ -26,7 +26,6 @@ FOUNDATION_EXPORT const unsigned char KeymanEngine4MacVersionString[]; #import #import #import -#import #import #import #import diff --git a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreHelperTests.m b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreHelperTests.m index 3b7f309c328..8a1086fed98 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreHelperTests.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4MacTests/CoreHelperTests.m @@ -15,7 +15,6 @@ #import "CoreTestStaticHelperMethods.h" #import "keyman_core_api.h" #import "CoreAction.h" -#import "ActionArrayOptimizer.h" @interface CoreHelperTests : XCTestCase @@ -23,10 +22,7 @@ @interface CoreHelperTests : XCTestCase @implementation CoreHelperTests -ActionArrayOptimizer *optimizer; - + (void)setUp { - optimizer = [[ActionArrayOptimizer alloc] init]; } - (void)tearDown { @@ -128,54 +124,6 @@ - (void)testConversionFromUnicharString_optionName_matchesLiteral { XCTAssertTrue([convertedString isEqual:optionNameString], @"Converted unichar string is not equal to literal string."); } - -/* -- (void)testOptimize_MultipleCharacterActions_CombinedToSingleAction { - CoreAction *characterAAction = [[CoreAction alloc] initWithType:CharacterAction actionContent:@"A" backspaceCount:0]; - CoreAction *characterBAction = [[CoreAction alloc] initWithType:CharacterAction actionContent:@"B" backspaceCount:0]; - CoreAction *endAction = [[CoreAction alloc] initWithType:EndAction actionContent:@"" backspaceCount:0]; - NSArray *coreArray = @[characterAAction, characterBAction, endAction]; - - NSArray *optimizedArray = [optimizer optimize:coreArray]; - XCTAssert(optimizedArray.count == 1, @"Expected 1 action"); - CoreAction *action = optimizedArray[0]; - XCTAssert([action.content isEqualToString:@"AB"], @"Expected combined string."); -} - -- (void)testOptimize_MultipleBackspaceActions_CombinedToSingleAction { - CoreAction *backspaceAction = [[CoreAction alloc] initWithType:CharacterBackspaceAction actionContent:@"" backspaceCount:1]; - CoreAction *anotherBackspaceAction = [[CoreAction alloc] initWithType:CharacterBackspaceAction actionContent:@"" backspaceCount:1]; - CoreAction *endAction = [[CoreAction alloc] initWithType:EndAction actionContent:@"" backspaceCount:0]; - NSArray *coreArray = @[backspaceAction, anotherBackspaceAction, endAction]; - - NSArray *optimizedArray = [optimizer optimize:coreArray]; - XCTAssert(optimizedArray.count == 1, @"Expected 1 action"); - CoreAction *action = optimizedArray[0]; - XCTAssert(action.backspaceCount==2, @"Expected a backspace count of 2."); -} -*/ -- (void)testOptimize_OneBackspaceAndOneCharacterAction_RetainedWithEndActionStripped { - CoreAction *backspaceAction = [[CoreAction alloc] initCharacterBackspaceAction:@""]; - CoreAction *characterAAction = [[CoreAction alloc] initCharacterAction:@"A"]; - CoreAction *endAction = [[CoreAction alloc] initWithType:EndAction actionContent:@"" backspaceCount:0 key:@"" value:@"" scope:0]; - NSArray *coreArray = @[backspaceAction, characterAAction, endAction]; - - NSArray *optimizedArray = [optimizer optimize:coreArray]; - XCTAssert(optimizedArray.count == 2, @"Expected two actions."); - CoreAction *action = optimizedArray[1]; - XCTAssert([action.content isEqualToString:@"A"], @"Expected CharacterAction."); -} - -- (void)testOptimize_OneCharacterAndOneBackspaceAction_CompactedToEmptyArray { - CoreAction *characterAAction = [[CoreAction alloc] initCharacterAction:@"A"]; - CoreAction *backspaceAction = [[CoreAction alloc] initCharacterBackspaceAction:@""]; - CoreAction *endAction = [[CoreAction alloc] initWithType:EndAction actionContent:@"" backspaceCount:0 key:@"" value:@"" scope:0]; - NSArray *coreArray = @[characterAAction, backspaceAction, endAction]; - - NSArray *optimizedArray = [optimizer optimize:coreArray]; - XCTAssert(optimizedArray.count == 0, @"Expected empty array"); -} - - (void)testHelperCreationPerformance { // This is an example of a performance test case. [self measureBlock:^{ From 6879af00e76d9acc2eb169b8d637e4474b2fd707 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Thu, 23 Nov 2023 14:26:46 +0700 Subject: [PATCH 06/10] remove some of the legacy backspace code --- .../Keyman4MacIM/KMInputMethodEventHandler.m | 234 +----------------- 1 file changed, 5 insertions(+), 229 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index eab1508ebdd..48bb4e38482 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -170,59 +170,6 @@ - (NSMutableString *)contextBuffer { return self.appDelegate.contextBuffer; } -- (NSRange)getSelectionRangefromClient:(id)client { - switch (_clientCanProvideSelectionInfo) { - case Unknown: - if ([client respondsToSelector:@selector(selectedRange)]) { - NSRange selRange = [client selectedRange]; - if (selRange.location != NSNotFound || selRange.length != NSNotFound) { - _clientCanProvideSelectionInfo = Yes; - [self.appDelegate logDebugMessage:@"Client can provide selection info."]; - [self.appDelegate logDebugMessage:@"selRange.location: %lu", (unsigned long)selRange.location]; - [self.appDelegate logDebugMessage:@"selRange.length: %lu", (unsigned long)selRange.length]; - return selRange; - } - } - _clientCanProvideSelectionInfo = No; - // REVIEW: For now, if the client reports its location as "not found", we're stuck assuming that we're - // still in the same location in the context. This may be totally untrue, but if the client can't report - // its location, we have nothing else to go on. - _clientSelectionCanChangeUnexpectedly = NO; - [self.appDelegate logDebugMessage:@"Client can NOT provide selection info!"]; - // fall through - case No: - return NSMakeRange(NSNotFound, NSNotFound); - case Yes: - case Unreliable: - { - NSRange selRange = [client selectedRange]; - [self.appDelegate logDebugMessage:@"selRange.location: %lu", (unsigned long)selRange.location]; - return selRange; - } - } -} - -- (void)updateContextBuffer:(id)sender { - [self.appDelegate logDebugMessage:@"*** updateContextBuffer ***"]; - [self.appDelegate logDebugMessage:@"sender: %@", sender]; - - NSRange selRange = [self getSelectionRangefromClient: sender]; - - // Since client can't tell us the actual current position, we assume the previously known location in the context. - // (It won't matter anyway if the client also fails to report its context.) - NSUInteger len = (_clientCanProvideSelectionInfo != Yes) ? _previousSelRange.length : selRange.location; - NSString *preBuffer = [self getLimitedContextFrom:sender at:len]; - - // 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; - [self.appDelegate logDebugMessage:@"contextBuffer = \"%@\"", self.contextBuffer.length?[self.contextBuffer codeString]:@"{empty}"]; - [self.appDelegate logDebugMessage:@"***"]; - _previousSelRange = selRange; -} - -(NSString *)getLimitedContextFrom:(id)sender at:(NSUInteger) len { if (![sender respondsToSelector:@selector(attributedSubstringFromRange:)]) return nil; @@ -260,31 +207,6 @@ -(void)setPendingBuffer:(NSString*)string [buffer setString:string]; } -// Append to (creating if necessary) the pending buffer. --(void)appendPendingBuffer:(NSString*)string -{ - NSMutableString* buffer = [self pendingBuffer]; - [buffer appendString:string]; -} - -- (void)updateContextBufferIfNeeded:(id)client { - // REVIEW: if self.appDelegate.lowLevelEventTap == nil under what circumstances might we be able to safely - // re-get the context? (Probably clientCanProvideSelectionInfo would need to be true, and it would only - // actually be useful if client respondsToSelector:@selector(attributedSubstringFromRange:), but even then - // we'd only want to do it if we have actually moved from our previous location. Otherwise, we wouldn't be - // able to handle dead keys correctly.) - if (self.appDelegate.contextChangedByLowLevelEvent) { - if (!_contextOutOfDate) { - [self.appDelegate logDebugMessage:@"Low-level event requires context to be re-retrieved."]; - } - _contextOutOfDate = YES; - self.appDelegate.contextChangedByLowLevelEvent = NO; - } - - if (_contextOutOfDate) - [self updateContextBuffer:client]; -} - - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender { CoreKeyOutput *output = nil; @@ -338,7 +260,6 @@ - (void)checkEventForSentryEasterEgg:(NSEvent *)event { } } - - (void)processUnhandledDeleteBack:(id)client updateEngineContext:(BOOL *)updateEngineContext { if ([self.appDelegate debugMode]) { NSLog(@"Processing an unhandled delete-back..."); @@ -408,164 +329,19 @@ - (BOOL)handleDeleteBackLowLevel:(NSEvent *)event { if(event.keyCode == kVK_Delete && _legacyMode && [self pendingBuffer].length > 0) { BOOL updateEngineContext = YES; if ([self.appDelegate debugMode]) { - NSLog(@"legacy: delete-back received, processing"); + NSLog(@"legacy: handleDeleteBackLowLevel, delete-back received, processing"); } [self processUnhandledDeleteBack:self.senderForDeleteBack updateEngineContext: &updateEngineContext]; self.ignoreNextDeleteBackHighLevel = YES; + } else { + if ([self.appDelegate debugMode]) { + NSLog(@"handleDeleteBackLowLevel: not processing..."); + } } return self.ignoreNextDeleteBackHighLevel; } -- (BOOL)deleteBack:(NSUInteger)n in:(id) client for:(NSEvent *) event { - if ([self.appDelegate debugMode]) - NSLog(@"Attempting to back-delete %li characters.", n); - NSRange selectedRange = [self getSelectionRangefromClient:client]; - NSInteger pos = selectedRange.location; - - NSLog(@"delete at position %ld", (long)pos); - NSLog(@"_legacyMode: %s", _legacyMode?"yes":"no"); - 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--) { - if ([client respondsToSelector:@selector(attributedSubstringFromRange:)]) - 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 { - NSLog(@"deleteBackLegacy"); - 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 postDeleteBacks"); - if (_clientCanProvideSelectionInfo == No || _clientCanProvideSelectionInfo == Unreliable) - NSLog(@"Cannot trust client to report accurate selection length - assuming no selection."); - } - - if (_pendingBuffer != nil && [[self pendingBuffer] length] > 0) { - // We shouldn't be sending out characters before the corresponding Delete Back events are received - // if this does happen, that's unexpected... - NSLog(@"Legacy mode: ERROR: did not find expected Delete Back event"); - } - - // Note: If pos is "not found", most likely the client can't accurately report the location. This might be - // dangerous, but for now let's go ahead and attempt to delete the characters we think should be there. - - if (_clientCanProvideSelectionInfo == Yes && selectedRange.length > 0) - n++; // First delete-back will delete the existing selection. - [self postDeleteBacks:n for:event]; - - CFRelease(_sourceFromOriginalEvent); - _sourceFromOriginalEvent = nil; - return YES; - } - return NO; -} - -- (void)postDeleteBacks:(NSUInteger)count for:(NSEvent *) event { - _numberOfPostedDeletesToExpect = count; - - _sourceFromOriginalEvent = CGEventCreateSourceFromEvent([event CGEvent]); - - for (int db = 0; db < count; db++) - { - if ([self.appDelegate debugMode]) { - NSLog(@"Posting a delete (down/up) at kCGHIDEventTap."); - } - [self.appDelegate postKeyboardEventWithSource:_sourceFromOriginalEvent code:kVK_Delete postCallback:^(CGEventRef eventToPost) { - CGEventPost(kCGHIDEventTap, eventToPost); - }]; - } -} - - (void)initiatePendingBufferProcessing:(id)sender { [self postKeyPressToFrontProcess:kProcessPendingBuffer from:NULL]; } From b909540dd335a2fdf6b6421f9077ba0afe198028 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Thu, 23 Nov 2023 14:51:36 +0700 Subject: [PATCH 07/10] pass through delete keystroke to avoid re-sending event --- mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 48bb4e38482..89544be9202 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -501,7 +501,7 @@ -(BOOL)applyKeymanCoreActions:(CoreKeyOutput*)output event: (NSEvent*)event clie // if output from Keyman Core indicates to emit the keystroke, // then return NO, so that the OS still handles the event - if (output.emitKeystroke) { + if (handledEvent && output.emitKeystroke) { [self.appDelegate logDebugMessage:@" *** InputMethodEventHandler applyKeymanCoreActions: emit keystroke true, returning false for handledEvent"]; handledEvent = NO; } @@ -516,8 +516,14 @@ -(BOOL)applyKeyOutputToTextInputClient:(CoreKeyOutput*)output keyDownEvent:(nonn [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, insert only scenario"]; [self insertAndReplaceTextForOutput:output client:client]; } else if (output.isDeleteOnlyScenario) { + if ((event.keyCode == kVK_Delete) && output.codePointsToDeleteBeforeInsert == 1) { + // let the delete pass through in the original event rather than sending a new delete + [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only scenario"]; + handledEvent = NO; + } else { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete only scenario"]; [self sendEvents:event forOutput:output]; + } } else if (output.isDeleteAndInsertScenario) { if (self.apiCompliance.mustBackspaceUsingEvents) { [self.appDelegate logDebugMessage:@"KXMInputMethodHandler applyOutputToTextInputClient, delete and insert scenario with events"]; From 27b1236d9020a1bd89c4762ca4e4905c8cdd196b Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 8 Dec 2023 11:40:21 +0700 Subject: [PATCH 08/10] eventTap changes, add delay to queued text event --- mac/Keyman4Mac/Keyman4Mac/AppDelegate.m | 79 +----- .../Keyman4MacIM/KMInputController.h | 2 +- .../Keyman4MacIM/KMInputController.m | 9 +- .../Keyman4MacIM/KMInputMethodAppDelegate.m | 15 +- .../Keyman4MacIM/KMInputMethodEventHandler.h | 2 +- .../Keyman4MacIM/KMInputMethodEventHandler.m | 231 ++++++++---------- mac/Keyman4MacIM/Keyman4MacIM/KeySender.h | 2 +- mac/Keyman4MacIM/Keyman4MacIM/KeySender.m | 56 +++-- .../Keyman4MacIM/TextApiCompliance.m | 9 +- 9 files changed, 167 insertions(+), 238 deletions(-) diff --git a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m index 7b467347a63..3578510fef8 100644 --- a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m +++ b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m @@ -101,7 +101,6 @@ - (BOOL)createEventTap { return YES; } -// TODO: investigate -- event handler for OSK only? CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { // If key map is not enable, return the event without modifying if (!isKeyMapEnabled) @@ -114,15 +113,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef BOOL handled = NO; if (type == NX_KEYDOWN) { + NSLog(@"AppDelegate eventTapFunction key down event: %@", event); // Key down event NSEvent *mEvent = [NSEvent eventWithCGEvent:event]; KMEngine *kme = [[KMEngine alloc] initWithKMX:kmx context:contextBuffer verboseLogging:debugMode]; - NSArray *actions = [kme processEvent:mEvent]; - //if (debugMode) - NSLog(@"%@", actions); - for (CoreAction *action in actions) { - if (action.isCharacter) { - NSString *output = action.content; + CoreKeyOutput *coreKeyOutput = [kme processEvent:mEvent]; + if (coreKeyOutput) { + if (coreKeyOutput.hasTextToInsert) { + NSString *output = coreKeyOutput.textToInsert; UniChar *outCStr = (UniChar *)[output cStringUsingEncoding:NSUTF16StringEncoding]; unsigned short kc = [mEvent keyCode]; CGEventRef kEventDown = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)kc, true); @@ -135,80 +133,17 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef CFRelease(kEventDown); CFRelease(kEventUp); } - else if (action.isMarkerBackspace) { - NSInteger n = action.backspaceCount; - NSUInteger dk = [contextBuffer deleteLastDeadkeys]; - n -= dk; - - for (int i = 0; i < n; i++) { - CGEventRef kEventDown = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)51, true); - CGEventRef kEventUp = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)51, false); - CGEventTapPostEvent(proxy, kEventDown); - CGEventTapPostEvent(proxy, kEventUp); - CFRelease(kEventDown); - CFRelease(kEventUp); - } - - [contextBuffer deleteLastNChars:n]; - } - else if (action.isMarker) { - [contextBuffer appendDeadkey:action.content]; - } - else if (action.actionType == EndAction) { - continue; - } - else if (action.actionType == EmitKeystrokeAction) { + if (coreKeyOutput.emitKeystroke) { return NULL; } - else if (action.actionType == AlertAction) { + if (coreKeyOutput.alert) { [[NSSound soundNamed:@"Tink"] play]; } handled = YES; } - - // Apply context changes if not handled - if (!handled) { - mEvent = [NSEvent eventWithCGEvent:event]; - unsigned short keyCode = [mEvent keyCode]; - if (keyCode <= 0x33) { // Main keys - if (keyCode == 0x24) // Enter - [contextBuffer appendString:@"\n"]; - else if (keyCode == 0x33) { // Backspace - [contextBuffer deleteLastDeadkeys]; - [contextBuffer deleteLastNChars:1]; - [contextBuffer deleteLastDeadkeys]; - } - else - [contextBuffer appendString:[mEvent characters]]; - } - else { - unichar ch = [[mEvent characters] characterAtIndex:0]; - if (ch >= 0x2A && ch <= 0x39) // Numpad char range - [contextBuffer appendString:[mEvent characters]]; - else if (keyCode == 0x4C) // Enter (Numpad) - [contextBuffer appendString:@"\n"]; - else if (keyCode >= 0x7B && keyCode <= 0x7E) // Arrow keys - contextBuffer = [NSMutableString stringWithString:@""]; // Clear context - else if (keyCode == 0x73 || keyCode == 0x77 || keyCode == 0x74 || keyCode == 0x79) { - // Home, End, Page Up, Page Down - contextBuffer = [NSMutableString stringWithString:@""]; // Clear context - } - else { - // Other keys - } - } - } - } - else { - // Mouse button up event (left | right) - contextBuffer = [NSMutableString stringWithString:@""]; // Clear context - if (debugMode) - NSLog(@"Mouse event"); } - if (debugMode) - NSLog(@"contextBuffer = %@\n***", [contextBuffer codeString]); return handled?NULL:event; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h index 5a31221350b..684db6fb386 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h @@ -13,6 +13,6 @@ @interface KMInputController : IMKInputController - (void)menuAction:(id)sender; -- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event; +- (void)handleBackspace:(NSEvent *)event; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index c2e27d9967e..9b6ee2f5b20 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -58,13 +58,12 @@ - (BOOL)handleEvent:(NSEvent *)event client:(id)sender { } // Passthrough from the app delegate low level event hook -// to the input method event handler for Delete Back. -- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event { +// to the input method event handler for handleBackspace. +- (void)handleBackspace:(NSEvent *)event { + [self.AppDelegate logDebugMessage:@"KMInputController handleBackspace, event = %@", event]; if(_eventHandler != nil) { - return [_eventHandler handleDeleteBackLowLevel:event]; + [_eventHandler handleBackspace:event]; } - - return NO; } - (void)activateServer:(id)sender { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 19e2677805a..c47e257996f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -113,7 +113,6 @@ - (void)initCompletion { forEventClass:kInternetEventClass andEventID:kAEGetURL]; - // TODO: use addGlobalMonitorForEventsMatchingMask instead (since we are not modifying events)? self.lowLevelEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, @@ -279,14 +278,12 @@ -(void) wakeUpWith:(id)newServer { _lastServerWithOSKShowing = nil; } -// TODO: revisit and remove what is no longer needed -// TODO: better (less impactful) to replace with global event monitor? just monitors with no ability to modify events CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { KMInputMethodAppDelegate *appDelegate = [KMInputMethodAppDelegate AppDelegate]; if (appDelegate != nil) { if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) { // kCGEventTapDisabledByUserInput most likely means we're "sleeping", in which case we want it to stay - // diabled until we get the wake-up call. + // disabled until we get the wake-up call. if (!appDelegate.sleeping) { // REVIEW: We might need to consider putting in some kind of counter/flag to ensure that the very next // event is not another disable so we don't end up in an endless cycle. @@ -331,14 +328,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef break; case kCGEventKeyDown: - NSLog(@"Event tap handling kCGEventKeyDown for keyCode: %hu", sysEvent.keyCode); - // Pass back delete events through to the input method event handler - // because some 'legacy' apps don't allow us to see back delete events - // that we have synthesized (and we need to see them, for serialization + NSLog(@"Event tap keydown event, keyCode: %hu, event: %@", sysEvent.keyCode, event); + // Pass back low-level backspace events to the input method event handler + // because some non-compliant apps do not allow us to see backspace events + // that we have generated (and we need to see them, for serialization // of events) if(sysEvent.keyCode == kVK_Delete && appDelegate.inputController != nil) { NSLog(@"Event tap handling kVK_Delete."); - [appDelegate.inputController handleDeleteBackLowLevel:sysEvent]; + [appDelegate.inputController handleBackspace:sysEvent]; } switch(sysEvent.keyCode) { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h index 10fa4338a0e..d8365511917 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.h @@ -16,7 +16,7 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id)sender; - (BOOL)handleEvent:(NSEvent *)event client:(id)sender; -- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event; +- (void)handleBackspace:(NSEvent *)event; - (void)deactivate; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 89544be9202..734e7977def 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -17,7 +17,8 @@ @interface KMInputMethodEventHandler () @property (nonatomic, retain) KeySender* keySender; @property (nonatomic, retain) TextApiCompliance* apiCompliance; -@property int generatedBackspaceCount; +@property NSUInteger generatedBackspaceCount; +@property BOOL backspaceHandledLowLevel; @property (nonatomic, retain) NSString* clientApplicationId; @property NSString *queuedText; @property BOOL contextChanged; @@ -27,12 +28,13 @@ @implementation KMInputMethodEventHandler const CGKeyCode kProcessPendingBuffer = 0xFF; const NSUInteger kMaxContext = 80; +const NSTimeInterval kQueuedTextEventDelayInSeconds = 0.1; //_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; +CGEventSourceRef _sourceForGeneratedEvent = nil; NSMutableString* _easterEggForSentry = nil; NSString* const kEasterEggText = @"Sentry force now"; NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; @@ -61,7 +63,6 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { _keySender = [[KeySender alloc] init]; _clientApplicationId = clientAppId; _generatedBackspaceCount = 0; - //BOOL legacy = [self isClientAppLegacy:clientAppId]; @@ -88,20 +89,18 @@ - (void)switchToLegacyMode { } - (void)deactivate { - if (_numberOfPostedDeletesToExpect > 0 || (_pendingBuffer != nil && _pendingBuffer.length > 0) || + if (_generatedBackspaceCount > 0 || (_queuedText != nil && _queuedText.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(@" _generatedBackspaceCount = %lu", _generatedBackspaceCount); + NSLog(@" _queuedText = \"%@\"", _queuedText == nil ? @"(NIL)" : (NSString*)[self queuedText]); NSLog(@" _keyCodeOfOriginalEvent = %hu", _keyCodeOfOriginalEvent); } - _numberOfPostedDeletesToExpect = 0; - // If _sourceFromOriginalEvent != nil, we should probably attempt to release and clear it. - // [self dealloc] - _pendingBuffer = nil; _keyCodeOfOriginalEvent = 0; + _generatedBackspaceCount = 0; + _queuedText = nil; } } @@ -110,6 +109,10 @@ - (void)dealloc { CFRelease(_sourceFromOriginalEvent); _sourceFromOriginalEvent = nil; } + if (_sourceForGeneratedEvent != nil) { + CFRelease(_sourceForGeneratedEvent); + _sourceForGeneratedEvent = nil; + } } - (void)handleCommand:(NSEvent *)event { @@ -208,6 +211,7 @@ -(void)setPendingBuffer:(NSString*)string } - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender { + [self.appDelegate logDebugMessage:@"handleEventWithKeymanEngine, event = %@", event]; CoreKeyOutput *output = nil; output = [self processEventWithKeymanEngine:event in:sender]; @@ -260,86 +264,34 @@ - (void)checkEventForSentryEasterEgg:(NSEvent *)event { } } -- (void)processUnhandledDeleteBack:(id)client updateEngineContext:(BOOL *)updateEngineContext { - 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) { - if (_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 (!_sourceFromOriginalEvent) { - NSLog(@"----- If not legacy mode, _sourceFromOriginalEvent should be retained until original code is posted -----"); - } - 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; +/** + handleBackspace: handles generated for a subset of non-compliant apps (such as Adobe apps) that do + not support replacing text with the insertText API but also intercept events so that we do not see them in handleEvent. + Other apps, such as Word, show the event both in the low + level EventTap and in the normal IM handleEvent. Thus, we set a flag after + processing a backspace here so that we do not process it again in handleEvent. + Note that a backspace that we handle here should never be passed on to Keyman + Core for processing, as it is already the result of processing. +*/ +- (void)handleBackspace:(NSEvent *)event { + [self.appDelegate logDebugMessage:@"KMInputMethodEventHandler handleBackspace, event = %@", event]; + NSLog(@" backspaceHandledLowLevel: %d, generatedBackspaceCount: %lu", _backspaceHandledLowLevel, (unsigned long)_generatedBackspaceCount); + self.backspaceHandledLowLevel = NO; + + if (self.generatedBackspaceCount > 0) { + self.generatedBackspaceCount--; + self.backspaceHandledLowLevel = YES; + + // if we just encountered the last backspace, then send event to insert queued text + if (self.generatedBackspaceCount == 0) { + [self performSelector:@selector(triggerInsertQueuedText:) withObject:event afterDelay:kQueuedTextEventDelayInSeconds]; } + } } -// handleDeleteBackLowLevel: handles the situation for Delete Back for -// some 'legacy' mode apps such as Adobe apps, because when we inject -// the Delete Back, it is passed through to the app but we never see it -// in our normal handleEvent function. However, other apps, such as Word, -// show the event both in the low level tap and in the normal IM -// handleEvent. Thus, we set a flag after processing a Delete Back here so -// that we don't accidentally process it twice. Note that a Delete Back that -// we handle here should never be passed on to Keyman Engine for transform, -// as it will be part of the output from the transform. -- (BOOL)handleDeleteBackLowLevel:(NSEvent *)event { - self.ignoreNextDeleteBackHighLevel = NO; - if(event.keyCode == kVK_Delete && _legacyMode && [self pendingBuffer].length > 0) { - BOOL updateEngineContext = YES; - if ([self.appDelegate debugMode]) { - NSLog(@"legacy: handleDeleteBackLowLevel, delete-back received, processing"); - } - [self processUnhandledDeleteBack:self.senderForDeleteBack updateEngineContext: &updateEngineContext]; - self.ignoreNextDeleteBackHighLevel = YES; - } else { - if ([self.appDelegate debugMode]) { - NSLog(@"handleDeleteBackLowLevel: not processing..."); - } - } - - return self.ignoreNextDeleteBackHighLevel; +- (void)triggerInsertQueuedText:(NSEvent *)event { + [self.appDelegate logDebugMessage:@"KMInputMethodEventHandler triggerInsertQueuedText"]; + [self.keySender sendKeymanKeyCodeForEvent:event]; } - (void)initiatePendingBufferProcessing:(id)sender { @@ -391,6 +343,7 @@ - (void) handleContextChangedByLowLevelEvent { // handleEvent implementation for core event processing - (BOOL)handleEvent:(NSEvent *)event client:(id)sender { + BOOL handled = NO; [self.appDelegate logDebugMessage:@"handleEvent event = %@", event]; [self checkTextApiCompliance:sender]; @@ -398,52 +351,60 @@ - (BOOL)handleEvent:(NSEvent *)event client:(id)sender { // mouse movement requires that the context be invalidated [self handleContextChangedByLowLevelEvent]; - if (event.type == NSKeyDown) { - [self reportContext:event forClient:sender]; - - // indicates that our generated backspace event(s) are consumed - // and we can insert text that followed the backspace(s) - if (event.keyCode == kKeymanEventKeyCode) { - [self insertQueuedText: event client:sender]; - return YES; - } - - if(event.keyCode == kVK_Delete && self.generatedBackspaceCount > 0) { - self.generatedBackspaceCount--; - [self.appDelegate logDebugMessage:@"handleEvent KVK_Delete, reducing generatedBackspaceCount to %d ", self.generatedBackspaceCount]; - return NO; - } - } - if (event.type == NSEventTypeFlagsChanged) { - // Mark the context as out of date for the command keys - switch([event keyCode]) { - case kVK_RightCommand: - case kVK_Command: - self.contextChanged = YES; - break; - case kVK_Shift: - case kVK_RightShift: - case kVK_CapsLock: - case kVK_Option: - case kVK_RightOption: - case kVK_Control: - case kVK_RightControl: - break; - } - return NO; + if (event.type == NSKeyDown) { + // indicates that our generated backspace event(s) are consumed + // and we can insert text that followed the backspace(s) + if (event.keyCode == kKeymanEventKeyCode) { + [self.appDelegate logDebugMessage:@"handleEvent, handling kKeymanEventKeyCode"]; + [self insertQueuedText: event client:sender]; + return YES; } - if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { - [self handleCommand:event]; - return NO; // let the client app handle all Command-key events. + // for some apps, handleEvent will not be called for the generated backspace + // but if it is, we need to let it pass through rather than process again in core + if((event.keyCode == kVK_Delete) && (self.backspaceHandledLowLevel)) { + [self.appDelegate logDebugMessage:@"handleEvent, allowing generated backspace to pass through"]; + self.backspaceHandledLowLevel = NO; + // return NO to pass through to client app + return NO; } + } + + if (event.type == NSEventTypeFlagsChanged) { + [self handleFlagsChangedEvent:[event keyCode]]; + return NO; + } - BOOL handled = [self handleEventWithKeymanEngine:event in: sender]; - if (event.type == NSKeyDown) { - [self.appDelegate logDebugMessage:@"event, keycode: %u, characters='%@' handled by KeymanEngine: %@", event.keyCode, event.characters, handled?@"yes":@"no"]; - } + if ((event.modifierFlags & NSEventModifierFlagCommand) == NSEventModifierFlagCommand) { + [self handleCommand:event]; + return NO; // let the client app handle all Command-key events. + } + + if (event.type == NSKeyDown) { + [self reportContext:event forClient:sender]; + handled = [self handleEventWithKeymanEngine:event in: sender]; + } - return handled; + [self.appDelegate logDebugMessage:@"event, keycode: %u, characters='%@' handled = %@", event.keyCode, event.characters, handled?@"yes":@"no"]; + return handled; +} + +-(void)handleFlagsChangedEvent:(short)keyCode { + // Mark the context as out of date for the command keys + switch(keyCode) { + case kVK_RightCommand: + case kVK_Command: + self.contextChanged = YES; + break; + case kVK_Shift: + case kVK_RightShift: + case kVK_CapsLock: + case kVK_Option: + case kVK_RightOption: + case kVK_Control: + case kVK_RightControl: + break; + } } /** @@ -623,17 +584,24 @@ -(void)insertAndReplaceText:(NSString *)text deleteCount:(int) replacementCount } -(void)sendEvents:(NSEvent *)event forOutput:(CoreKeyOutput*)output { + [self.appDelegate logDebugMessage:@"sendEvents called"]; + + _sourceForGeneratedEvent = CGEventCreateSourceFromEvent([event CGEvent]); + + self.generatedBackspaceCount = output.codePointsToDeleteBeforeInsert; + if (output.hasCodePointsToDelete) { for (int i = 0; i < output.codePointsToDeleteBeforeInsert; i++) { - self.generatedBackspaceCount++; - [self.keySender sendBackspaceforSourceEvent:event]; + [self.appDelegate logDebugMessage:@"sendEvents queueing backspace"]; + [self.keySender sendBackspaceforEventSource:_sourceForGeneratedEvent]; } } if (output.hasTextToInsert) { + [self.appDelegate logDebugMessage:@"sendEvents, queueing text to insert: %@", output.textToInsert]; + self.queuedText = output.textToInsert; - [self.keySender sendKeymanKeyCodeForEvent:event]; } } @@ -641,6 +609,7 @@ -(void)insertQueuedText: (NSEvent *)event client:(id) client { if (self.queuedText.length> 0) { [self.appDelegate logDebugMessage:@"insertQueuedText, inserting %@", self.queuedText]; [self insertAndReplaceText:self.queuedText deleteCount:0 client:client]; + self.queuedText = nil; } else { [self.appDelegate logDebugMessage:@"handleQueuedText called but no text to insert"]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.h b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.h index 672dbbca040..fdd332794d6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.h @@ -16,7 +16,7 @@ extern const CGKeyCode kKeymanEventKeyCode; - (instancetype)init; - (void)sendKeyDown:(NSUInteger)keyCode forSourceEvent:(NSEvent *)event includeKeyUp:(BOOL)includeKeyUpEvent; -- (void)sendBackspaceforSourceEvent:(NSEvent *)event; +- (void)sendBackspaceforEventSource:(CGEventSourceRef)eventSource; - (void)sendKeymanKeyCodeForEvent:(NSEvent *)event; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m index 108ba598cdc..1b1caf3d301 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m @@ -16,9 +16,6 @@ const CGKeyCode kKeymanEventKeyCode = 0xFF; @interface KeySender () - -@property (readonly) CGEventSourceRef eventSource; - @end @implementation KeySender @@ -29,9 +26,6 @@ -(KMInputMethodAppDelegate *)appDelegate { -(instancetype)init { self = [super init]; - if (self) { - _eventSource = CGEventSourceCreate(kCGEventSourceStatePrivate); - } return self; } @@ -62,20 +56,54 @@ - (void)sendKeyDown:(NSUInteger)keyCode forSourceEvent:(NSEvent *)event includeK } } --(void) dealloc { - CFRelease(self.eventSource); +- (void)sendBackspaceforEventSource:(CGEventSourceRef)eventSource { + [self.appDelegate logDebugMessage:@"KeySender sendBackspaceforSourceEvent"]; + + [self postKeyboardEventWithSource:eventSource code:kVK_Delete postCallback:^(CGEventRef eventToPost) { + CGEventPost(kCGHIDEventTap, eventToPost); + }]; } -- (void)sendBackspaceforSourceEvent:(NSEvent *)event { - [self.appDelegate logDebugMessage:@"KeySender sendBackspaceforSourceEvent"]; - [self sendKeyDown:kVK_Delete forSourceEvent:event includeKeyUp:YES]; +- (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent{ + + CGEventRef ev = CGEventCreateKeyboardEvent (source, virtualKey, true); //down + if (postEvent) + postEvent(ev); + CFRelease(ev); + ev = CGEventCreateKeyboardEvent (source, virtualKey, false); //up + if (postEvent) + postEvent(ev); + CFRelease(ev); } +/** + sendKeymanKeyCodeForEvent sends the kKeymanEventKeyCode to the + frontmost application to indicate that all the backspaces have been processed + and we can insert the queuedText to the client + */ + - (void)sendKeymanKeyCodeForEvent:(NSEvent *)event { [self.appDelegate logDebugMessage:@"KeySender sendKeymanKeyCodeForEvent"]; - // this is not a real keycode, so we do not need a key up event - // kKeymanEventKeyCode is used to indicate that all the backspaces have been processed - // and we can insert the queuedText to the client + [self sendKeyDown:kKeymanEventKeyCode forSourceEvent:event includeKeyUp:NO]; + + // Returns the frontmost app, which is the app that receives key events. + NSRunningApplication *app = NSWorkspace.sharedWorkspace.frontmostApplication; + pid_t processId = app.processIdentifier; + NSString *appName = app.localizedName; + + [self.appDelegate logDebugMessage:@"sendKeymanKeyCodeForEvent keyCode %lu to app %@ with pid %d", (unsigned long)kKeymanEventKeyCode, appName, processId]; + + CGEventRef cgevent = [event CGEvent]; + + // use source from event to generate new event + CGEventSourceRef source = CGEventCreateSourceFromEvent(cgevent); + CGEventRef keyDownEvent = CGEventCreateKeyboardEvent(source, kKeymanEventKeyCode, true); + + // TODO: add version check + CGEventPostToPid(processId, keyDownEvent); + CFRelease(keyDownEvent); + + // this is not a real keycode, so we do not need a key up event } @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index 829f847216f..cf88e9b1ed5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -177,10 +177,11 @@ - (BOOL)containedInHardCodedNoncompliantAppList:(NSString *)clientAppId { [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || //[clientAppId isEqual: @"com.microsoft.Word"] || // 2020-11-24[mcd]: Appears to work well in Word 16.43, disable legacy by default [clientAppId isEqual: @"org.openoffice.script"] || - [clientAppId isEqual: @"com.adobe.illustrator"] || - [clientAppId isEqual: @"com.adobe.InDesign"] || - [clientAppId isEqual: @"com.adobe.Photoshop"] || - [clientAppId isEqual: @"com.adobe.AfterEffects"] || + // Adobe apps automatically detected as non-compliant + //[clientAppId isEqual: @"com.adobe.illustrator"] || + //[clientAppId isEqual: @"com.adobe.InDesign"] || + //[clientAppId isEqual: @"com.adobe.Photoshop"] || + //[clientAppId isEqual: @"com.adobe.AfterEffects"] || //[clientAppId isEqual: @"com.microsoft.VSCode"] || // 2023-05-29[sgs]: Works with 1.78.2, disable legacy by default [clientAppId isEqual: @"com.google.Chrome"] || [clientAppId hasPrefix: @"net.java"] || From 71d4710d89a4911dde7760028b941c358098ba47 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Wed, 13 Dec 2023 15:40:16 +0700 Subject: [PATCH 09/10] changes for review comments --- mac/Keyman4Mac/Keyman4Mac/AppDelegate.m | 1 - .../Keyman4MacIM/KMInputMethodAppDelegate.m | 2 ++ .../Keyman4MacIM/KMInputMethodEventHandler.m | 25 +++++++--------- mac/Keyman4MacIM/Keyman4MacIM/KeySender.m | 30 +++++++++++-------- .../Keyman4MacIM/TextApiCompliance.m | 2 +- .../KeymanEngine4Mac/CoreWrapper/CoreAction.h | 2 -- .../KeymanEngine4Mac/CoreWrapper/CoreHelper.m | 17 +++++------ .../CoreWrapper/CoreKeyOutput.h | 4 +-- 8 files changed, 39 insertions(+), 44 deletions(-) diff --git a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m index 3578510fef8..dadf633b65d 100644 --- a/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m +++ b/mac/Keyman4Mac/Keyman4Mac/AppDelegate.m @@ -129,7 +129,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef CGEventKeyboardSetUnicodeString(kEventUp, output.length, outCStr); CGEventTapPostEvent(proxy, kEventDown); CGEventTapPostEvent(proxy, kEventUp); - [contextBuffer appendString:output]; CFRelease(kEventDown); CFRelease(kEventUp); } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index c47e257996f..301164db1a9 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -336,6 +336,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if(sysEvent.keyCode == kVK_Delete && appDelegate.inputController != nil) { NSLog(@"Event tap handling kVK_Delete."); [appDelegate.inputController handleBackspace:sysEvent]; + } else { + NSLog(@"Event tap not handling kVK_Delete, appDelegate.inputController = %@", appDelegate.inputController); } switch(sysEvent.keyCode) { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 734e7977def..1c0e3931acf 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -510,16 +510,14 @@ -(void)applyNonTextualOutput:(CoreKeyOutput*)output { } -(void) persistOptions:(NSDictionary*)options{ - if(options.count>0) { - for(NSString *key in options) { - NSString *value = [options objectForKey:key]; - if(key && value) { - [self.appDelegate logDebugMessage:@"applyNonTextualOutput calling writePersistedOptions, key: %@, value: %@", key, value]; - [self.appDelegate writePersistedOptions:key withValue:value]; - } - else { - [self.appDelegate logDebugMessage:@"applyNonTextualOutput, invalid values in optionsToPersist, not writing to UserDefaults, key: %@, value: %@", key, value]; - } + for(NSString *key in options) { + NSString *value = [options objectForKey:key]; + if(key && value) { + [self.appDelegate logDebugMessage:@"applyNonTextualOutput calling writePersistedOptions, key: %@, value: %@", key, value]; + [self.appDelegate writePersistedOptions:key withValue:value]; + } + else { + [self.appDelegate logDebugMessage:@"applyNonTextualOutput, invalid values in optionsToPersist, not writing to UserDefaults, key: %@, value: %@", key, value]; } } } @@ -527,10 +525,10 @@ -(void) persistOptions:(NSDictionary*)options{ -(void) applyCapsLock:(CapsLockState)capsLockState { switch(capsLockState) { case On: - //TODO: handle this + //TODO: handle this, Issue #10244 break; case Off: - //TODO: handle this + //TODO: handle this, Issue #10244 break; case Unchanged: // do nothing @@ -591,8 +589,7 @@ -(void)sendEvents:(NSEvent *)event forOutput:(CoreKeyOutput*)output { self.generatedBackspaceCount = output.codePointsToDeleteBeforeInsert; if (output.hasCodePointsToDelete) { - for (int i = 0; i < output.codePointsToDeleteBeforeInsert; i++) - { + for (int i = 0; i < output.codePointsToDeleteBeforeInsert; i++) { [self.appDelegate logDebugMessage:@"sendEvents queueing backspace"]; [self.keySender sendBackspaceforEventSource:_sourceForGeneratedEvent]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m index 1b1caf3d301..18892382983 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m @@ -33,9 +33,9 @@ - (void)sendKeyDown:(NSUInteger)keyCode forSourceEvent:(NSEvent *)event includeK // Returns the frontmost app, which is the app that receives key events. NSRunningApplication *app = NSWorkspace.sharedWorkspace.frontmostApplication; pid_t processId = app.processIdentifier; - NSString *appName = app.localizedName; + NSString *bundleId = app.bundleIdentifier; - [self.appDelegate logDebugMessage:@"sendKeyDown keyCode %lu to app %@ with pid %d", (unsigned long)keyCode, appName, processId]; + [self.appDelegate logDebugMessage:@"sendKeyDown keyCode %lu to app %@ with pid %d", (unsigned long)keyCode, bundleId, processId]; if (keyCode < 0x100) { CGEventRef cgevent = [event CGEvent]; @@ -57,7 +57,7 @@ - (void)sendKeyDown:(NSUInteger)keyCode forSourceEvent:(NSEvent *)event includeK } - (void)sendBackspaceforEventSource:(CGEventSourceRef)eventSource { - [self.appDelegate logDebugMessage:@"KeySender sendBackspaceforSourceEvent"]; + [self.appDelegate logDebugMessage:@"KeySender sendBackspaceforEventSource"]; [self postKeyboardEventWithSource:eventSource code:kVK_Delete postCallback:^(CGEventRef eventToPost) { CGEventPost(kCGHIDEventTap, eventToPost); @@ -65,15 +65,19 @@ - (void)sendBackspaceforEventSource:(CGEventSourceRef)eventSource { } - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent{ + + if (postEvent) { + [self.appDelegate logDebugMessage:@"KeySender postKeyboardEventWithSource for virtualKey: @%", virtualKey]; + } else { + [self.appDelegate logDebugMessage:@"KeySender postKeyboardEventWithSource callback not specified", virtualKey]; + } - CGEventRef ev = CGEventCreateKeyboardEvent (source, virtualKey, true); //down - if (postEvent) - postEvent(ev); - CFRelease(ev); - ev = CGEventCreateKeyboardEvent (source, virtualKey, false); //up - if (postEvent) - postEvent(ev); - CFRelease(ev); + CGEventRef ev = CGEventCreateKeyboardEvent (source, virtualKey, true); //down + postEvent(ev); + CFRelease(ev); + ev = CGEventCreateKeyboardEvent (source, virtualKey, false); //up + postEvent(ev); + CFRelease(ev); } /** @@ -90,9 +94,9 @@ - (void)sendKeymanKeyCodeForEvent:(NSEvent *)event { // Returns the frontmost app, which is the app that receives key events. NSRunningApplication *app = NSWorkspace.sharedWorkspace.frontmostApplication; pid_t processId = app.processIdentifier; - NSString *appName = app.localizedName; + NSString *bundleId = app.bundleIdentifier; - [self.appDelegate logDebugMessage:@"sendKeymanKeyCodeForEvent keyCode %lu to app %@ with pid %d", (unsigned long)kKeymanEventKeyCode, appName, processId]; + [self.appDelegate logDebugMessage:@"sendKeymanKeyCodeForEvent keyCode %lu to app %@ with pid %d", (unsigned long)kKeymanEventKeyCode, bundleId, processId]; CGEventRef cgevent = [event CGEvent]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index cf88e9b1ed5..079e3205676 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -177,7 +177,7 @@ - (BOOL)containedInHardCodedNoncompliantAppList:(NSString *)clientAppId { [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || //[clientAppId isEqual: @"com.microsoft.Word"] || // 2020-11-24[mcd]: Appears to work well in Word 16.43, disable legacy by default [clientAppId isEqual: @"org.openoffice.script"] || - // Adobe apps automatically detected as non-compliant + // 2023-12-13[sgs]: Adobe apps automatically detected as non-compliant //[clientAppId isEqual: @"com.adobe.illustrator"] || //[clientAppId isEqual: @"com.adobe.InDesign"] || //[clientAppId isEqual: @"com.adobe.Photoshop"] || diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreAction.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreAction.h index a05c03391f6..302b1e7ca95 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreAction.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreAction.h @@ -5,8 +5,6 @@ * Keyman * * Created by Shawn Schantz on 2023-02-21. - * - * Description... */ #import diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m index 47ae04a83c0..b1c0a65fa27 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreHelper.m @@ -58,13 +58,14 @@ -(unsigned long long) unicharStringLength:(unichar const *)string { * Return the string length (number of unicode scalar, or UTF32, values) excluding the terminating null. */ -(unsigned long) scalarValueStringLength:(UTF32Char const *)string { - unsigned long length = 0lu; - if(NULL == string) return length; - - while('\0' != string[length]) - length++; - + unsigned long length = 0lu; + if(NULL == string) { return length; + } + while('\0' != string[length]) { + length++; + } + return length; } -(instancetype)initWithDebugMode:(BOOL)debugMode { @@ -181,10 +182,6 @@ -(NSString*)utf32CStringToString:(UTF32Char*)utf32CString { if (utf32StringLength > 0) { characterString=[[NSString alloc] initWithBytes:utf32CString length:utf32StringLength*4 encoding:NSUTF32LittleEndianStringEncoding]; - /* - NSData * characterData = [[NSData alloc] initWithBytes:utf32CString length:utf32StringLength*4]; - [self logDebugMessage:@"utf32ValueToString data: '%@', string: %@", characterData, characterString]; - */ } return characterString; diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h index 8b7938fa57a..e9b9517a63e 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/CoreWrapper/CoreKeyOutput.h @@ -2,11 +2,9 @@ * Keyman is copyright (C) SIL International. MIT License. * * CoreKeyOutput.h - * KeymanEngine4Mac + * Keyman * * Created by Shawn Schantz on 2023-10-31. - * - * Description... */ #import From 4b5495e41b9954227d63b5af587fe1ea01922d54 Mon Sep 17 00:00:00 2001 From: Shawn Schantz <89134789+sgschantz@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:57:13 +0700 Subject: [PATCH 10/10] Update mac/Keyman4MacIM/Keyman4MacIM/KeySender.m add guard clause Co-authored-by: Marc Durdin --- mac/Keyman4MacIM/Keyman4MacIM/KeySender.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m index 18892382983..9c1bcead13e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KeySender.m @@ -66,11 +66,12 @@ - (void)sendBackspaceforEventSource:(CGEventSourceRef)eventSource { - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent{ - if (postEvent) { - [self.appDelegate logDebugMessage:@"KeySender postKeyboardEventWithSource for virtualKey: @%", virtualKey]; - } else { + if (!postEvent) { [self.appDelegate logDebugMessage:@"KeySender postKeyboardEventWithSource callback not specified", virtualKey]; + return; } + + [self.appDelegate logDebugMessage:@"KeySender postKeyboardEventWithSource for virtualKey: @%", virtualKey]; CGEventRef ev = CGEventCreateKeyboardEvent (source, virtualKey, true); //down postEvent(ev);