From 0bc9468ba2c6db60d24a1b855fd0f15e9c71e184 Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 28 Jan 2022 15:02:00 +0800 Subject: [PATCH] Splits Input Method controller into two classes. --- McBopomofo.xcodeproj/project.pbxproj | 8 +- Source/InputMethodController.h | 28 +- Source/InputMethodController.mm | 1161 ++------------------------ Source/InputState.swift | 19 + Source/KeyHandler.h | 59 ++ Source/KeyHandler.mm | 1154 +++++++++++++++++++++++++ Source/LanguageModelManager.mm | 6 +- 7 files changed, 1308 insertions(+), 1127 deletions(-) create mode 100644 Source/KeyHandler.h create mode 100644 Source/KeyHandler.mm diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 22e2e58f..e6b48956 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 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 */; }; 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 */; }; @@ -214,6 +215,8 @@ 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 = ""; }; 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 = ""; }; @@ -301,8 +304,10 @@ 6A0D4EC715FC0D6400ABF4B3 /* InputMethodController.mm */, D41355D6278D7409005E5CBD /* LanguageModelManager.h */, D41355D7278D7409005E5CBD /* LanguageModelManager.mm */, - D461B791279DAC010070E734 /* InputState.swift */, + D4E569DA27A34CC100AC2CEF /* KeyHandler.h */, + D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */, D456576D279E4F7B00DF6BC9 /* KeyHandlerInput.swift */, + D461B791279DAC010070E734 /* InputState.swift */, D427F76B278CA1BA004A2160 /* AppDelegate.swift */, D44FB74427915555003C80A6 /* Preferences.swift */, D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, @@ -703,6 +708,7 @@ D47B92C027972AD100458394 /* main.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 */, diff --git a/Source/InputMethodController.h b/Source/InputMethodController.h index f4fc1130..11055f70 100644 --- a/Source/InputMethodController.h +++ b/Source/InputMethodController.h @@ -23,35 +23,9 @@ #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; -} - -- (BOOL)handleInput:(KeyHandlerInput *)input - state:(InputState *)state - stateCallback:(void (^)(InputState *))stateCallback -candidateSelectionCallback:(void (^)(void))candidateSelectionCallback - errorCallback:(void (^)(void))errorCallback; +@interface McBopomofoInputMethodController : IMKInputController - (void)handleState:(InputState *)newState client:(id)client; diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index fe9fc415..3daf35bf 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -21,12 +21,9 @@ // 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" // Swift Packages @@ -36,27 +33,14 @@ @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; -// input modes -static NSString *const kBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.Bopomofo"; -static NSString *const kPlainBopomofoModeIdentifier = @"org.openvanilla.inputmethod.McBopomofo.PlainBopomofo"; - 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) { @@ -65,17 +49,13 @@ static inline NSString *LocalizationNotNeeded(NSString *s) { @interface McBopomofoInputMethodController () { - 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 input mode - NSString *_inputMode; - + KeyHandler *_keyHandler; InputState *_state; } @end @@ -83,6 +63,9 @@ static inline NSString *LocalizationNotNeeded(NSString *s) { @interface McBopomofoInputMethodController (VTCandidateController) @end +@interface McBopomofoInputMethodController (KeyHandlerDelegate) +@end + @interface McBopomofoInputMethodController (UI) + (VTHorizontalCandidateController *)horizontalCandidateController; + (VTVerticalCandidateController *)verticalCandidateController; @@ -91,41 +74,8 @@ static inline NSString *LocalizationNotNeeded(NSString *s) { - (void)_hideTooltip; @end -// sort helper -class NodeAnchorDescendingSorter -{ -public: - bool operator()(const NodeAnchor &a, const NodeAnchor &b) const { - return a.node->key().length() > b.node->key().length(); - } -}; - -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; -} - @implementation McBopomofoInputMethodController -- (void)dealloc -{ - // clean up everything - if (_bpmfReadingBuffer) { - delete _bpmfReadingBuffer; - } - - if (_builder) { - delete _builder; - } -} - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)client { @@ -134,21 +84,8 @@ static double FindHighestScore(const vector &nodes, double epsilon) self = [super initWithServer:server delegate:delegate client:client]; if (self) { - // 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("-"); - - - _inputMode = kBopomofoModeIdentifier; + _keyHandler = [[KeyHandler alloc] init]; + _keyHandler.delegate = self; _state = [[InputStateEmpty alloc] init]; } @@ -159,6 +96,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:@""]; @@ -171,14 +109,14 @@ static double FindHighestScore(const vector &nodes, double epsilon) 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 { @@ -213,37 +151,14 @@ static double FindHighestScore(const vector &nodes, double epsilon) [self handleState:newState 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); - + [_keyHandler synchWithPrefereneces]; [(AppDelegate * )[NSApp delegate] checkForUpdate]; } - (void)deactivateServer:(id)client { + [_keyHandler clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; [self handleState:empty client:client]; @@ -254,44 +169,24 @@ static double FindHighestScore(const vector &nodes, double epsilon) - (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 { 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 NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; [sender overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; - - _inputMode = newInputMode; - _languageModel = newLanguageModel; - - if (_builder) { - delete _builder; - _builder = new BlockReadingBuilder(_languageModel); - _builder->setJoinSeparator("-"); - } - - - if (!_bpmfReadingBuffer->isEmpty()) { - _bpmfReadingBuffer->clear(); - } - + [_keyHandler clear]; + _keyHandler.inputMode = newInputMode; InputState *empty = [[InputState alloc] init]; [self handleState:empty client:sender]; - } } @@ -302,778 +197,6 @@ static double FindHighestScore(const vector &nodes, double epsilon) return NSKeyDownMask | NSFlagsChangedMask; } -- (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 isOptionlHold] || [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. - 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)) { - 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]; - 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()) { - _bpmfReadingBuffer->clear(); - InputStateCommitting *commiting = [[InputStateCommitting alloc] initWithPoppedText:@" "]; - stateCallback(commiting); - 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 == '`') { - // zonble: make candidate state - if ([self _handlePunctuation:string("_punctuation_list") state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback: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 optioon is enabled, we clear everythiong including the composing - // buffer, walked nodes and the reading. - 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]; - marking.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]; - marking.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]; - 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]; - 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]]) { - InputStateInputting *current = (InputStateInputting *) state; - NSString *composingBuffer = current.composingBuffer; - InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:composingBuffer]; - stateCallback(committing); - InputState *empty = [[InputState 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) { - 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 _writeUserPhrase]) { - 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]; - marking.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]; - marking.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; - - BOOL cancelCandidateKey = - (charCode == 27) || - ((_inputMode == kPlainBopomofoModeIdentifier) && - (charCode == 8 || [input isDelete])); - - if (cancelCandidateKey) { - if (_inputMode == kPlainBopomofoModeIdentifier) { - _builder->clear(); - _walkedNodes.clear(); - } - InputState *inputting = [self _buildInputtingState]; - stateCallback(inputting); - return YES; - } - - if (charCode == 13 || [input isEnter]) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex]; - 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 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]; - InputStateEmpty *empty = [[InputStateEmpty alloc] init]; - [self handleInput:input state:empty stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]; - } - return YES; - } - } - - errorCallback(); - candidateSelectionCallback(); - return YES; -} - - (BOOL)handleEvent:(NSEvent *)event client:(id)client { if ([event type] == NSFlagsChanged) { @@ -1116,7 +239,7 @@ static double FindHighestScore(const vector &nodes, double epsilon) } KeyHandlerInput *input = [[KeyHandlerInput alloc] initWithEvent:event isVerticalMode:useVerticalMode]; - BOOL result = [self handleInput:input state:_state stateCallback:^(InputState *state) { + BOOL result = [_keyHandler handleInput:input state:_state stateCallback:^(InputState *state) { [self handleState:state client:client]; } candidateSelectionCallback:^{ NSLog(@"candidateSelectionCallback "); @@ -1128,160 +251,6 @@ static double FindHighestScore(const vector &nodes, double epsilon) return result; } -#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 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++; - } - } - } - } - - // 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 tht 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()]; - // Chinese conversion. - poppedText = [self _convertToSimplifiedChineseIfRequired:poppedText]; - _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; -} #pragma mark - States Handling @@ -1337,6 +306,8 @@ static double FindHighestScore(const vector &nodes, double epsilon) [self _handleInputStateDeactive:(InputStateDeactive *) newState previous:previous client:client]; } else if ([newState isKindOfClass:[InputStateEmpty class]]) { [self _handleInputStateEmpty:(InputStateEmpty *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateEmptyIgnoringPreviousState class]]) { + [self _handleInputStateEmptyIgnoringPrevious:(InputStateEmptyIgnoringPreviousState *) newState previous:previous client:client]; } else if ([newState isKindOfClass:[InputStateCommitting class]]) { [self _handleInputStateCommitting:(InputStateCommitting *) newState previous:previous client:client]; } else if ([newState isKindOfClass:[InputStateInputting class]]) { @@ -1350,18 +321,13 @@ static double FindHighestScore(const vector &nodes, double epsilon) - (void)_handleInputStateDeactive:(InputStateDeactive *)state previous:(InputState *)previous client:(id)client { - // clean up reading buffer residues - if (!_bpmfReadingBuffer->isEmpty()) { - _bpmfReadingBuffer->clear(); - [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - } - // commit any residue in the composing buffer if ([previous isKindOfClass:[InputStateInputting class]]) { - NSString *buffer = [(InputStateInputting *) _state composingBuffer]; + NSString *buffer = [(InputStateInputting *) previous composingBuffer]; [self _commitText:buffer client:client]; } - + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + _currentDeferredClient = nil; _currentCandidateClient = nil; @@ -1374,12 +340,19 @@ static double FindHighestScore(const vector &nodes, double epsilon) { // commit any residue in the composing buffer if ([previous isKindOfClass:[InputStateInputting class]]) { - NSString *buffer = [(InputStateInputting *) _state composingBuffer]; + NSString *buffer = [(InputStateInputting *) previous composingBuffer]; [self _commitText:buffer client:client]; } - _builder->clear(); - _walkedNodes.clear(); + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; +} + +- (void)_handleInputStateEmptyIgnoringPrevious:(InputStateEmptyIgnoringPreviousState *)state previous:(InputState *)previous client:(id)client +{ +// [client insertText:@"" replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; gCurrentCandidateController.visible = NO; [self _hideTooltip]; } @@ -1388,9 +361,6 @@ static double FindHighestScore(const vector &nodes, double epsilon) { NSString *poppedText = [state poppedText]; [self _commitText:poppedText client:client]; - - _builder->clear(); - _walkedNodes.clear(); gCurrentCandidateController.visible = NO; [self _hideTooltip]; } @@ -1439,7 +409,7 @@ static double FindHighestScore(const vector &nodes, double epsilon) // 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)]; - if (_inputMode == kPlainBopomofoModeIdentifier && [state.candidates count] == 1) { + if ([_keyHandler inputMode] == kPlainBopomofoModeIdentifier && [state.candidates count] == 1) { NSString *buffer = [self _convertToSimplifiedChineseIfRequired:state.candidates.firstObject]; [client insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; InputStateEmpty *empty = [[InputStateEmpty alloc] init]; @@ -1514,26 +484,8 @@ static double FindHighestScore(const vector &nodes, double epsilon) } gCurrentCandidateController.visible = YES; - } -#pragma mark - User phrases - - -- (BOOL)_writeUserPhrase -{ - return NO; -// NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings]; -// if (![currentMarkedPhrase length]) { -// [self beep]; -// return NO; -// } - -// return [LanguageModelManager writeUserPhrase:currentMarkedPhrase]; -} - - - #pragma mark - Misc menu items - (void)showPreferences:(id)sender @@ -1552,7 +504,7 @@ static double FindHighestScore(const vector &nodes, double epsilon) - (void)toggleChineseConverter:(id)sender { BOOL chineseConversionEnabled = [Preferences toggleChineseConversionEnabled]; - [NotifierController notifyWithMessage: + [NotifierController notifyWithMessage: chineseConversionEnabled ? NSLocalizedString(@"Chinese conversion on", @"") : NSLocalizedString(@"Chinese conversion off", @"") stay:NO]; @@ -1630,7 +582,6 @@ static double FindHighestScore(const vector &nodes, double epsilon) [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } - @end #pragma mark - @@ -1664,18 +615,11 @@ static double FindHighestScore(const vector &nodes, double epsilon) // candidate selected, override the node with selection string selectedValue = [[state.candidates objectAtIndex:index] UTF8String]; + [_keyHandler fixNodeWithvalue:selectedValue]; + InputStateInputting *inputting = [_keyHandler _buildInputtingState]; - size_t cursorIndex = [self _actualCandidateCursorIndex]; - _builder->grid().fixNodeSelectedCandidate(cursorIndex, selectedValue); - if (_inputMode != kPlainBopomofoModeIdentifier) { - _userOverrideModel->observe(_walkedNodes, cursorIndex, selectedValue, [[NSDate date] timeIntervalSince1970]); - } - - [self _walk]; - - InputStateInputting *inputting = [self _buildInputtingState]; - - if (_inputMode == kPlainBopomofoModeIdentifier) { + if ([_keyHandler inputMode] == kPlainBopomofoModeIdentifier) { + [_keyHandler clear]; InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:inputting.composingBuffer]; [self handleState:committing client:_currentCandidateClient]; InputStateEmpty *empty = [[InputStateEmpty alloc] init]; @@ -1688,6 +632,31 @@ static double FindHighestScore(const vector &nodes, double epsilon) @end +@implementation McBopomofoInputMethodController (KeyHandlerDelegate) + +- (nonnull VTCandidateController *)candidateControllerForKeyHanlder:(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 diff --git a/Source/InputState.swift b/Source/InputState.swift index 6bf56b3b..0e0d5e9c 100644 --- a/Source/InputState.swift +++ b/Source/InputState.swift @@ -41,6 +41,13 @@ class InputStateEmpty: InputState { } } +/// 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 = "" @@ -159,6 +166,18 @@ class InputStateMarking: InputStateNotEmpty { let state = InputStateInputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) return state } + + @objc var validToWrite: Bool { + return self.markedRange.length >= kMinMarkRangeLength && self.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.. +#import "McBopomofo-Swift.h" +@import CandidateUI; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kBopomofoModeIdentifier; +extern NSString *const kPlainBopomofoModeIdentifier; + +@class KeyHandler; + +@protocol KeyHandlerDelegate +- (VTCandidateController *)candidateControllerForKeyHanlder:(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)synchWithPrefereneces; +- (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..97ea7389 --- /dev/null +++ b/Source/KeyHandler.mm @@ -0,0 +1,1154 @@ +// 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" + +// 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 everytime 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)synchWithPrefereneces +{ + 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 isOptionlHold] || [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]; + 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 *commiting = [[InputStateCommitting alloc] initWithPoppedText:@" "]; + stateCallback(commiting); + 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 optioon is enabled, we clear everythiong 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]; + marking.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]; + marking.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]; + 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]; + 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]]) { + [self clear]; + + InputStateInputting *current = (InputStateInputting *) state; + NSString *composingBuffer = current.composingBuffer; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:composingBuffer]; + stateCallback(committing); + InputState *empty = [[InputState 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]; + marking.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]; + marking.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 candidateControllerForKeyHanlder:self]; + + BOOL cancelCandidateKey = (charCode == 27) || [input isDelete] || + ((_inputMode == kPlainBopomofoModeIdentifier) && (charCode == 8)); + + 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 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.delegate keyHandler:self didSelectCandidateAtIndex:candidateIndex candidateController:gCurrentCandidateController]; + [self clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [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 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++; + } + } + } + } + + // 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 tht 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/LanguageModelManager.mm b/Source/LanguageModelManager.mm index 06940d9d..b4306bb8 100644 --- a/Source/LanguageModelManager.mm +++ b/Source/LanguageModelManager.mm @@ -174,7 +174,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 +188,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 +196,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo } NSMutableString *currentMarkedPhrase = [NSMutableString string]; - if (shuoldAddLineBreakAtFront) { + if (addLineBreakAtFront) { [currentMarkedPhrase appendString:@"\n"]; } [currentMarkedPhrase appendString:userPhrase];