KeyHandler // Swiftify: day 2.

This commit is contained in:
ShikiSuen 2022-04-15 09:33:07 +08:00
parent 5c8defca03
commit 43c2c1dfe4
3 changed files with 324 additions and 91 deletions

View File

@ -50,10 +50,8 @@ extern InputMode imeModeNULL;
- (BOOL)handleInput:(keyParser *)input
state:(InputState *)state
stateCallback:(void (^)(InputState *))stateCallback
errorCallback:(void (^)(void))errorCallback
NS_SWIFT_NAME(handle(input:state:stateCallback:errorCallback:));
errorCallback:(void (^)(void))errorCallback NS_SWIFT_NAME(handle(input:state:stateCallback:errorCallback:));
- (void)ensurePhoneticParser;
- (void)fixNodeWithValue:(NSString *)value NS_SWIFT_NAME(fixNode(value:));
- (void)clear;
@ -63,8 +61,40 @@ extern InputMode imeModeNULL;
@property(strong, nonatomic) InputMode inputMode;
@property(weak, nonatomic) id<KeyHandlerDelegate> delegate;
// The following items need to be exposed to Swift:
- (NSString *)_popOverflowComposingTextAndWalk;
- (BOOL)_handleCandidateState:(InputState *)state
input:(keyParser *)input
stateCallback:(void (^)(InputState *))stateCallback
errorCallback:(void (^)(void))errorCallback
NS_SWIFT_NAME(handleCandidate(state:input:stateCallback:errorCallback:));
- (BOOL)_handleMarkingState:(InputState *)state
input:(keyParser *)input
stateCallback:(void (^)(InputState *))stateCallback
errorCallback:(void (^)(void))errorCallback
NS_SWIFT_NAME(handleMarking(state:input:stateCallback:errorCallback:));
- (BOOL)checkWhetherToneMarkerConfirmsPhoneticReadingBuffer;
- (BOOL)chkKeyValidity:(UniChar)value;
- (BOOL)isPhoneticReadingBufferEmpty;
- (NSString *)getCompositionFromPhoneticReadingBuffer;
- (NSString *)getSyllableCompositionFromPhoneticReadingBuffer;
- (void)clearPhoneticReadingBuffer;
- (void)combinePhoneticReadingBufferKey:(UniChar)charCode;
- (void)doBackSpaceToPhoneticReadingBuffer;
- (void)removeBuilderAndReset:(BOOL)shouldReset;
- (void)createNewBuilder;
- (void)setInputModesToLM:(BOOL)isCHS;
- (void)syncBaseLMPrefs;
- (void)ensurePhoneticParser;
- (BOOL)ifLangModelHasUnigramsForKey:(NSString *)reading;
- (void)insertReadingToBuilderAtCursor:(NSString *)reading;
- (BOOL)isPrintable:(UniChar)charCode;
- (void)dealWithOverrideModelSuggestions;
- (NSMutableArray *)getCandidatesArray;
- (NSInteger)getBuilderCursorIndex;
- (NSInteger)getBuilderLength;
@end

View File

