InputHandler // Implement stroke composition support.

This commit is contained in:
ShikiSuen 2022-10-17 00:13:08 +08:00
parent 23e02b9132
commit c0ef70fe0d
6 changed files with 191 additions and 41 deletions

View File

@ -29,7 +29,7 @@ extension vChewingLM {
///
public var isLoaded: Bool { !charDefMap.isEmpty }
/// 使
public var allowedKeys: [String] { Array(keyNameMap.keys) }
public var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated }
///
public func convertKeyToDisplay(char: String) -> String {
keyNameMap[char] ?? char

View File

@ -57,6 +57,7 @@ public class InputHandler: InputHandlerProtocol {
///
let kEpsilon: Double = 0.000_001
public var calligrapher = "" //
public var composer: Tekkon.Composer = .init() //
public var compositor: Megrez.Compositor //
public var currentUOM: vChewingLM.LMUserOverride
@ -83,6 +84,7 @@ public class InputHandler: InputHandlerProtocol {
public func clear() {
composer.clear()
compositor.clear()
calligrapher.removeAll()
}
// MARK: - Functions dealing with Megrez.
@ -336,6 +338,8 @@ public class InputHandler: InputHandlerProtocol {
// MARK: - Extracted methods and functions (Tekkon).
var isComposerOrCalligrapherEmpty: Bool { prefs.cassetteEnabled ? calligrapher.isEmpty : composer.isEmpty }
/// _
var currentKeyboardParser: String { currentKeyboardParserType.name + "_" }
var currentKeyboardParserType: KeyboardParser { .init(rawValue: prefs.keyboardParser) ?? .ofStandard }
@ -363,6 +367,22 @@ public class InputHandler: InputHandlerProtocol {
composer.phonabetCombinationCorrectionEnabled = prefs.autoCorrectReadingCombination
}
func clearComposerAndCalligrapher() {
_ = prefs.cassetteEnabled ? calligrapher.removeAll() : composer.clear()
}
func letComposerAndCalligrapherDoBackSpace() {
_ = prefs.cassetteEnabled ? calligrapher = String(calligrapher.dropLast(1)) : composer.doBackSpace()
}
///
/// 調調
var previousParsableCalligraph: String? {
if compositor.cursor == 0 { return nil }
let cursorPrevious = max(compositor.cursor - 1, 0)
return compositor.keys[cursorPrevious]
}
///
/// 調調
var previousParsableReading: (String, String, Bool)? {
@ -390,6 +410,18 @@ public class InputHandler: InputHandlerProtocol {
return theComposer.hasIntonation(withNothingElse: true)
}
var readingForDisplay: String {
if !prefs.cassetteEnabled {
return composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer)
}
if !prefs.showTranslatedStrokesInCompositionBuffer { return calligrapher }
var result = calligrapher.charComponents
for idx in 0..<result.count {
result[idx] = currentLM.currentCassette.convertKeyToDisplay(char: result[idx])
}
return result.joined()
}
// MARK: - Extracted methods and functions (Megrez).
///

View File

@ -168,8 +168,12 @@ extension InputHandler {
]
let punctuation: String = arrPunctuations.joined()
let isInputValid: Bool =
prefs.cassetteEnabled
? currentLM.currentCassette.allowedKeys.contains(input.text) : composer.inputValidityCheck(key: input.charCode)
var shouldAutoSelectCandidate: Bool =
composer.inputValidityCheck(key: input.charCode) || currentLM.hasUnigramsFor(key: customPunctuation)
isInputValid || currentLM.hasUnigramsFor(key: customPunctuation)
|| currentLM.hasUnigramsFor(key: punctuation)
if !shouldAutoSelectCandidate, input.isUpperCaseASCIILetterKey {

View File

@ -17,9 +17,11 @@ extension InputHandler {
/// - input:
/// - Returns: IMK
func handleComposition(input: InputSignalProtocol) -> Bool? {
handlePhonabetComposition(input: input)
prefs.cassetteEnabled ? handleCassetteComposition(input: input) : handlePhonabetComposition(input: input)
}
// MARK: (Handle BPMF Keys)
/// InputHandler.HandleInput()
/// - Parameters:
/// - input:
@ -27,8 +29,6 @@ extension InputHandler {
private func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
// MARK: (Handle BPMF Keys)
var keyConsumedByReading = false
let skipPhoneticHandling =
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
@ -76,7 +76,7 @@ extension InputHandler {
var composeReading = composer.hasIntonation() && composer.inputValidityCheck(key: input.charCode) //
// Enter Space _composer
// Enter Space composer
// |=
composeReading = composeReading || (!composer.isEmpty && (input.isSpace || input.isEnter))
if composeReading {
@ -160,3 +160,106 @@ extension InputHandler {
return nil
}
}
// MARK: -
extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameters:
/// - input:
/// - Returns: IMK
private func handleCassetteComposition(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
var keyConsumedByStrokes = false
let skipStrokeHandling =
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|| input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold
var isStrokesFull: Bool { calligrapher.count >= currentLM.currentCassette.maxKeyLength }
if !skipStrokeHandling && currentLM.currentCassette.allowedKeys.contains(input.text) {
if isStrokesFull {
calligrapher = String(calligrapher.dropLast(1))
}
calligrapher.append(input.text)
keyConsumedByStrokes = true
if !isStrokesFull {
delegate.switchState(generateStateOfInputting())
return true
}
}
var compoundStrokes = isStrokesFull //
// Enter Space calligrapher
// |=
compoundStrokes = compoundStrokes || (!calligrapher.isEmpty && (input.isSpace || input.isEnter))
if compoundStrokes {
//
if !currentLM.hasUnigramsFor(key: calligrapher) {
delegate.callError("B49C0979_Cassette語彙庫內無「\(calligrapher)」的匹配記錄。")
calligrapher.removeAll()
//
switch compositor.isEmpty {
case false: delegate.switchState(generateStateOfInputting())
case true: delegate.switchState(IMEState.ofAbortion())
}
return true // IMK
}
//
compositor.insertKey(calligrapher)
//
walk()
// App
let textToCommit = commitOverflownComposition
//
retrieveUOMSuggestions(apply: true)
//
calligrapher.removeAll()
// setInlineDisplayWithCursor()
var inputting = generateStateOfInputting()
inputting.textToCommit = textToCommit
delegate.switchState(inputting)
///
if prefs.useSCPCTypingMode {
let candidateState: IMEStateProtocol = generateStateOfCandidates()
switch candidateState.candidates.count {
case 2...: delegate.switchState(candidateState)
case 1:
let firstCandidate = candidateState.candidates.first! //
let reading: String = firstCandidate.0
let text: String = firstCandidate.1
delegate.switchState(IMEState.ofCommitting(textToCommit: text))
if !prefs.associatedPhrasesEnabled {
delegate.switchState(IMEState.ofEmpty())
} else {
let associatedPhrases = generateStateOfAssociates(withPair: .init(key: reading, value: text))
delegate.switchState(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases)
}
default: break
}
}
// SessionCtl IMK
return true
}
/// 調
if keyConsumedByStrokes {
// setInlineDisplayWithCursor()
delegate.switchState(generateStateOfInputting())
return true
}
return nil
}
}

View File

@ -112,7 +112,7 @@ extension InputHandler {
// MARK: (Calling candidate window using Up / Down or PageUp / PageDn.)
if state.hasComposition, composer.isEmpty, !input.isOptionHold,
if state.hasComposition, isComposerOrCalligrapherEmpty, !input.isOptionHold,
input.isCursorClockLeft || input.isCursorClockRight || input.isSpace
|| input.isPageDown || input.isPageUp || (input.isTab && prefs.specifyShiftTabKeyBehavior)
{
@ -181,7 +181,7 @@ extension InputHandler {
if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold, state.type != .ofDeactivated {
if input.isOptionHold {
if currentLM.hasUnigramsFor(key: "_punctuation_list") {
if composer.isEmpty {
if isComposerOrCalligrapherEmpty {
compositor.insertKey("_punctuation_list")
walk()
// App
@ -268,7 +268,7 @@ extension InputHandler {
///
/// F1-F12
/// 便
if state.hasComposition || !composer.isEmpty {
if state.hasComposition || !isComposerOrCalligrapherEmpty {
delegate.callError("Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
delegate.callError("A9BFF20E")
delegate.switchState(state)

View File

@ -23,7 +23,7 @@ extension InputHandler {
/// IMEStateData NSAttributeString
var displayTextSegments: [String] = compositor.walkedNodes.values
var cursor = convertCursorForDisplay(compositor.cursor)
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer)
let reading: String = readingForDisplay //
if !reading.isEmpty {
var newDisplayTextSegments = [String]()
var temporaryNode = ""
@ -67,10 +67,11 @@ extension InputHandler {
continue
}
if !theNode.isReadingMismatched {
for _ in 0..<strNodeValue.count {
guard readingCursorIndex < rawCursor else { continue }
composedStringCursorIndex += 1
readingCursorIndex += 1
strNodeValue.forEach { _ in
if readingCursorIndex < rawCursor {
composedStringCursorIndex += 1
readingCursorIndex += 1
}
}
continue
}
@ -227,7 +228,7 @@ extension InputHandler {
return false
}
guard composer.isEmpty else {
guard isComposerOrCalligrapherEmpty else {
//
delegate.callError("A9B69908D")
delegate.switchState(state)
@ -243,7 +244,7 @@ extension InputHandler {
delegate.switchState(inputting)
//
guard prefs.useSCPCTypingMode, composer.isEmpty else { return true }
guard prefs.useSCPCTypingMode, isComposerOrCalligrapherEmpty else { return true }
let candidateState = generateStateOfCandidates()
switch candidateState.candidates.count {
@ -339,14 +340,22 @@ extension InputHandler {
guard state.type == .ofInputting else { return false }
// macOS Shift+BackSpace
switch prefs.specifyShiftBackSpaceKeyBehavior {
shiftBksp: switch prefs.specifyShiftBackSpaceKeyBehavior {
case 0:
guard input.isShiftHold, composer.isEmpty else { break }
guard let prevReading = previousParsableReading else { break }
// prevReading 調調
compositor.dropKey(direction: .rear)
walk() // Walk walk
prevReading.1.charComponents.forEach { composer.receiveKey(fromPhonabet: $0) }
if prefs.cassetteEnabled {
guard input.isShiftHold, calligrapher.isEmpty else { break shiftBksp }
guard let prevReading = previousParsableCalligraph else { break shiftBksp }
compositor.dropKey(direction: .rear)
walk() // Walk walk
calligrapher = prevReading
} else {
guard input.isShiftHold, isComposerOrCalligrapherEmpty else { break shiftBksp }
guard let prevReading = previousParsableReading else { break shiftBksp }
// prevReading 調調
compositor.dropKey(direction: .rear)
walk() // Walk walk
prevReading.1.charComponents.forEach { composer.receiveKey(fromPhonabet: $0) }
}
delegate.switchState(generateStateOfInputting())
return true
case 1:
@ -360,9 +369,11 @@ extension InputHandler {
return true
}
if composer.hasIntonation(withNothingElse: true) {
composer.clear()
} else if composer.isEmpty {
let isConfirm: Bool = prefs.cassetteEnabled ? input.isSpace : composer.hasIntonation(withNothingElse: true)
if isConfirm {
clearComposerAndCalligrapher()
} else if isComposerOrCalligrapherEmpty {
if compositor.cursor > 0 {
compositor.dropKey(direction: .rear)
walk()
@ -372,10 +383,10 @@ extension InputHandler {
return true
}
} else {
composer.doBackSpace()
letComposerAndCalligrapherDoBackSpace()
}
switch composer.isEmpty && compositor.isEmpty {
switch isComposerOrCalligrapherEmpty && compositor.isEmpty {
case false: delegate.switchState(generateStateOfInputting())
case true: delegate.switchState(IMEState.ofAbortion())
}
@ -398,17 +409,17 @@ extension InputHandler {
return true
}
if compositor.cursor == compositor.length, composer.isEmpty {
if compositor.cursor == compositor.length, isComposerOrCalligrapherEmpty {
delegate.callError("9B69938D")
delegate.switchState(state)
return true
}
if composer.isEmpty {
if isComposerOrCalligrapherEmpty {
compositor.dropKey(direction: .front)
walk()
} else {
composer.clear()
clearComposerAndCalligrapher()
}
let inputting = generateStateOfInputting()
@ -428,7 +439,7 @@ extension InputHandler {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !composer.isEmpty { delegate.callError("9B6F908D") }
if !isComposerOrCalligrapherEmpty { delegate.callError("9B6F908D") }
delegate.switchState(state)
return true
}
@ -442,7 +453,7 @@ extension InputHandler {
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
if !isComposerOrCalligrapherEmpty {
delegate.callError("ABC44080")
delegate.switchState(state)
return true
@ -468,7 +479,7 @@ extension InputHandler {
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
if !isComposerOrCalligrapherEmpty {
delegate.callError("9B69908D")
delegate.switchState(state)
return true
@ -499,9 +510,9 @@ extension InputHandler {
/// macOS Windows 使
delegate.switchState(IMEState.ofAbortion())
} else {
if composer.isEmpty { return true }
///
composer.clear()
if isComposerOrCalligrapherEmpty { return true }
///
clearComposerAndCalligrapher()
switch compositor.isEmpty {
case false: delegate.switchState(generateStateOfInputting())
case true: delegate.switchState(IMEState.ofAbortion())
@ -521,7 +532,7 @@ extension InputHandler {
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
if !isComposerOrCalligrapherEmpty {
delegate.callError("B3BA5257")
delegate.switchState(state)
return true
@ -584,7 +595,7 @@ extension InputHandler {
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
if !isComposerOrCalligrapherEmpty {
delegate.callError("6ED95318")
delegate.switchState(state)
return true
@ -643,7 +654,7 @@ extension InputHandler {
func rotateCandidate(reverseOrder: Bool) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
if isComposerOrCalligrapherEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
guard state.type == .ofInputting else {
guard state.type == .ofEmpty else {
delegate.callError("6044F081")
@ -653,7 +664,7 @@ extension InputHandler {
return false
}
guard composer.isEmpty else {
guard isComposerOrCalligrapherEmpty else {
delegate.callError("A2DAF7BC")
return true
}