From e7dcbe7e91880bb66b47c9af6313e16b86a659e9 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sun, 30 Jan 2022 00:20:32 +0800 Subject: [PATCH] Zonble: IMC // Refactoring IMController. - This commit already has vChewing Feature Customizations Applied. --- .../Engine/ControllerModules/InputState.swift | 219 ++ Source/Engine/ControllerModules/KeyHandler.h | 49 + Source/Engine/ControllerModules/KeyHandler.mm | 1179 +++++++++++ .../ControllerModules/KeyHandlerInput.swift | 178 ++ .../KeyValueBlobReader.cpp | 0 .../KeyValueBlobReader.h | 0 .../Engine/Gramambular/BlockReadingBuilder.h | 31 +- Source/Engine/Keyboard/EmacsKeyHelper.swift | 28 - Source/InputMethodController.h | 39 +- Source/InputMethodController.mm | 1757 ++++------------- Source/LanguageModelManager.mm | 24 +- Source/en.lproj/Localizable.strings | 4 +- Source/ja.lproj/Localizable.strings | 4 +- Source/zh-Hans.lproj/Localizable.strings | 4 +- Source/zh-Hant.lproj/Localizable.strings | 4 +- vChewing.xcodeproj/project.pbxproj | 32 +- 16 files changed, 2074 insertions(+), 1478 deletions(-) create mode 100644 Source/Engine/ControllerModules/InputState.swift create mode 100644 Source/Engine/ControllerModules/KeyHandler.h create mode 100644 Source/Engine/ControllerModules/KeyHandler.mm create mode 100644 Source/Engine/ControllerModules/KeyHandlerInput.swift rename Source/Engine/{vChewing => ControllerModules}/KeyValueBlobReader.cpp (100%) rename Source/Engine/{vChewing => ControllerModules}/KeyValueBlobReader.h (100%) delete mode 100644 Source/Engine/Keyboard/EmacsKeyHelper.swift diff --git a/Source/Engine/ControllerModules/InputState.swift b/Source/Engine/ControllerModules/InputState.swift new file mode 100644 index 00000000..81d0af54 --- /dev/null +++ b/Source/Engine/ControllerModules/InputState.swift @@ -0,0 +1,219 @@ +/* + * InputState.cpp + * + * Copyright 2021-2022 vChewing Project (3-Clause BSD License). + * Derived from 2011-2022 OpenVanilla Project (MIT License). + * Some rights reserved. See "LICENSE.TXT" for details. + */ + +import Cocoa + +/// Represents the states for the input method controller. +/// +/// An input method is actually a finite state machine. It receives the inputs +/// from hardware like keyboard and mouse, changes its state, updates user +/// interface by the state, and finally produces the text output and then them +/// to the client apps. It should be a one-way data flow, and the user interface +/// and text output should follow unconditionally one single data source. +/// +/// The InputState class is for representing what the input controller is doing, +/// and the place to store the variables that could be used. For example, the +/// array for the candidate list is useful only when the user is choosing a +/// candidate, and the array should not exist when the input controller is in +/// another state. +/// +/// They are immutable objects. When the state changes, the controller should +/// create a new state object to replace the current state instead of modifying +/// the existing one. +/// +/// vChewing's input controller has following possible states: +/// +/// - Deactivated: The user is not using vChewing yet. +/// - Empty: The user has switched to vChewing but did not input anything yet, +/// or, he or she has committed text into the client apps and starts a new +/// input phase. +/// - Committing: The input controller is sending text to the client apps. +/// - Inputting: The user has inputted something and the input buffer is +/// visible. +/// - Marking: The user is creating a area in the input buffer and about to +/// create a new user phrase. +/// - Choosing Candidate: The candidate window is open to let the user to choose +/// one among the candidates. +class InputState: NSObject { +} + +/// Represents that the input controller is deactivated. +class InputStateDeactivated: InputState { + override var description: String { + "" + } +} + +/// Represents that the composing buffer is empty. +class InputStateEmpty: InputState { + @objc var composingBuffer: String { + "" + } +} + +/// Represents that the composing buffer is empty. +class InputStateEmptyIgnoringPreviousState: InputState { + @objc var composingBuffer: String { + "" + } +} + +/// Represents that the input controller is committing text into client app. +class InputStateCommitting: InputState { + @objc private(set) var poppedText: String = "" + + @objc convenience init(poppedText: String) { + self.init() + self.poppedText = poppedText + } + + override var description: String { + "" + } +} + +/// Represents that the composing buffer is not empty. +class InputStateNotEmpty: InputState { + @objc private(set) var composingBuffer: String = "" + @objc private(set) var cursorIndex: UInt = 0 + + @objc init(composingBuffer: String, cursorIndex: UInt) { + self.composingBuffer = composingBuffer + self.cursorIndex = cursorIndex + } + + override var description: String { + "" + } +} + +/// Represents that the user is inputting text. +class InputStateInputting: InputStateNotEmpty { + @objc var bpmfReading: String = "" + @objc var bpmfReadingCursorIndex: UInt8 = 0 + @objc var poppedText: String = "" + + @objc override init(composingBuffer: String, cursorIndex: UInt) { + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + } + + @objc var attributedString: NSAttributedString { + let attributedSting = NSAttributedString(string: composingBuffer, attributes: [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 0 + ]) + return attributedSting + } + + override var description: String { + "" + } +} + +private let kMinMarkRangeLength = 2 +private let kMaxMarkRangeLength = Preferences.maxCandidateLength + +/// Represents that the user is marking a range in the composing buffer. +class InputStateMarking: InputStateNotEmpty { + @objc private(set) var markerIndex: UInt + @objc private(set) var markedRange: NSRange + @objc var tooltip: String { + + if Preferences.phraseReplacementEnabled { + return NSLocalizedString("⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: "") + } + + if markedRange.length == 0 { + return "" + } + + let text = (composingBuffer as NSString).substring(with: markedRange) + if markedRange.length < kMinMarkRangeLength { + return String(format: NSLocalizedString("\"%@\" length must ≥ 2 for a user phrase.", comment: ""), text) + } else if (markedRange.length > kMaxMarkRangeLength) { + return String(format: NSLocalizedString("\"%@\" length should ≤ %d for a user phrase.", comment: ""), text, kMaxMarkRangeLength) + } + return String(format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: ""), text) + } + + @objc private(set) var readings: [String] = [] + + @objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) { + self.markerIndex = markerIndex + let begin = min(cursorIndex, markerIndex) + let end = max(cursorIndex, markerIndex) + markedRange = NSMakeRange(Int(begin), Int(end - begin)) + self.readings = readings + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + } + + @objc var attributedString: NSAttributedString { + let attributedSting = NSMutableAttributedString(string: composingBuffer) + let end = markedRange.location + markedRange.length + + attributedSting.setAttributes([ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 0 + ], range: NSRange(location: 0, length: markedRange.location)) + attributedSting.setAttributes([ + .underlineStyle: NSUnderlineStyle.thick.rawValue, + .markedClauseSegment: 1 + ], range: markedRange) + attributedSting.setAttributes([ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 2 + ], range: NSRange(location: end, + length: composingBuffer.count - end)) + return attributedSting + } + + override var description: String { + "" + } + + @objc func convertToInputting() -> InputStateInputting { + let state = InputStateInputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + return state + } + + @objc var validToWrite: Bool { + markedRange.length >= kMinMarkRangeLength && markedRange.length <= kMaxMarkRangeLength + } + + @objc var userPhrase: String { + let text = (composingBuffer as NSString).substring(with: markedRange) + let end = markedRange.location + markedRange.length + let readings = readings[markedRange.location.." + } +} diff --git a/Source/Engine/ControllerModules/KeyHandler.h b/Source/Engine/ControllerModules/KeyHandler.h new file mode 100644 index 00000000..f4be044e --- /dev/null +++ b/Source/Engine/ControllerModules/KeyHandler.h @@ -0,0 +1,49 @@ +/* + * KeyHandler.h + * + * Copyright 2021-2022 vChewing Project (3-Clause BSD License). + * Derived from 2011-2022 OpenVanilla Project (MIT License). + * Some rights reserved. See "LICENSE.TXT" for details. + */ + +#import +#import +#import "vChewing-Swift.h" + +@class KeyHandlerInput; +@class InputState; +@class InputStateInputting; +@class InputStateMarking; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kBopomofoModeIdentifierCHS; +extern NSString *const kBopomofoModeIdentifierCHT; + +@class KeyHandler; + +@protocol KeyHandlerDelegate +- (VTCandidateController *)candidateControllerForKeyHandler:(KeyHandler *)keyHandler; +- (void)keyHandler:(KeyHandler *)keyHandler didSelectCandidateAtIndex:(NSInteger)index candidateController:(VTCandidateController *)controller; +- (BOOL)keyHandler:(KeyHandler *)keyHandler didRequestWriteUserPhraseWithState:(InputStateMarking *)state; +@end + +@interface KeyHandler : NSObject + +- (BOOL)handleInput:(KeyHandlerInput *)input + state:(InputState *)state + stateCallback:(void (^)(InputState *))stateCallback +candidateSelectionCallback:(void (^)(void))candidateSelectionCallback + errorCallback:(void (^)(void))errorCallback; + +- (void)syncWithPreferences; +- (void)fixNodeWithValue:(std::string)value; +- (void)clear; + +- (InputStateInputting *)_buildInputtingState; + +@property (strong, nonatomic) NSString *inputMode; +@property (weak, nonatomic) id delegate; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Engine/ControllerModules/KeyHandler.mm b/Source/Engine/ControllerModules/KeyHandler.mm new file mode 100644 index 00000000..195afee6 --- /dev/null +++ b/Source/Engine/ControllerModules/KeyHandler.mm @@ -0,0 +1,1179 @@ +/* + * KeyHandler.mm + * + * Copyright 2021-2022 vChewing Project (3-Clause BSD License). + * Derived from 2011-2022 OpenVanilla Project (MIT License). + * Some rights reserved. See "LICENSE.TXT" for details. + */ + +#import "Mandarin.hh" +#import "Gramambular.h" +#import "vChewingLM.h" +#import "UserOverrideModel.h" +#import "LanguageModelManager.h" +#import "OVUTF8Helper.h" +#import "KeyHandler.h" +#import "vChewing-Swift.h" + +using namespace std; +using namespace Taiyan::Mandarin; +using namespace Taiyan::Gramambular; +using namespace vChewing; +using namespace OpenVanilla; + +NSString *const kBopomofoModeIdentifierCHT = @"org.atelierInmu.inputmethod.vChewing.TradBopomofo"; +NSString *const kBopomofoModeIdentifierCHS = @"org.atelierInmu.inputmethod.vChewing.SimpBopomofo"; + +static const double kEpsilon = 0.000001; + +static double FindHighestScore(const vector &nodes, double epsilon) { + double highestScore = 0.0; + for (auto ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { + double score = ni->node->highestUnigramScore(); + if (score > highestScore) { + highestScore = score; + } + } + return highestScore + epsilon; +} + +// sort helper +class NodeAnchorDescendingSorter +{ +public: + bool operator()(const NodeAnchor &a, const NodeAnchor &b) const { + return a.node->key().length() > b.node->key().length(); + } +}; + +// if DEBUG is defined, a DOT file (GraphViz format) will be written to the +// specified path every time the grid is walked +#if DEBUG +static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot"; +#endif + + +@implementation KeyHandler +{ + // the reading buffer that takes user input + Taiyan::Mandarin::BopomofoReadingBuffer *_bpmfReadingBuffer; + + // language model + vChewing::vChewingLM *_languageModel; + + // user override model + vChewing::UserOverrideModel *_userOverrideModel; + + // the grid (lattice) builder for the unigrams (and bigrams) + Taiyan::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; + vChewingLM *newLanguageModel; + UserOverrideModel *newUserOverrideModel; + + if ([value isKindOfClass:[NSString class]] && [value isEqual:kBopomofoModeIdentifierCHS]) { + newInputMode = kBopomofoModeIdentifierCHS; + newLanguageModel = [LanguageModelManager languageModelCoreCHS]; + newUserOverrideModel = [LanguageModelManager userOverrideModelCHS]; + } else { + newInputMode = kBopomofoModeIdentifierCHT; + newLanguageModel = [LanguageModelManager languageModelCoreCHT]; + newUserOverrideModel = [LanguageModelManager userOverrideModelCHT]; + } + + // 自 Preferences 模組讀入自訂語彙置換功能開關狀態。 + newLanguageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); + + // 自 Preferences 模組讀取全字庫模式開關狀態。 + newLanguageModel->setCNSEnabled(Preferences.cns11643Enabled); + + // Only apply the changes if the value is changed + if (![_inputMode isEqualToString:newInputMode]) { + [[NSUserDefaults standardUserDefaults] synchronize]; + + _inputMode = newInputMode; + _languageModel = newLanguageModel; + _userOverrideModel = newUserOverrideModel; + + 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 languageModelCoreCHT]; + _languageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); + _languageModel->setCNSEnabled(Preferences.cns11643Enabled); + _userOverrideModel = [LanguageModelManager userOverrideModelCHT]; + + _builder = new BlockReadingBuilder(_languageModel); + + // each Mandarin syllable is separated by a hyphen + _builder->setJoinSeparator("-"); + _inputMode = kBopomofoModeIdentifierCHT; + } + return self; +} + +- (void)syncWithPreferences +{ + NSInteger layout = Preferences.keyboardLayout; + switch (layout) { + case KeyboardLayoutStandard: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); + break; + case KeyboardLayoutEten: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETenLayout()); + break; + case KeyboardLayoutHsu: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HsuLayout()); + break; + case KeyboardLayoutEten26: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETen26Layout()); + break; + case KeyboardLayoutHanyuPinyin: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HanyuPinyinLayout()); + break; + case KeyboardLayoutIBM: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::IBMLayout()); + break; + default: + _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); + Preferences.keyboardLayout = KeyboardLayoutStandard; + } +} + +- (void)fixNodeWithValue:(std::string)value +{ + size_t cursorIndex = [self _actualCandidateCursorIndex]; + _builder->grid().fixNodeSelectedCandidate(cursorIndex, value); + if (Preferences.useWinNT351BPMF) { + _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; + vChewingEmacsKey emacsKey = input.emacsKey; + + // if the inputText is empty, it's a function key combination, we ignore it + if (![input.inputText length]) { + return NO; + } + + // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it + BOOL isFunctionKey = ([input isCommandHold] || [input isControlHold] || [input isOptionHold] || [input isNumericPad]); + if (![state isKindOfClass:[InputStateNotEmpty class]] && isFunctionKey) { + return NO; + } + + // Caps Lock processing : if Caps Lock is on, temporarily disable bopomofo. + if (charCode == 8 || charCode == 13 || [input isAbsorbedArrowKey] || [input isExtraChooseCandidateKey] || [input isCursorForward] || [input isCursorBackward]) { + // do nothing if backspace is pressed -- we ignore the key + } else if ([input isCapsLockOn]) { + // process all possible combination, we hope. + [self clear]; + InputStateEmpty *emptyState = [[InputStateEmpty alloc] init]; + stateCallback(emptyState); + + // first commit everything in the buffer. + if ([input isShiftHold]) { + return NO; + } + + // if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char insertions. + if (charCode < 0x80 && !isprint(charCode)) { + return NO; + } + + // when shift is pressed, don't do further processing, since it outputs capital letter anyway. + InputStateCommitting *committingState = [[InputStateCommitting alloc] initWithPoppedText:[input.inputText lowercaseString]]; + stateCallback(committingState); + stateCallback(emptyState); + return YES; + } + + if ([input isNumericPad]) { + if (![input isLeft] && ![input isRight] && ![input isDown] && ![input isUp] && charCode != 32 && isprint(charCode)) { + [self clear]; + InputStateEmpty *emptyState = [[InputStateEmpty alloc] init]; + stateCallback(emptyState); + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:[input.inputText lowercaseString]]; + stateCallback(committing); + stateCallback(emptyState); + return YES; + } + } + + // MARK: Handle Candidates + if ([state isKindOfClass:[InputStateChoosingCandidate class]]) { + return [self _handleCandidateState:(InputStateChoosingCandidate *) state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]; + } + + // MARK: Handle Marking + if ([state isKindOfClass:[InputStateMarking class]]) { + InputStateMarking *marking = (InputStateMarking *) state; + if ([self _handleMarkingState:(InputStateMarking *) state input:input stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]) { + return YES; + } + state = [marking convertToInputting]; + stateCallback(state); + } + + bool composeReading = false; + + // MARK: Handle BPMF Keys + // see if it's valid BPMF reading + if (_bpmfReadingBuffer->isValidKey((char) charCode)) { + _bpmfReadingBuffer->combineKey((char) charCode); + + // if we have a tone marker, we have to insert the reading to the + // builder in other words, if we don't have a tone marker, we just + // update the composing buffer + composeReading = _bpmfReadingBuffer->hasToneMarker(); + if (!composeReading) { + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + return YES; + } + } + + // see if we have composition if Enter/Space is hit and buffer is not empty + // this is bit-OR'ed so that the tone marker key is also taken into account + composeReading |= (!_bpmfReadingBuffer->isEmpty() && (charCode == 32 || charCode == 13)); + if (composeReading) { + // combine the reading + string reading = _bpmfReadingBuffer->syllable().composedString(); + + // see if we have a unigram for this + if (!_languageModel->hasUnigramsForKey(reading)) { + errorCallback(); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + return YES; + } + + // and insert it into the lattice + _builder->insertReadingAtCursor(reading); + + // then walk the lattice + NSString *poppedText = [self _popOverflowComposingTextAndWalk]; + + // get user override model suggestion + string overrideValue = (Preferences.useWinNT351BPMF) ? "" : + _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); + + // 模擬 WINNT 351 ㄅ半注音,就是每個漢字都自動要選字的那種注音。 + // 嚴格來講不能算純正的ㄅ半注音,畢竟候選字的順序不可能會像當年那樣了。 + // 如果簡體中文用戶不知道ㄅ半注音是什麼的話,拿全拼輸入法來比喻恐怕比較恰當。 + if (Preferences.useWinNT351BPMF) { + InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode]; + if (choosingCandidates.candidates.count == 1) { + [self clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:choosingCandidates.candidates.firstObject]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else { + stateCallback(choosingCandidates); + } + } + + // and tells the client that the key is consumed + return YES; + } + + // MARK: Space and Down + // keyCode 125 = Down, charCode 32 = Space + if (_bpmfReadingBuffer->isEmpty() && + [state isKindOfClass:[InputStateNotEmpty class]] && + ([input isExtraChooseCandidateKey] || charCode == 32 || (input.useVerticalMode && ([input isVerticalModeOnlyChooseCandidateKey])))) { + if (charCode == 32) { + // if the spacebar is NOT set to be a selection key + if ([input isShiftHold] || !Preferences.chooseCandidateUsingSpace) { + if (_builder->cursorIndex() >= _builder->length()) { + [self clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:@" "]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else if (_languageModel->hasUnigramsForKey(" ")) { + _builder->insertReadingAtCursor(" "); + NSString *poppedText = [self _popOverflowComposingTextAndWalk]; + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + } + return YES; + + } + } + InputStateChoosingCandidate *choosingCandidates = [self _buildCandidateState:(InputStateNotEmpty *) state useVerticalMode:input.useVerticalMode]; + stateCallback(choosingCandidates); + return YES; + } + + // MARK: Esc + if (charCode == 27) { + return [self _handleEscWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Cursor backward + if ([input isCursorBackward] || emacsKey == vChewingEmacsKeyBackward) { + return [self _handleBackwardWithState:state input:input stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Cursor forward + if ([input isCursorForward] || emacsKey == vChewingEmacsKeyForward) { + return [self _handleForwardWithState:state input:input stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Home + if ([input isHome] || emacsKey == vChewingEmacsKeyHome) { + return [self _handleHomeWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: End + if ([input isEnd] || emacsKey == vChewingEmacsKeyEnd) { + 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 == vChewingEmacsKeyDelete) { + return [self _handleDeleteWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Enter + if (charCode == 13) { + return [self _handleEnterWithState:state stateCallback:stateCallback errorCallback:errorCallback]; + } + + // MARK: Punctuation list + if ((char) charCode == '`') { + if (_languageModel->hasUnigramsForKey(string("_punctuation_list"))) { + if (_bpmfReadingBuffer->isEmpty()) { + _builder->insertReadingAtCursor(string("_punctuation_list")); + NSString *poppedText = [self _popOverflowComposingTextAndWalk]; + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + InputStateChoosingCandidate *choosingCandidate = [self _buildCandidateState:inputting useVerticalMode:input.useVerticalMode]; + stateCallback(choosingCandidate); + } else { // If there is still unfinished bpmf reading, ignore the punctuation + errorCallback(); + } + return YES; + } + } + + // MARK: Punctuation + // if nothing is matched, see if it's a punctuation key for current layout. + string layout = [self _currentLayout]; + string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_"); + string customPunctuation = punctuationNamePrefix + layout + string(1, (char) charCode); + if ([self _handlePunctuation:customPunctuation state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { + return YES; + } + + // if nothing is matched, see if it's a punctuation key. + string punctuation = punctuationNamePrefix + string(1, (char) charCode); + if ([self _handlePunctuation:punctuation state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { + return YES; + } + + if ((char) charCode >= 'A' && (char) charCode <= 'Z') { + string letter = string("_letter_") + string(1, (char) charCode); + if ([self _handlePunctuation:letter state:state usingVerticalMode:input.useVerticalMode stateCallback:stateCallback errorCallback:errorCallback]) { + return YES; + } + } + + // still nothing, then we update the composing buffer (some app has + // strange behavior if we don't do this, "thinking" the key is not + // actually consumed) + if ([state isKindOfClass:[InputStateNotEmpty class]] || !_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + return NO; +} + +- (BOOL)_handleEscWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + BOOL escToClearInputBufferEnabled = Preferences.escToCleanInputBuffer; + + if (escToClearInputBufferEnabled) { + // if the option is enabled, we clear everything including the composing + // buffer, walked nodes and the reading. + [self clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + } else { + // if reading is not empty, we cancel the reading; Apple's built-in + // Zhuyin (and the erstwhile Hanin) has a default option that Esc + // "cancels" the current composed character and revert it to + // Bopomofo reading, in odds with the expectation of users from + // other platforms + + if (_bpmfReadingBuffer->isEmpty()) { + // no nee to beep since the event is deliberately triggered by user + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + } else { + _bpmfReadingBuffer->clear(); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } + } + return YES; +} + +- (BOOL)_handleBackwardWithState:(InputState *)state input:(KeyHandlerInput *)input stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + InputStateInputting *currentState = (InputStateInputting *) state; + + if ([input isShiftHold]) { + // Shift + left + if (_builder->cursorIndex() > 0) { + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex - 1 readings: [self _currentReadings]]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + } else { + if (_builder->cursorIndex() > 0) { + _builder->setCursorIndex(_builder->cursorIndex() - 1); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + } + return YES; +} + +- (BOOL)_handleForwardWithState:(InputState *)state input:(KeyHandlerInput *)input stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + InputStateInputting *currentState = (InputStateInputting *) state; + + if ([input isShiftHold]) { + // Shift + Right + if (_builder->cursorIndex() < _builder->length()) { + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:currentState.cursorIndex + 1 readings: [self _currentReadings]]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + } else { + if (_builder->cursorIndex() < _builder->length()) { + _builder->setCursorIndex(_builder->cursorIndex() + 1); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + } + + return YES; +} + +- (BOOL)_handleHomeWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex()) { + _builder->setCursorIndex(0); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + + return YES; +} + +- (BOOL)_handleEndWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + stateCallback(state); + return YES; + } + + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex() != _builder->length()) { + _builder->setCursorIndex(_builder->length()); + InputStateInputting *inputting = [self _buildInputtingState]; + stateCallback(inputting); + } else { + errorCallback(); + stateCallback(state); + } + + return YES; +} + +- (BOOL)_handleAbsorbedArrowKeyWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + } + stateCallback(state); + return YES; +} + +- (BOOL)_handleBackspaceWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (_bpmfReadingBuffer->isEmpty()) { + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex()) { + _builder->deleteReadingBeforeCursor(); + [self _walk]; + } else { + errorCallback(); + stateCallback(state); + return YES; + } + } else { + _bpmfReadingBuffer->backspace(); + } + + InputStateInputting *inputting = [self _buildInputtingState]; + if (!inputting.composingBuffer.length) { + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + } else { + stateCallback(inputting); + } + return YES; +} + +- (BOOL)_handleDeleteWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (_bpmfReadingBuffer->isEmpty()) { + if (![state isKindOfClass:[InputStateInputting class]]) { + return NO; + } + + if (_builder->cursorIndex() != _builder->length()) { + _builder->deleteReadingAfterCursor(); + [self _walk]; + InputStateInputting *inputting = [self _buildInputtingState]; + if (!inputting.composingBuffer.length) { + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + } else { + stateCallback(inputting); + } + } else { + errorCallback(); + stateCallback(state); + } + } else { + errorCallback(); + stateCallback(state); + } + + return YES; +} + +- (BOOL)_handleEnterWithState:(InputState *)state stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if ([state isKindOfClass:[InputStateInputting class]]) { + if (Preferences.useWinNT351BPMF) { + if (!_bpmfReadingBuffer->isEmpty()) { + errorCallback(); + } + return YES; + } + + [self clear]; + + InputStateInputting *current = (InputStateInputting *) state; + NSString *composingBuffer = current.composingBuffer; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:composingBuffer]; + stateCallback(committing); + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + stateCallback(empty); + return YES; + } + + return NO; +} + +- (BOOL)_handlePunctuation:(string)customPunctuation state:(InputState *)state usingVerticalMode:(BOOL)useVerticalMode stateCallback:(void (^)(InputState *))stateCallback errorCallback:(void (^)(void))errorCallback +{ + if (!_languageModel->hasUnigramsForKey(customPunctuation)) { + return NO; + } + + NSString *poppedText; + if (_bpmfReadingBuffer->isEmpty()) { + _builder->insertReadingAtCursor(customPunctuation); + poppedText = [self _popOverflowComposingTextAndWalk]; + } else { // If there is still unfinished bpmf reading, ignore the punctuation + errorCallback(); + stateCallback(state); + return YES; + } + + InputStateInputting *inputting = [self _buildInputtingState]; + inputting.poppedText = poppedText; + stateCallback(inputting); + + if (Preferences.useWinNT351BPMF && _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 == vChewingEmacsKeyBackward) + && ([input isShiftHold])) { + NSUInteger index = state.markerIndex; + if (index > 0) { + index -= 1; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + return YES; + } + + // Shift + Right + if (([input isCursorForward] || input.emacsKey == vChewingEmacsKeyForward) + && ([input isShiftHold])) { + NSUInteger index = state.markerIndex; + if (index < state.composingBuffer.length) { + index += 1; + InputStateMarking *marking = [[InputStateMarking alloc] initWithComposingBuffer:state.composingBuffer cursorIndex:state.cursorIndex markerIndex:index readings:state.readings]; + stateCallback(marking); + } else { + errorCallback(); + stateCallback(state); + } + return YES; + } + return NO; +} + + +- (BOOL)_handleCandidateState:(InputStateChoosingCandidate *)state + input:(KeyHandlerInput *)input + stateCallback:(void (^)(InputState *))stateCallback + candidateSelectionCallback:(void (^)(void))candidateSelectionCallback + errorCallback:(void (^)(void))errorCallback; +{ + NSString *inputText = input.inputText; + UniChar charCode = input.charCode; + VTCandidateController *gCurrentCandidateController = [self.delegate candidateControllerForKeyHandler:self]; + + BOOL cancelCandidateKey = (charCode == 27) || (charCode == 8) || [input isDelete]; + + if (cancelCandidateKey) { + if (Preferences.useWinNT351BPMF) { + [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 == vChewingEmacsKeyNextPage) { + 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 == vChewingEmacsKeyBackward) { + 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 == vChewingEmacsKeyForward) { + 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 == vChewingEmacsKeyHome) { + if (gCurrentCandidateController.selectedCandidateIndex == 0) { + errorCallback(); + } else { + gCurrentCandidateController.selectedCandidateIndex = 0; + } + + candidateSelectionCallback(); + return YES; + } + + if (([input isEnd] || input.emacsKey == vChewingEmacsKeyEnd) && [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 (Preferences.useWinNT351BPMF) { + string layout = [self _currentLayout]; + string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_") : string("_punctuation_"); + string customPunctuation = punctuationNamePrefix + layout + string(1, (char) charCode); + string punctuation = punctuationNamePrefix + string(1, (char) charCode); + + BOOL shouldAutoSelectCandidate = _bpmfReadingBuffer->isValidKey((char) charCode) || _languageModel->hasUnigramsForKey(customPunctuation) || + _languageModel->hasUnigramsForKey(punctuation); + + if (!shouldAutoSelectCandidate && (char) charCode >= 'A' && (char) charCode <= 'Z') { + string letter = string("_letter_") + string(1, (char) charCode); + if (_languageModel->hasUnigramsForKey(letter)) { + shouldAutoSelectCandidate = YES; + } + } + + if (shouldAutoSelectCandidate) { + NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:0]; + if (candidateIndex != NSUIntegerMax) { + [self.delegate keyHandler:self didSelectCandidateAtIndex:candidateIndex candidateController:gCurrentCandidateController]; + [self clear]; + InputStateEmptyIgnoringPreviousState *empty = [[InputStateEmptyIgnoringPreviousState alloc] init]; + stateCallback(empty); + [self handleInput:input state:empty stateCallback:stateCallback candidateSelectionCallback:candidateSelectionCallback errorCallback:errorCallback]; + } + return YES; + } + } + + errorCallback(); + candidateSelectionCallback(); + return YES; +} + +#pragma mark - States Building + +- (InputStateInputting *)_buildInputtingState +{ + // "updating the composing buffer" means to request the client to "refresh" the text input buffer + // with our "composing text" + NSMutableString *composingBuffer = [[NSMutableString alloc] init]; + NSInteger composedStringCursorIndex = 0; + + size_t readingCursorIndex = 0; + size_t builderCursorIndex = _builder->cursorIndex(); + + // we must do some Unicode codepoint counting to find the actual cursor location for the client + // i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars + // locations + for (vector::iterator wi = _walkedNodes.begin(), we = _walkedNodes.end(); wi != we; ++wi) { + if ((*wi).node) { + string nodeStr = (*wi).node->currentKeyValue().value; + vector codepoints = OVUTF8Helper::SplitStringByCodePoint(nodeStr); + size_t codepointCount = codepoints.size(); + + NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()]; + [composingBuffer appendString:valueString]; + + // this re-aligns the cursor index in the composed string + // (the actual cursor on the screen) with the builder's logical + // cursor (reading) cursor; each built node has a "spanning length" + // (e.g. two reading blocks has a spanning length of 2), and we + // accumulate those lengths to calculate the displayed cursor + // index + size_t spanningLength = (*wi).spanningLength; + if (readingCursorIndex + spanningLength <= builderCursorIndex) { + composedStringCursorIndex += [valueString length]; + readingCursorIndex += spanningLength; + } else { + for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++) { + composedStringCursorIndex += [[NSString stringWithUTF8String:codepoints[i].c_str()] length]; + readingCursorIndex++; + } + } + } + } + + // now we gather all the info, we separate the composing buffer to two parts, head and tail, + // and insert the reading text (the Mandarin syllable) in between them; + // the reading text is what the user is typing + NSString *head = [composingBuffer substringToIndex:composedStringCursorIndex]; + NSString *reading = [NSString stringWithUTF8String:_bpmfReadingBuffer->composedString().c_str()]; + NSString *tail = [composingBuffer substringFromIndex:composedStringCursorIndex]; + NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]]; + NSInteger cursorIndex = composedStringCursorIndex + [reading length]; + + InputStateInputting *newState = [[InputStateInputting alloc] initWithComposingBuffer:composedText cursorIndex:cursorIndex]; + return newState; +} + +- (void)_walk +{ + // retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation + // of the best possible Mandarain characters given the input syllables, + // using the Viterbi algorithm implemented in the Gramambular library + Walker walker(&_builder->grid()); + + // the reverse walk traces the trellis from the end + _walkedNodes = walker.reverseWalk(_builder->grid().width()); + + // then we reverse the nodes so that we get the forward-walked nodes + reverse(_walkedNodes.begin(), _walkedNodes.end()); + + // if DEBUG is defined, a GraphViz file is written to kGraphVizOutputfile +#if DEBUG + string dotDump = _builder->grid().dumpDOT(); + NSString *dotStr = [NSString stringWithUTF8String:dotDump.c_str()]; + NSError *error = nil; + + BOOL __unused success = [dotStr writeToFile:kGraphVizOutputfile atomically:YES encoding:NSUTF8StringEncoding error:&error]; +#endif +} + +- (NSString *)_popOverflowComposingTextAndWalk +{ + // in an ideal world, we can as well let the user type forever, + // but because the Viterbi algorithm has a complexity of O(N^2), + // the walk will become slower as the number of nodes increase, + // therefore we need to "pop out" overflown text -- they usually + // lose their influence over the whole MLE anyway -- so that when + // the user type along, the already composed text at front will + // be popped out + + NSString *poppedText = @""; + NSInteger composingBufferSize = Preferences.composingBufferSize; + + if (_builder->grid().width() > (size_t) composingBufferSize) { + if (_walkedNodes.size() > 0) { + NodeAnchor &anchor = _walkedNodes[0]; + poppedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()]; + _builder->removeHeadReadings(anchor.spanningLength); + } + } + + [self _walk]; + return poppedText; +} + +- (InputStateChoosingCandidate *)_buildCandidateState:(InputStateNotEmpty *)currentState useVerticalMode:(BOOL)useVerticalMode +{ + NSMutableArray *candidatesArray = [[NSMutableArray alloc] init]; + + size_t cursorIndex = [self _actualCandidateCursorIndex]; + vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); + + // sort the nodes, so that longer nodes (representing longer phrases) are placed at the top of the candidate list + stable_sort(nodes.begin(), nodes.end(), NodeAnchorDescendingSorter()); + + // then use the C++ trick to retrieve the candidates for each node at/crossing the cursor + for (vector::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { + const vector &candidates = (*ni).node->candidates(); + for (vector::const_iterator ci = candidates.begin(), ce = candidates.end(); ci != ce; ++ci) { + [candidatesArray addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]]; + } + } + + InputStateChoosingCandidate *state = [[InputStateChoosingCandidate alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex candidates:candidatesArray useVerticalMode:useVerticalMode]; + return state; +} + +- (size_t)_actualCandidateCursorIndex +{ + size_t cursorIndex = _builder->cursorIndex(); + if (Preferences.selectPhraseAfterCursorAsCandidate) { + // MS Phonetics IME style, phrase is *after* the cursor, i.e. cursor is always *before* the phrase + if (cursorIndex < _builder->length()) { + ++cursorIndex; + } + } else { + if (!cursorIndex) { + ++cursorIndex; + } + } + + return cursorIndex; +} + +- (NSArray *)_currentReadings +{ + NSMutableArray *readingsArray = [[NSMutableArray alloc] init]; + vector v = _builder->readings(); + for (vector::iterator it_i = v.begin(); it_i != v.end(); ++it_i) { + [readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]]; + } + return readingsArray; +} + +@end diff --git a/Source/Engine/ControllerModules/KeyHandlerInput.swift b/Source/Engine/ControllerModules/KeyHandlerInput.swift new file mode 100644 index 00000000..6946e3e7 --- /dev/null +++ b/Source/Engine/ControllerModules/KeyHandlerInput.swift @@ -0,0 +1,178 @@ +/* + * KeyHandlerInput.swift + * + * Copyright 2021-2022 vChewing Project (3-Clause BSD License). + * Derived from 2011-2022 OpenVanilla Project (MIT License). + * Some rights reserved. See "LICENSE.TXT" for details. + */ + +import Cocoa + +enum KeyCode: UInt16 { + case none = 0 + case enter = 76 + case up = 126 + case down = 125 + case left = 123 + case right = 124 + case pageUp = 116 + case pageDown = 121 + case home = 115 + case end = 119 + case delete = 117 +} + +class KeyHandlerInput: NSObject { + @objc private (set) var useVerticalMode: Bool + @objc private (set) var inputText: String? + @objc private (set) var charCode: UInt16 + private var keyCode: UInt16 + private var flags: NSEvent.ModifierFlags + private var cursorForwardKey: KeyCode + private var cursorBackwardKey: KeyCode + private var extraChooseCandidateKey: KeyCode + private var absorbedArrowKey: KeyCode + private var verticalModeOnlyChooseCandidateKey: KeyCode + @objc private (set) var emacsKey: vChewingEmacsKey + + @objc init(inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, isVerticalMode: Bool) { + self.inputText = inputText + self.keyCode = keyCode + self.charCode = charCode + self.flags = flags + useVerticalMode = isVerticalMode + emacsKey = EmacsKeyHelper.detect(charCode: charCode, flags: flags) + cursorForwardKey = useVerticalMode ? .down : .right + cursorBackwardKey = useVerticalMode ? .up : .left + extraChooseCandidateKey = useVerticalMode ? .left : .down + absorbedArrowKey = useVerticalMode ? .right : .up + verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none + super.init() + } + + @objc init(event: NSEvent, isVerticalMode: Bool) { + inputText = event.characters + keyCode = event.keyCode + flags = event.modifierFlags + useVerticalMode = isVerticalMode + let charCode: UInt16 = { + guard let inputText = event.characters, inputText.count > 0 else { + return 0 + } + let first = inputText[inputText.startIndex].utf16.first! + return first + }() + self.charCode = charCode + emacsKey = EmacsKeyHelper.detect(charCode: charCode, flags: event.modifierFlags) + cursorForwardKey = useVerticalMode ? .down : .right + cursorBackwardKey = useVerticalMode ? .up : .left + extraChooseCandidateKey = useVerticalMode ? .left : .down + absorbedArrowKey = useVerticalMode ? .right : .up + verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none + super.init() + } + + @objc var isShiftHold: Bool { + flags.contains([.shift]) + } + + @objc var isCommandHold: Bool { + flags.contains([.command]) + } + + @objc var isControlHold: Bool { + flags.contains([.control]) + } + + @objc var isOptionHold: Bool { + flags.contains([.option]) + } + + @objc var isCapsLockOn: Bool { + flags.contains([.capsLock]) + } + + @objc var isNumericPad: Bool { + flags.contains([.numericPad]) + } + + @objc var isEnter: Bool { + KeyCode(rawValue: keyCode) == KeyCode.enter + } + + @objc var isUp: Bool { + KeyCode(rawValue: keyCode) == KeyCode.up + } + + @objc var isDown: Bool { + KeyCode(rawValue: keyCode) == KeyCode.down + } + + @objc var isLeft: Bool { + KeyCode(rawValue: keyCode) == KeyCode.left + } + + @objc var isRight: Bool { + KeyCode(rawValue: keyCode) == KeyCode.right + } + + @objc var isPageUp: Bool { + KeyCode(rawValue: keyCode) == KeyCode.pageUp + } + + @objc var isPageDown: Bool { + KeyCode(rawValue: keyCode) == KeyCode.pageDown + } + + @objc var isHome: Bool { + KeyCode(rawValue: keyCode) == KeyCode.home + } + + @objc var isEnd: Bool { + KeyCode(rawValue: keyCode) == KeyCode.end + } + + @objc var isDelete: Bool { + KeyCode(rawValue: keyCode) == KeyCode.delete + } + + @objc var isCursorBackward: Bool { + KeyCode(rawValue: keyCode) == cursorBackwardKey + } + + @objc var isCursorForward: Bool { + KeyCode(rawValue: keyCode) == cursorForwardKey + } + + @objc var isAbsorbedArrowKey: Bool { + KeyCode(rawValue: keyCode) == absorbedArrowKey + } + + @objc var isExtraChooseCandidateKey: Bool { + KeyCode(rawValue: keyCode) == extraChooseCandidateKey + } + + @objc var isVerticalModeOnlyChooseCandidateKey: Bool { + KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey + } + +} + +@objc enum vChewingEmacsKey: UInt16 { + case none = 0 + case forward = 6 // F + case backward = 2 // B + case home = 1 // A + case end = 5 // E + case delete = 4 // D + case nextPage = 22 // V +} + +class EmacsKeyHelper: NSObject { + @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { + if flags.contains(.control) { + return vChewingEmacsKey(rawValue: charCode) ?? .none + } + return .none; + } +} diff --git a/Source/Engine/vChewing/KeyValueBlobReader.cpp b/Source/Engine/ControllerModules/KeyValueBlobReader.cpp similarity index 100% rename from Source/Engine/vChewing/KeyValueBlobReader.cpp rename to Source/Engine/ControllerModules/KeyValueBlobReader.cpp diff --git a/Source/Engine/vChewing/KeyValueBlobReader.h b/Source/Engine/ControllerModules/KeyValueBlobReader.h similarity index 100% rename from Source/Engine/vChewing/KeyValueBlobReader.h rename to Source/Engine/ControllerModules/KeyValueBlobReader.h diff --git a/Source/Engine/Gramambular/BlockReadingBuilder.h b/Source/Engine/Gramambular/BlockReadingBuilder.h index af4e4b97..1a68d8d0 100644 --- a/Source/Engine/Gramambular/BlockReadingBuilder.h +++ b/Source/Engine/Gramambular/BlockReadingBuilder.h @@ -34,9 +34,7 @@ namespace Taiyan { void setJoinSeparator(const string& separator); const string joinSeparator() const; - size_t markerCursorIndex() const; - void setMarkerCursorIndex(size_t inNewIndex); - vector readingsAtRange(size_t begin, size_t end) const; + vector readings() const; Grid& grid(); @@ -49,7 +47,6 @@ namespace Taiyan { static const size_t MaximumBuildSpanLength = 10; size_t m_cursorIndex; - size_t m_markerCursorIndex; vector m_readings; Grid m_grid; @@ -60,14 +57,12 @@ namespace Taiyan { inline BlockReadingBuilder::BlockReadingBuilder(LanguageModel *inLM) : m_LM(inLM) , m_cursorIndex(0) - , m_markerCursorIndex(SIZE_MAX) { } inline void BlockReadingBuilder::clear() { m_cursorIndex = 0; - m_markerCursorIndex = SIZE_MAX; m_readings.clear(); m_grid.clear(); } @@ -86,21 +81,6 @@ namespace Taiyan { { m_cursorIndex = inNewIndex > m_readings.size() ? m_readings.size() : inNewIndex; } - - inline size_t BlockReadingBuilder::markerCursorIndex() const - { - return m_markerCursorIndex; - } - - inline void BlockReadingBuilder::setMarkerCursorIndex(size_t inNewIndex) - { - if (inNewIndex == SIZE_MAX) { - m_markerCursorIndex = SIZE_MAX; - return; - } - - m_markerCursorIndex = inNewIndex > m_readings.size() ? m_readings.size() : inNewIndex; - } inline void BlockReadingBuilder::insertReadingAtCursor(const string& inReading) { @@ -111,12 +91,9 @@ namespace Taiyan { m_cursorIndex++; } - inline vector BlockReadingBuilder::readingsAtRange(size_t begin, size_t end) const { - vector v; - for (size_t i = begin; i < end; i++) { - v.push_back(m_readings[i]); - } - return v; + inline vector BlockReadingBuilder::readings() const + { + return m_readings; } inline bool BlockReadingBuilder::deleteReadingBeforeCursor() diff --git a/Source/Engine/Keyboard/EmacsKeyHelper.swift b/Source/Engine/Keyboard/EmacsKeyHelper.swift deleted file mode 100644 index 45e2818b..00000000 --- a/Source/Engine/Keyboard/EmacsKeyHelper.swift +++ /dev/null @@ -1,28 +0,0 @@ -/* - * EmacsKeyHelper.swift - * - * Copyright 2021-2022 vChewing Project (3-Clause BSD License). - * Derived from 2011-2022 OpenVanilla Project (MIT License). - * Some rights reserved. See "LICENSE.TXT" for details. - */ - -import Cocoa - -@objc enum vChewingEmacsKey: UInt16 { - case none = 0 - case forward = 6 // F - case backward = 2 // B - case home = 1 // A - case end = 5 // E - case delete = 4 // D - case nextPage = 22 // V -} - -class EmacsKeyHelper: NSObject { - @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { - if flags.contains(.control) { - return vChewingEmacsKey(rawValue: charCode) ?? .none - } - return .none; - } -} diff --git a/Source/InputMethodController.h b/Source/InputMethodController.h index 75987ec2..3ae0fe35 100644 --- a/Source/InputMethodController.h +++ b/Source/InputMethodController.h @@ -8,43 +8,8 @@ #import #import -#import "Mandarin.hh" -#import "Gramambular.h" -#import "vChewingLM.h" -#import "UserOverrideModel.h" +#import "vChewing-Swift.h" @interface vChewingInputMethodController : IMKInputController -{ -@private - // the reading buffer that takes user input - Taiyan::Mandarin::BopomofoReadingBuffer* _bpmfReadingBuffer; - - // language model - vChewing::vChewingLM *_languageModel; - - // user override model - vChewing::UserOverrideModel *_userOverrideModel; - - // the grid (lattice) builder for the unigrams (and bigrams) - Taiyan::Gramambular::BlockReadingBuilder* _builder; - - // latest walked path (trellis) using the Viterbi algorithm - std::vector _walkedNodes; - - // the latest composing buffer that is updated to the foreground app - NSMutableString *_composingBuffer; - NSInteger _latestReadingCursor; - - // the current text input client; we need to keep this when candidate panel is on - id _currentCandidateClient; - - // a special deferred client for Terminal.app fix - id _currentDeferredClient; - - // currently available candidates - NSMutableArray *_candidates; - - // current input mode - NSString *_inputMode; -} +- (void)handleState:(InputState *)newState client:(id)client; @end diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index c09ba8a2..53bb1143 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -6,125 +6,65 @@ * Some rights reserved. See "LICENSE.TXT" for details. */ +#import "vChewingLM.h" #import "InputMethodController.h" -#import -#import -#import -#import "OVUTF8Helper.h" +#import "KeyHandler.h" #import "LanguageModelManager.h" -#import "vChewing-Swift.h" -// C++ namespace usages using namespace std; -using namespace Taiyan::Mandarin; -using namespace Taiyan::Gramambular; using namespace vChewing; -using namespace OpenVanilla; static const NSInteger kMinKeyLabelSize = 10; -// input modes -static NSString *const kBopomofoModeIdentifierCHT = @"org.atelierInmu.inputmethod.vChewing.TradBopomofo"; -static NSString *const kBopomofoModeIdentifierCHS = @"org.atelierInmu.inputmethod.vChewing.SimpBopomofo"; - -// key code enums -enum { - kEnterKeyCode = 76, - kUpKeyCode = 126, - kDownKeyCode = 125, - kLeftKeyCode = 123, - kRightKeyCode = 124, - kPageUpKeyCode = 116, - kPageDownKeyCode = 121, - kHomeKeyCode = 115, - kEndKeyCode = 119, - kDeleteKeyCode = 117 -}; - VTCandidateController *gCurrentCandidateController = nil; -// if DEBUG is defined, a DOT file (GraphViz format) will be written to the -// specified path everytime the grid is walked -#if DEBUG -static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot"; -#endif - -// https://clang-analyzer.llvm.org/faq.html __attribute__((annotate("returns_localized_nsstring"))) static inline NSString *LocalizationNotNeeded(NSString *s) { return s; } +@interface vChewingInputMethodController () +{ + // the current text input client; we need to keep this when candidate panel is on + id _currentCandidateClient; + + // a special deferred client for Terminal.app fix + id _currentDeferredClient; + + KeyHandler *_keyHandler; + InputState *_state; +} +@end + @interface vChewingInputMethodController (VTCandidateController) @end -// sort helper -class NodeAnchorDescendingSorter -{ -public: - bool operator()(const NodeAnchor& a, const NodeAnchor &b) const { - return a.node->key().length() > b.node->key().length(); - } -}; +@interface vChewingInputMethodController (KeyHandlerDelegate) +@end -static const double kEpsilon = 0.000001; - -static double FindHighestScore(const vector& nodes, double epsilon) { - double highestScore = 0.0; - for (auto ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { - double score = ni->node->highestUnigramScore(); - if (score > highestScore) { - highestScore = score; - } - } - return highestScore + epsilon; -} +@interface vChewingInputMethodController (UI) ++ (VTHorizontalCandidateController *)horizontalCandidateController; ++ (VTVerticalCandidateController *)verticalCandidateController; ++ (TooltipController *)tooltipController; +- (void)_showTooltip:(NSString *)tooltip composingBuffer:(NSString *)composingBuffer cursorIndex:(NSInteger)cursorIndex client:(id)client; +- (void)_hideTooltip; +@end @implementation vChewingInputMethodController -- (void)dealloc -{ - // clean up everything - if (_bpmfReadingBuffer) { - delete _bpmfReadingBuffer; - } - - if (_builder) { - delete _builder; - } - // the two client pointers are weak pointers (i.e. we don't retain them) - // therefore we don't do anything about it -} - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)client { - // an instance is initialized whenever a text input client (a Mac app) requires - // text input from an IME + // an instance is initialized whenever a text input client (a Mac app) requires + // text input from an IME - self = [super initWithServer:server delegate:delegate client:client]; - if (self) { - _candidates = [[NSMutableArray alloc] init]; + self = [super initWithServer:server delegate:delegate client:client]; + if (self) { + _keyHandler = [[KeyHandler alloc] init]; + _keyHandler.delegate = self; + _state = [[InputStateEmpty alloc] init]; + } - // create the reading buffer - _bpmfReadingBuffer = new BopomofoReadingBuffer(BopomofoKeyboardLayout::StandardLayout()); - - // create the lattice builder - _languageModel = [LanguageModelManager languageModelCoreCHT]; - _languageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); - _languageModel->setCNSEnabled(Preferences.cns11643Enabled); - _userOverrideModel = [LanguageModelManager userOverrideModelCHT]; - - _builder = new BlockReadingBuilder(_languageModel); - - // each Mandarin syllable is separated by a hyphen - _builder->setJoinSeparator("-"); - - // create the composing buffer - _composingBuffer = [[NSMutableString alloc] init]; - - _inputMode = kBopomofoModeIdentifierCHT; - } - - return self; + return self; } - (NSMenu *)menu @@ -147,7 +87,8 @@ static double FindHighestScore(const vector& nodes, double epsilon) chineseConversionMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl; chineseConversionMenuItem.state = Preferences.chineseConversionEnabled ? NSControlStateValueOn : NSControlStateValueOff; - NSMenuItem *halfWidthPunctuationMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@""]; + NSMenuItem *halfWidthPunctuationMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Half-Width Punctuation Mode", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@"H"]; + halfWidthPunctuationMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl; halfWidthPunctuationMenuItem.state = Preferences.halfWidthPunctuationEnabled ? NSControlStateValueOn : NSControlStateValueOff; if (optionKeyPressed) { @@ -195,1298 +136,325 @@ static double FindHighestScore(const vector& nodes, double epsilon) // reset the state _currentDeferredClient = nil; _currentCandidateClient = nil; - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; + [_keyHandler clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [self handleState:empty client:client]; - // checks and populates the default settings - switch (Preferences.keyboardLayout) { - case KeyboardLayoutStandard: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); - break; - case KeyboardLayoutEten: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETenLayout()); - break; - case KeyboardLayoutHsu: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HsuLayout()); - break; - case KeyboardLayoutEten26: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::ETen26Layout()); - break; - case KeyboardLayoutHanyuPinyin: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::HanyuPinyinLayout()); - break; - case KeyboardLayoutIBM: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::IBMLayout()); - break; - default: - _bpmfReadingBuffer->setKeyboardLayout(BopomofoKeyboardLayout::StandardLayout()); - Preferences.keyboardLayout = KeyboardLayoutStandard; - } - - [(AppDelegate *)[NSApp delegate] checkForUpdate]; + // checks and populates the default settings + [_keyHandler syncWithPreferences]; + [(AppDelegate *) NSApp.delegate checkForUpdate]; } - (void)deactivateServer:(id)client { - // clean up reading buffer residues - if (!_bpmfReadingBuffer->isEmpty()) { - _bpmfReadingBuffer->clear(); - [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - } - - // commit any residue in the composing buffer - [self commitComposition:client]; - - _currentDeferredClient = nil; - _currentCandidateClient = nil; - - gCurrentCandidateController.delegate = nil; - gCurrentCandidateController.visible = NO; - [_candidates removeAllObjects]; - - [self _hideTooltip]; + [_keyHandler clear]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [self handleState:empty client:client]; + InputStateDeactivated *inactive = [[InputStateDeactivated alloc] init]; + [self handleState:inactive client:client]; } - (void)setValue:(id)value forTag:(long)tag client:(id)sender { NSString *newInputMode; - vChewingLM *newLanguageModel; - UserOverrideModel *newUserOverrideModel; if ([value isKindOfClass:[NSString class]] && [value isEqual:kBopomofoModeIdentifierCHS]) { newInputMode = kBopomofoModeIdentifierCHS; - newLanguageModel = [LanguageModelManager languageModelCoreCHS]; - newUserOverrideModel = [LanguageModelManager userOverrideModelCHS]; } else { newInputMode = kBopomofoModeIdentifierCHT; - newLanguageModel = [LanguageModelManager languageModelCoreCHT]; - newUserOverrideModel = [LanguageModelManager userOverrideModelCHT]; } + + if (![_keyHandler.inputMode isEqualToString:newInputMode]) { + [[NSUserDefaults standardUserDefaults] synchronize]; - // 自 Preferences 模組讀入自訂語彙置換功能開關狀態。 - newLanguageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled); + // Remember to override the keyboard layout again -- treat this as an activate event. + NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; + [sender overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; + [_keyHandler clear]; + _keyHandler.inputMode = newInputMode; + InputState *empty = [[InputState alloc] init]; + [self handleState:empty client:sender]; + } - // 自 Preferences 模組讀取全字庫模式開關狀態。 - newLanguageModel->setCNSEnabled(Preferences.cns11643Enabled); - - // Only apply the changes if the value is changed - if (![_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; - _userOverrideModel = newUserOverrideModel; - - if (!_bpmfReadingBuffer->isEmpty()) { - _bpmfReadingBuffer->clear(); - [self updateClientComposingBuffer:sender]; - } - - if ([_composingBuffer length] > 0) { - [self commitComposition:sender]; - } - - if (_builder) { - delete _builder; - _builder = new BlockReadingBuilder(_languageModel); - _builder->setJoinSeparator("-"); - } - } } #pragma mark - IMKServerInput protocol methods +- (NSUInteger)recognizedEvents:(id)sender +{ + return NSEventMaskKeyDown | NSEventMaskFlagsChanged; +} + +- (BOOL)handleEvent:(NSEvent *)event client:(id)client +{ + if ([event type] == NSEventMaskFlagsChanged) { + NSString *functionKeyKeyboardLayoutID = Preferences.functionKeyboardLayout; + NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; + + // If no override is needed, just return NO. + if ([functionKeyKeyboardLayoutID isEqualToString:basisKeyboardLayoutID]) { + return NO; + } + + // Function key pressed. + BOOL includeShift = Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey; + if ((event.modifierFlags & ~NSEventModifierFlagShift) || ((event.modifierFlags & NSEventModifierFlagShift) && includeShift)) { + // Override the keyboard layout and let the OS do its thing + [client overrideKeyboardWithKeyboardNamed:functionKeyKeyboardLayoutID]; + return NO; + } + + // Revert to the basis layout when the function key is released + [client overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; + return NO; + } + + NSRect textFrame = NSZeroRect; + NSDictionary *attributes = nil; + BOOL useVerticalMode = NO; + + @try { + attributes = [client attributesForCharacterIndex:0 lineHeightRectangle:&textFrame]; + useVerticalMode = attributes[@"IMKTextOrientation"] && [attributes[@"IMKTextOrientation"] integerValue] == 0; + } + @catch (NSException *e) { + // exception may raise while using Twitter.app's search filed. + } + + if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && [NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { + // special handling for com.apple.Terminal + _currentDeferredClient = client; + } + + KeyHandlerInput *input = [[KeyHandlerInput alloc] initWithEvent:event isVerticalMode:useVerticalMode]; + BOOL result = [_keyHandler handleInput:input state:_state stateCallback:^(InputState *state) { + [self handleState:state client:client]; + } candidateSelectionCallback:^{ + NSLog(@"candidate window updated."); + } errorCallback:^{ + [clsSFX beep]; + }]; + + return result; +} + +#pragma mark - States Handling + - (NSString *)_convertToKangXi:(NSString *)text { + if (!Preferences.chineseConversionEnabled) { + return text; // 沒啟用的話就不要轉換。 + } // return [VXHanConvert convertToSimplifiedFrom:text]; // VXHanConvert 這個引擎有點落後了,不支援詞組轉換、且修改轉換表的過程很麻煩。 // OpenCC 引擎別的都還好,就是有點肥。改日換成純 ObjC 的 OpenCC 實現方案。 return [OpenCCBridge convertToKangXi:text]; } -- (void)commitComposition:(id)client +- (void)_commitText:(NSString *)text client:(id)client { - // if it's Terminal, we don't commit at the first call (the client of which will not be IPMDServerClientWrapper) - // then we defer the update in the next runloop round -- so that the composing buffer is not - // meaninglessly flushed, an annoying bug in Terminal.app since Mac OS X 10.5 - if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && ![NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { - if (_currentDeferredClient) { - [self performSelector:@selector(updateClientComposingBuffer:) withObject:_currentDeferredClient afterDelay:0.0]; - } - return; - } + NSString *buffer = [self _convertToKangXi:text]; + if (!buffer.length) { + return;; + } - // Chinese conversion. - NSString *buffer = _composingBuffer; - - if (Preferences.chineseConversionEnabled) { - buffer = [self _convertToKangXi:_composingBuffer]; - } - - // commit the text, clear the state - [client insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; - gCurrentCandidateController.visible = NO; - [_candidates removeAllObjects]; - [self _hideTooltip]; + // if it's Terminal, we don't commit at the first call (the client of which will not be IPMDServerClientWrapper) + // then we defer the update in the next runloop round -- so that the composing buffer is not + // meaninglessly flushed, an annoying bug in Terminal.app since Mac OS X 10.5 + if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && ![NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { + if (_currentDeferredClient) { + id currentDeferredClient = _currentDeferredClient; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [currentDeferredClient insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + }); + } + return; + } + [client insertText:buffer replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } -NS_INLINE size_t min(size_t a, size_t b) { return a < b ? a : b; } -NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - -// TODO: bug #28 is more likely to live in this method. -- (void)updateClientComposingBuffer:(id)client +- (void)handleState:(InputState *)newState client:(id)client { - // "updating the composing buffer" means to request the client to "refresh" the text input buffer - // with our "composing text" +// NSLog(@"new state: %@ / current state: %@", newState, _state); - [_composingBuffer setString:@""]; - NSInteger composedStringCursorIndex = 0; + // We need to set the state to the member variable since the candidate + // window need to read the candidates from it. + InputState *previous = _state; + _state = newState; - 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]; - - if (_bpmfReadingBuffer->isEmpty() && _builder->markerCursorIndex() != SIZE_MAX) { - // if there is a marked range, we need to tear the string into three parts. - NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:composedText]; - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - [attrString setAttributes:@{ - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - NSMarkedClauseSegmentAttributeName: @0 - } range:NSMakeRange(0, begin)]; - [attrString setAttributes:@{ - NSUnderlineStyleAttributeName: @(NSUnderlineStyleThick), - NSMarkedClauseSegmentAttributeName: @1 - } range:NSMakeRange(begin, end - begin)]; - [attrString setAttributes:@{ - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - NSMarkedClauseSegmentAttributeName: @2 - } range:NSMakeRange(end, [composedText length] - end)]; - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put ths composing buffer - [client setMarkedText:attrString selectionRange:NSMakeRange((NSInteger)_builder->markerCursorIndex(), 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _latestReadingCursor = (NSInteger)_builder->markerCursorIndex(); - [self _showCurrentMarkedTextTooltipWithClient:client]; - } - else { - // we must use NSAttributedString so that the cursor is visible -- - // can't just use NSString - NSDictionary *attrDict = @{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - NSMarkedClauseSegmentAttributeName: @0}; - NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:composedText attributes:attrDict]; - - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put ths composing buffer - [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _latestReadingCursor = cursorIndex; - [self _hideTooltip]; - } + if ([newState isKindOfClass:[InputStateDeactivated class]]) { + [self _handleDeactivated:(InputStateDeactivated *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateEmpty class]]) { + [self _handleEmpty:(InputStateEmpty *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateEmptyIgnoringPreviousState class]]) { + [self _handleEmptyIgnoringPrevious:(InputStateEmptyIgnoringPreviousState *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateCommitting class]]) { + [self _handleCommitting:(InputStateCommitting *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateInputting class]]) { + [self _handleInputting:(InputStateInputting *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateMarking class]]) { + [self _handleMarking:(InputStateMarking *) newState previous:previous client:client]; + } else if ([newState isKindOfClass:[InputStateChoosingCandidate class]]) { + [self _handleChoosingCandidate:(InputStateChoosingCandidate *) newState previous:previous client:client]; + } } -- (void)walk +- (void)_handleDeactivated:(InputStateDeactivated *)state previous:(InputState *)previous client:(id)client { - // retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation - // of the best possible Mandarain characters given the input syllables, - // using the Viterbi algorithm implemented in the Gramambular library - Walker walker(&_builder->grid()); + // commit any residue in the composing buffer + if ([previous isKindOfClass:[InputStateInputting class]]) { + NSString *buffer = ((InputStateInputting *) previous).composingBuffer; + [self _commitText:buffer client:client]; + } + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - // the reverse walk traces the trellis from the end - _walkedNodes = walker.reverseWalk(_builder->grid().width()); + _currentDeferredClient = nil; + _currentCandidateClient = nil; - // 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 + gCurrentCandidateController.delegate = nil; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; } -- (void)popOverflowComposingTextAndWalk:(id)client +- (void)_handleEmpty:(InputStateEmpty *)state previous:(InputState *)previous client:(id)client { - // in an ideal world, we can as well let the user type forever, - // but because the Viterbi algorithm has a complexity of O(N^2), - // the walk will become slower as the number of nodes increase, - // therefore we need to "pop out" overflown text -- they usually - // lose their influence over the whole MLE anyway -- so tht when - // the user type along, the already composed text at front will - // be popped out + // commit any residue in the composing buffer + if ([previous isKindOfClass:[InputStateInputting class]]) { + NSString *buffer = ((InputStateInputting *) previous).composingBuffer; + [self _commitText:buffer client:client]; + } - NSInteger composingBufferSize = Preferences.composingBufferSize; - - if (_builder->grid().width() > (size_t)composingBufferSize) { - if (_walkedNodes.size() > 0) { - NodeAnchor &anchor = _walkedNodes[0]; - NSString *popedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()]; - // Chinese conversion. - BOOL chineseConversionEnabled = Preferences.chineseConversionEnabled; - if (chineseConversionEnabled) { - popedText = [self _convertToKangXi:popedText]; - } - [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _builder->removeHeadReadings(anchor.spanningLength); - } - } - - [self walk]; + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; } -- (void)beep +- (void)_handleEmptyIgnoringPrevious:(InputStateEmptyIgnoringPreviousState *)state previous:(InputState *)previous client:(id)client { - // use the vChewing beep. - [clsSFX beep]; + [client setMarkedText:@"" selectionRange:NSMakeRange(0, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; } -- (string)_currentLayout +- (void)_handleCommitting:(InputStateCommitting *)state previous:(InputState *)previous client:(id)client { - NSString *keyboardLayoutName = Preferences.keyboardLayoutName; - string layout = string(keyboardLayoutName.UTF8String) + string("_"); - return layout; + NSString *poppedText = state.poppedText; + [self _commitText:poppedText client:client]; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; } -- (BOOL)handleInputText:(NSString*)inputText key:(NSInteger)keyCode modifiers:(NSUInteger)flags client:(id)client +- (void)_handleInputting:(InputStateInputting *)state previous:(InputState *)previous client:(id)client { - NSRect textFrame = NSZeroRect; - NSDictionary *attributes = nil; + NSString *poppedText = state.poppedText; + if (poppedText.length) { + [self _commitText:poppedText client:client]; + } - bool composeReading = false; - BOOL useVerticalMode = NO; + NSUInteger cursorIndex = state.cursorIndex; + NSAttributedString *attrString = state.attributedString; - @try { - attributes = [client attributesForCharacterIndex:0 lineHeightRectangle:&textFrame]; - useVerticalMode = [attributes objectForKey:@"IMKTextOrientation"] && [[attributes objectForKey:@"IMKTextOrientation"] integerValue] == 0; - } - @catch (NSException *e) { - // exception may raise while using Twitter.app's search filed. - } + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put ths composing buffer + [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - NSInteger cursorForwardKey = useVerticalMode ? kDownKeyCode : kRightKeyCode; - NSInteger cursorBackwardKey = useVerticalMode ? kUpKeyCode : kLeftKeyCode; - NSInteger extraChooseCandidateKey = useVerticalMode ? kLeftKeyCode : kDownKeyCode; - NSInteger absorbedArrowKey = useVerticalMode ? kRightKeyCode : kUpKeyCode; - NSInteger verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : 0; - - // get the unicode character code - UniChar charCode = [inputText length] ? [inputText characterAtIndex:0] : 0; - - vChewingEmacsKey emacsKey = [EmacsKeyHelper detectWithCharCode:charCode flags:flags]; - - if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && [NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { - // special handling for com.apple.Terminal - _currentDeferredClient = client; - } - - // if the inputText is empty, it's a function key combination, we ignore it - if (![inputText length]) { - return NO; - } - - // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it - if (![_composingBuffer length] && - _bpmfReadingBuffer->isEmpty() && - ((flags & NSEventModifierFlagCommand) || (flags & NSEventModifierFlagControl) || (flags & NSEventModifierFlagOption) || (flags & NSEventModifierFlagNumericPad))) { - return NO; - } - - // Caps Lock processing : if Caps Lock is on, temporarily disable bopomofo. - if (charCode == 8 || charCode == 13 || keyCode == absorbedArrowKey || keyCode == extraChooseCandidateKey || keyCode == cursorForwardKey || keyCode == cursorBackwardKey) { - // do nothing if backspace is pressed -- we ignore the key - } - else if (flags & NSAlphaShiftKeyMask) { - // process all possible combination, we hope. - if ([_composingBuffer length]) { - [self commitComposition:client]; - } - - // first commit everything in the buffer. - if (flags & NSEventModifierFlagShift) { - return NO; - } - - // if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char insertions. - if (charCode < 0x80 && !isprint(charCode)) { - return NO; - } - - // when shift is pressed, don't do further processing, since it outputs capital letter anyway. - NSString *popedText = [inputText lowercaseString]; - [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - return YES; - } - - if (flags & NSEventModifierFlagNumericPad) { - if (keyCode != kLeftKeyCode && keyCode != kRightKeyCode && keyCode != kDownKeyCode && keyCode != kUpKeyCode && charCode != 32 && isprint(charCode)) { - if ([_composingBuffer length]) { - [self commitComposition:client]; - } - - NSString *popedText = [inputText lowercaseString]; - [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - return YES; - } - } - - // if we have candidate, it means we need to pass the event to the candidate handler - if ([_candidates count]) { - return [self _handleCandidateEventWithInputText:inputText charCode:charCode keyCode:keyCode emacsKey:(vChewingEmacsKey)emacsKey]; - } - - // If we have marker index. - if (_builder->markerCursorIndex() != SIZE_MAX) { - // ESC - if (charCode == 27) { - _builder->setMarkerCursorIndex(SIZE_MAX); - [self updateClientComposingBuffer:client]; - return YES; - } - // Enter - if (charCode == 13) { - if ([self _writeUserPhrase]) { - _builder->setMarkerCursorIndex(SIZE_MAX); - } - else { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - // Shift + left - if ((keyCode == cursorBackwardKey || emacsKey == vChewingEmacsKeyBackward) - && (flags & NSEventModifierFlagShift)) { - if (_builder->markerCursorIndex() > 0) { - _builder->setMarkerCursorIndex(_builder->markerCursorIndex() - 1); - } - else { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - // Shift + Right - if ((keyCode == cursorForwardKey || emacsKey == vChewingEmacsKeyForward) - && (flags & NSEventModifierFlagShift)) { - if (_builder->markerCursorIndex() < _builder->length()) { - _builder->setMarkerCursorIndex(_builder->markerCursorIndex() + 1); - } - else { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - - _builder->setMarkerCursorIndex(SIZE_MAX); - } - - // see if it's valid BPMF reading - if (_bpmfReadingBuffer->isValidKey((char)charCode)) { - _bpmfReadingBuffer->combineKey((char)charCode); - - // if we have a tone marker, we have to insert the reading to the - // builder in other words, if we don't have a tone marker, we just - // update the composing buffer - composeReading = _bpmfReadingBuffer->hasToneMarker(); - if (!composeReading) { - [self updateClientComposingBuffer:client]; - return YES; - } - } - - // see if we have composition if Enter/Space is hit and buffer is not empty - // this is bit-OR'ed so that the tone marker key is also taken into account - composeReading |= (!_bpmfReadingBuffer->isEmpty() && (charCode == 32 || charCode == 13)); - if (composeReading) { - // combine the reading - string reading = _bpmfReadingBuffer->syllable().composedString(); - - // see if we have a unigram for this - if (!_languageModel->hasUnigramsForKey(reading)) { - [self beep]; - [self updateClientComposingBuffer:client]; - return YES; - } - - // and insert it into the lattice - _builder->insertReadingAtCursor(reading); - - // then walk the lattice - [self popOverflowComposingTextAndWalk:client]; - - // get user override model suggestion - string overrideValue = _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(), [[NSDate date] timeIntervalSince1970]); - - if (!overrideValue.empty()) { - size_t cursorIndex = [self actualCandidateCursorIndex]; - vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); - double highestScore = FindHighestScore(nodes, kEpsilon); - _builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue, highestScore); - } - - // then update the text - _bpmfReadingBuffer->clear(); - [self updateClientComposingBuffer:client]; - - // 模擬 WINNT 351 ㄅ半注音,就是每個漢字都自動要選字的那種注音。 - // 嚴格來講不能算純正的ㄅ半注音,畢竟候選字的順序不可能會像當年那樣了。 - // 如果簡體中文用戶不知道ㄅ半注音是什麼的話,拿全拼輸入法來比喻恐怕比較恰當。 - if (Preferences.useWinNT351BPMF) { - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - } - - // and tells the client that the key is consumed - return YES; - } - - // keyCode 125 = Down, charCode 32 = Space - if (_bpmfReadingBuffer->isEmpty() && [_composingBuffer length] > 0 && (keyCode == extraChooseCandidateKey || charCode == 32 || (useVerticalMode && (keyCode == verticalModeOnlyChooseCandidateKey)))) { - if (charCode == 32) { - // if the spacebar is NOT set to be a selection key - if ((flags & NSEventModifierFlagShift) != 0 || !Preferences.chooseCandidateUsingSpace) { - if (_builder->cursorIndex() >= _builder->length()) { - [_composingBuffer appendString:@" "]; - [self commitComposition:client]; - _bpmfReadingBuffer->clear(); - } - else if (_languageModel->hasUnigramsForKey(" ")) { - _builder->insertReadingAtCursor(" "); - [self popOverflowComposingTextAndWalk:client]; - [self updateClientComposingBuffer:client]; - } - return YES; - - } - } - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - return YES; - } - - // Esc - if (charCode == 27) { - BOOL escToClearInputBufferEnabled = Preferences.escToCleanInputBuffer; - - if (escToClearInputBufferEnabled) { - // if the optioon is enabled, we clear everythiong including the composing - // buffer, walked nodes and the reading. - if (![_composingBuffer length]) { - return NO; - } - _bpmfReadingBuffer->clear(); - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; - } - else { - // if reading is not empty, we cancel the reading; Apple's built-in - // Zhuyin (and the erstwhile Hanin) has a default option that Esc - // "cancels" the current composed character and revert it to - // Bopomofo reading, in odds with the expectation of users from - // other platforms - - if (_bpmfReadingBuffer->isEmpty()) { - // no nee to beep since the event is deliberately triggered by user - - if (![_composingBuffer length]) { - return NO; - } - } - else { - _bpmfReadingBuffer->clear(); - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // handle cursor backward - if (keyCode == cursorBackwardKey || emacsKey == vChewingEmacsKeyBackward) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (flags & NSEventModifierFlagShift) { - // Shift + left - if (_builder->cursorIndex() > 0) { - _builder->setMarkerCursorIndex(_builder->cursorIndex() - 1); - } - else { - [self beep]; - } - } else { - if (_builder->cursorIndex() > 0) { - _builder->setCursorIndex(_builder->cursorIndex() - 1); - } - else { - [self beep]; - } - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // handle cursor forward - if (keyCode == cursorForwardKey || emacsKey == vChewingEmacsKeyForward) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (flags & NSEventModifierFlagShift) { - // Shift + Right - if (_builder->cursorIndex() < _builder->length()) { - _builder->setMarkerCursorIndex(_builder->cursorIndex() + 1); - } else { - [self beep]; - } - } else { - if (_builder->cursorIndex() < _builder->length()) { - _builder->setCursorIndex(_builder->cursorIndex() + 1); - } - else { - [self beep]; - } - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - if (keyCode == kHomeKeyCode || emacsKey == vChewingEmacsKeyHome) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex()) { - _builder->setCursorIndex(0); - } - else { - [self beep]; - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - if (keyCode == kEndKeyCode || emacsKey == vChewingEmacsKeyEnd) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - else { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex() != _builder->length()) { - _builder->setCursorIndex(_builder->length()); - } - else { - [self beep]; - } - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - if (keyCode == absorbedArrowKey || keyCode == extraChooseCandidateKey) { - if (!_bpmfReadingBuffer->isEmpty()) { - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - - // Backspace - if (charCode == 8) { - if (_bpmfReadingBuffer->isEmpty()) { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex()) { - _builder->deleteReadingBeforeCursor(); - [self walk]; - } - else { - [self beep]; - } - } - else { - _bpmfReadingBuffer->backspace(); - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // Delete - if (keyCode == kDeleteKeyCode || emacsKey == vChewingEmacsKeyDelete) { - if (_bpmfReadingBuffer->isEmpty()) { - if (![_composingBuffer length]) { - return NO; - } - - if (_builder->cursorIndex() != _builder->length()) { - _builder->deleteReadingAfterCursor(); - [self walk]; - } - else { - [self beep]; - } - } - else { - [self beep]; - } - - [self updateClientComposingBuffer:client]; - return YES; - } - - // Enter - if (charCode == 13) { - if (![_composingBuffer length]) { - return NO; - } - - [self commitComposition:client]; - return YES; - } - - // punctuation list - if ((char)charCode == '`') { - if (_languageModel->hasUnigramsForKey(string("_punctuation_list"))) { - if (_bpmfReadingBuffer->isEmpty()) { - _builder->insertReadingAtCursor(string("_punctuation_list")); - [self popOverflowComposingTextAndWalk:client]; - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - } - else { // If there is still unfinished bpmf reading, ignore the punctuation - [self beep]; - } - [self updateClientComposingBuffer:client]; - return YES; - } - } - - // if nothing is matched, see if it's a punctuation key for current layout. - string layout = [self _currentLayout]; - string punctuationNamePrefix = Preferences.halfWidthPunctuationEnabled ? string("_half_punctuation_"): string("_punctuation_"); - string customPunctuation = punctuationNamePrefix + layout + string(1, (char)charCode); - if ([self _handlePunctuation:customPunctuation usingVerticalMode:useVerticalMode client:client]) { - return YES; - } - - // if nothing is matched, see if it's a punctuation key. - string punctuation = punctuationNamePrefix + string(1, (char)charCode); - if ([self _handlePunctuation:punctuation usingVerticalMode:useVerticalMode client:client]) { - return YES; - } - - if ((char)charCode >= 'A' && (char)charCode <= 'Z') { - if ([_composingBuffer length]) { - string letter = string("_letter_") + string(1, (char)charCode); - if ([self _handlePunctuation:letter usingVerticalMode:useVerticalMode client:client]) { - return YES; - } - } - } - - // still nothing, then we update the composing buffer (some app has - // strange behavior if we don't do this, "thinking" the key is not - // actually consumed) - if ([_composingBuffer length] || !_bpmfReadingBuffer->isEmpty()) { - [self beep]; - [self updateClientComposingBuffer:client]; - return YES; - } - - return NO; + gCurrentCandidateController.visible = NO; + [self _hideTooltip]; } -- (BOOL)_handlePunctuation:(string)customPunctuation usingVerticalMode:(BOOL)useVerticalMode client:(id)client +- (void)_handleMarking:(InputStateMarking *)state previous:(InputState *)previous client:(id)client { - if (_languageModel->hasUnigramsForKey(customPunctuation)) { - if (_bpmfReadingBuffer->isEmpty()) { - _builder->insertReadingAtCursor(customPunctuation); - [self popOverflowComposingTextAndWalk:client]; - } - else { // If there is still unfinished bpmf reading, ignore the punctuation - [self beep]; - } - [self updateClientComposingBuffer:client]; + NSUInteger cursorIndex = state.cursorIndex; + NSAttributedString *attrString = state.attributedString; - if (Preferences.useWinNT351BPMF && _bpmfReadingBuffer->isEmpty()) { - [self collectCandidates]; - if ([_candidates count] == 1) { - [self commitComposition:client]; - } - else { - [self _showCandidateWindowUsingVerticalMode:useVerticalMode client:client]; - } - } - return YES; - } - return NO; + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put ths composing buffer + [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + + gCurrentCandidateController.visible = NO; + if (state.tooltip.length) { + [self _showTooltip:state.tooltip composingBuffer:state.composingBuffer cursorIndex:state.markerIndex client:client]; + } else { + [self _hideTooltip]; + } } -- (BOOL)_handleCandidateEventWithInputText:(NSString *)inputText charCode:(UniChar)charCode keyCode:(NSUInteger)keyCode emacsKey:(vChewingEmacsKey)emacsKey +- (void)_handleChoosingCandidate:(InputStateChoosingCandidate *)state previous:(InputState *)previous client:(id)client { - BOOL cancelCandidateKey = - (charCode == 27) || - (Preferences.useWinNT351BPMF && - (charCode == 8 || keyCode == kDeleteKeyCode)); + NSUInteger cursorIndex = state.cursorIndex; + NSAttributedString *attrString = state.attributedString; - if (cancelCandidateKey) { - gCurrentCandidateController.visible = NO; - [_candidates removeAllObjects]; + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put ths composing buffer + [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - if (Preferences.useWinNT351BPMF) { - _builder->clear(); - _walkedNodes.clear(); - [_composingBuffer setString:@""]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (charCode == 13 || keyCode == kEnterKeyCode) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex]; - return YES; - } - else if (charCode == 32 || keyCode == kPageDownKeyCode || emacsKey == vChewingEmacsKeyNextPage) { - BOOL updated = [gCurrentCandidateController showNextPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kPageUpKeyCode) { - BOOL updated = [gCurrentCandidateController showPreviousPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kLeftKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController showPreviousPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (emacsKey == vChewingEmacsKeyBackward) { - BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kRightKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController highlightNextCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController showNextPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (emacsKey == vChewingEmacsKeyForward) { - BOOL updated = [gCurrentCandidateController highlightNextCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if (keyCode == kUpKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController showPreviousPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (keyCode == kDownKeyCode) { - if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { - BOOL updated = [gCurrentCandidateController showNextPage]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - BOOL updated = [gCurrentCandidateController highlightNextCandidate]; - if (!updated) { - [self beep]; - } - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - } - else if (keyCode == kHomeKeyCode || emacsKey == vChewingEmacsKeyHome) { - if (gCurrentCandidateController.selectedCandidateIndex == 0) { - [self beep]; - - } - else { - gCurrentCandidateController.selectedCandidateIndex = 0; - } - - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else if ((keyCode == kEndKeyCode || emacsKey == vChewingEmacsKeyEnd) && [_candidates count] > 0) { - if (gCurrentCandidateController.selectedCandidateIndex == [_candidates count] - 1) { - [self beep]; - } - else { - gCurrentCandidateController.selectedCandidateIndex = [_candidates count] - 1; - } - - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } - else { - NSInteger index = NSNotFound; - for (NSUInteger j = 0, c = [gCurrentCandidateController.keyLabels count]; j < c; j++) { - if ([inputText compare:[gCurrentCandidateController.keyLabels objectAtIndex:j] options:NSCaseInsensitiveSearch] == NSOrderedSame) { - index = j; - break; - } - } - - [gCurrentCandidateController.keyLabels indexOfObject:inputText]; - if (index != NSNotFound) { - NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:index]; - if (candidateIndex != NSUIntegerMax) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:candidateIndex]; - return YES; - } - } - - if (Preferences.useWinNT351BPMF) { - string layout = [self _currentLayout]; - string customPunctuation = string("_punctuation_") + layout + string(1, (char)charCode); - string punctuation = string("_punctuation_") + string(1, (char)charCode); - - BOOL shouldAutoSelectCandidate = _bpmfReadingBuffer->isValidKey((char)charCode) || _languageModel->hasUnigramsForKey(customPunctuation) || - _languageModel->hasUnigramsForKey(punctuation); - - if (shouldAutoSelectCandidate) { - NSUInteger candidateIndex = [gCurrentCandidateController candidateIndexAtKeyLabelIndex:0]; - if (candidateIndex != NSUIntegerMax) { - [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:candidateIndex]; - return [self handleInputText:inputText key:keyCode modifiers:0 client:_currentCandidateClient]; - } - } - } - - [self beep]; - [self updateClientComposingBuffer:_currentCandidateClient]; - return YES; - } + if (![previous isKindOfClass:[InputStateChoosingCandidate class]]) { + [self _showCandidateWindowWithState:state client:client]; + } } -- (NSUInteger)recognizedEvents:(id)sender +- (void)_showCandidateWindowWithState:(InputStateChoosingCandidate *)state client:(id)client { - return NSKeyDownMask | NSFlagsChangedMask; -} + // set the candidate panel style + BOOL useVerticalMode = state.useVerticalMode; -- (BOOL)handleEvent:(NSEvent *)event client:(id)client -{ - if ([event type] == NSFlagsChanged) { - NSString *functionKeyKeyboardLayoutID = Preferences.functionKeyboardLayout; - NSString *basisKeyboardLayoutID = Preferences.basisKeyboardLayout; + if (useVerticalMode) { + gCurrentCandidateController = [vChewingInputMethodController verticalCandidateController]; + } else if (Preferences.useHorizontalCandidateList) { + gCurrentCandidateController = [vChewingInputMethodController horizontalCandidateController]; + } else { + gCurrentCandidateController = [vChewingInputMethodController verticalCandidateController]; + } - // If no override is needed, just return NO. - if ([functionKeyKeyboardLayoutID isEqualToString:basisKeyboardLayoutID]) { - return NO; - } + // set the attributes for the candidate panel (which uses NSAttributedString) + NSInteger textSize = Preferences.candidateListTextSize; - // Function key pressed. - BOOL includeShift = Preferences.functionKeyKeyboardLayoutOverrideIncludeShiftKey; - if (([event modifierFlags] & ~NSEventModifierFlagShift) || (([event modifierFlags] & NSEventModifierFlagShift) && includeShift)) { - // Override the keyboard layout and let the OS do its thing - [client overrideKeyboardWithKeyboardNamed:functionKeyKeyboardLayoutID]; - return NO; - } + NSInteger keyLabelSize = textSize / 2; + if (keyLabelSize < kMinKeyLabelSize) { + keyLabelSize = kMinKeyLabelSize; + } - // Revert back to the basis layout when the function key is released - [client overrideKeyboardWithKeyboardNamed:basisKeyboardLayoutID]; - return NO; - } + NSString *ctFontName = Preferences.candidateTextFontName; + NSString *klFontName = Preferences.candidateKeyLabelFontName; + NSString *candidateKeys = Preferences.candidateKeys; - NSString *inputText = [event characters]; - NSInteger keyCode = [event keyCode]; - NSUInteger flags = [event modifierFlags]; - return [self handleInputText:inputText key:keyCode modifiers:flags client:client]; -} + gCurrentCandidateController.keyLabelFont = klFontName ? [NSFont fontWithName:klFontName size:keyLabelSize] : [NSFont systemFontOfSize:keyLabelSize]; + gCurrentCandidateController.candidateFont = ctFontName ? [NSFont fontWithName:ctFontName size:textSize] : [NSFont systemFontOfSize:textSize]; -#pragma mark - Private methods + NSMutableArray *keyLabels = [@[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9"] mutableCopy]; -+ (VTHorizontalCandidateController *)horizontalCandidateController -{ - static VTHorizontalCandidateController *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[VTHorizontalCandidateController alloc] init]; - }); - return instance; -} + if (candidateKeys.length > 1) { + [keyLabels removeAllObjects]; + for (NSUInteger i = 0, c = candidateKeys.length; i < c; i++) { + [keyLabels addObject:[candidateKeys substringWithRange:NSMakeRange(i, 1)]]; + } + } -+ (VTVerticalCandidateController *)verticalCandidateController -{ - static VTVerticalCandidateController *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[VTVerticalCandidateController alloc] init]; - }); - return instance; -} + gCurrentCandidateController.keyLabels = keyLabels; + gCurrentCandidateController.delegate = self; + [gCurrentCandidateController reloadData]; + _currentCandidateClient = client; -+ (TooltipController *)tooltipController -{ - static TooltipController *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[TooltipController alloc] init]; - }); - return instance; -} + NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); + NSInteger cursor = state.cursorIndex; + if (cursor == state.composingBuffer.length && cursor != 0) { + cursor--; + } -- (void)collectCandidates -{ - // returns the candidate - [_candidates removeAllObjects]; + // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch + @try { + [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; + } + @catch (NSException *exception) { + NSLog(@"lineHeightRectangle %@", exception); + } - size_t cursorIndex = [self actualCandidateCursorIndex]; - vector nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex); + if (useVerticalMode) { + [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; + } else { + [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; + } - // sort the nodes, so that longer nodes (representing longer phrases) are placed at the top of the candidate list - stable_sort(nodes.begin(), nodes.end(), NodeAnchorDescendingSorter()); - - // then use the C++ trick to retrieve the candidates for each node at/crossing the cursor - for (vector::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni) { - const vector& candidates = (*ni).node->candidates(); - for (vector::const_iterator ci = candidates.begin(), ce = candidates.end(); ci != ce; ++ci) { - [_candidates addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]]; - } - } -} - -- (size_t)actualCandidateCursorIndex -{ - size_t cursorIndex = _builder->cursorIndex(); - if (Preferences.selectPhraseAfterCursorAsCandidate) { - // MS Phonetics IME style, phrase is *after* the cursor, i.e. cursor is always *before* the phrase - if (cursorIndex < _builder->length()) { - ++cursorIndex; - } - } - else { - if (!cursorIndex) { - ++cursorIndex; - } - } - - return cursorIndex; -} - -- (void)_showCandidateWindowUsingVerticalMode:(BOOL)useVerticalMode client:(id)client -{ - // set the candidate panel style - - if (useVerticalMode) { - gCurrentCandidateController = [vChewingInputMethodController verticalCandidateController]; - } - else if (Preferences.useHorizontalCandidateList) { - gCurrentCandidateController = [vChewingInputMethodController horizontalCandidateController]; - } - else { - gCurrentCandidateController = [vChewingInputMethodController verticalCandidateController]; - } - - // set the attributes for the candidate panel (which uses NSAttributedString) - NSInteger textSize = Preferences.candidateListTextSize; - - NSInteger keyLabelSize = textSize / 2; - if (keyLabelSize < kMinKeyLabelSize) { - keyLabelSize = kMinKeyLabelSize; - } - - NSString *ctFontName = Preferences.candidateTextFontName; - NSString *klFontName = Preferences.candidateKeyLabelFontName; - NSString *ckeys = Preferences.candidateKeys; - - gCurrentCandidateController.keyLabelFont = klFontName ? [NSFont fontWithName:klFontName size:keyLabelSize] : [NSFont systemFontOfSize:keyLabelSize]; - gCurrentCandidateController.candidateFont = ctFontName ? [NSFont fontWithName:ctFontName size:textSize] : [NSFont systemFontOfSize:textSize]; - - NSMutableArray *keyLabels = [NSMutableArray arrayWithObjects:@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", nil]; - - if ([ckeys length] > 1) { - [keyLabels removeAllObjects]; - for (NSUInteger i = 0, c = [ckeys length]; i < c; i++) { - [keyLabels addObject:[ckeys substringWithRange:NSMakeRange(i, 1)]]; - } - } - - gCurrentCandidateController.keyLabels = keyLabels; - [self collectCandidates]; - - if (Preferences.useWinNT351BPMF && [_candidates count] == 1) { - [self commitComposition:client]; - return; - } - - gCurrentCandidateController.delegate = self; - [gCurrentCandidateController reloadData]; - - // update the composing text, set the client - [self updateClientComposingBuffer:client]; - _currentCandidateClient = client; - - NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); - - NSInteger cursor = _latestReadingCursor; - if (cursor == [_composingBuffer length] && cursor != 0) { - cursor--; - } - - // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch - @try { - [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; - } - @catch (NSException *exception) { - NSLog(@"lineHeightRectangle %@", exception); - } - - if (useVerticalMode) { - [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; - } - else { - [gCurrentCandidateController setWindowTopLeftPoint:NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0) bottomOutOfScreenAdjustmentHeight:lineHeightRect.size.height + 4.0]; - } - - gCurrentCandidateController.visible = YES; -} - -#pragma mark - User phrases - -- (NSString *)_currentMarkedText -{ - if (_builder->markerCursorIndex() < 0) { - return @""; - } - if (!_bpmfReadingBuffer->isEmpty()) { - return @""; - } - - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - // A phrase should contain at least two characters. - if (end - begin < 1) { - return @""; - } - - NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); - NSString *selectedText = [_composingBuffer substringWithRange:range]; - return selectedText; -} - -- (NSString *)_currentMarkedTextAndReadings -{ - if (_builder->markerCursorIndex() < 0) { - return @""; - } - if (!_bpmfReadingBuffer->isEmpty()) { - return @""; - } - - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - // A phrase should contain at least two characters. - if (end - begin < 2) { - return @""; - } - if (end - begin > Preferences.maxCandidateLength) { - return @""; - } - - NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); - NSString *selectedText = [_composingBuffer substringWithRange:range]; - NSMutableString *string = [[NSMutableString alloc] init]; - [string appendString:selectedText]; - [string appendString:@" "]; - NSMutableArray *readingsArray = [[NSMutableArray alloc] init]; - vector v = _builder->readingsAtRange(begin, end); - for(vector::iterator it_i=v.begin(); it_i!=v.end(); ++it_i) { - [readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]]; - } - [string appendString:[readingsArray componentsJoinedByString:@"-"]]; - return string; -} - -- (BOOL)_writeUserPhrase -{ - NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings]; - if (![currentMarkedPhrase length]) { - [self beep]; - return NO; - } - - return [LanguageModelManager writeUserPhrase:currentMarkedPhrase inputMode:_inputMode]; -} - -- (void)_showCurrentMarkedTextTooltipWithClient:(id)client -{ - NSString *text = [self _currentMarkedText]; - NSInteger length = text.length; - if (!length) { - [self _hideTooltip]; - } - else if (Preferences.phraseReplacementEnabled) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", @""), text]; - [self _showTooltip:message client:client]; - } - else if (length < 2) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"\"%@\" length must ≥ 2 for a user phrase.", @""), text]; - [self _showTooltip:message client:client]; - } - else if (length > Preferences.maxCandidateLength) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"\"%@\" length too long for a user phrase.", @""), text]; - [self _showTooltip:message client:client]; - } - else { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"\"%@\" selected. ENTER to add user phrase.", @""), text]; - [self _showTooltip:message client:client]; - } -} - -- (void)_showTooltip:(NSString *)tooltip client:(id)client -{ - NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); - - NSInteger cursor = _latestReadingCursor; - if (cursor == [_composingBuffer length] && cursor != 0) { - cursor--; - } - - // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch - @try { - [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; - } - @catch (NSException *exception) { - NSLog(@"lineHeightRectangle %@", exception); - } - - [[vChewingInputMethodController tooltipController] showTooltip:tooltip atPoint:lineHeightRect.origin]; -} - -- (void)_hideTooltip -{ - if ([vChewingInputMethodController tooltipController].window.isVisible) { - [[vChewingInputMethodController tooltipController] hide]; - } + gCurrentCandidateController.visible = YES; } #pragma mark - Misc menu items @@ -1517,28 +485,26 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)toggleHalfWidthPunctuation:(id)sender { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-result" - [Preferences toggleHalfWidthPunctuationEnabled]; -#pragma GCC diagnostic pop + [NotifierController notifyWithMessage:[NSString stringWithFormat:@"%@%@%@", NSLocalizedString(@"Half-Width Punctuation Mode", @""), @"\n", [Preferences toggleHalfWidthPunctuationEnabled] ? NSLocalizedString(@"NotificationSwitchON", @"") : NSLocalizedString(@"NotificationSwitchOFF", @"")] stay:NO]; } - (void)togglePhraseReplacementEnabled:(id)sender { - if (_inputMode == kBopomofoModeIdentifierCHT) { - BOOL enabled = [Preferences togglePhraseReplacementEnabled]; - vChewingLM *lm = [LanguageModelManager languageModelCoreCHT]; - lm->setPhraseReplacementEnabled(enabled); - } else { - BOOL enabled = [Preferences togglePhraseReplacementEnabled]; - vChewingLM *lm = [LanguageModelManager languageModelCoreCHS]; - lm->setPhraseReplacementEnabled(enabled); - } + if (_keyHandler.inputMode == kBopomofoModeIdentifierCHT){ + [LanguageModelManager languageModelCoreCHT]->setPhraseReplacementEnabled([Preferences togglePhraseReplacementEnabled]); + + } else { + [LanguageModelManager languageModelCoreCHS]->setPhraseReplacementEnabled([Preferences togglePhraseReplacementEnabled]); + } } - (void)toggleCNS11643Enabled:(id)sender { - _languageModel->setCNSEnabled([Preferences toggleCNS11643Enabled]); + if (_keyHandler.inputMode == kBopomofoModeIdentifierCHT){ + [LanguageModelManager languageModelCoreCHT]->setCNSEnabled([Preferences toggleCNS11643Enabled]); + } else { + [LanguageModelManager languageModelCoreCHS]->setCNSEnabled([Preferences toggleCNS11643Enabled]); + } // 注意上面這一行已經動過開關了,所以接下來就不要 toggle。 [NotifierController notifyWithMessage:[NSString stringWithFormat:@"%@%@%@", NSLocalizedString(@"CNS11643 Mode", @""), @"\n", [Preferences cns11643Enabled] ? NSLocalizedString(@"NotificationSwitchON", @"") : NSLocalizedString(@"NotificationSwitchOFF", @"")] stay:NO]; } @@ -1551,18 +517,18 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)checkForUpdate:(id)sender { - [(AppDelegate *)[[NSApplication sharedApplication] delegate] checkForUpdateForced:YES]; + [(AppDelegate *) NSApp.delegate checkForUpdateForced:YES]; } - (BOOL)_checkUserFiles { - if (![LanguageModelManager checkIfUserLanguageModelFilesExist] ) { - NSString *content = [NSString stringWithFormat:NSLocalizedString(@"Please check the permission of at \"%@\".", @""), [LanguageModelManager dataFolderPath]]; - [[NonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; - return NO; - } + if (![LanguageModelManager checkIfUserLanguageModelFilesExist]) { + NSString *content = [NSString stringWithFormat:NSLocalizedString(@"Please check the permission of at \"%@\".", @""), [LanguageModelManager dataFolderPath]]; + [[NonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; + return NO; + } - return YES; + return YES; } - (void)_openUserFile:(NSString *)path @@ -1575,17 +541,17 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (void)openUserPhrases:(id)sender { - [self _openUserFile:[LanguageModelManager userPhrasesDataPath:_inputMode]]; + [self _openUserFile:[LanguageModelManager userPhrasesDataPath:_keyHandler.inputMode]]; } - (void)openExcludedPhrases:(id)sender { - [self _openUserFile:[LanguageModelManager excludedPhrasesDataPath:_inputMode]]; + [self _openUserFile:[LanguageModelManager excludedPhrasesDataPath:_keyHandler.inputMode]]; } - (void)openPhraseReplacement:(id)sender { - [self _openUserFile:[LanguageModelManager phraseReplacementDataPath:_inputMode]]; + [self _openUserFile:[LanguageModelManager phraseReplacementDataPath:_keyHandler.inputMode]]; } - (void)reloadUserPhrases:(id)sender @@ -1609,37 +575,132 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } - (NSUInteger)candidateCountForController:(VTCandidateController *)controller { - return [_candidates count]; + if ([_state isKindOfClass:[InputStateChoosingCandidate class]]) { + InputStateChoosingCandidate *state = (InputStateChoosingCandidate *) _state; + return state.candidates.count; + } + return 0; } - (NSString *)candidateController:(VTCandidateController *)controller candidateAtIndex:(NSUInteger)index { - return [_candidates objectAtIndex:index]; + if ([_state isKindOfClass:[InputStateChoosingCandidate class]]) { + InputStateChoosingCandidate *state = (InputStateChoosingCandidate *) _state; + return state.candidates[index]; + } + return @""; } - (void)candidateController:(VTCandidateController *)controller didSelectCandidateAtIndex:(NSUInteger)index { - gCurrentCandidateController.visible = NO; + gCurrentCandidateController.visible = NO; - // candidate selected, override the node with selection - string selectedValue = [[_candidates objectAtIndex:index] UTF8String]; + if ([_state isKindOfClass:[InputStateChoosingCandidate class]]) { + InputStateChoosingCandidate *state = (InputStateChoosingCandidate *) _state; - size_t cursorIndex = [self actualCandidateCursorIndex]; - _builder->grid().fixNodeSelectedCandidate(cursorIndex, selectedValue); - if (!Preferences.useWinNT351BPMF) { - _userOverrideModel->observe(_walkedNodes, cursorIndex, selectedValue, [[NSDate date] timeIntervalSince1970]); - } + // candidate selected, override the node with selection + string selectedValue = [state.candidates[index] UTF8String]; + [_keyHandler fixNodeWithValue:selectedValue]; + InputStateInputting *inputting = [_keyHandler _buildInputtingState]; - [_candidates removeAllObjects]; - - [self walk]; - [self updateClientComposingBuffer:_currentCandidateClient]; - - if (Preferences.useWinNT351BPMF) { - [self commitComposition:_currentCandidateClient]; - return; - } + if (Preferences.useWinNT351BPMF) { + [_keyHandler clear]; + InputStateCommitting *committing = [[InputStateCommitting alloc] initWithPoppedText:inputting.composingBuffer]; + [self handleState:committing client:_currentCandidateClient]; + InputStateEmpty *empty = [[InputStateEmpty alloc] init]; + [self handleState:empty client:_currentCandidateClient]; + } else { + [self handleState:inputting client:_currentCandidateClient]; + } + } } @end +#pragma mark - Implementation + +@implementation vChewingInputMethodController (KeyHandlerDelegate) + +- (nonnull VTCandidateController *)candidateControllerForKeyHandler:(nonnull KeyHandler *)keyHandler +{ + return gCurrentCandidateController; +} + +- (BOOL)keyHandler:(nonnull KeyHandler *)keyHandler didRequestWriteUserPhraseWithState:(nonnull InputStateMarking *)state +{ + if (!state.validToWrite) { + return NO; + } + NSString *userPhrase = state.userPhrase; + return [LanguageModelManager writeUserPhrase:userPhrase inputMode:_keyHandler.inputMode]; + return YES; +} + +- (void)keyHandler:(nonnull KeyHandler *)keyHandler didSelectCandidateAtIndex:(NSInteger)index candidateController:(nonnull VTCandidateController *)controller +{ + [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:index]; +} + +@end + + +@implementation vChewingInputMethodController (UI) + ++ (VTHorizontalCandidateController *)horizontalCandidateController +{ + static VTHorizontalCandidateController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[VTHorizontalCandidateController alloc] init]; + }); + return instance; +} + ++ (VTVerticalCandidateController *)verticalCandidateController +{ + static VTVerticalCandidateController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[VTVerticalCandidateController alloc] init]; + }); + return instance; +} + ++ (TooltipController *)tooltipController +{ + static TooltipController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[TooltipController alloc] init]; + }); + return instance; +} + +- (void)_showTooltip:(NSString *)tooltip composingBuffer:(NSString *)composingBuffer cursorIndex:(NSInteger)cursorIndex client:(id)client +{ + NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); + + NSUInteger cursor = (NSUInteger) cursorIndex; + if (cursor == composingBuffer.length && cursor != 0) { + cursor--; + } + + // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch + @try { + [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; + } + @catch (NSException *exception) { + NSLog(@"%@", exception); + } + + [[vChewingInputMethodController tooltipController] showTooltip:tooltip atPoint:lineHeightRect.origin]; +} + +- (void)_hideTooltip +{ + if ([vChewingInputMethodController tooltipController].window.isVisible) { + [[vChewingInputMethodController tooltipController] hide]; + } +} + +@end diff --git a/Source/LanguageModelManager.mm b/Source/LanguageModelManager.mm index c70b20d9..ceefa187 100644 --- a/Source/LanguageModelManager.mm +++ b/Source/LanguageModelManager.mm @@ -7,15 +7,9 @@ */ #import "LanguageModelManager.h" -#import -#import -#import -#import "OVUTF8Helper.h" using namespace std; -using namespace Taiyan::Gramambular; using namespace vChewing; -using namespace OpenVanilla; static const int kUserOverrideModelCapacity = 500; static const double kObservedOverrideHalflife = 5400.0; // 1.5 hr. @@ -72,7 +66,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing NSLog(@"User Data Folder N/A."); return NO; } - if (![self checkIfFileExist:[self cnsDataPath]]) { + if (![self ensureFileExists:[self cnsDataPath]]) { NSLog(@"Extracted CNS Data Not Found."); return NO; } @@ -122,7 +116,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing return YES; } -+ (BOOL)checkIfFileExist:(NSString *)filePath ++ (BOOL)ensureFileExists:(NSString *)filePath { if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { BOOL result = [[@"" dataUsingEncoding:NSUTF8StringEncoding] writeToFile:filePath atomically:YES]; @@ -139,22 +133,22 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing if (![self checkIfUserDataFolderExists]) { return NO; } - if (![self checkIfFileExist:[self userPhrasesDataPath:kBopomofoModeIdentifierCHT]]) { + if (![self ensureFileExists:[self userPhrasesDataPath:kBopomofoModeIdentifierCHT]]) { return NO; } - if (![self checkIfFileExist:[self excludedPhrasesDataPath:kBopomofoModeIdentifierCHT]]) { + if (![self ensureFileExists:[self excludedPhrasesDataPath:kBopomofoModeIdentifierCHT]]) { return NO; } - if (![self checkIfFileExist:[self phraseReplacementDataPath:kBopomofoModeIdentifierCHT]]) { + if (![self ensureFileExists:[self phraseReplacementDataPath:kBopomofoModeIdentifierCHT]]) { return NO; } - if (![self checkIfFileExist:[self userPhrasesDataPath:kBopomofoModeIdentifierCHS]]) { + if (![self ensureFileExists:[self userPhrasesDataPath:kBopomofoModeIdentifierCHS]]) { return NO; } - if (![self checkIfFileExist:[self excludedPhrasesDataPath:kBopomofoModeIdentifierCHS]]) { + if (![self ensureFileExists:[self excludedPhrasesDataPath:kBopomofoModeIdentifierCHS]]) { return NO; } - if (![self checkIfFileExist:[self phraseReplacementDataPath:kBopomofoModeIdentifierCHS]]) { + if (![self ensureFileExists:[self phraseReplacementDataPath:kBopomofoModeIdentifierCHS]]) { return NO; } return YES; @@ -188,7 +182,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing + (NSString *)dataFolderPath { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDirectory, YES); - NSString *appSupportPath = [paths objectAtIndex:0]; + NSString *appSupportPath = paths[0]; NSString *userDictPath = [appSupportPath stringByAppendingPathComponent:@"vChewing"]; return userDictPath; } diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index 0732f0da..26ba4bb1 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -21,9 +21,9 @@ "Unable to create the user phrase file." = "Unable to create the user phrase file."; "Please check the permission of at \"%@\"." = "Please check the permission of at \"%@\"."; "Edit Excluded Phrases" = "Edit Excluded Phrases…"; -"Use Half-Width Punctuations" = "Use Half-Width Punctuations"; +"Half-Width Punctuation Mode" = "Half-Width Punctuation Mode"; "\"%@\" length must ≥ 2 for a user phrase." = "\"%@\" length must ≥ 2 for a user phrase."; -"\"%@\" length too long for a user phrase." = "\"%@\" length too long for a user phrase."; +"\"%@\" length should ≤ %d for a user phrase." = "\"%@\" length should ≤ %d for a user phrase."; "\"%@\" selected. ENTER to add user phrase." = "\"%@\" selected. ENTER to add user phrase."; "Edit Phrase Replacement Table" = "Edit Phrase Replacement Table…"; "Use Phrase Replacement" = "Use Phrase Replacement"; diff --git a/Source/ja.lproj/Localizable.strings b/Source/ja.lproj/Localizable.strings index a7a60c75..73d5cbee 100644 --- a/Source/ja.lproj/Localizable.strings +++ b/Source/ja.lproj/Localizable.strings @@ -21,9 +21,9 @@ "Unable to create the user phrase file." = "ユーザー辞書ファイルの作成は失敗しました。"; "Please check the permission of at \"%@\"." = "「%@」に書き出す権限は不足らしい。"; "Edit Excluded Phrases" = "辞書条目排除表を編集…"; -"Use Half-Width Punctuations" = "半角句読機能を起用"; +"Half-Width Punctuation Mode" = "半角句読モード"; "\"%@\" length must ≥ 2 for a user phrase." = "「%@」もう1つ文字のお選びを。"; -"\"%@\" length too long for a user phrase." = "「%@」文字数過剰で登録不可。"; +"\"%@\" length should ≤ %d for a user phrase." = "「%@」文字数過剰で登録不可、%d 文字以内にして下さい。"; "\"%@\" selected. ENTER to add user phrase." = "「%@」を ENTER で辞書に登録。"; "Edit Phrase Replacement Table" = "言葉置換表を編集…"; "Use Phrase Replacement" = "言葉置換機能"; diff --git a/Source/zh-Hans.lproj/Localizable.strings b/Source/zh-Hans.lproj/Localizable.strings index e9926ac7..7a3e14c1 100644 --- a/Source/zh-Hans.lproj/Localizable.strings +++ b/Source/zh-Hans.lproj/Localizable.strings @@ -21,9 +21,9 @@ "Unable to create the user phrase file." = "无法创建自订语汇档案。"; "Please check the permission of at \"%@\"." = "请检查此处的存取权限:\"%@\"."; "Edit Excluded Phrases" = "编辑要滤除的语汇…"; -"Use Half-Width Punctuations" = "啟用半角標點輸出"; +"Half-Width Punctuation Mode" = "半角标点模式"; "\"%@\" length must ≥ 2 for a user phrase." = "「%@」字数不足以自订语汇。"; -"\"%@\" length too long for a user phrase." = "「%@」字数太长、无法自订。"; +"\"%@\" length should ≤ %d for a user phrase." = "「%@」字数超过 %d、无法自订。"; "\"%@\" selected. ENTER to add user phrase." = "「%@」敲 Enter 添入自订语汇。"; "Edit Phrase Replacement Table" = "编辑语汇置换表…"; "Use Phrase Replacement" = "使用语汇置换"; diff --git a/Source/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index aac48d3f..1066c72f 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -21,9 +21,9 @@ "Unable to create the user phrase file." = "無法創建自訂語彙檔案。"; "Please check the permission of at \"%@\"." = "請檢查此處的存取權限:\"%@\"."; "Edit Excluded Phrases" = "編輯要濾除的語彙…"; -"Use Half-Width Punctuations" = "啟用半形標點輸出"; +"Half-Width Punctuation Mode" = "半形標點模式"; "\"%@\" length must ≥ 2 for a user phrase." = "「%@」字數不足以自訂語彙。"; -"\"%@\" length too long for a user phrase." = "「%@」字數太長、無法自訂。"; +"\"%@\" length should ≤ %d for a user phrase." = "「%@」字數超過 %d、無法自訂。"; "\"%@\" selected. ENTER to add user phrase." = "「%@」敲 Enter 添入自訂語彙。"; "Edit Phrase Replacement Table" = "編輯語彙置換表…"; "Use Phrase Replacement" = "使用語彙置換"; diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index abe9cde9..172e274f 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -23,12 +23,14 @@ 5B810D9F27A3A5E50032C1A9 /* LMConsolidator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B810D9D27A3A5E50032C1A9 /* LMConsolidator.mm */; }; 5B9DD0A927A7950D00ED335A /* FSEventStreamHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9DD0A827A7950D00ED335A /* FSEventStreamHelper.swift */; }; 5BC2D2882793B434002C0BEC /* KeyValueBlobReader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5BC2D2862793B434002C0BEC /* KeyValueBlobReader.cpp */; }; - 5BC2D28B2793B8FB002C0BEC /* EmacsKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC2D28A2793B8FB002C0BEC /* EmacsKeyHelper.swift */; }; 5BC2D28D2793B98F002C0BEC /* PreferencesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC2D28C2793B98F002C0BEC /* PreferencesModule.swift */; }; 5BC3EE1B278FC48C00F5E44C /* VerticalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */; }; 5BC3EE1C278FC48C00F5E44C /* VTCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */; }; 5BC3EE1D278FC48C00F5E44C /* HorizontalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE1A278FC48C00F5E44C /* HorizontalCandidateController.swift */; }; 5BC3FB83278492DE0022E99A /* data-chs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BC3FB82278492DE0022E99A /* data-chs.txt */; }; + 5BC772AA27A5A1E800CA8391 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC772A927A5A1E800CA8391 /* InputState.swift */; }; + 5BC772AC27A5A31200CA8391 /* KeyHandlerInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC772AB27A5A31200CA8391 /* KeyHandlerInput.swift */; }; + 5BC772B027A5B3AB00CA8391 /* KeyHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5BC772AD27A5A31B00CA8391 /* KeyHandler.mm */; }; 5BD0D19427940E9D0008F48E /* Fart.aif in Resources */ = {isa = PBXBuildFile; fileRef = 5BD0D19327940E9D0008F48E /* Fart.aif */; }; 5BD0D19A27943D390008F48E /* clsSFX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD0D19927943D390008F48E /* clsSFX.swift */; }; 5BD0D19F279454F60008F48E /* Beep.aif in Resources */ = {isa = PBXBuildFile; fileRef = 5BD0D19E279454F60008F48E /* Beep.aif */; }; @@ -129,7 +131,6 @@ 5BA923AC2791B7C20001323A /* vChewingInstaller-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "vChewingInstaller-Bridging-Header.h"; sourceTree = ""; }; 5BC2D2842793B434002C0BEC /* KeyValueBlobReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyValueBlobReader.h; sourceTree = ""; }; 5BC2D2862793B434002C0BEC /* KeyValueBlobReader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = KeyValueBlobReader.cpp; sourceTree = ""; }; - 5BC2D28A2793B8FB002C0BEC /* EmacsKeyHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmacsKeyHelper.swift; sourceTree = ""; }; 5BC2D28C2793B98F002C0BEC /* PreferencesModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesModule.swift; sourceTree = ""; }; 5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalCandidateController.swift; sourceTree = ""; }; 5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VTCandidateController.swift; sourceTree = ""; }; @@ -144,6 +145,10 @@ 5BC4BC5B2796E6A90023BBD5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = Source/ja.lproj/InfoPlist.strings; sourceTree = ""; }; 5BC4BC5C2796E6A90023BBD5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = Source/ja.lproj/Localizable.strings; sourceTree = ""; }; 5BC4BC5E2796F5C40023BBD5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Source/en.lproj/preferences.strings; sourceTree = ""; }; + 5BC772A927A5A1E800CA8391 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = ""; }; + 5BC772AB27A5A31200CA8391 /* KeyHandlerInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyHandlerInput.swift; sourceTree = ""; }; + 5BC772AD27A5A31B00CA8391 /* KeyHandler.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyHandler.mm; sourceTree = ""; }; + 5BC772AE27A5A31B00CA8391 /* KeyHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyHandler.h; sourceTree = ""; }; 5BD0D19327940E9D0008F48E /* Fart.aif */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.aif; sourceTree = ""; }; 5BD0D19927943D390008F48E /* clsSFX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = clsSFX.swift; sourceTree = ""; }; 5BD0D19E279454F60008F48E /* Beep.aif */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.aif; sourceTree = ""; }; @@ -271,21 +276,17 @@ path = LanguageModel; sourceTree = ""; }; - 5BC2D2832793B434002C0BEC /* vChewing */ = { + 5BC2D2832793B434002C0BEC /* ControllerModules */ = { isa = PBXGroup; children = ( + 5BC772AE27A5A31B00CA8391 /* KeyHandler.h */, + 5BC772AD27A5A31B00CA8391 /* KeyHandler.mm */, + 5BC772AB27A5A31200CA8391 /* KeyHandlerInput.swift */, 5BC2D2842793B434002C0BEC /* KeyValueBlobReader.h */, 5BC2D2862793B434002C0BEC /* KeyValueBlobReader.cpp */, + 5BC772A927A5A1E800CA8391 /* InputState.swift */, ); - path = vChewing; - sourceTree = ""; - }; - 5BC2D2892793B8DB002C0BEC /* Keyboard */ = { - isa = PBXGroup; - children = ( - 5BC2D28A2793B8FB002C0BEC /* EmacsKeyHelper.swift */, - ); - path = Keyboard; + path = ControllerModules; sourceTree = ""; }; 5BD0D19827943D270008F48E /* SFX */ = { @@ -452,8 +453,7 @@ isa = PBXGroup; children = ( 5BD0D19827943D270008F48E /* SFX */, - 5BC2D2892793B8DB002C0BEC /* Keyboard */, - 5BC2D2832793B434002C0BEC /* vChewing */, + 5BC2D2832793B434002C0BEC /* ControllerModules */, 5BA8DAFE27928120009C9FFF /* LanguageModel */, 6A0D4F1315FC0EB100ABF4B3 /* Gramambular */, ); @@ -711,7 +711,7 @@ buildActionMask = 2147483647; files = ( 5BDF2CFE2791BE4400838ADB /* InputSourceHelper.swift in Sources */, - 5BC2D28B2793B8FB002C0BEC /* EmacsKeyHelper.swift in Sources */, + 5BC772AC27A5A31200CA8391 /* KeyHandlerInput.swift in Sources */, 5BDD25F5279D6CFE00AA18F8 /* AWFileHash.m in Sources */, 5BDD25F8279D6D1200AA18F8 /* zip.m in Sources */, 5BD0D19A27943D390008F48E /* clsSFX.swift in Sources */, @@ -727,6 +727,7 @@ 5BE798A42792E58A00337FF9 /* TooltipController.swift in Sources */, 5BDD25F6279D6D0200AA18F8 /* SSZipArchive.m in Sources */, 5BDF2D062791DFF200838ADB /* AppDelegate.swift in Sources */, + 5BC772B027A5B3AB00CA8391 /* KeyHandler.mm in Sources */, 5BC3EE1B278FC48C00F5E44C /* VerticalCandidateController.swift in Sources */, 5B42B64027876FDC00BB9B9F /* UserOverrideModel.mm in Sources */, 5BDD25F7279D6D1200AA18F8 /* unzip.m in Sources */, @@ -743,6 +744,7 @@ 5BC3EE1C278FC48C00F5E44C /* VTCandidateController.swift in Sources */, 5BDF2D032791C71200838ADB /* NonModalAlertWindowController.swift in Sources */, 5BC3EE1D278FC48C00F5E44C /* HorizontalCandidateController.swift in Sources */, + 5BC772AA27A5A1E800CA8391 /* InputState.swift in Sources */, 5BDD25FD279D6D6300AA18F8 /* CNSLM.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0;