@ -198,9 +198,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
_walkedNodes.clear();
}
- (std::string)_currentMandarinParser
- (NSString *)_currentMandarinParser
{
return std::string(mgrPrefs.mandarinParserName.UTF8String) + std::string("_");
return [mgrPrefs.mandarinParserName stringByAppendingString:@"_"];
}
// MARK: - Handling Input
@ -331,10 +331,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
if (composeReading)
{
// combine the reading
std::string reading = [[self getSyllableCompositionFromPhoneticReadingBuffer] UTF8String];
NSString *reading = [self getSyllableCompositionFromPhoneticReadingBuffer];
// see if we have an unigram for this
if (!_languageModel->hasUnigramsForKey(reading))
if (![self ifLangModelHasUnigramsForKey:reading])
{
[IME prtDebugIntel:@"B49C0979"];
errorCallback();
@ -344,25 +344,13 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
// and insert it into the lattice
_builder->insertReadingAtCursor(reading);
[self insertReadingToBuilderAtCursor:reading];
// then walk the lattice
NSString *poppedText = [self _popOverflowComposingTextAndWalk];
// get user override model suggestion
std::string overrideValue = (mgrPrefs.useSCPCTypingMode)
? ""
: _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(),
[[NSDate date] timeIntervalSince1970]);
if (!overrideValue.empty())
{
size_t cursorIndex = [self _actualCandidateCursorIndex];
std::vector<Gramambular::NodeAnchor> nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex);
double highestScore = FindHighestScore(nodes, kEpsilon);
_builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue,
static_cast<float>(highestScore));
}
[self dealWithOverrideModelSuggestions];
// then update the text
[self clearPhoneticReadingBuffer];
@ -420,7 +408,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// if the spacebar is NOT set to be a selection key
if ([input isShiftHold] || !mgrPrefs.chooseCandidateUsingSpace)
{
if (_builder->cursorIndex() >= _builder->length())
if ([self getBuilderCursorIndex] >= [self getBuilderLength])
{
NSString *composingBuffer = [(InputStateNotEmpty *)state composingBuffer];
if (composingBuffer.length)
@ -435,9 +423,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
stateCallback(empty);
}
else if (_languageModel->hasUnigramsForKey(" "))
else if ([self ifLangModelHasUnigramsForKey:@" "])
{
_builder->insertReadingAtCursor(" ");
[self insertReadingToBuilderAtCursor:@" "];
NSString *poppedText = [self _popOverflowComposingTextAndWalk];
InputStateInputting *inputting = (InputStateInputting *)[self buildInputtingState];
inputting.poppedText = poppedText;
@ -508,11 +496,11 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
{
if (![input isOptionHold])
{
if (_languageModel->hasUnigramsForKey("_punctuation_list"))
if ([self ifLangModelHasUnigramsForKey:@"_punctuation_list "])
{
if ([self isPhoneticReadingBufferEmpty])
{
_builder->insertReadingAtCursor(string("_punctuation_list"));
[self insertReadingToBuilderAtCursor:@"_punctuation_list"];
NSString *poppedText = [self _popOverflowComposingTextAndWalk];
InputStateInputting *inputting = (InputStateInputting *)[self buildInputtingState];
inputting.poppedText = poppedText;
@ -546,19 +534,21 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// MARK: Punctuation
// if nothing is matched, see if it's a punctuation key for current layout.
std::string punctuationNamePrefix;
NSString *punctuationNamePrefix;
if ([input isOptionHold])
punctuationNamePrefix = std::string("_alt_punctuation_");
punctuationNamePrefix = @"_alt_punctuation_";
else if ([input isControlHold])
punctuationNamePrefix = std::string("_ctrl_punctuation_");
punctuationNamePrefix = @"_ctrl_punctuation_";
else if (mgrPrefs.halfWidthPunctuationEnabled)
punctuationNamePrefix = std::string("_half_punctuation_");
punctuationNamePrefix = @"_half_punctuation_";
else
punctuationNamePrefix = std::string("_punctuation_");
punctuationNamePrefix = @"_punctuation_";
std::string parser = [self _currentMandarinParser];
std::string customPunctuation = punctuationNamePrefix + parser + std::string(1, (char)charCode);
NSString *parser = [self _currentMandarinParser];
NSArray *arrCustomPunctuations =
@[ punctuationNamePrefix, parser, [NSString stringWithFormat:@"%c", (char)charCode] ];
NSString *customPunctuation = [arrCustomPunctuations componentsJoinedByString:@""];
if ([self _handlePunctuation:customPunctuation
state:state
usingVerticalMode:input.useVerticalMode
@ -567,7 +557,9 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
return YES;
// if nothing is matched, see if it's a punctuation key.
std::string punctuation = punctuationNamePrefix + std::string(1, (char)charCode);
NSArray *arrPunctuations = @[ punctuationNamePrefix, [NSString stringWithFormat:@"%c", (char)charCode] ];
NSString *punctuation = [arrPunctuations componentsJoinedByString:@""];
if ([self _handlePunctuation:punctuation
state:state
usingVerticalMode:input.useVerticalMode
@ -575,12 +567,10 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:errorCallback])
return YES;
// Lukhnos 這裡的處理反而會使得 Apple 倚天注音動態鍵盤佈局「敲不了半形大寫英文」的缺點曝露無疑,所以注釋掉。
// 至於他試圖用這種處理來解決的上游 UPR293
// 的問題,其實針對詞庫檔案的排序做點手腳就可以解決。威注音本來也就是這麼做的。
if (/*[state isKindOfClass:[InputStateNotEmpty class]] && */ [input isUpperCaseASCIILetterKey])
// 這裡不使用小麥注音 2.2. 的組字區處理方式,而是直接由詞庫負責。
if ([input isUpperCaseASCIILetterKey])
{
std::string letter = std::string("_letter_") + std::string(1, (char)charCode);
NSString *letter = [NSString stringWithFormat:@"%@%c", @"_letter_", (char)charCode];
if ([self _handlePunctuation:letter
state:state
usingVerticalMode:input.useVerticalMode
@ -957,19 +947,19 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
return YES;
}
- (BOOL)_handlePunctuation:(std::string)customPunctuation
- (BOOL)_handlePunctuation:(NSString *)customPunctuation
state:(InputState *)state
usingVerticalMode:(BOOL)useVerticalMode
stateCallback:(void (^)(InputState *))stateCallback
errorCallback:(void (^)(void))errorCallback
{
if (!_languageModel->hasUnigramsForKey(customPunctuation))
if (![self ifLangModelHasUnigramsForKey:customPunctuation])
return NO;
NSString *poppedText;
if ([self isPhoneticReadingBufferEmpty])
{
_builder->insertReadingAtCursor(customPunctuation);
[self insertReadingToBuilderAtCursor:customPunctuation];
poppedText = [self _popOverflowComposingTextAndWalk];
}
else
@ -1389,28 +1379,34 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
if (mgrPrefs.useSCPCTypingMode)
{
std::string punctuationNamePrefix;
if ([input isOptionHold])
punctuationNamePrefix = std::string("_alt_punctuation_");
else if ([input isControlHold])
punctuationNamePrefix = std::string("_ctrl_punctuation_");
else if (mgrPrefs.halfWidthPunctuationEnabled)
punctuationNamePrefix = std::string("_half_punctuation_");
else
punctuationNamePrefix = std::string("_punctuation_");
NSString *punctuationNamePrefix;
std::string parser = [self _currentMandarinParser];
std::string customPunctuation = punctuationNamePrefix + parser + std::string(1, (char)charCode);
std::string punctuation = punctuationNamePrefix + std::string(1, (char)charCode);
if ([input isOptionHold])
punctuationNamePrefix = @"_alt_punctuation_";
else if ([input isControlHold])
punctuationNamePrefix = @"_ctrl_punctuation_";
else if (mgrPrefs.halfWidthPunctuationEnabled)
punctuationNamePrefix = @"_half_punctuation_";
else
punctuationNamePrefix = @"_punctuation_";
NSString *parser = [self _currentMandarinParser];
NSArray *arrCustomPunctuations =
@[ punctuationNamePrefix, parser, [NSString stringWithFormat:@"%c", (char)charCode] ];
NSString *customPunctuation = [arrCustomPunctuations componentsJoinedByString:@""];
NSArray *arrPunctuations = @[ punctuationNamePrefix, [NSString stringWithFormat:@"%c", (char)charCode] ];
NSString *punctuation = [arrPunctuations componentsJoinedByString:@""];
BOOL shouldAutoSelectCandidate = [self chkKeyValidity:charCode] ||
_languageModel->hasUnigramsForKey(customPunctuation) ||
_languageModel->hasUnigramsForKey(punctuation);
[self ifLangModelHasUnigramsForKey:customPunctuation] ||
[self ifLangModelHasUnigramsForKey:punctuation];
if (!shouldAutoSelectCandidate && [input isUpperCaseASCIILetterKey])
{
std::string letter = std::string("_letter_") + std::string(1, (char)charCode);
if (_languageModel->hasUnigramsForKey(letter))
NSString *letter = [NSString stringWithFormat:@"%@%c", @"_letter_", (char)charCode];
if ([self ifLangModelHasUnigramsForKey:letter])
shouldAutoSelectCandidate = YES;
}
@ -1598,34 +1594,6 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
return poppedText;
}
- (InputStateChoosingCandidate *)_buildCandidateState:(InputStateNotEmpty *)currentState
useVerticalMode:(BOOL)useVerticalMode
{
NSMutableArray *candidatesArray = [[NSMutableArray alloc] init];
size_t cursorIndex = [self _actualCandidateCursorIndex];
std::vector<Gramambular::NodeAnchor> 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 (std::vector<Gramambular::NodeAnchor>::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni)
{
const std::vector<Gramambular::KeyValuePair> &candidates = (*ni).node->candidates();
for (std::vector<Gramambular::KeyValuePair>::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;
}
// NON-SWIFTIFIABLE
- (size_t)_actualCandidateCursorIndex
{
@ -1787,7 +1755,69 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
}
#pragma mark - 威注音認為有必要單獨拿出來處理的部分。
// ----
- (BOOL)ifLangModelHasUnigramsForKey:(NSString *)reading
{
return _languageModel->hasUnigramsForKey((std::string)[reading UTF8String]);
}
- (void)insertReadingToBuilderAtCursor:(NSString *)reading
{
_builder->insertReadingAtCursor((std::string)[reading UTF8String]);
}
- (void)dealWithOverrideModelSuggestions
{
// 這一整段都太 C++ 且只出現一次,就整個端過來了。
// 拆開封裝的話,只會把問題搞得更麻煩而已。
std::string overrideValue =
(mgrPrefs.useSCPCTypingMode)
? ""
: _userOverrideModel->suggest(_walkedNodes, _builder->cursorIndex(), [[NSDate date] timeIntervalSince1970]);
if (!overrideValue.empty())
{
size_t cursorIndex = [self _actualCandidateCursorIndex];
std::vector<Gramambular::NodeAnchor> nodes = _builder->grid().nodesCrossingOrEndingAt(cursorIndex);
double highestScore = FindHighestScore(nodes, kEpsilon);
_builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue,
static_cast<float>(highestScore));
}
}
- (NSInteger)getBuilderCursorIndex
{
return _builder->cursorIndex();
}
- (NSInteger)getBuilderLength
{
return _builder->length();
}
- (NSMutableArray *)getCandidatesArray
{
NSMutableArray<NSString *> *candidatesArray = [[NSMutableArray alloc] init];
size_t cursorIndex = [self _actualCandidateCursorIndex];
std::vector<Gramambular::NodeAnchor> 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 (std::vector<Gramambular::NodeAnchor>::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni)
{
const std::vector<Gramambular::KeyValuePair> &candidates = (*ni).node->candidates();
for (std::vector<Gramambular::KeyValuePair>::const_iterator ci = candidates.begin(), ce = candidates.end();
ci != ce; ++ci)
[candidatesArray addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]];
}
return candidatesArray;
}
#pragma mark - 威注音認為有必要單獨拿出來處理的部分,交給 Swift 則有些困難。
- (BOOL)isPrintable:(UniChar)charCode
{

View File

@ -29,13 +29,15 @@ import Cocoa
@objc extension KeyHandler {
func handleInputSwift(
input: keyParser,
state: InputState,
state inState: InputState,
stateCallback: @escaping (InputState) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
let charCode: UniChar = input.charCode
// let emacsKey: vChewingEmacsKey = input.emacsKey
var state = inState // Turn this incoming constant to variable.
let emacsKey: vChewingEmacsKey = input.emacsKey
let inputText: String = input.inputText ?? ""
let emptyState = InputState.Empty()
// Ignore the input if its inputText is empty.
// Reason: such inputs may be functional key combinations.
@ -62,7 +64,6 @@ import Cocoa
} else if input.isCapsLockOn {
// Process all possible combination, we hope.
clear()
let emptyState = InputState.Empty()
stateCallback(emptyState)
// When shift is pressed, don't do further processing...
@ -91,7 +92,6 @@ import Cocoa
&& !input.isUp && !input.isSpace && isPrintable(charCode)
{
clear()
let emptyState = InputState.Empty()
stateCallback(emptyState)
let committing = InputState.Committing(poppedText: inputText.lowercased())
stateCallback(committing)
@ -100,6 +100,161 @@ import Cocoa
}
}
// MARK: - Handle Candidates.
if state is InputState.ChoosingCandidate {
return handleCandidate(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: - Handle Associated Phrases.
if state is InputState.AssociatedPhrases {
let result = handleCandidate(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback)
if result {
return true
} else {
stateCallback(emptyState)
}
}
// MARK: - Handle Marking.
if state is InputState.Marking {
let marking = state as! InputState.Marking
if handleMarking(
state: state as! InputState.Marking, input: input, stateCallback: stateCallback,
errorCallback: errorCallback)
{
return true
}
state = marking.convertToInputting()
stateCallback(state)
}
// MARK: - Handle BPMF Keys.
var composeReading: Bool = false
let skipPhoneticHandling = input.isReservedKey || input.isControlHold || input.isOptionHold
// See if Phonetic reading is valid.
if !skipPhoneticHandling && chkKeyValidity(charCode) {
combinePhoneticReadingBufferKey(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 = checkWhetherToneMarkerConfirmsPhoneticReadingBuffer()
if !composeReading {
let inputting = buildInputtingState() as! InputState.Inputting
stateCallback(inputting)
return true
}
}
// See if we have composition if Enter/Space is hit and buffer is not empty.
// We use "|=" conditioning so that the tone marker key is also taken into account.
// However, Swift does not support "|=".
composeReading = composeReading || (!isPhoneticReadingBufferEmpty() && (input.isSpace || input.isEnter))
if composeReading {
let reading = getSyllableCompositionFromPhoneticReadingBuffer()
if !ifLangModelHasUnigrams(forKey: reading) {
IME.prtDebugIntel("B49C0979")
errorCallback()
let inputting = buildInputtingState() as! InputState.Inputting
stateCallback(inputting)
return true
}
// ... and insert it into the lattice grid...
insertReadingToBuilder(atCursor: reading)
// ... then walk the lattice grid...
let poppedText = _popOverflowComposingTextAndWalk()
// ... get and tweak override model suggestion if possible...
dealWithOverrideModelSuggestions()
// ... then update the text.
clearPhoneticReadingBuffer()
let inputting = buildInputtingState() as! InputState.Inputting
inputting.poppedText = poppedText
stateCallback(inputting)
if mgrPrefs.useSCPCTypingMode {
let choosingCandidates: InputState.ChoosingCandidate = _buildCandidateState(
inputting,
useVerticalMode: input.useVerticalMode)
if choosingCandidates.candidates.count == 1 {
clear()
let text: String = choosingCandidates.candidates.first ?? ""
let committing = InputState.Committing(poppedText: text)
stateCallback(committing)
if !mgrPrefs.associatedPhrasesEnabled {
stateCallback(emptyState)
} else {
let associatedPhrases =
buildAssociatePhraseState(
withKey: text,
useVerticalMode: input.useVerticalMode) as? InputState.AssociatedPhrases
if let associatedPhrases = associatedPhrases {
stateCallback(associatedPhrases)
} else {
stateCallback(emptyState)
}
}
} else {
stateCallback(choosingCandidates)
}
}
return true
}
// MARK: - Calling candidate window using Space or Down or PageUp / PageDn.
if isPhoneticReadingBufferEmpty() && (state is InputState.NotEmpty)
&& (input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse || input.isSpace
|| input.isPageDown || input.isPageUp || input.isTab
|| (input.useVerticalMode && (input.isVerticalModeOnlyChooseCandidateKey)))
{
if input.isSpace {
// If the spacebar is NOT set to be a selection key
if input.isShiftHold || !mgrPrefs.chooseCandidateUsingSpace {
if getBuilderCursorIndex() >= getBuilderLength() {
let composingBuffer = (state as! InputState.NotEmpty).composingBuffer
if (composingBuffer.count) != 0 {
let committing = InputState.Committing(poppedText: composingBuffer)
stateCallback(committing)
}
clear()
let committing = InputState.Committing(poppedText: " ")
stateCallback(committing)
let empty = InputState.Empty()
stateCallback(empty)
} else if ifLangModelHasUnigrams(forKey: " ") {
insertReadingToBuilder(atCursor: " ")
let poppedText = _popOverflowComposingTextAndWalk()
let inputting = buildInputtingState() as! InputState.Inputting
inputting.poppedText = poppedText
stateCallback(inputting)
}
return true
}
}
let choosingCandidates = _buildCandidateState(
state as! InputState.NotEmpty,
useVerticalMode: input.useVerticalMode)
stateCallback(choosingCandidates)
return true
}
// MARK: - Function Keys.
// MARK: Esc
// MARK: - Still Nothing.
// Still nothing? Then we update the composing buffer.
// Note that some app has strange behavior if we don't do this,
@ -118,3 +273,21 @@ import Cocoa
return false
}
}
// MARK: - State managements.
@objc extension KeyHandler {
func _buildCandidateState(
_ currentState: InputState.NotEmpty,
useVerticalMode: Bool
) -> InputState.ChoosingCandidate {
let candidatesArray = getCandidatesArray()
let state = InputState.ChoosingCandidate(
composingBuffer: currentState.composingBuffer,
cursorIndex: currentState.cursorIndex,
candidates: candidatesArray as! [String],
useVerticalMode: useVerticalMode)
return state
}
}