diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index e5e1f53b..c4ae5ad4 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -49,15 +49,23 @@ D427F7B6279086F6004A2160 /* InputSourceHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7B5279086F6004A2160 /* InputSourceHelper */; }; D427F7C127908EFC004A2160 /* OpenCCBridge in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7C027908EFC004A2160 /* OpenCCBridge */; }; D44FB74527915565003C80A6 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74427915555003C80A6 /* Preferences.swift */; }; - D44FB74727919D35003C80A6 /* EmacsKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74627919C83003C80A6 /* EmacsKeyHelper.swift */; }; D44FB74A2791B829003C80A6 /* VXHanConvert in Frameworks */ = {isa = PBXBuildFile; productRef = D44FB7492791B829003C80A6 /* VXHanConvert */; }; D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */; }; + D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */; }; + D461B792279DAC010070E734 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; }; D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; }; D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; }; D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; }; D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; }; D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; }; + D4E569DC27A34D0E00AC2CEF /* KeyHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */; }; + D4E569DF27A40F1400AC2CEF /* KeyHandlerBopomofoTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D4E569DE27A40F1400AC2CEF /* KeyHandlerBopomofoTests.mm */; }; + D4E569E027A4123200AC2CEF /* KeyHandlerInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */; }; + D4E569E127A4128300AC2CEF /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; }; + D4E569E227A412E700AC2CEF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74427915555003C80A6 /* Preferences.swift */; }; + D4E569E427A414CB00AC2CEF /* data-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6AD7CBC715FE555000691B5B /* data-plain-bpmf.txt */; }; + D4E569E527A414CB00AC2CEF /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */; }; D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBE0279AF8B30071253C /* AppDelegate.swift */; }; D4F0BBE4279B08900071253C /* BundleTranslocate.m in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBE3279B08900071253C /* BundleTranslocate.m */; }; @@ -200,10 +208,11 @@ D427F7B2279086B5004A2160 /* InputSourceHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = InputSourceHelper; path = Packages/InputSourceHelper; sourceTree = ""; }; D427F7BF27908EAC004A2160 /* OpenCCBridge */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OpenCCBridge; path = Packages/OpenCCBridge; sourceTree = ""; }; D44FB74427915555003C80A6 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; - D44FB74627919C83003C80A6 /* EmacsKeyHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmacsKeyHelper.swift; sourceTree = ""; }; D44FB7482791B346003C80A6 /* VXHanConvert */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = VXHanConvert; path = Packages/VXHanConvert; sourceTree = ""; }; D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PhraseReplacementMap.cpp; sourceTree = ""; }; D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhraseReplacementMap.h; sourceTree = ""; }; + D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerInput.swift; sourceTree = ""; }; + D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = ""; }; D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = ""; }; @@ -212,6 +221,10 @@ D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = ""; }; D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = ""; }; + D4E569DA27A34CC100AC2CEF /* KeyHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyHandler.h; sourceTree = ""; }; + D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyHandler.mm; sourceTree = ""; }; + D4E569DD27A40F1300AC2CEF /* McBopomofoTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "McBopomofoTests-Bridging-Header.h"; sourceTree = ""; }; + D4E569DE27A40F1400AC2CEF /* KeyHandlerBopomofoTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyHandlerBopomofoTests.mm; sourceTree = ""; }; D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveUtil.swift; sourceTree = ""; }; D4F0BBE0279AF8B30071253C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D4F0BBE2279B08900071253C /* BundleTranslocate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BundleTranslocate.h; sourceTree = ""; }; @@ -299,12 +312,15 @@ 6A0D4EC715FC0D6400ABF4B3 /* InputMethodController.mm */, D41355D6278D7409005E5CBD /* LanguageModelManager.h */, D41355D7278D7409005E5CBD /* LanguageModelManager.mm */, - D47B92BF27972AC800458394 /* main.swift */, + D4E569DA27A34CC100AC2CEF /* KeyHandler.h */, + D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */, + D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */, + D461B791279DAC010070E734 /* InputState.swift */, D427F76B278CA1BA004A2160 /* AppDelegate.swift */, D44FB74427915555003C80A6 /* Preferences.swift */, - D44FB74627919C83003C80A6 /* EmacsKeyHelper.swift */, D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */, + D47B92BF27972AC800458394 /* main.swift */, 6A0D4EF615FC0DA600ABF4B3 /* McBopomofo-Prefix.pch */, D427A9BF25ED28CC005D43E0 /* McBopomofo-Bridging-Header.h */, ); @@ -486,6 +502,8 @@ children = ( D485D3B82796A8A000657FF3 /* PreferencesTests.swift */, D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */, + D4E569DE27A40F1400AC2CEF /* KeyHandlerBopomofoTests.mm */, + D4E569DD27A40F1300AC2CEF /* McBopomofoTests-Bridging-Header.h */, ); path = McBopomofoTests; sourceTree = ""; @@ -593,6 +611,7 @@ }; D485D3B52796A8A000657FF3 = { CreatedOnToolsVersion = 13.2.1; + LastSwiftMigration = 1320; TestTargetID = 6A0D4EA115FC0D2D00ABF4B3; }; }; @@ -664,6 +683,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D4E569E527A414CB00AC2CEF /* data.txt in Resources */, + D4E569E427A414CB00AC2CEF /* data-plain-bpmf.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -696,11 +717,13 @@ files = ( D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */, 6ACC3D442793701600F1B140 /* ParselessPhraseDB.cpp in Sources */, + D461B792279DAC010070E734 /* InputState.swift in Sources */, D47B92C027972AD100458394 /* main.swift in Sources */, - D44FB74727919D35003C80A6 /* EmacsKeyHelper.swift in Sources */, D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */, D44FB74527915565003C80A6 /* Preferences.swift in Sources */, + D4E569DC27A34D0E00AC2CEF /* KeyHandler.mm in Sources */, D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */, + D456576E279E4F7B00DF6BC9 /* KeyHandlerInput.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */, 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */, D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */, @@ -727,8 +750,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D4E569DF27A40F1400AC2CEF /* KeyHandlerBopomofoTests.mm in Sources */, D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */, + D4E569E227A412E700AC2CEF /* Preferences.swift in Sources */, + D4E569E127A4128300AC2CEF /* InputState.swift in Sources */, D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */, + D4E569E027A4123200AC2CEF /* KeyHandlerInput.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1226,6 +1253,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -1246,6 +1274,11 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.10; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -1255,6 +1288,7 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "McBopomofoTests/McBopomofoTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/McBopomofo.app/Contents/MacOS/McBopomofo"; @@ -1270,6 +1304,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -1285,6 +1320,11 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); MACOSX_DEPLOYMENT_TARGET = 10.10; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; @@ -1293,6 +1333,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "McBopomofoTests/McBopomofoTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/McBopomofo.app/Contents/MacOS/McBopomofo"; diff --git a/McBopomofoTests/KeyHandlerBopomofoTests.mm b/McBopomofoTests/KeyHandlerBopomofoTests.mm new file mode 100644 index 00000000..7c2589cf --- /dev/null +++ b/McBopomofoTests/KeyHandlerBopomofoTests.mm @@ -0,0 +1,655 @@ +#import +#import "KeyHandler.h" +#import "LanguageModelManager.h" +#import "McBopomofoTests-Swift.h" + +@interface KeyHandlerBopomofoTests : XCTestCase + +@end + +@implementation KeyHandlerBopomofoTests + +- (void)setUp +{ + [LanguageModelManager loadDataModels]; +} + +- (void)tearDown +{ +} + +- (void)testPunctuationComma +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"<" keyCode:0 charCode:'<' flags:NSEventModifierFlagShift isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@","], @"It should be , but %@", composingBuffer); +} + +- (void)testPunctuationPeriod +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@">" keyCode:0 charCode:'>' flags:NSEventModifierFlagShift isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"。"], @"It should be 。 but %@", composingBuffer); +} + +- (void)testInputtingNihao +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"c" keyCode:0 charCode:'c' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"l" keyCode:0 charCode:'l' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); +} + +- (void)testCommittingNihao +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"c" keyCode:0 charCode:'c' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"l" keyCode:0 charCode:'l' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + __block NSInteger count = 0; + + __block InputState *empty; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:0 charCode:13 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + if (!count) { + state = inState; + } + empty = inState; + count++; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateCommitting")], @"It should be a committing state %@.", NSStringFromClass([state class])); + NSString *poppedText = [(InputStateCommitting *)state poppedText]; + XCTAssertTrue([poppedText isEqualToString:@"你好"], @"It should be 你好 but %@", poppedText); + + XCTAssertTrue([empty isKindOfClass:NSClassFromString(@"McBopomofo.InputStateEmpty")], @"It should be an empty state %@.", NSStringFromClass([state class])); +} + +- (void)testDelete +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"c" keyCode:0 charCode:'c' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"l" keyCode:0 charCode:'l' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 2); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:123 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 1); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:117 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你"], @"It should be 你 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 1); + + __block BOOL errorCalled = NO; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:117 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + errorCalled = YES; + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你"], @"It should be 你 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 1); + XCTAssertTrue(errorCalled); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:123 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你"], @"It should be 你 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 0); + + errorCalled = NO; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:117 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + errorCalled = YES; + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateEmptyIgnoringPreviousState")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + XCTAssertFalse(errorCalled); +} + +- (void)testBackspace +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"c" keyCode:0 charCode:'c' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"l" keyCode:0 charCode:'l' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:0 charCode:8 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你"], @"It should be 你 but %@", composingBuffer); + + __block InputStateEmpty *empty; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:0 charCode:8 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + empty = (InputStateEmpty *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([empty isKindOfClass:NSClassFromString(@"McBopomofo.InputStateEmptyIgnoringPreviousState")], @"It should be a inputting state %@.", NSStringFromClass([state class])); +} + +- (void)testCursor +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"c" keyCode:0 charCode:'c' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"l" keyCode:0 charCode:'l' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 2); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:123 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 1); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:123 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 0); + + __block BOOL errorCalled = NO; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:123 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + errorCalled = YES; + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 0); + XCTAssertTrue(errorCalled); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:124 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 1); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:124 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 2); + + errorCalled = NO; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:124 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = (InputStateInputting *)inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + errorCalled = YES; + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 2); + XCTAssertTrue(errorCalled); +} + +- (void)testCandidateWithDown +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:125 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateChoosingCandidate")], @"It should be a inputting state %@.", NSStringFromClass([state class])); + NSArray *candidates = [(InputStateChoosingCandidate *)state candidates]; + XCTAssertTrue([candidates containsObject:@"你"]); + +} + +- (void)testHomeAndEnd +{ + KeyHandler *handler = [[KeyHandler alloc] init]; + handler.inputMode = kBopomofoModeIdentifier; + + KeyHandlerInput *input; + __block InputState *state; + state = [[InputStateEmpty alloc] init]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"s" keyCode:0 charCode:'s' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"u" keyCode:0 charCode:'u' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"c" keyCode:0 charCode:'c' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"l" keyCode:0 charCode:'l' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + input = [[KeyHandlerInput alloc] initWithInputText:@"3" keyCode:0 charCode:'3' flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + NSString *composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 2); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:115 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 0); + + input = [[KeyHandlerInput alloc] initWithInputText:@" " keyCode:119 charCode:0 flags:0 isVerticalMode:0]; + [handler handleInput:input state:state stateCallback:^(InputState * inState) { + state = inState; + } candidateSelectionCallback:^{ + } errorCallback:^{ + }]; + + XCTAssertTrue([state isKindOfClass:NSClassFromString(@"McBopomofo.InputStateInputting")], @"It should be an inputting state %@.", NSStringFromClass([state class])); + composingBuffer = [(InputStateInputting *)state composingBuffer]; + XCTAssertTrue([composingBuffer isEqualToString:@"你好"], @"It should be 你好 but %@", composingBuffer); + XCTAssertEqual([(InputStateInputting *)state cursorIndex], 2); + +} + +@end + diff --git a/McBopomofoTests/McBopomofoTests-Bridging-Header.h b/McBopomofoTests/McBopomofoTests-Bridging-Header.h new file mode 100644 index 00000000..1b2cb5d6 --- /dev/null +++ b/McBopomofoTests/McBopomofoTests-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/McBopomofoTests/PreferencesTests.swift b/McBopomofoTests/PreferencesTests.swift index 03e71781..5d966f6d 100644 --- a/McBopomofoTests/PreferencesTests.swift +++ b/McBopomofoTests/PreferencesTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import McBopomofo +//@testable import McBopomofo class PreferencesTests: XCTestCase { diff --git a/Source/EmacsKeyHelper.swift b/Source/EmacsKeyHelper.swift deleted file mode 100644 index f86b93e2..00000000 --- a/Source/EmacsKeyHelper.swift +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2022 and onwards The McBopomofo Authors. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. - -import Cocoa - -@objc enum McBopomofoEmacsKey: UInt16 { - case none = 0 - case forward = 6 // F - case backward = 2 // B - case home = 1 // A - case end = 5 // E - case delete = 4 // D - case nextPage = 22 // V -} - -class EmacsKeyHelper: NSObject { - @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> McBopomofoEmacsKey { - if flags.contains(.control) { - return McBopomofoEmacsKey(rawValue: charCode) ?? .none - } - return .none; - } -} diff --git a/Source/Engine/Gramambular/BlockReadingBuilder.h b/Source/Engine/Gramambular/BlockReadingBuilder.h index 76d681a3..8c503fcc 100644 --- a/Source/Engine/Gramambular/BlockReadingBuilder.h +++ b/Source/Engine/Gramambular/BlockReadingBuilder.h @@ -53,9 +53,7 @@ namespace Formosa { void setJoinSeparator(const string& separator); const string joinSeparator() const; - size_t markerCursorIndex() const; - void setMarkerCursorIndex(size_t inNewIndex); - vector readingsAtRange(size_t begin, size_t end) const; + vector readings() const; Grid& grid(); @@ -68,7 +66,6 @@ namespace Formosa { static const size_t MaximumBuildSpanLength = 6; size_t m_cursorIndex; - size_t m_markerCursorIndex; vector m_readings; Grid m_grid; @@ -79,14 +76,12 @@ namespace Formosa { inline BlockReadingBuilder::BlockReadingBuilder(LanguageModel *inLM) : m_LM(inLM) , m_cursorIndex(0) - , m_markerCursorIndex(SIZE_MAX) { } inline void BlockReadingBuilder::clear() { m_cursorIndex = 0; - m_markerCursorIndex = SIZE_MAX; m_readings.clear(); m_grid.clear(); } @@ -105,21 +100,6 @@ namespace Formosa { { m_cursorIndex = inNewIndex > m_readings.size() ? m_readings.size() : inNewIndex; } - - inline size_t BlockReadingBuilder::markerCursorIndex() const - { - return m_markerCursorIndex; - } - - inline void BlockReadingBuilder::setMarkerCursorIndex(size_t inNewIndex) - { - if (inNewIndex == SIZE_MAX) { - m_markerCursorIndex = SIZE_MAX; - return; - } - - m_markerCursorIndex = inNewIndex > m_readings.size() ? m_readings.size() : inNewIndex; - } inline void BlockReadingBuilder::insertReadingAtCursor(const string& inReading) { @@ -130,12 +110,9 @@ namespace Formosa { m_cursorIndex++; } - inline vector BlockReadingBuilder::readingsAtRange(size_t begin, size_t end) const { - vector v; - for (size_t i = begin; i < end; i++) { - v.push_back(m_readings[i]); - } - return v; + inline vector BlockReadingBuilder::readings() const + { + return m_readings; } inline bool BlockReadingBuilder::deleteReadingBeforeCursor() diff --git a/Source/InputMethodController.h b/Source/InputMethodController.h index 1519be6f..11055f70 100644 --- a/Source/InputMethodController.h +++ b/Source/InputMethodController.h @@ -23,44 +23,10 @@ #import #import -#import "Mandarin.h" -#import "Gramambular.h" -#import "McBopomofoLM.h" -#import "UserOverrideModel.h" +#import "McBopomofo-Swift.h" @interface McBopomofoInputMethodController : IMKInputController -{ -@private - // the reading buffer that takes user input - Formosa::Mandarin::BopomofoReadingBuffer* _bpmfReadingBuffer; - // language model - McBopomofo::McBopomofoLM *_languageModel; - - // user override model - McBopomofo::UserOverrideModel *_userOverrideModel; - - // the grid (lattice) builder for the unigrams (and bigrams) - Formosa::Gramambular::BlockReadingBuilder* _builder; - - // latest walked path (trellis) using the Viterbi algorithm - std::vector _walkedNodes; - - // the latest composing buffer that is updated to the foreground app - NSMutableString *_composingBuffer; - NSInteger _latestReadingCursor; - - // the current text input client; we need to keep this when candidate panel is on - id _currentCandidateClient; - - // a special deferred client for Terminal.app fix - id _currentDeferredClient; - - // current available candidates - NSMutableArray *_candidates; - - // current input mode - NSString *_inputMode; -} +- (void)handleState:(InputState *)newState client:(id)client; @end diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index 8d49183b..80ae1d80 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -21,103 +21,60 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. +#import "McBopomofoLM.h" #import "InputMethodController.h" -#import -#import -#import -#import "OVStringHelper.h" -#import "OVUTF8Helper.h" +#import "KeyHandler.h" #import "LanguageModelManager.h" -#import "McBopomofo-Swift.h" +// Swift Packages @import CandidateUI; @import NotifierUI; @import TooltipUI; @import OpenCCBridge; @import VXHanConvert; -// C++ namespace usages +//// C++ namespace usages using namespace std; -using namespace Formosa::Mandarin; -using namespace Formosa::Gramambular; using namespace McBopomofo; -using namespace OpenVanilla; static const NSInteger kMinKeyLabelSize = 10; -static const NSInteger kMinMarkRangeLength = 2; -static const NSInteger kMaxMarkRangeLength = 6; - -// input modes -static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo"; -static NSString *const kPlainBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.PlainBopomofo"; - -// key code enums -enum { - kEnterKeyCode = 76, - kUpKeyCode = 126, - kDownKeyCode = 125, - kLeftKeyCode = 123, - kRightKeyCode = 124, - kPageUpKeyCode = 116, - kPageDownKeyCode = 121, - kHomeKeyCode = 115, - kEndKeyCode = 119, - kDeleteKeyCode = 117 -}; VTCandidateController *gCurrentCandidateController = nil; -// if DEBUG is defined, a DOT file (GraphViz format) will be written to the -// specified path everytime the grid is walked -#if DEBUG -static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot"; -#endif - // https://clang-analyzer.llvm.org/faq.html __attribute__((annotate("returns_localized_nsstring"))) static inline NSString *LocalizationNotNeeded(NSString *s) { return s; } +@interface McBopomofoInputMethodController () +{ + // the current text input client; we need to keep this when candidate panel is on + id _currentCandidateClient; + + // a special deferred client for Terminal.app fix + id _currentDeferredClient; + + KeyHandler *_keyHandler; + InputState *_state; +} +@end + @interface McBopomofoInputMethodController (VTCandidateController) @end -// sort helper -class NodeAnchorDescendingSorter -{ -public: - bool operator()(const NodeAnchor& a, const NodeAnchor &b) const { - return a.node->key().length() > b.node->key().length(); - } -}; +@interface McBopomofoInputMethodController (KeyHandlerDelegate) +@end -static const double kEpsilon = 0.000001; - -static double FindHighestScore(const vector& nodes, double epsilon) { - double highestScore = 0.0; - for (auto ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { - double score = ni->node->highestUnigramScore(); - if (score > highestScore) { - highestScore = score; - } - } - return highestScore + epsilon; -} +@interface McBopomofoInputMethodController (UI) ++ (VTHorizontalCandidateController *)horizontalCandidateController; ++ (VTVerticalCandidateController *)verticalCandidateController; ++ (TooltipController *)tooltipController; +- (void)_showTooltip:(NSString *)tooltip composingBuffer:(NSString *)composingBuffer cursorIndex:(NSInteger)cursorIndex client:(id)client; +- (void)_hideTooltip; +@end @implementation McBopomofoInputMethodController -- (void)dealloc -{ - // clean up everything - if (_bpmfReadingBuffer) { - delete _bpmfReadingBuffer; - } - - if (_builder) { - delete _builder; - } - // the two client pointers are weak pointers (i.e. we don't retain them) - // therefore we don't do anything about it -} - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)client { @@ -126,25 +83,9 @@ static double FindHighestScore(const vector& nodes, double epsilon) self = [super initWithServer:server delegate:delegate client:client]; if (self) { - _candidates = [[NSMutableArray alloc] init]; - - // create the reading buffer - _bpmfReadingBuffer = new BopomofoReadingBuffer(BopomofoKeyboardLayout::StandardLayout()); - - // create the lattice builder - _languageModel = [LanguageModelManager languageModelMcBopomofo]; - _languageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); - _userOverrideModel = [LanguageModelManager userOverrideModel]; - - _builder = new BlockReadingBuilder(_languageModel); - - // each Mandarin syllable is separated by a hyphen - _builder->setJoinSeparator("-"); - - // create the composing buffer - _composingBuffer = [[NSMutableString alloc] init]; - - _inputMode = kBopomofoModeIdentifier; + _keyHandler = [[KeyHandler alloc] init]; + _keyHandler.delegate = self; + _state = [[InputStateEmpty alloc] init]; } return self; @@ -154,6 +95,7 @@ static double FindHighestScore(const vector& nodes, double epsilon) { // a menu instance (autoreleased) is requested every time the user click on the input menu NSMenu *menu = [[NSMenu alloc] initWithTitle:LocalizationNotNeeded(@"Input Method Menu")]; + NSString *inputMode = _keyHandler.inputMode; [menu addItemWithTitle:NSLocalizedString(@"McBopomofo Preferences", @"") action:@selector(showPreferences:) keyEquivalent:@""]; @@ -161,23 +103,23 @@ static double FindHighestScore(const vector& nodes, double epsilon) chineseConversionMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl; chineseConversionMenuItem.state = Preferences.chineseConversionEnabled ? NSControlStateValueOn : NSControlStateValueOff; - NSMenuItem *halfWidthPunctuationMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@""]; + NSMenuItem *halfWidthPunctuationMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@"h"]; + halfWidthPunctuationMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl; halfWidthPunctuationMenuItem.state = Preferences.halfWidthPunctuationEnabled ? NSControlStateValueOn : NSControlStateValueOff; BOOL optionKeyPressed = [[NSEvent class] respondsToSelector:@selector(modifierFlags)] && ([NSEvent modifierFlags] & NSEventModifierFlagOption); - if (_inputMode == kBopomofoModeIdentifier && optionKeyPressed) { + if (inputMode == kBopomofoModeIdentifier && optionKeyPressed) { NSMenuItem *phaseReplacementMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Phrase Replacement", @"") action:@selector(togglePhraseReplacementEnabled:) keyEquivalent:@""]; phaseReplacementMenuItem.state = Preferences.phraseReplacementEnabled ? NSControlStateValueOn : NSControlStateValueOff; } [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:NSLocalizedString(@"User Phrases", @"") action:NULL keyEquivalent:@""]; - if (_inputMode == kPlainBopomofoModeIdentifier) { + if (inputMode == kPlainBopomofoModeIdentifier) { NSMenuItem *editExcludedPhrasesItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Edit Excluded Phrases", @"") action:@selector(openExcludedPhrasesPlainBopomofo:) keyEquivalent:@""]; [menu addItem:editExcludedPhrasesItem]; - } - else { + } else { [menu addItemWithTitle:NSLocalizedString(@"Edit User Phrases", @"") action:@selector(openUserPhrases:) keyEquivalent:@""]; [menu addItemWithTitle:NSLocalizedString(@"Edit Excluded Phrases", @"") action:@selector(openExcludedPhrasesMcBopomofo:) keyEquivalent:@""]; if (optionKeyPressed) { @@ -205,107 +147,113 @@ static double FindHighestScore(const vector& nodes, double epsilon) // reset the state _currentDeferredClient = nil; _currentCandidateClient = nil; - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; + + [_keyHandler clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [self handleState:empty client:client]; // checks and populates the default settings - switch (Preferences.keyboardLayout) { - case KeyboardLayoutStandard: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); - break; - case KeyboardLayoutEten: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETenLayout()); - break; - case KeyboardLayoutHsu: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HsuLayout()); - break; - case KeyboardLayoutEten26: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETen26Layout()); - break; - case KeyboardLayoutHanyuPinyin: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HanyuPinyinLayout()); - break; - case KeyboardLayoutIBM: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::IBMLayout()); - break; - default: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); - Preferences.keyboardLayout = KeyboardLayoutStandard; - } - - _languageModel->setExternalConverterEnabled(Preferences.chineseConversionStyle == 1); - - [(AppDelegate *)[NSApp delegate] checkForUpdate]; + [_keyHandler syncWithPreferences]; + [(AppDelegate *) NSApp.delegate checkForUpdate]; } - (void)deactivateServer:(id)client { - // clean up reading buffer residues - if (!_bpmfReadingBuffer->isEmpty()) { - _bpmfReadingBuffer->clear(); - [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - } + [_keyHandler clear]; - // commit any residue in the composing buffer - [self commitComposition:client]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [self handleState:empty client:client]; - _currentDeferredClient = nil; - _currentCandidateClient = nil; - - gCurrentCandidateController.delegate = nil; - gCurrentCandidateController.visible = NO; - [_candidates removeAllObjects]; - - [self _hideTooltip]; + InputStateDeactivated *inactive = [[InputStateDeactivated alloc] init]; + [self handleState:inactive client:client]; } - (void)setValue:(id)value forTag:(long)tag client:(id)sender { NSString *newInputMode; - McBopomofoLM *newLanguageModel; if ([value isKindOfClass:[NSString class]] && [value isEqual:kPlainBopomofoModeIdentifier]) { newInputMode = kPlainBopomofoModeIdentifier; - newLanguageModel = [LanguageModelManager languageModelPlainBopomofo]; - newLanguageModel->setPhraseReplacementEnabled(false); - } - else { + } else { newInputMode = kBopomofoModeIdentifier; - newLanguageModel = [LanguageModelManager languageModelMcBopomofo]; - newLanguageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); } - newLanguageModel->setExternalConverterEnabled(Preferences.chineseConversionStyle == 1); // Only apply the changes if the value is changed - if (![_inputMode isEqualToString:newInputMode]) { + if (![_keyHandler.inputMode isEqualToString:newInputMode]) { [[NSUserDefaults standardUserDefaults] synchronize]; - // Remember to override the keyboard layout again -- treat this as an activate eventy + // Remember to override the keyboard layout again -- treat this as an activate event. NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; [sender overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; - - _inputMode = newInputMode; - _languageModel = newLanguageModel; - - if (!_bpmfReadingBuffer->isEmpty()) { - _bpmfReadingBuffer->clear(); - [self updateClientComposingBuffer:sender]; - } - - if ([_composingBuffer length] > 0) { - [self commitComposition:sender]; - } - - if (_builder) { - delete _builder; - _builder = new BlockReadingBuilder(_languageModel); - _builder->setJoinSeparator("-"); - } + [_keyHandler clear]; + _keyHandler.inputMode = newInputMode; + InputState *empty = [[InputState alloc] init]; + [self handleState:empty client:sender]; } } -#pragma mark - IMKServerInput protocol methods +#pragma mark - IMKServerInput protocol methods + +- (NSUInteger)recognizedEvents:(id)sender +{ + return NSEventMaskKeyDown | NSEventMaskFlagsChanged; +} + +- (BOOL)handleEvent:(NSEvent *)event client:(id)client +{ + if ([event type] == NSEventMaskFlagsChanged) { + NSString *functionKeyKeyboardLayoutID = Preferences.functionKeyboardLayout; + NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; + + // If no override is needed, just return NO. + if ([functionKeyKeyboardLayoutID isEqualToString:basisKeyboardLayoutID]) { + return NO; + } + + // Function key pressed. + BOOL includeShift = Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey; + if ((event.modifierFlags & ~NSEventModifierFlagShift) || ((event.modifierFlags & NSEventModifierFlagShift) && includeShift)) { + // Override the keyboard layout and let the OS do its thing + [client overrideKeyboardWithKeyboardNamed:functionKeyKeyboardLayoutID]; + return NO; + } + + // Revert to the basis layout when the function key is released + [client overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; + return NO; + } + + NSRect textFrame = NSZeroRect; + NSDictionary *attributes = nil; + BOOL useVerticalMode = NO; + + @try { + attributes = [client attributesForCharacterIndex:0 lineHeightRectangle:&textFrame]; + useVerticalMode = attributes[@"IMKTextOrientation"] && [attributes[@"IMKTextOrientation"] integerValue] == 0; + } + @catch (NSException *e) { + // exception may raise while using Twitter.app's search filed. + } + + if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && [NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { + // special handling for com.apple.Terminal + _currentDeferredClient = client; + } + + KeyHandlerInput *input = [[KeyHandlerInput alloc] initWithEvent:event isVerticalMode:useVerticalMode]; + BOOL result = [_keyHandler handleInput:input state:_state stateCallback:^(InputState *state) { + [self handleState:state client:client]; + } candidateSelectionCallback:^{ + NSLog(@"candidate window updated."); + } errorCallback:^{ + NSBeep(); + }]; + + return result; +} + + +#pragma mark - States Handling - (NSString *)_convertToSimplifiedChineseIfRequired:(NSString *)text { @@ -320,995 +268,162 @@ static double FindHighestScore(const vector& nodes, double epsilon) if (Preferences.chineseConversionEngine == 1) { return [VXHanConvert convertToSimplifiedFrom:text]; } + return [OpenCCBridge convertToSimplified:text]; } -- (void)commitComposition:(id)client +- (void)_commitText:(NSString *)text client:(id)client { + NSString *buffer = [self _convertToSimplifiedChineseIfRequired:text]; + if (!buffer.length) { + return;; + } + // if it's Terminal, we don't commit at the first call (the client of which will not be IPMDServerClientWrapper) // then we defer the update in the next runloop round -- so that the composing buffer is not // meaninglessly flushed, an annoying bug in Terminal.app since Mac OS X 10.5 if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && ![NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { if (_currentDeferredClient) { - [self performSelector:@selector(updateClientComposingBuffer:) withObject:_currentDeferredClient afterDelay:0.0]; + id currentDeferredClient = _currentDeferredClient; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [currentDeferredClient insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + }); } return; } - - // Chinese conversion. - NSString *buffer = [self _convertToSimplifiedChineseIfRequired:_composingBuffer]; - - // commit the text, clear the state [client insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; +} + +- (void)handleState:(InputState *)newState client:(id)client +{ +// NSLog(@"new state: %@ / current state: %@", newState, _state); + + // We need to set the state to the member variable since the candidate + // window need to read the candidates from it. + InputState *previous = _state; + _state = newState; + + if ([newState isKindOfClass:[InputStateDeactivated class]]) { + [self _handleDeactivated:(InputStateDeactivated *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateEmpty class]]) { + [self _handleEmpty:(InputStateEmpty *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateEmptyIgnoringPreviousState class]]) { + [self _handleEmptyIgnoringPrevious:(InputStateEmptyIgnoringPreviousState *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateCommitting class]]) { + [self _handleCommitting:(InputStateCommitting *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateInputting class]]) { + [self _handleInputting:(InputStateInputting *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateMarking class]]) { + [self _handleMarking:(InputStateMarking *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateChoosingCandidate class]]) { + [self _handleChoosingCandidate:(InputStateChoosingCandidate *) newState previous:previous client:client]; + } +} + +- (void)_handleDeactivated:(InputStateDeactivated *)state previous:(InputState *)previous client:(id)client +{ + // commit any residue in the composing buffer + if ([previous isKindOfClass:[InputStateInputting class]]) { + NSString *buffer = ((InputStateInputting *) previous).composingBuffer; + [self _commitText:buffer client:client]; + } + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + + _currentDeferredClient = nil; + _currentCandidateClient = nil; + + gCurrentCandidateController.delegate = nil; gCurrentCandidateController.visible = NO; - [_candidates removeAllObjects]; [self _hideTooltip]; } -NS_INLINE size_t min(size_t a, size_t b) { return a < b ? a : b; } -NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - -// TODO: bug #28 is more likely to live in this method. -- (void)updateClientComposingBuffer:(id)client +- (void)_handleEmpty:(InputStateEmpty *)state previous:(InputState *)previous client:(id)client { - // "updating the composing buffer" means to request the client to "refresh" the text input buffer - // with our "composing text" - - [_composingBuffer setString:@""]; - NSInteger composedStringCursorIndex = 0; - - size_t readingCursorIndex = 0; - size_t builderCursorIndex = _builder->cursorIndex(); - - // we must do some Unicode codepoint counting to find the actual cursor location for the client - // i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars - // locations - for (vector::iterator wi = _walkedNodes.begin(), we = _walkedNodes.end() ; wi != we ; ++wi) { - if ((*wi).node) { - string nodeStr = (*wi).node->currentKeyValue().value; - vector codepoints = OVUTF8Helper::SplitStringByCodePoint(nodeStr); - size_t codepointCount = codepoints.size(); - - NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()]; - [_composingBuffer appendString:valueString]; - - // this re-aligns the cursor index in the composed string - // (the actual cursor on the screen) with the builder's logical - // cursor (reading) cursor; each built node has a "spanning length" - // (e.g. two reading blocks has a spanning length of 2), and we - // accumulate those lengthes to calculate the displayed cursor - // index - size_t spanningLength = (*wi).spanningLength; - if (readingCursorIndex + spanningLength <= builderCursorIndex) { - composedStringCursorIndex += [valueString length]; - readingCursorIndex += spanningLength; - } - else { - for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++) { - composedStringCursorIndex += [[NSString stringWithUTF8String:codepoints[i].c_str()] length]; - readingCursorIndex++; - } - } - } + // commit any residue in the composing buffer + if ([previous isKindOfClass:[InputStateInputting class]]) { + NSString *buffer = ((InputStateInputting *) previous).composingBuffer; + [self _commitText:buffer client:client]; } - // now we gather all the info, we separate the composing buffer to two parts, head and tail, - // and insert the reading text (the Mandarin syllable) in between them; - // the reading text is what the user is typing - NSString *head = [_composingBuffer substringToIndex:composedStringCursorIndex]; - NSString *reading = [NSString stringWithUTF8String:_bpmfReadingBuffer->composedString().c_str()]; - NSString *tail = [_composingBuffer substringFromIndex:composedStringCursorIndex]; - NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]]; - NSInteger cursorIndex = composedStringCursorIndex + [reading length]; + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; +} - if (_bpmfReadingBuffer->isEmpty() && _builder->markerCursorIndex() != SIZE_MAX) { - // if there is a marked range, we need to tear the string into three parts. - NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:composedText]; - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - [attrString setAttributes:@{ - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - NSMarkedClauseSegmentAttributeName: @0 - } range:NSMakeRange(0, begin)]; - [attrString setAttributes:@{ - NSUnderlineStyleAttributeName: @(NSUnderlineStyleThick), - NSMarkedClauseSegmentAttributeName: @1 - } range:NSMakeRange(begin, end - begin)]; - [attrString setAttributes:@{ - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - NSMarkedClauseSegmentAttributeName: @2 - } range:NSMakeRange(end, [composedText length] - end)]; - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put ths composing buffer - [client setMarkedText:attrString selectionRange:NSMakeRange((NSInteger)_builder->markerCursorIndex(), 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _latestReadingCursor = (NSInteger)_builder->markerCursorIndex(); - [self _showCurrentMarkedTextTooltipWithClient:client]; +- (void)_handleEmptyIgnoringPrevious:(InputStateEmptyIgnoringPreviousState *)state previous:(InputState *)previous client:(id)client +{ + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; +} + +- (void)_handleCommitting:(InputStateCommitting *)state previous:(InputState *)previous client:(id)client +{ + NSString *poppedText = state.poppedText; + [self _commitText:poppedText client:client]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; +} + +- (void)_handleInputting:(InputStateInputting *)state previous:(InputState *)previous client:(id)client +{ + NSString *poppedText = state.poppedText; + if (poppedText.length) { + [self _commitText:poppedText client:client]; } - else { - // we must use NSAttributedString so that the cursor is visible -- - // can't just use NSString - NSDictionary *attrDict = @{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - NSMarkedClauseSegmentAttributeName: @0}; - NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:composedText attributes:attrDict]; - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put ths composing buffer - [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _latestReadingCursor = cursorIndex; + NSUInteger cursorIndex = state.cursorIndex; + NSAttributedString *attrString = state.attributedString; + + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put ths composing buffer + [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; +} + +- (void)_handleMarking:(InputStateMarking *)state previous:(InputState *)previous client:(id)client +{ + NSUInteger cursorIndex = state.cursorIndex; + NSAttributedString *attrString = state.attributedString; + + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put ths composing buffer + [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + + gCurrentCandidateController.visible = NO; + if (state.tooltip.length) { + [self _showTooltip:state.tooltip composingBuffer:state.composingBuffer cursorIndex:state.markerIndex client:client]; + } else { [self _hideTooltip]; } } -- (void)walk +- (void)_handleChoosingCandidate:(InputStateChoosingCandidate *)state previous:(InputState *)previous client:(id)client { - // retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation - // of the best possible Mandarain characters given the input syllables, - // using the Viterbi algorithm implemented in the Gramambular library - Walker walker(&_builder->grid()); + NSUInteger cursorIndex = state.cursorIndex; + NSAttributedString *attrString = state.attributedString; - // the reverse walk traces the trellis from the end - _walkedNodes = walker.reverseWalk(_builder->grid().width()); + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put ths composing buffer + [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - // then we reverse the nodes so that we get the forward-walked nodes - reverse(_walkedNodes.begin(), _walkedNodes.end()); - - // if DEBUG is defined, a GraphViz file is written to kGraphVizOutputfile -#if DEBUG - string dotDump = _builder->grid().dumpDOT(); - NSString *dotStr = [NSString stringWithUTF8String:dotDump.c_str()]; - NSError *error = nil; - - BOOL __unused success = [dotStr writeToFile:kGraphVizOutputfile atomically:YES encoding:NSUTF8StringEncoding error:&error]; -#endif -} - -- (void)popOverflowComposingTextAndWalk:(id)client -{ - // in an ideal world, we can as well let the user type forever, - // but because the Viterbi algorithm has a complexity of O(N^2), - // the walk will become slower as the number of nodes increase, - // therefore we need to "pop out" overflown text -- they usually - // lose their influence over the whole MLE anyway -- so tht when - // the user type along, the already composed text at front will - // be popped out - - NSInteger composingBufferSize = Preferences.composingBufferSize; - - if (_builder->grid().width() > (size_t)composingBufferSize) { - if (_walkedNodes.size() > 0) { - NodeAnchor &anchor = _walkedNodes[0]; - NSString *popedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()]; - // Chinese conversion. - popedText = [self _convertToSimplifiedChineseIfRequired:popedText]; - [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _builder->removeHeadReadings(anchor.spanningLength); - } - } - - [self walk]; -} - -- (void)beep -{ - // use the system's default sound (configurable in System Preferences) to give a warning - NSBeep(); -} - -- (string)_currentLayout -{ - NSString *keyboardLayoutName = Preferences.keyboardLayoutName; - string layout = string(keyboardLayoutName.UTF8String) + string("_"); - return layout; -} - -- (BOOL)handleInputText:(NSString*)inputText key:(NSInteger)keyCode modifiers:(NSUInteger)flags client:(id)client -{ - NSRect textFrame = NSZeroRect; - NSDictionary *attributes = nil; - - bool composeReading = false; - BOOL useVerticalMode = NO; - - @try { - attributes = [client attributesForCharacterIndex:0 lineHeightRectangle:&textFrame]; - useVerticalMode = [attributes objectForKey:@"IMKTextOrientation"] && [[attributes objectForKey:@"IMKTextOrientation"] integerValue] == 0; - } - @catch (NSException *e) { - // exception may raise while using Twitter.app's search filed. - } - - NSInteger cursorForwardKey = useVerticalMode ? kDownKeyCode : kRightKeyCode; - NSInteger cursorBackwardKey = useVerticalMode ? kUpKeyCode : kLeftKeyCode; - NSInteger extraChooseCandidateKey = useVerticalMode ? kLeftKeyCode : kDownKeyCode; - NSInteger absorbedArrowKey = useVerticalMode ? kRightKeyCode : kUpKeyCode; - NSInteger verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : 0; - - // get the unicode character code - UniChar charCode = [inputText length] ? [inputText characterAtIndex:0] : 0; - - McBopomofoEmacsKey emacsKey = [EmacsKeyHelper detectWithCharCode:charCode flags:flags]; - - if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && [NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { - // special handling for com.apple.Terminal - _currentDeferredClient = client; - } - - // if the inputText is empty, it's a function key combination, we ignore it - if (![inputText length]) { - return NO; - } - - // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it - if (![_composingBuffer length] && - _bpmfReadingBuffer->isEmpty() && - ((flags & NSEventModifierFlagCommand) || (flags & NSEventModifierFlagControl) || (flags & NSEventModifierFlagOption) || (flags & NSEventModifierFlagNumericPad))) { - return NO; - } - - // Caps Lock processing : if Caps Lock is on, temporarily disable bopomofo. - if (charCode == 8 || charCode == 13 || keyCode == absorbedArrowKey || keyCode == extraChooseCandidateKey || keyCode == cursorForwardKey || keyCode == cursorBackwardKey) { - // do nothing if backspace is pressed -- we ignore the key - } - else if (flags & NSAlphaShiftKeyMask) { - // process all possible combination, we hope. - if ([_composingBuffer length]) { - [self commitComposition:client]; - } - - // first commit everything in the buffer. - if (flags & NSEventModifierFlagShift) { - return NO; - } - - // if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char insertions. - if (charCode < 0x80 && !isprint(charCode)) { - return NO; - } - - // when shift is pressed, don't do further processing, since it outputs capital letter anyway. - NSString *popedText = [inputText lowercaseString]; - [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - return YES; - } - - if (flags & NSEventModifierFlagNumericPad) { - if (keyCode != kLeftKeyCode && keyCode != kRightKeyCode && keyCode != kDownKeyCode && keyCode != kUpKeyCode && charCode != 32 && isprint(charCode)) { - if ([_composingBuffer length]) { - [self commitComposition:client]; - } - - NSString *popedText = [inputText lowercaseString]; - [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - return YES; - } - } - - // if we have candidate, it means we need to pass the event to the candidate handler - if ([_candidates count]) { - return [self _handleCandidateEventWithInputText:inputText charCode:charCode keyCode:keyCode emacsKey:(McBopomofoEmacsKey)emacsKey]; - } - - // If we have marker index. - if (_builder->markerCursorIndex() != SIZE_MAX) { - // ESC - if (charCode == 27) { - _builder->setMarkerCursorIndex(SIZE_MAX); - [self updateClientComposingBuffer:client]; - return YES; - } - // Enter - if (charCode == 13) { - if ([self _writeUserPhrase]) { - _builder->setMarkerCursorIndex(SIZE_MAX); - } - else { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - // Shift + left - if ((keyCode == cursorBackwardKey || emacsKey == McBopomofoEmacsKeyBackward) - && (flags & NSEventModifierFlagShift)) { - if (_builder->markerCursorIndex() > 0) { - _builder->setMarkerCursorIndex(_builder->markerCursorIndex() - 1); - } - else { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - // Shift + Right - if ((keyCode == cursorForwardKey || emacsKey == McBopomofoEmacsKeyForward) - && (flags & NSEventModifierFlagShift)) { - if (_builder->markerCursorIndex() < _builder->length()) { - _builder->setMarkerCursorIndex(_builder->markerCursorIndex() + 1); - } - else { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - - _builder->setMarkerCursorIndex(SIZE_MAX); - } - - // see if it's valid BPMF reading - if (_bpmfReadingBuffer->isValidKey((char)charCode)) { - _bpmfReadingBuffer->combineKey((char)charCode); - - // if we have a tone marker, we have to insert the reading to the - // builder in other words, if we don't have a tone marker, we just - // update the composing buffer - composeReading = _bpmfReadingBuffer->hasToneMarker(); - if (!composeReading) { - [self updateClientComposingBuffer:client]; - return YES; - } - } - - // see if we have composition if Enter/Space is hit and buffer is not empty - // this is bit-OR'ed so that the tone marker key is also taken into account - composeReading |= (!_bpmfReadingBuffer->isEmpty() && (charCode == 32 || charCode == 13)); - if (composeReading) { - // combine the reading - string reading = _bpmfReadingBuffer->syllable().composedString(); - - // see if we have a unigram for this - if (!_languageModel->hasUnigramsForKey(reading)) { - [self beep]; - [self updateClientComposingBuffer:client]; - return YES; - } - - // and insert it into the lattice - _builder->insertReadingAtCursor(reading); - - // then walk the lattice - [self popOverflowComposingTextAndWalk:client]; - - // get user override model suggestion - string overrideValue = (_inputMode == kPlainBopomofoModeIdentifier) ? "" : - _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(), [[NSDate date] timeIntervalSince1970]); - - if (!overrideValue.empty()) { - size_t cursorIndex = [self actualCandidateCursorIndex]; - vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); - double highestScore = FindHighestScore(nodes, kEpsilon); - _builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue, highestScore); - } - - // then update the text - _bpmfReadingBuffer->clear(); - [self updateClientComposingBuffer:client]; - - if (_inputMode == kPlainBopomofoModeIdentifier) { - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - } - - // and tells the client that the key is consumed - return YES; - } - - // keyCode 125 = Down, charCode 32 = Space - if (_bpmfReadingBuffer->isEmpty() && [_composingBuffer length] > 0 && (keyCode == extraChooseCandidateKey || charCode == 32 || (useVerticalMode && (keyCode == verticalModeOnlyChooseCandidateKey)))) { - if (charCode == 32) { - // if the spacebar is NOT set to be a selection key - if ((flags & NSEventModifierFlagShift) != 0 || !Preferences.chooseCandidateUsingSpace) { - if (_builder->cursorIndex() >= _builder->length()) { - [_composingBuffer appendString:@" "]; - [self commitComposition:client]; - _bpmfReadingBuffer->clear(); - } - else if (_languageModel->hasUnigramsForKey(" ")) { - _builder->insertReadingAtCursor(" "); - [self popOverflowComposingTextAndWalk:client]; - [self updateClientComposingBuffer:client]; - } - return YES; - - } - } - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - return YES; - } - - // Esc - if (charCode == 27) { - BOOL escToClearInputBufferEnabled = Preferences.escToCleanInputBuffer; - - if (escToClearInputBufferEnabled) { - // if the optioon is enabled, we clear everythiong including the composing - // buffer, walked nodes and the reading. - if (![_composingBuffer length]) { - return NO; - } - _bpmfReadingBuffer->clear(); - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; - } - else { - // if reading is not empty, we cancel the reading; Apple's built-in - // Zhuyin (and the erstwhile Hanin) has a default option that Esc - // "cancels" the current composed character and revert it to - // Bopomofo reading, in odds with the expectation of users from - // other platforms - - if (_bpmfReadingBuffer->isEmpty()) { - // no nee to beep since the event is deliberately triggered by user - - if (![_composingBuffer length]) { - return NO; - } - } - else { - _bpmfReadingBuffer->clear(); - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // handle cursor backward - if (keyCode == cursorBackwardKey || emacsKey == McBopomofoEmacsKeyBackward) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (flags & NSEventModifierFlagShift) { - // Shift + left - if (_builder->cursorIndex() > 0) { - _builder->setMarkerCursorIndex(_builder->cursorIndex() - 1); - } - else { - [self beep]; - } - } else { - if (_builder->cursorIndex() > 0) { - _builder->setCursorIndex(_builder->cursorIndex() - 1); - } - else { - [self beep]; - } - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // handle cursor forward - if (keyCode == cursorForwardKey || emacsKey == McBopomofoEmacsKeyForward) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (flags & NSEventModifierFlagShift) { - // Shift + Right - if (_builder->cursorIndex() < _builder->length()) { - _builder->setMarkerCursorIndex(_builder->cursorIndex() + 1); - } else { - [self beep]; - } - } else { - if (_builder->cursorIndex() < _builder->length()) { - _builder->setCursorIndex(_builder->cursorIndex() + 1); - } - else { - [self beep]; - } - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - if (keyCode == kHomeKeyCode || emacsKey == McBopomofoEmacsKeyHome) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex()) { - _builder->setCursorIndex(0); - } - else { - [self beep]; - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - if (keyCode == kEndKeyCode || emacsKey == McBopomofoEmacsKeyEnd) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex() != _builder->length()) { - _builder->setCursorIndex(_builder->length()); - } - else { - [self beep]; - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - if (keyCode == absorbedArrowKey || keyCode == extraChooseCandidateKey) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - - // Backspace - if (charCode == 8) { - if (_bpmfReadingBuffer->isEmpty()) { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex()) { - _builder->deleteReadingBeforeCursor(); - [self walk]; - } - else { - [self beep]; - } - } - else { - _bpmfReadingBuffer->backspace(); - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // Delete - if (keyCode == kDeleteKeyCode || emacsKey == McBopomofoEmacsKeyDelete) { - if (_bpmfReadingBuffer->isEmpty()) { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex() != _builder->length()) { - _builder->deleteReadingAfterCursor(); - [self walk]; - } - else { - [self beep]; - } - } - else { - [self beep]; - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // Enter - if (charCode == 13) { - if (![_composingBuffer length]) { - return NO; - } - - [self commitComposition:client]; - return YES; - } - - // punctuation list - if ((char)charCode == '`') { - if (_languageModel->hasUnigramsForKey(string("_punctuation_list"))) { - if (_bpmfReadingBuffer->isEmpty()) { - _builder->insertReadingAtCursor(string("_punctuation_list")); - [self popOverflowComposingTextAndWalk:client]; - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - } - else { // If there is still unfinished bpmf reading, ignore the punctuation - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - } - - // if nothing is matched, see if it's a punctuation key for current layout. - string layout = [self _currentLayout]; - string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_"): string("_punctuation_"); - string customPunctuation = punctuationNamePrefix + layout + string(1, (char)charCode); - if ([self _handlePunctuation:customPunctuation usingVerticalMode:useVerticalMode client:client]) { - return YES; - } - - // if nothing is matched, see if it's a punctuation key. - string punctuation = punctuationNamePrefix + string(1, (char)charCode); - if ([self _handlePunctuation:punctuation usingVerticalMode:useVerticalMode client:client]) { - return YES; - } - - if ((char)charCode >= 'A' && (char)charCode <= 'Z') { - if ([_composingBuffer length]) { - string letter = string("_letter_") + string(1, (char)charCode); - if ([self _handlePunctuation:letter usingVerticalMode:useVerticalMode client:client]) { - return YES; - } - } - } - - // still nothing, then we update the composing buffer (some app has - // strange behavior if we don't do this, "thinking" the key is not - // actually consumed) - if ([_composingBuffer length] || !_bpmfReadingBuffer->isEmpty()) { - [self beep]; - [self updateClientComposingBuffer:client]; - return YES; - } - - return NO; -} - -- (BOOL)_handlePunctuation:(string)customPunctuation usingVerticalMode:(BOOL)useVerticalMode client:(id)client -{ - if (_languageModel->hasUnigramsForKey(customPunctuation)) { - if (_bpmfReadingBuffer->isEmpty()) { - _builder->insertReadingAtCursor(customPunctuation); - [self popOverflowComposingTextAndWalk:client]; - } - else { // If there is still unfinished bpmf reading, ignore the punctuation - [self beep]; - } - [self updateClientComposingBuffer:client]; - - if (_inputMode == kPlainBopomofoModeIdentifier && _bpmfReadingBuffer->isEmpty()) { - [self collectCandidates]; - if ([_candidates count] == 1) { - [self commitComposition:client]; - } - else { - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - } - } - return YES; - } - return NO; -} - -- (BOOL)_handleCandidateEventWithInputText:(NSString *)inputText charCode:(UniChar)charCode keyCode:(NSUInteger)keyCode emacsKey:(McBopomofoEmacsKey)emacsKey -{ - BOOL cancelCandidateKey = - (charCode == 27) || - ((_inputMode == kPlainBopomofoModeIdentifier) && - (charCode == 8 || keyCode == kDeleteKeyCode)); - - if (cancelCandidateKey) { - gCurrentCandidateController.visible = NO; - [_candidates removeAllObjects]; - - if (_inputMode == kPlainBopomofoModeIdentifier) { - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (charCode == 13 || keyCode == kEnterKeyCode) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex]; - return YES; - } - else if (charCode == 32 || keyCode == kPageDownKeyCode || emacsKey == McBopomofoEmacsKeyNextPage) { - BOOL updated = [gCurrentCandidateController showNextPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kPageUpKeyCode) { - BOOL updated = [gCurrentCandidateController showPreviousPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kLeftKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController showPreviousPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (emacsKey == McBopomofoEmacsKeyBackward) { - BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kRightKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController highlightNextCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController showNextPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (emacsKey == McBopomofoEmacsKeyForward) { - BOOL updated = [gCurrentCandidateController highlightNextCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kUpKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController showPreviousPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (keyCode == kDownKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController showNextPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController highlightNextCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (keyCode == kHomeKeyCode || emacsKey == McBopomofoEmacsKeyHome) { - if (gCurrentCandidateController.selectedCandidateIndex == 0) { - [self beep]; - - } - else { - gCurrentCandidateController.selectedCandidateIndex = 0; - } - - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if ((keyCode == kEndKeyCode || emacsKey == McBopomofoEmacsKeyEnd) && [_candidates count] > 0) { - if (gCurrentCandidateController.selectedCandidateIndex == [_candidates count] - 1) { - [self beep]; - } - else { - gCurrentCandidateController.selectedCandidateIndex = [_candidates count] - 1; - } - - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - NSInteger index = NSNotFound; - for (NSUInteger j = 0, c = [gCurrentCandidateController.keyLabels count]; j < c; j++) { - if ([inputText compare:[gCurrentCandidateController.keyLabels objectAtIndex:j] options:NSCaseInsensitiveSearch] == NSOrderedSame) { - index = j; - break; - } - } - - [gCurrentCandidateController.keyLabels indexOfObject:inputText]; - if (index != NSNotFound) { - NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index]; - if (candidateIndex != NSUIntegerMax) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:candidateIndex]; - return YES; - } - } - - if (_inputMode == kPlainBopomofoModeIdentifier) { - string layout = [self _currentLayout]; - string customPunctuation = string("_punctuation_") + layout + string(1, (char)charCode); - string punctuation = string("_punctuation_") + string(1, (char)charCode); - - BOOL shouldAutoSelectCandidate = _bpmfReadingBuffer->isValidKey((char)charCode) || _languageModel->hasUnigramsForKey(customPunctuation) || - _languageModel->hasUnigramsForKey(punctuation); - - if (shouldAutoSelectCandidate) { - NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:0]; - if (candidateIndex != NSUIntegerMax) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:candidateIndex]; - return [self handleInputText:inputText key:keyCode modifiers:0 client:_currentCandidateClient]; - } - } - } - - [self beep]; - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; + if (![previous isKindOfClass:[InputStateChoosingCandidate class]]) { + [self _showCandidateWindowWithState:state client:client]; } } -- (NSUInteger)recognizedEvents:(id)sender -{ - return NSKeyDownMask | NSFlagsChangedMask; -} - -- (BOOL)handleEvent:(NSEvent *)event client:(id)client -{ - if ([event type] == NSFlagsChanged) { - NSString *functionKeyKeyboardLayoutID = Preferences.functionKeyboardLayout; - NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; - - // If no override is needed, just return NO. - if ([functionKeyKeyboardLayoutID isEqualToString:basisKeyboardLayoutID]) { - return NO; - } - - // Function key pressed. - BOOL includeShift = Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey; - if (([event modifierFlags] & ~NSEventModifierFlagShift) || (([event modifierFlags] & NSEventModifierFlagShift) && includeShift)) { - // Override the keyboard layout and let the OS do its thing - [client overrideKeyboardWithKeyboardNamed:functionKeyKeyboardLayoutID]; - return NO; - } - - // Revert back to the basis layout when the function key is released - [client overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; - return NO; - } - - NSString *inputText = [event characters]; - NSInteger keyCode = [event keyCode]; - NSUInteger flags = [event modifierFlags]; - return [self handleInputText:inputText key:keyCode modifiers:flags client:client]; -} - -#pragma mark - Private methods - -+ (VTHorizontalCandidateController *)horizontalCandidateController -{ - static VTHorizontalCandidateController *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[VTHorizontalCandidateController alloc] init]; - }); - return instance; -} - -+ (VTVerticalCandidateController *)verticalCandidateController -{ - static VTVerticalCandidateController *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[VTVerticalCandidateController alloc] init]; - }); - return instance; -} - -+ (TooltipController *)tooltipController -{ - static TooltipController *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[TooltipController alloc] init]; - }); - return instance; -} - -- (void)collectCandidates -{ - // returns the candidate - [_candidates removeAllObjects]; - - size_t cursorIndex = [self actualCandidateCursorIndex]; - vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); - - // sort the nodes, so that longer nodes (representing longer phrases) are placed at the top of the candidate list - stable_sort(nodes.begin(), nodes.end(), NodeAnchorDescendingSorter()); - - // then use the C++ trick to retrieve the candidates for each node at/crossing the cursor - for (vector::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { - const vector& candidates = (*ni).node->candidates(); - for (vector::const_iterator ci = candidates.begin(), ce = candidates.end(); ci != ce; ++ci) { - [_candidates addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]]; - } - } -} - -- (size_t)actualCandidateCursorIndex -{ - size_t cursorIndex = _builder->cursorIndex(); - if (Preferences.selectPhraseAfterCursorAsCandidate) { - // MS Phonetics IME style, phrase is *after* the cursor, i.e. cursor is always *before* the phrase - if (cursorIndex < _builder->length()) { - ++cursorIndex; - } - } - else { - if (!cursorIndex) { - ++cursorIndex; - } - } - - return cursorIndex; -} - -- (void)_showCandidateWindowUsingVerticalMode:(BOOL)useVerticalMode client:(id)client +- (void)_showCandidateWindowWithState:(InputStateChoosingCandidate *)state client:(id)client { // set the candidate panel style + BOOL useVerticalMode = state.useVerticalMode; if (useVerticalMode) { gCurrentCandidateController = [McBopomofoInputMethodController verticalCandidateController]; - } - else if (Preferences.useHorizontalCandidateList) { + } else if (Preferences.useHorizontalCandidateList) { gCurrentCandidateController = [McBopomofoInputMethodController horizontalCandidateController]; - } - else { + } else { gCurrentCandidateController = [McBopomofoInputMethodController verticalCandidateController]; } @@ -1322,39 +437,28 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NSString *ctFontName = Preferences.candidateTextFontName; NSString *klFontName = Preferences.candidateKeyLabelFontName; - NSString *ckeys = Preferences.candidateKeys; + NSString *candidateKeys = Preferences.candidateKeys; gCurrentCandidateController.keyLabelFont = klFontName ? [NSFont fontWithName:klFontName size:keyLabelSize] : [NSFont systemFontOfSize:keyLabelSize]; gCurrentCandidateController.candidateFont = ctFontName ? [NSFont fontWithName:ctFontName size:textSize] : [NSFont systemFontOfSize:textSize]; - NSMutableArray *keyLabels = [NSMutableArray arrayWithObjects:@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", nil]; + NSMutableArray *keyLabels = [@[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9"] mutableCopy]; - if ([ckeys length] > 1) { + if (candidateKeys.length > 1) { [keyLabels removeAllObjects]; - for (NSUInteger i = 0, c = [ckeys length]; i < c; i++) { - [keyLabels addObject:[ckeys substringWithRange:NSMakeRange(i, 1)]]; + for (NSUInteger i = 0, c = candidateKeys.length; i < c; i++) { + [keyLabels addObject:[candidateKeys substringWithRange:NSMakeRange(i, 1)]]; } } gCurrentCandidateController.keyLabels = keyLabels; - [self collectCandidates]; - - if (_inputMode == kPlainBopomofoModeIdentifier && [_candidates count] == 1) { - [self commitComposition:client]; - return; - } - gCurrentCandidateController.delegate = self; [gCurrentCandidateController reloadData]; - - // update the composing text, set the client - [self updateClientComposingBuffer:client]; _currentCandidateClient = client; NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); - - NSInteger cursor = _latestReadingCursor; - if (cursor == [_composingBuffer length] && cursor != 0) { + NSInteger cursor = state.cursorIndex; + if (cursor == state.composingBuffer.length && cursor != 0) { cursor--; } @@ -1368,136 +472,11 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if (useVerticalMode) { [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; - } - else { + } else { [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; } gCurrentCandidateController.visible = YES; - -} - -#pragma mark - User phrases - -- (NSString *)_currentMarkedText -{ - if (_builder->markerCursorIndex() < 0) { - return @""; - } - if (!_bpmfReadingBuffer->isEmpty()) { - return @""; - } - - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - // A phrase should contian at least two characters. - if (end - begin < 1) { - return @""; - } - - NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); - NSString *selectedText = [_composingBuffer substringWithRange:range]; - return selectedText; -} - -- (NSString *)_currentMarkedTextAndReadings -{ - if (_builder->markerCursorIndex() < 0) { - return @""; - } - if (!_bpmfReadingBuffer->isEmpty()) { - return @""; - } - - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - // A phrase should contian at least two characters. - if (end - begin < kMinMarkRangeLength) { - return @""; - } - if (end - begin > kMaxMarkRangeLength) { - return @""; - } - - NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); - NSString *selectedText = [_composingBuffer substringWithRange:range]; - NSMutableString *string = [[NSMutableString alloc] init]; - [string appendString:selectedText]; - [string appendString:@" "]; - NSMutableArray *readingsArray = [[NSMutableArray alloc] init]; - vector v = _builder->readingsAtRange(begin, end); - for(vector::iterator it_i=v.begin(); it_i!=v.end(); ++it_i) { - [readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]]; - } - [string appendString:[readingsArray componentsJoinedByString:@"-"]]; - return string; -} - -- (BOOL)_writeUserPhrase -{ - NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings]; - if (![currentMarkedPhrase length]) { - [self beep]; - return NO; - } - - return [LanguageModelManager writeUserPhrase:currentMarkedPhrase]; -} - -- (void)_showCurrentMarkedTextTooltipWithClient:(id)client -{ - NSString *text = [self _currentMarkedText]; - NSInteger length = text.length; - if (!length) { - [self _hideTooltip]; - } - else if (Preferences.phraseReplacementEnabled) { - NSString *message = NSLocalizedString(@"Phrase replacement mode is on. Not suggested to add phrase in the mode.", @""); - [self _showTooltip:message client:client]; - } - else if (Preferences.chineseConversionStyle == 1 && Preferences.chineseConversionEnabled) { - NSString *message = NSLocalizedString(@"Model based Chinese conversion is on. Not suggested to add phrase in the mode.", @""); - [self _showTooltip:message client:client]; - } - else if (length < kMinMarkRangeLength) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text]; - [self _showTooltip:message client:client]; - } - else if (length > kMaxMarkRangeLength) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". A phrase cannot be longer than 6 characters.", @""), text]; - [self _showTooltip:message client:client]; - } - else { - NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". Press enter to add a new phrase.", @""), text]; - [self _showTooltip:messsage client:client]; - } -} - -- (void)_showTooltip:(NSString *)tooltip client:(id)client -{ - NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); - - NSInteger cursor = _latestReadingCursor; - if (cursor == [_composingBuffer length] && cursor != 0) { - cursor--; - } - - // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch - @try { - [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; - } - @catch (NSException *exception) { - NSLog(@"%@", exception); - } - - [[McBopomofoInputMethodController tooltipController] showTooltip:tooltip atPoint:lineHeightRect.origin]; -} - -- (void)_hideTooltip -{ - if ([McBopomofoInputMethodController tooltipController].window.isVisible) { - [[McBopomofoInputMethodController tooltipController] hide]; - } } #pragma mark - Misc menu items @@ -1508,26 +487,21 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } if ([IMKInputController instancesRespondToSelector:@selector(showPreferences:)]) { [super showPreferences:sender]; } else { - [(AppDelegate *)[NSApp delegate] showPreferences]; + [(AppDelegate *) NSApp.delegate showPreferences]; } [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } - (void)toggleChineseConverter:(id)sender { - BOOL chineseConversionEnabled = [Preferences toggleChineseConversionEnabled]; - [NotifierController notifyWithMessage: - chineseConversionEnabled ? - NSLocalizedString(@"Chinese conversion on", @"") : - NSLocalizedString(@"Chinese conversion off", @"") stay:NO]; + BOOL enabled = [Preferences toggleChineseConversionEnabled]; + [NotifierController notifyWithMessage:enabled ? NSLocalizedString(@"Chinese conversion on", @"") : NSLocalizedString(@"Chinese conversion off", @"") stay:NO]; } - (void)toggleHalfWidthPunctuation:(id)sender { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-result" - [Preferences toggleHalfWidthPunctuationEnabled]; -#pragma GCC diagnostic pop + BOOL enabled = [Preferences toggleHalfWidthPunctuationEnabled]; + [NotifierController notifyWithMessage:enabled ? NSLocalizedString(@"Half-width punctuation on", @"") : NSLocalizedString(@"Half-width punctuation off", @"") stay:NO]; } - (void)togglePhraseReplacementEnabled:(id)sender @@ -1539,12 +513,12 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)checkForUpdate:(id)sender { - [(AppDelegate *)[[NSApplication sharedApplication] delegate] checkForUpdateForced:YES]; + [(AppDelegate *) NSApp.delegate checkForUpdateForced:YES]; } - (BOOL)_checkUserFiles { - if (![LanguageModelManager checkIfUserLanguageModelFilesExist] ) { + if (![LanguageModelManager checkIfUserLanguageModelFilesExist]) { NSString *content = [NSString stringWithFormat:NSLocalizedString(@"Please check the permission of at \"%@\".", @""), [LanguageModelManager dataFolderPath]]; [[NonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; return NO; @@ -1594,8 +568,6 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } - - @end #pragma mark - @@ -1604,35 +576,129 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (NSUInteger)candidateCountForController:(VTCandidateController *)controller { - return [_candidates count]; + if ([_state isKindOfClass:[InputStateChoosingCandidate class]]) { + InputStateChoosingCandidate *state = (InputStateChoosingCandidate *) _state; + return state.candidates.count; + } + return 0; } - (NSString *)candidateController:(VTCandidateController *)controller candidateAtIndex:(NSUInteger)index { - return [_candidates objectAtIndex:index]; + if ([_state isKindOfClass:[InputStateChoosingCandidate class]]) { + InputStateChoosingCandidate *state = (InputStateChoosingCandidate *) _state; + return state.candidates[index]; + } + return @""; } - (void)candidateController:(VTCandidateController *)controller didSelectCandidateAtIndex:(NSUInteger)index { gCurrentCandidateController.visible = NO; - // candidate selected, override the node with selection - string selectedValue = [[_candidates objectAtIndex:index] UTF8String]; + if ([_state isKindOfClass:[InputStateChoosingCandidate class]]) { + InputStateChoosingCandidate *state = (InputStateChoosingCandidate *) _state; - size_t cursorIndex = [self actualCandidateCursorIndex]; - _builder->grid().fixNodeSelectedCandidate(cursorIndex, selectedValue); - if (_inputMode != kPlainBopomofoModeIdentifier) { - _userOverrideModel->observe(_walkedNodes, cursorIndex, selectedValue, [[NSDate date] timeIntervalSince1970]); - } + // candidate selected, override the node with selection + string selectedValue = [state.candidates[index] UTF8String]; + [_keyHandler fixNodeWithValue:selectedValue]; + InputStateInputting *inputting = [_keyHandler _buildInputtingState]; - [_candidates removeAllObjects]; - - [self walk]; - [self updateClientComposingBuffer:_currentCandidateClient]; - - if (_inputMode == kPlainBopomofoModeIdentifier) { - [self commitComposition:_currentCandidateClient]; - return; + if (_keyHandler.inputMode == kPlainBopomofoModeIdentifier) { + [_keyHandler clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:inputting.composingBuffer]; + [self handleState:committing client:_currentCandidateClient]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [self handleState:empty client:_currentCandidateClient]; + } else { + [self handleState:inputting client:_currentCandidateClient]; + } + } +} + +@end + +@implementation McBopomofoInputMethodController (KeyHandlerDelegate) + +- (nonnull VTCandidateController *)candidateControllerForKeyHandler:(nonnull KeyHandler *)keyHandler +{ + return gCurrentCandidateController; +} + +- (BOOL)keyHandler:(nonnull KeyHandler *)keyHandler didRequestWriteUserPhraseWithState:(nonnull InputStateMarking *)state +{ + if (!state.validToWrite) { + return NO; + } + NSString *userPhrase = state.userPhrase; + [LanguageModelManager writeUserPhrase:userPhrase]; + return YES; +} + +- (void)keyHandler:(nonnull KeyHandler *)keyHandler didSelectCandidateAtIndex:(NSInteger)index candidateController:(nonnull VTCandidateController *)controller +{ + [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:index]; +} + +@end + + +@implementation McBopomofoInputMethodController (UI) + ++ (VTHorizontalCandidateController *)horizontalCandidateController +{ + static VTHorizontalCandidateController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[VTHorizontalCandidateController alloc] init]; + }); + return instance; +} + ++ (VTVerticalCandidateController *)verticalCandidateController +{ + static VTVerticalCandidateController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[VTVerticalCandidateController alloc] init]; + }); + return instance; +} + ++ (TooltipController *)tooltipController +{ + static TooltipController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[TooltipController alloc] init]; + }); + return instance; +} + +- (void)_showTooltip:(NSString *)tooltip composingBuffer:(NSString *)composingBuffer cursorIndex:(NSInteger)cursorIndex client:(id)client +{ + NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); + + NSUInteger cursor = (NSUInteger) cursorIndex; + if (cursor == composingBuffer.length && cursor != 0) { + cursor--; + } + + // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch + @try { + [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; + } + @catch (NSException *exception) { + NSLog(@"%@", exception); + } + + [[McBopomofoInputMethodController tooltipController] showTooltip:tooltip atPoint:lineHeightRect.origin]; +} + +- (void)_hideTooltip +{ + if ([McBopomofoInputMethodController tooltipController].window.isVisible) { + [[McBopomofoInputMethodController tooltipController] hide]; } } diff --git a/Source/InputState.swift b/Source/InputState.swift new file mode 100644 index 00000000..4266f562 --- /dev/null +++ b/Source/InputState.swift @@ -0,0 +1,236 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +import Cocoa + +/// Represents the states for the input method controller. +/// +/// An input method is actually a finite state machine. It receives the inputs +/// from hardware like keyboard and mouse, changes its state, updates user +/// interface by the state, and finally produces the text output and then them +/// to the client apps. It should be a one-way data flow, and the user interface +/// and text output should follow unconditionally one single data source. +/// +/// The InputState class is for representing what the input controller is doing, +/// and the place to store the variables that could be used. For example, the +/// array for the candidate list is useful only when the user is choosing a +/// candidate, and the array should not exist when the input controller is in +/// another state. +/// +/// They are immutable objects. When the state changes, the controller should +/// create a new state object to replace the current state instead of modifying +/// the existing one. +/// +/// McBopomofo's input controller has following possible states: +/// +/// - Deactivated: The user is not using McBopomofo yet. +/// - Empty: The user has switched to McBopomofo but did not input anything yet, +/// or, he or she has committed text into the client apps and starts a new +/// input phase. +/// - Committing: The input controller is sending text to the client apps. +/// - Inputting: The user has inputted something and the input buffer is +/// visible. +/// - Marking: The user is creating a area in the input buffer and about to +/// create a new user phrase. +/// - Choosing Candidate: The candidate window is open to let the user to choose +/// one among the candidates. +class InputState: NSObject { +} + +/// Represents that the input controller is deactivated. +class InputStateDeactivated: InputState { + override var description: String { + "" + } +} + +/// Represents that the composing buffer is empty. +class InputStateEmpty: InputState { + @objc var composingBuffer: String { + "" + } +} + +/// Represents that the composing buffer is empty. +class InputStateEmptyIgnoringPreviousState: InputState { + @objc var composingBuffer: String { + "" + } +} + +/// Represents that the input controller is committing text into client app. +class InputStateCommitting: InputState { + @objc private(set) var poppedText: String = "" + + @objc convenience init(poppedText: String) { + self.init() + self.poppedText = poppedText + } + + override var description: String { + "" + } +} + +/// Represents that the composing buffer is not empty. +class InputStateNotEmpty: InputState { + @objc private(set) var composingBuffer: String = "" + @objc private(set) var cursorIndex: UInt = 0 + + @objc init(composingBuffer: String, cursorIndex: UInt) { + self.composingBuffer = composingBuffer + self.cursorIndex = cursorIndex + } + + override var description: String { + "" + } +} + +/// Represents that the user is inputting text. +class InputStateInputting: InputStateNotEmpty { + @objc var bpmfReading: String = "" + @objc var bpmfReadingCursorIndex: UInt8 = 0 + @objc var poppedText: String = "" + + @objc override init(composingBuffer: String, cursorIndex: UInt) { + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + } + + @objc var attributedString: NSAttributedString { + let attributedSting = NSAttributedString(string: composingBuffer, attributes: [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 0 + ]) + return attributedSting + } + + override var description: String { + "" + } +} + +private let kMinMarkRangeLength = 2 +private let kMaxMarkRangeLength = 6 + +/// Represents that the user is marking a range in the composing buffer. +class InputStateMarking: InputStateNotEmpty { + @objc private(set) var markerIndex: UInt + @objc private(set) var markedRange: NSRange + @objc var tooltip: String { + + if Preferences.phraseReplacementEnabled { + return NSLocalizedString("Phrase replacement mode is on. Not suggested to add phrase in the mode.", comment: "") + } + if Preferences.chineseConversionStyle == 1 && Preferences.chineseConversionEnabled { + return NSLocalizedString("Model based Chinese conversion is on. Not suggested to add phrase in the mode.", comment: "") + } + if markedRange.length == 0 { + return "" + } + + let text = (composingBuffer as NSString).substring(with: markedRange) + if markedRange.length < kMinMarkRangeLength { + return String(format: NSLocalizedString("You are now selecting \"%@\". You can add a phrase with two or more characters.", comment: ""), text) + } else if (markedRange.length > kMaxMarkRangeLength) { + return String(format: NSLocalizedString("You are now selecting \"%@\". A phrase cannot be longer than %d characters.", comment: ""), text, kMaxMarkRangeLength) + } + return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text) + } + + @objc private(set) var readings: [String] = [] + + @objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) { + self.markerIndex = markerIndex + let begin = min(cursorIndex, markerIndex) + let end = max(cursorIndex, markerIndex) + markedRange = NSMakeRange(Int(begin), Int(end - begin)) + self.readings = readings + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + } + + @objc var attributedString: NSAttributedString { + let attributedSting = NSMutableAttributedString(string: composingBuffer) + let end = markedRange.location + markedRange.length + + attributedSting.setAttributes([ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 0 + ], range: NSRange(location: 0, length: markedRange.location)) + attributedSting.setAttributes([ + .underlineStyle: NSUnderlineStyle.thick.rawValue, + .markedClauseSegment: 1 + ], range: markedRange) + attributedSting.setAttributes([ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 2 + ], range: NSRange(location: end, + length: composingBuffer.count - end)) + return attributedSting + } + + override var description: String { + "" + } + + @objc func convertToInputting() -> InputStateInputting { + let state = InputStateInputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + return state + } + + @objc var validToWrite: Bool { + markedRange.length >= kMinMarkRangeLength && markedRange.length <= kMaxMarkRangeLength + } + + @objc var userPhrase: String { + let text = (composingBuffer as NSString).substring(with: markedRange) + let end = markedRange.location + markedRange.length + let readings = readings[markedRange.location.." + } +} diff --git a/Source/KeyHandler.h b/Source/KeyHandler.h new file mode 100644 index 00000000..ac531d6a --- /dev/null +++ b/Source/KeyHandler.h @@ -0,0 +1,64 @@ +// Copyright (c) 2011 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +#import +#import +@import CandidateUI; + +@class KeyHandlerInput; +@class InputState; +@class InputStateInputting; +@class InputStateMarking; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kBopomofoModeIdentifier; +extern NSString *const kPlainBopomofoModeIdentifier; + +@class KeyHandler; + +@protocol KeyHandlerDelegate +- (VTCandidateController *)candidateControllerForKeyHandler:(KeyHandler *)keyHandler; +- (void)keyHandler:(KeyHandler *)keyHandler didSelectCandidateAtIndex:(NSInteger)index candidateController:(VTCandidateController *)controller; +- (BOOL)keyHandler:(KeyHandler *)keyHandler didRequestWriteUserPhraseWithState:(InputStateMarking *)state; +@end + +@interface KeyHandler : NSObject + +- (BOOL)handleInput:(KeyHandlerInput *)input + state:(InputState *)state + stateCallback:(void (^)(InputState *))stateCallback +candidateSelectionCallback:(void (^)(void))candidateSelectionCallback + errorCallback:(void (^)(void))errorCallback; + +- (void)syncWithPreferences; +- (void)fixNodeWithValue:(std::string)value; +- (void)clear; + +- (InputStateInputting *)_buildInputtingState; + +@property (strong, nonatomic) NSString *inputMode; +@property (weak, nonatomic) id delegate; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm new file mode 100644 index 00000000..efe095ec --- /dev/null +++ b/Source/KeyHandler.mm @@ -0,0 +1,1183 @@ +// Copyright (c) 2011 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +#import "Mandarin.h" +#import "Gramambular.h" +#import "McBopomofoLM.h" +#import "UserOverrideModel.h" +#import "LanguageModelManager.h" +#import "OVUTF8Helper.h" +#import "KeyHandler.h" +#import "McBopomofo-Swift.h" + +// C++ namespace usages +using namespace std; +using namespace Formosa::Mandarin; +using namespace Formosa::Gramambular; +using namespace McBopomofo; +using namespace OpenVanilla; + +NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo"; +NSString *const kPlainBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.PlainBopomofo"; + +static const double kEpsilon = 0.000001; + +static double FindHighestScore(const vector &nodes, double epsilon) { + double highestScore = 0.0; + for (auto ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { + double score = ni->node->highestUnigramScore(); + if (score > highestScore) { + highestScore = score; + } + } + return highestScore + epsilon; +} + +// sort helper +class NodeAnchorDescendingSorter +{ +public: + bool operator()(const NodeAnchor &a, const NodeAnchor &b) const { + return a.node->key().length() > b.node->key().length(); + } +}; + +// if DEBUG is defined, a DOT file (GraphViz format) will be written to the +// specified path every time the grid is walked +#if DEBUG +static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot"; +#endif + + +@implementation KeyHandler +{ + // the reading buffer that takes user input + Formosa::Mandarin::BopomofoReadingBuffer *_bpmfReadingBuffer; + + // language model + McBopomofo::McBopomofoLM *_languageModel; + + // user override model + McBopomofo::UserOverrideModel *_userOverrideModel; + + // the grid (lattice) builder for the unigrams (and bigrams) + Formosa::Gramambular::BlockReadingBuilder *_builder; + + // latest walked path (trellis) using the Viterbi algorithm + std::vector _walkedNodes; + + NSString *_inputMode; +} + +//@synthesize inputMode = _inputMode; +@synthesize delegate = _delegate; + +- (NSString *)inputMode +{ + return _inputMode; +} + +- (void)setInputMode:(NSString *)value +{ + NSString *newInputMode; + McBopomofoLM *newLanguageModel; + + if ([value isKindOfClass:[NSString class]] && [value isEqual:kPlainBopomofoModeIdentifier]) { + newInputMode = kPlainBopomofoModeIdentifier; + newLanguageModel = [LanguageModelManager languageModelPlainBopomofo]; + newLanguageModel->setPhraseReplacementEnabled(false); + } else { + newInputMode = kBopomofoModeIdentifier; + newLanguageModel = [LanguageModelManager languageModelMcBopomofo]; + newLanguageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); + } + newLanguageModel->setExternalConverterEnabled(Preferences.chineseConversionStyle == 1); + + // Only apply the changes if the value is changed + if (![_inputMode isEqualToString:newInputMode]) { + _inputMode = newInputMode; + _languageModel = newLanguageModel; + + if (_builder) { + delete _builder; + _builder = new BlockReadingBuilder(_languageModel); + _builder->setJoinSeparator("-"); + } + + if (!_bpmfReadingBuffer->isEmpty()) { + _bpmfReadingBuffer->clear(); + } + } +} + +- (void)dealloc +{ + // clean up everything + if (_bpmfReadingBuffer) { + delete _bpmfReadingBuffer; + } + + if (_builder) { + delete _builder; + } +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _bpmfReadingBuffer = new BopomofoReadingBuffer(BopomofoKeyboardLayout::StandardLayout()); + + // create the lattice builder + _languageModel = [LanguageModelManager languageModelMcBopomofo]; + _languageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); + _userOverrideModel = [LanguageModelManager userOverrideModel]; + + _builder = new BlockReadingBuilder(_languageModel); + + // each Mandarin syllable is separated by a hyphen + _builder->setJoinSeparator("-"); + _inputMode = kBopomofoModeIdentifier; + } + return self; +} + +- (void)syncWithPreferences +{ + NSInteger layout = Preferences.keyboardLayout; + switch (layout) { + case KeyboardLayoutStandard: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); + break; + case KeyboardLayoutEten: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETenLayout()); + break; + case KeyboardLayoutHsu: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HsuLayout()); + break; + case KeyboardLayoutEten26: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETen26Layout()); + break; + case KeyboardLayoutHanyuPinyin: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HanyuPinyinLayout()); + break; + case KeyboardLayoutIBM: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::IBMLayout()); + break; + default: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); + Preferences.keyboardLayout = KeyboardLayoutStandard; + } + _languageModel->setExternalConverterEnabled(Preferences.chineseConversionStyle == 1); +} + +- (void)fixNodeWithValue:(std::string)value +{ + size_t cursorIndex = [self _actualCandidateCursorIndex]; + _builder->grid().fixNodeSelectedCandidate(cursorIndex, value); + if (_inputMode != kPlainBopomofoModeIdentifier) { + _userOverrideModel->observe(_walkedNodes, cursorIndex, value, [[NSDate date] timeIntervalSince1970]); + } + [self _walk]; +} + +- (void)clear +{ + _bpmfReadingBuffer->clear(); + _builder->clear(); + _walkedNodes.clear(); +} + +- (string)_currentLayout +{ + NSString *keyboardLayoutName = Preferences.keyboardLayoutName; + string layout = string(keyboardLayoutName.UTF8String) + string("_"); + return layout; +} + +- (BOOL)handleInput:(KeyHandlerInput *)input state:(InputState *)inState stateCallback:(void (^)(InputState *))stateCallback candidateSelectionCallback:(void (^)(void))candidateSelectionCallback errorCallback:(void (^)(void))errorCallback +{ + InputState *state = inState; + UniChar charCode = input.charCode; + McBopomofoEmacsKey emacsKey = input.emacsKey; + + // if the inputText is empty, it's a function key combination, we ignore it + if (![input.inputText length]) { + return NO; + } + + // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it + BOOL isFunctionKey = ([input isCommandHold] || [input isControlHold] || [input isOptionHold] || [input isNumericPad]); + if (![state isKindOfClass:[InputStateNotEmpty class]] && isFunctionKey) { + return NO; + } + + // Caps Lock processing : if Caps Lock is on, temporarily disable bopomofo. + if (charCode == 8 || charCode == 13 || [input isAbsorbedArrowKey] || [input isExtraChooseCandidateKey] || [input isCursorForward] || [input isCursorBackward]) { + // do nothing if backspace is pressed -- we ignore the key + } else if ([input isCapsLockOn]) { + // process all possible combination, we hope. + [self clear]; + InputStateEmpty *emptyState = [[InputStateEmpty alloc] init]; + stateCallback(emptyState); + + // first commit everything in the buffer. + if ([input isShiftHold]) { + return NO; + } + + // if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char insertions. + if (charCode < 0x80 && !isprint(charCode)) { + return NO; + } + + // when shift is pressed, don't do further processing, since it outputs capital letter anyway. + InputStateCommitting *committingState = [[InputStateCommitting alloc] initWithPoppedText:[input.inputText lowercaseString]]; + stateCallback(committingState); + stateCallback(emptyState); + return YES; + } + + if ([input isNumericPad]) { + if (![input isLeft] && ![input isRight] && ![input isDown] && ![input isUp] && charCode != 32 && isprint(charCode)) { + [self clear]; + InputStateEmpty *emptyState = [[InputStateEmpty alloc] init]; + stateCallback(emptyState); + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:[input.inputText lowercaseString]]; + stateCallback(committing); + stateCallback(emptyState); + return YES; + } + } + + // MARK: Handle Candidates + if ([state isKindOfClass:[InputStateChoosingCandidate class]]) { + return [self _handleCandidateState:(InputStateChoosingCandidate *) state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]; + } + + // MARK: Handle Marking + if ([state isKindOfClass:[InputStateMarking class]]) { + InputStateMarking *marking = (InputStateMarking *) state; + if ([self _handleMarkingState:(InputStateMarking *) state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]) { + return YES; + } + state = [marking convertToInputting]; + stateCallback(state); + } + + bool composeReading = false; + + // MARK: Handle BPMF Keys + // see if it's valid BPMF reading + if (_bpmfReadingBuffer->isValidKey((char) charCode)) { + _bpmfReadingBuffer->combineKey((char) charCode); + + // if we have a tone marker, we have to insert the reading to the + // builder in other words, if we don't have a tone marker, we just + // update the composing buffer + composeReading = _bpmfReadingBuffer->hasToneMarker(); + if (!composeReading) { + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + return YES; + } + } + + // see if we have composition if Enter/Space is hit and buffer is not empty + // this is bit-OR'ed so that the tone marker key is also taken into account + composeReading |= (!_bpmfReadingBuffer->isEmpty() && (charCode == 32 || charCode == 13)); + if (composeReading) { + // combine the reading + string reading = _bpmfReadingBuffer->syllable().composedString(); + + // see if we have a unigram for this + if (!_languageModel->hasUnigramsForKey(reading)) { + errorCallback(); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + return YES; + } + + // and insert it into the lattice + _builder->insertReadingAtCursor(reading); + + // then walk the lattice + NSString *poppedText = [self _popOverflowComposingTextAndWalk]; + + // get user override model suggestion + string overrideValue = (_inputMode == kPlainBopomofoModeIdentifier) ? "" : + _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(), [[NSDate date] timeIntervalSince1970]); + + if (!overrideValue.empty()) { + size_t cursorIndex = [self _actualCandidateCursorIndex]; + vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); + double highestScore = FindHighestScore(nodes, kEpsilon); + _builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue, highestScore); + } + + // then update the text + _bpmfReadingBuffer->clear(); + + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + + if (_inputMode == kPlainBopomofoModeIdentifier) { + InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode]; + if (choosingCandidates.candidates.count == 1) { + [self clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:choosingCandidates.candidates.firstObject]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else { + stateCallback(choosingCandidates); + } + } + + // and tells the client that the key is consumed + return YES; + } + + // MARK: Space and Down + // keyCode 125 = Down, charCode 32 = Space + if (_bpmfReadingBuffer->isEmpty() && + [state isKindOfClass:[InputStateNotEmpty class]] && + ([input isExtraChooseCandidateKey] || charCode == 32 || (input.useVerticalMode && ([input isVerticalModeOnlyChooseCandidateKey])))) { + if (charCode == 32) { + // if the spacebar is NOT set to be a selection key + if ([input isShiftHold] || !Preferences.chooseCandidateUsingSpace) { + if (_builder->cursorIndex() >= _builder->length()) { + [self clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:@" "]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else if (_languageModel->hasUnigramsForKey(" ")) { + _builder->insertReadingAtCursor(" "); + NSString *poppedText = [self _popOverflowComposingTextAndWalk]; + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + } + return YES; + + } + } + InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:(InputStateNotEmpty *) state useVerticalMode:input.useVerticalMode]; + stateCallback(choosingCandidates); + return YES; + } + + // MARK: Esc + if (charCode == 27) { + return [self _handleEscWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Cursor backward + if ([input isCursorBackward] || emacsKey == McBopomofoEmacsKeyBackward) { + return [self _handleBackwardWithState:state input:input stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Cursor forward + if ([input isCursorForward] || emacsKey == McBopomofoEmacsKeyForward) { + return [self _handleForwardWithState:state input:input stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Home + if ([input isHome] || emacsKey == McBopomofoEmacsKeyHome) { + return [self _handleHomeWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: End + if ([input isEnd] || emacsKey == McBopomofoEmacsKeyEnd) { + return [self _handleEndWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: AbsorbedArrowKey + if ([input isAbsorbedArrowKey] || [input isExtraChooseCandidateKey]) { + return [self _handleAbsorbedArrowKeyWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Backspace + if (charCode == 8) { + return [self _handleBackspaceWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Delete + if ([input isDelete] || emacsKey == McBopomofoEmacsKeyDelete) { + return [self _handleDeleteWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Enter + if (charCode == 13) { + return [self _handleEnterWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Punctuation list + if ((char) charCode == '`') { + if (_languageModel->hasUnigramsForKey(string("_punctuation_list"))) { + if (_bpmfReadingBuffer->isEmpty()) { + _builder->insertReadingAtCursor(string("_punctuation_list")); + NSString *poppedText = [self _popOverflowComposingTextAndWalk]; + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + InputStateChoosingCandidate *choosingCandidate = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode]; + stateCallback(choosingCandidate); + } else { // If there is still unfinished bpmf reading, ignore the punctuation + errorCallback(); + } + return YES; + } + } + + // MARK: Punctuation + // if nothing is matched, see if it's a punctuation key for current layout. + string layout = [self _currentLayout]; + string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_"); + string customPunctuation = punctuationNamePrefix + layout + string(1, (char) charCode); + if ([self _handlePunctuation:customPunctuation state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { + return YES; + } + + // if nothing is matched, see if it's a punctuation key. + string punctuation = punctuationNamePrefix + string(1, (char) charCode); + if ([self _handlePunctuation:punctuation state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { + return YES; + } + + if ((char) charCode >= 'A' && (char) charCode <= 'Z') { + string letter = string("_letter_") + string(1, (char) charCode); + if ([self _handlePunctuation:letter state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { + return YES; + } + } + + // still nothing, then we update the composing buffer (some app has + // strange behavior if we don't do this, "thinking" the key is not + // actually consumed) + if ([state isKindOfClass:[InputStateNotEmpty class]] || !_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + return NO; +} + +- (BOOL)_handleEscWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + BOOL escToClearInputBufferEnabled = Preferences.escToCleanInputBuffer; + + if (escToClearInputBufferEnabled) { + // if the option is enabled, we clear everything including the composing + // buffer, walked nodes and the reading. + [self clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else { + // if reading is not empty, we cancel the reading; Apple's built-in + // Zhuyin (and the erstwhile Hanin) has a default option that Esc + // "cancels" the current composed character and revert it to + // Bopomofo reading, in odds with the expectation of users from + // other platforms + + if (_bpmfReadingBuffer->isEmpty()) { + // no nee to beep since the event is deliberately triggered by user + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + } else { + _bpmfReadingBuffer->clear(); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } + } + return YES; +} + +- (BOOL)_handleBackwardWithState:(InputState *)state input:(KeyHandlerInput *)input stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + InputStateInputting *currentState = (InputStateInputting *) state; + + if ([input isShiftHold]) { + // Shift + left + if (_builder->cursorIndex() > 0) { + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex - 1 readings: [self _currentReadings]]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + } else { + if (_builder->cursorIndex() > 0) { + _builder->setCursorIndex(_builder->cursorIndex() - 1); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + } + return YES; +} + +- (BOOL)_handleForwardWithState:(InputState *)state input:(KeyHandlerInput *)input stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + InputStateInputting *currentState = (InputStateInputting *) state; + + if ([input isShiftHold]) { + // Shift + Right + if (_builder->cursorIndex() < _builder->length()) { + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex + 1 readings: [self _currentReadings]]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + } else { + if (_builder->cursorIndex() < _builder->length()) { + _builder->setCursorIndex(_builder->cursorIndex() + 1); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + } + + return YES; +} + +- (BOOL)_handleHomeWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex()) { + _builder->setCursorIndex(0); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + + return YES; +} + +- (BOOL)_handleEndWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex() != _builder->length()) { + _builder->setCursorIndex(_builder->length()); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + + return YES; +} + +- (BOOL)_handleAbsorbedArrowKeyWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + } + stateCallback(state); + return YES; +} + +- (BOOL)_handleBackspaceWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (_bpmfReadingBuffer->isEmpty()) { + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex()) { + _builder->deleteReadingBeforeCursor(); + [self _walk]; + } else { + errorCallback(); + stateCallback(state); + return YES; + } + } else { + _bpmfReadingBuffer->backspace(); + } + + InputStateInputting *inputting = [self _buildInputtingState]; + if (!inputting.composingBuffer.length) { + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + } else { + stateCallback(inputting); + } + return YES; +} + +- (BOOL)_handleDeleteWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (_bpmfReadingBuffer->isEmpty()) { + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex() != _builder->length()) { + _builder->deleteReadingAfterCursor(); + [self _walk]; + InputStateInputting *inputting = [self _buildInputtingState]; + if (!inputting.composingBuffer.length) { + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + } else { + stateCallback(inputting); + } + } else { + errorCallback(); + stateCallback(state); + } + } else { + errorCallback(); + stateCallback(state); + } + + return YES; +} + +- (BOOL)_handleEnterWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if ([state isKindOfClass:[InputStateInputting class]]) { + if (_inputMode == kPlainBopomofoModeIdentifier) { + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + } + return YES; + } + + [self clear]; + + InputStateInputting *current = (InputStateInputting *) state; + NSString *composingBuffer = current.composingBuffer; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:composingBuffer]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + return YES; + } + + return NO; +} + +- (BOOL)_handlePunctuation:(string)customPunctuation state:(InputState *)state usingVerticalMode:(BOOL)useVerticalMode stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_languageModel->hasUnigramsForKey(customPunctuation)) { + return NO; + } + + NSString *poppedText; + if (_bpmfReadingBuffer->isEmpty()) { + _builder->insertReadingAtCursor(customPunctuation); + poppedText = [self _popOverflowComposingTextAndWalk]; + } else { // If there is still unfinished bpmf reading, ignore the punctuation + errorCallback(); + stateCallback(state); + return YES; + } + + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + + if (_inputMode == kPlainBopomofoModeIdentifier && _bpmfReadingBuffer->isEmpty()) { + InputStateChoosingCandidate *candidateState = [self _buildCandidateState:inputting useVerticalMode:useVerticalMode]; + + if ([candidateState.candidates count] == 1) { + [self clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:candidateState.candidates.firstObject]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else { + stateCallback(candidateState); + } + } + return YES; +} + + +- (BOOL)_handleMarkingState:(InputStateMarking *)state + input:(KeyHandlerInput *)input + stateCallback:(void (^)(InputState *))stateCallback + candidateSelectionCallback:(void (^)(void))candidateSelectionCallback + errorCallback:(void (^)(void))errorCallback +{ + UniChar charCode = input.charCode; + + if (charCode == 27) { + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + return YES; + } + + // Enter + if (charCode == 13) { + if (![self.delegate keyHandler:self didRequestWriteUserPhraseWithState:state]) { + errorCallback(); + return YES; + } + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + return YES; + } + + // Shift + left + if (([input isCursorBackward] || input.emacsKey == McBopomofoEmacsKeyBackward) + && ([input isShiftHold])) { + NSUInteger index = state.markerIndex; + if (index > 0) { + index -= 1; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + return YES; + } + + // Shift + Right + if (([input isCursorForward] || input.emacsKey == McBopomofoEmacsKeyForward) + && ([input isShiftHold])) { + NSUInteger index = state.markerIndex; + if (index < state.composingBuffer.length) { + index += 1; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + return YES; + } + return NO; +} + + +- (BOOL)_handleCandidateState:(InputStateChoosingCandidate *)state + input:(KeyHandlerInput *)input + stateCallback:(void (^)(InputState *))stateCallback + candidateSelectionCallback:(void (^)(void))candidateSelectionCallback + errorCallback:(void (^)(void))errorCallback; +{ + NSString *inputText = input.inputText; + UniChar charCode = input.charCode; + VTCandidateController *gCurrentCandidateController = [self.delegate candidateControllerForKeyHandler:self]; + + BOOL cancelCandidateKey = (charCode == 27) || (charCode == 8) || [input isDelete]; + + if (cancelCandidateKey) { + if (_inputMode == kPlainBopomofoModeIdentifier) { + [self clear]; + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + } else { + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } + return YES; + } + + if (charCode == 13 || [input isEnter]) { + [self.delegate keyHandler:self didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex candidateController:gCurrentCandidateController]; + return YES; + } + + if (charCode == 32 || [input isPageDown] || input.emacsKey == McBopomofoEmacsKeyNextPage) { + BOOL updated = [gCurrentCandidateController showNextPage]; + if (!updated) { + errorCallback(); + } + candidateSelectionCallback(); + return YES; + } + + if ([input isPageUp]) { + BOOL updated = [gCurrentCandidateController showPreviousPage]; + if (!updated) { + errorCallback(); + } + candidateSelectionCallback(); + return YES; + } + + if ([input isLeft]) { + if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { + BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; + if (!updated) { + errorCallback(); + } + } else { + BOOL updated = [gCurrentCandidateController showPreviousPage]; + if (!updated) { + errorCallback(); + } + } + candidateSelectionCallback(); + return YES; + } + + if (input.emacsKey == McBopomofoEmacsKeyBackward) { + BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; + if (!updated) { + errorCallback(); + } + candidateSelectionCallback(); + return YES; + } + + if ([input isRight]) { + if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { + BOOL updated = [gCurrentCandidateController highlightNextCandidate]; + if (!updated) { + errorCallback(); + } + } else { + BOOL updated = [gCurrentCandidateController showNextPage]; + if (!updated) { + errorCallback(); + } + } + candidateSelectionCallback(); + return YES; + } + + if (input.emacsKey == McBopomofoEmacsKeyForward) { + BOOL updated = [gCurrentCandidateController highlightNextCandidate]; + if (!updated) { + errorCallback(); + } + candidateSelectionCallback(); + return YES; + } + + if ([input isUp]) { + if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { + BOOL updated = [gCurrentCandidateController showPreviousPage]; + if (!updated) { + errorCallback(); + } + } else { + BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; + if (!updated) { + errorCallback(); + } + } + candidateSelectionCallback(); + return YES; + } + + if ([input isDown]) { + if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { + BOOL updated = [gCurrentCandidateController showNextPage]; + if (!updated) { + errorCallback(); + } + } else { + BOOL updated = [gCurrentCandidateController highlightNextCandidate]; + if (!updated) { + errorCallback(); + } + } + candidateSelectionCallback(); + return YES; + } + + if ([input isHome] || input.emacsKey == McBopomofoEmacsKeyHome) { + if (gCurrentCandidateController.selectedCandidateIndex == 0) { + errorCallback(); + } else { + gCurrentCandidateController.selectedCandidateIndex = 0; + } + + candidateSelectionCallback(); + return YES; + } + + if (([input isEnd] || input.emacsKey == McBopomofoEmacsKeyEnd) && [state.candidates count] > 0) { + if (gCurrentCandidateController.selectedCandidateIndex == [state.candidates count] - 1) { + errorCallback(); + } else { + gCurrentCandidateController.selectedCandidateIndex = [state.candidates count] - 1; + } + + candidateSelectionCallback(); + return YES; + } + + NSInteger index = NSNotFound; + for (NSUInteger j = 0, c = [gCurrentCandidateController.keyLabels count]; j < c; j++) { + if ([inputText compare:[gCurrentCandidateController.keyLabels objectAtIndex:j] options:NSCaseInsensitiveSearch] == NSOrderedSame) { + index = j; + break; + } + } + + [gCurrentCandidateController.keyLabels indexOfObject:inputText]; + + if (index != NSNotFound) { + NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index]; + if (candidateIndex != NSUIntegerMax) { + [self.delegate keyHandler:self didSelectCandidateAtIndex:candidateIndex candidateController:gCurrentCandidateController]; + return YES; + } + } + + if (_inputMode == kPlainBopomofoModeIdentifier) { + string layout = [self _currentLayout]; + string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_"); + string customPunctuation = punctuationNamePrefix + layout + string(1, (char) charCode); + string punctuation = punctuationNamePrefix + string(1, (char) charCode); + + BOOL shouldAutoSelectCandidate = _bpmfReadingBuffer->isValidKey((char) charCode) || _languageModel->hasUnigramsForKey(customPunctuation) || + _languageModel->hasUnigramsForKey(punctuation); + + if (!shouldAutoSelectCandidate && (char) charCode >= 'A' && (char) charCode <= 'Z') { + string letter = string("_letter_") + string(1, (char) charCode); + if (_languageModel->hasUnigramsForKey(letter)) { + shouldAutoSelectCandidate = YES; + } + } + + if (shouldAutoSelectCandidate) { + NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:0]; + if (candidateIndex != NSUIntegerMax) { + [self.delegate keyHandler:self didSelectCandidateAtIndex:candidateIndex candidateController:gCurrentCandidateController]; + [self clear]; + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + [self handleInput:input state:empty stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]; + } + return YES; + } + } + + errorCallback(); + candidateSelectionCallback(); + return YES; +} + +#pragma mark - States Building + +- (InputStateInputting *)_buildInputtingState +{ + // "updating the composing buffer" means to request the client to "refresh" the text input buffer + // with our "composing text" + NSMutableString *composingBuffer = [[NSMutableString alloc] init]; + NSInteger composedStringCursorIndex = 0; + + size_t readingCursorIndex = 0; + size_t builderCursorIndex = _builder->cursorIndex(); + + // we must do some Unicode codepoint counting to find the actual cursor location for the client + // i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars + // locations + for (vector::iterator wi = _walkedNodes.begin(), we = _walkedNodes.end(); wi != we; ++wi) { + if ((*wi).node) { + string nodeStr = (*wi).node->currentKeyValue().value; + vector codepoints = OVUTF8Helper::SplitStringByCodePoint(nodeStr); + size_t codepointCount = codepoints.size(); + + NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()]; + [composingBuffer appendString:valueString]; + + // this re-aligns the cursor index in the composed string + // (the actual cursor on the screen) with the builder's logical + // cursor (reading) cursor; each built node has a "spanning length" + // (e.g. two reading blocks has a spanning length of 2), and we + // accumulate those lengths to calculate the displayed cursor + // index + size_t spanningLength = (*wi).spanningLength; + if (readingCursorIndex + spanningLength <= builderCursorIndex) { + composedStringCursorIndex += [valueString length]; + readingCursorIndex += spanningLength; + } else { + for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++) { + composedStringCursorIndex += [[NSString stringWithUTF8String:codepoints[i].c_str()] length]; + readingCursorIndex++; + } + } + } + } + + // now we gather all the info, we separate the composing buffer to two parts, head and tail, + // and insert the reading text (the Mandarin syllable) in between them; + // the reading text is what the user is typing + NSString *head = [composingBuffer substringToIndex:composedStringCursorIndex]; + NSString *reading = [NSString stringWithUTF8String:_bpmfReadingBuffer->composedString().c_str()]; + NSString *tail = [composingBuffer substringFromIndex:composedStringCursorIndex]; + NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]]; + NSInteger cursorIndex = composedStringCursorIndex + [reading length]; + + InputStateInputting *newState = [[InputStateInputting alloc] initWithComposingBuffer:composedText cursorIndex:cursorIndex]; + return newState; +} + +- (void)_walk +{ + // retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation + // of the best possible Mandarain characters given the input syllables, + // using the Viterbi algorithm implemented in the Gramambular library + Walker walker(&_builder->grid()); + + // the reverse walk traces the trellis from the end + _walkedNodes = walker.reverseWalk(_builder->grid().width()); + + // then we reverse the nodes so that we get the forward-walked nodes + reverse(_walkedNodes.begin(), _walkedNodes.end()); + + // if DEBUG is defined, a GraphViz file is written to kGraphVizOutputfile +#if DEBUG + string dotDump = _builder->grid().dumpDOT(); + NSString *dotStr = [NSString stringWithUTF8String:dotDump.c_str()]; + NSError *error = nil; + + BOOL __unused success = [dotStr writeToFile:kGraphVizOutputfile atomically:YES encoding:NSUTF8StringEncoding error:&error]; +#endif +} + +- (NSString *)_popOverflowComposingTextAndWalk +{ + // in an ideal world, we can as well let the user type forever, + // but because the Viterbi algorithm has a complexity of O(N^2), + // the walk will become slower as the number of nodes increase, + // therefore we need to "pop out" overflown text -- they usually + // lose their influence over the whole MLE anyway -- so that when + // the user type along, the already composed text at front will + // be popped out + + NSString *poppedText = @""; + NSInteger composingBufferSize = Preferences.composingBufferSize; + + if (_builder->grid().width() > (size_t) composingBufferSize) { + if (_walkedNodes.size() > 0) { + NodeAnchor &anchor = _walkedNodes[0]; + poppedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()]; + _builder->removeHeadReadings(anchor.spanningLength); + } + } + + [self _walk]; + return poppedText; +} + +- (InputStateChoosingCandidate *)_buildCandidateState:(InputStateNotEmpty *)currentState useVerticalMode:(BOOL)useVerticalMode +{ + NSMutableArray *candidatesArray = [[NSMutableArray alloc] init]; + + size_t cursorIndex = [self _actualCandidateCursorIndex]; + vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); + + // sort the nodes, so that longer nodes (representing longer phrases) are placed at the top of the candidate list + stable_sort(nodes.begin(), nodes.end(), NodeAnchorDescendingSorter()); + + // then use the C++ trick to retrieve the candidates for each node at/crossing the cursor + for (vector::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { + const vector &candidates = (*ni).node->candidates(); + for (vector::const_iterator ci = candidates.begin(), ce = candidates.end(); ci != ce; ++ci) { + [candidatesArray addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]]; + } + } + + InputStateChoosingCandidate *state = [[InputStateChoosingCandidate alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex candidates:candidatesArray useVerticalMode:useVerticalMode]; + return state; +} + +- (size_t)_actualCandidateCursorIndex +{ + size_t cursorIndex = _builder->cursorIndex(); + if (Preferences.selectPhraseAfterCursorAsCandidate) { + // MS Phonetics IME style, phrase is *after* the cursor, i.e. cursor is always *before* the phrase + if (cursorIndex < _builder->length()) { + ++cursorIndex; + } + } else { + if (!cursorIndex) { + ++cursorIndex; + } + } + + return cursorIndex; +} + +- (NSArray *)_currentReadings +{ + NSMutableArray *readingsArray = [[NSMutableArray alloc] init]; + vector v = _builder->readings(); + for (vector::iterator it_i = v.begin(); it_i != v.end(); ++it_i) { + [readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]]; + } + return readingsArray; +} + +@end diff --git a/Source/KeyHandlerInput.swift b/Source/KeyHandlerInput.swift new file mode 100644 index 00000000..e0c3ea61 --- /dev/null +++ b/Source/KeyHandlerInput.swift @@ -0,0 +1,193 @@ +// Copyright (c) 2022 and onwards The McBopomofo Authors. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +import Cocoa + +enum KeyCode: UInt16 { + case none = 0 + case enter = 76 + case up = 126 + case down = 125 + case left = 123 + case right = 124 + case pageUp = 116 + case pageDown = 121 + case home = 115 + case end = 119 + case delete = 117 +} + +class KeyHandlerInput: NSObject { + @objc private (set) var useVerticalMode: Bool + @objc private (set) var inputText: String? + @objc private (set) var charCode: UInt16 + private var keyCode: UInt16 + private var flags: NSEvent.ModifierFlags + private var cursorForwardKey: KeyCode + private var cursorBackwardKey: KeyCode + private var extraChooseCandidateKey: KeyCode + private var absorbedArrowKey: KeyCode + private var verticalModeOnlyChooseCandidateKey: KeyCode + @objc private (set) var emacsKey: McBopomofoEmacsKey + + @objc init(inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, isVerticalMode: Bool) { + self.inputText = inputText + self.keyCode = keyCode + self.charCode = charCode + self.flags = flags + useVerticalMode = isVerticalMode + emacsKey = EmacsKeyHelper.detect(charCode: charCode, flags: flags) + cursorForwardKey = useVerticalMode ? .down : .right + cursorBackwardKey = useVerticalMode ? .up : .left + extraChooseCandidateKey = useVerticalMode ? .left : .down + absorbedArrowKey = useVerticalMode ? .right : .up + verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none + super.init() + } + + @objc init(event: NSEvent, isVerticalMode: Bool) { + inputText = event.characters + keyCode = event.keyCode + flags = event.modifierFlags + useVerticalMode = isVerticalMode + let charCode: UInt16 = { + guard let inputText = event.characters, inputText.count > 0 else { + return 0 + } + let first = inputText[inputText.startIndex].utf16.first! + return first + }() + self.charCode = charCode + emacsKey = EmacsKeyHelper.detect(charCode: charCode, flags: event.modifierFlags) + cursorForwardKey = useVerticalMode ? .down : .right + cursorBackwardKey = useVerticalMode ? .up : .left + extraChooseCandidateKey = useVerticalMode ? .left : .down + absorbedArrowKey = useVerticalMode ? .right : .up + verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none + super.init() + } + + @objc var isShiftHold: Bool { + flags.contains([.shift]) + } + + @objc var isCommandHold: Bool { + flags.contains([.command]) + } + + @objc var isControlHold: Bool { + flags.contains([.control]) + } + + @objc var isOptionHold: Bool { + flags.contains([.option]) + } + + @objc var isCapsLockOn: Bool { + flags.contains([.capsLock]) + } + + @objc var isNumericPad: Bool { + flags.contains([.numericPad]) + } + + @objc var isEnter: Bool { + KeyCode(rawValue: keyCode) == KeyCode.enter + } + + @objc var isUp: Bool { + KeyCode(rawValue: keyCode) == KeyCode.up + } + + @objc var isDown: Bool { + KeyCode(rawValue: keyCode) == KeyCode.down + } + + @objc var isLeft: Bool { + KeyCode(rawValue: keyCode) == KeyCode.left + } + + @objc var isRight: Bool { + KeyCode(rawValue: keyCode) == KeyCode.right + } + + @objc var isPageUp: Bool { + KeyCode(rawValue: keyCode) == KeyCode.pageUp + } + + @objc var isPageDown: Bool { + KeyCode(rawValue: keyCode) == KeyCode.pageDown + } + + @objc var isHome: Bool { + KeyCode(rawValue: keyCode) == KeyCode.home + } + + @objc var isEnd: Bool { + KeyCode(rawValue: keyCode) == KeyCode.end + } + + @objc var isDelete: Bool { + KeyCode(rawValue: keyCode) == KeyCode.delete + } + + @objc var isCursorBackward: Bool { + KeyCode(rawValue: keyCode) == cursorBackwardKey + } + + @objc var isCursorForward: Bool { + KeyCode(rawValue: keyCode) == cursorForwardKey + } + + @objc var isAbsorbedArrowKey: Bool { + KeyCode(rawValue: keyCode) == absorbedArrowKey + } + + @objc var isExtraChooseCandidateKey: Bool { + KeyCode(rawValue: keyCode) == extraChooseCandidateKey + } + + @objc var isVerticalModeOnlyChooseCandidateKey: Bool { + KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey + } + +} + +@objc enum McBopomofoEmacsKey: UInt16 { + case none = 0 + case forward = 6 // F + case backward = 2 // B + case home = 1 // A + case end = 5 // E + case delete = 4 // D + case nextPage = 22 // V +} + +class EmacsKeyHelper: NSObject { + @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> McBopomofoEmacsKey { + if flags.contains(.control) { + return McBopomofoEmacsKey(rawValue: charCode) ?? .none + } + return .none; + } +} diff --git a/Source/LanguageModelManager.mm b/Source/LanguageModelManager.mm index 06940d9d..632617da 100644 --- a/Source/LanguageModelManager.mm +++ b/Source/LanguageModelManager.mm @@ -22,20 +22,13 @@ // OTHER DEALINGS IN THE SOFTWARE. #import "LanguageModelManager.h" -#import -#import -#import -#import "OVStringHelper.h" -#import "OVUTF8Helper.h" #import "McBopomofo-Swift.h" @import VXHanConvert; @import OpenCCBridge; using namespace std; -using namespace Formosa::Gramambular; using namespace McBopomofo; -using namespace OpenVanilla; static const int kUserOverrideModelCapacity = 500; static const double kObservedOverrideHalflife = 5400.0; // 1.5 hr. @@ -174,7 +167,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo return NO; } - BOOL shuoldAddLineBreakAtFront = NO; + BOOL addLineBreakAtFront = NO; NSString *path = [self userPhrasesDataPathMcBopomofo]; if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { @@ -188,7 +181,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo NSData *data = [readFile readDataToEndOfFile]; const void *bytes = [data bytes]; if (*(char *)bytes != '\n') { - shuoldAddLineBreakAtFront = YES; + addLineBreakAtFront = YES; } [readFile closeFile]; } @@ -196,7 +189,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo } NSMutableString *currentMarkedPhrase = [NSMutableString string]; - if (shuoldAddLineBreakAtFront) { + if (addLineBreakAtFront) { [currentMarkedPhrase appendString:@"\n"]; } [currentMarkedPhrase appendString:userPhrase]; @@ -218,7 +211,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo + (NSString *)dataFolderPath { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDirectory, YES); - NSString *appSupportPath = [paths objectAtIndex:0]; + NSString *appSupportPath = paths[0]; NSString *userDictPath = [appSupportPath stringByAppendingPathComponent:@"McBopomofo"]; return userDictPath; } diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index ba78261c..3bf5af15 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -69,7 +69,7 @@ "You are now selecting \"%@\". Press enter to add a new phrase." = "You are now selecting \"%@\". Press enter to add a new phrase."; -"You are now selecting \"%@\". A phrase cannot be longer than 6 characters." = "You are now selecting \"%@\". A phrase cannot be longer than 6 characters."; +"You are now selecting \"%@\". A phrase cannot be longer than %d characters." = "You are now selecting \"%@\". A phrase cannot be longer than %d characters."; "Chinese conversion on" = "Chinese conversion on"; @@ -94,3 +94,7 @@ "Phrase replacement mode is on. Not suggested to add phrase in the mode." = "Phrase replacement mode is on. Not suggested to add phrase in the mode."; "Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "Model based Chinese conversion is on. Not suggested to add phrase in the mode."; + +"Half-width punctuation on" = "Half-width punctuation on"; + +"Half-width punctuation off" = "Half-width punctuation off"; diff --git a/Source/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index b7c711a0..97da1229 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -69,7 +69,7 @@ "You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了 \"%@\"。按下 Enter 就可以加入到使用者詞彙中。"; -"You are now selecting \"%@\". A phrase cannot be longer than 6 characters." = "您目前選擇了 \"%@\"。自訂詞彙不能超過六個字元。"; +"You are now selecting \"%@\". A phrase cannot be longer than %d characters." = "您目前選擇了 \"%@\"。自訂詞彙不能超過 %d 個字元。"; "Chinese conversion on" = "已經切換到簡體中文模式"; @@ -94,3 +94,7 @@ "Phrase replacement mode is on. Not suggested to add phrase in the mode." = "詞彙轉換已開啟,不建議在此模式下加詞。"; "Model based Chinese conversion is on. Not suggested to add phrase in the mode." = "您已開啟將語言模型轉為簡體中文,不建議在此模式下加詞。"; + +"Half-width punctuation on" = "已經切換到半型標點模式"; + +"Half-width punctuation off" = "已經切回到全型標點模式";