vChewing-macOS/Packages/vChewing_MainAssembly/Sources/MainAssembly/InputHandler/InputHandler_HandleComposit...

485 lines
23 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
/// InputHandler.HandleInput()
import AppKit
import Shared
import Tekkon
extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
func handleComposition(input: InputSignalProtocol) -> Bool? {
//
let hardRequirementMet = !input.text.isEmpty && input.charCode.isPrintable
switch currentTypingMethod {
case .codePoint where hardRequirementMet:
return handleCodePointComposition(input: input)
case .haninKeyboardSymbol where [[], .shift].contains(input.keyModifierFlags):
return handleHaninKeyboardSymbolModeInput(input: input)
case .vChewingFactory where hardRequirementMet && prefs.cassetteEnabled:
return handleCassetteComposition(input: input)
case .vChewingFactory where hardRequirementMet && !prefs.cassetteEnabled:
return handlePhonabetComposition(input: input)
default: return nil
}
}
}
// MARK: - (Handle BPMF Keys)
private extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
var inputText = (input.inputTextIgnoringModifiers ?? input.text)
inputText = inputText.lowercased().applyingTransformFW2HW(reverse: false)
let existedIntonation = composer.intonation
var overrideHappened = false
// 調 keyConsumedByReading
// Space
// 調
// 調調
var keyConsumedByReading = false
let skipPhoneticHandling =
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|| input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold
let confirmCombination = input.isSpace || input.isEnter
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
guard condition else { return }
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
guard var keyToNarrate = maybeKey else { return }
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
}
// inputValidityCheck() charCode UniChar
// keyConsumedByReading
// composer.receiveKey() String UniChar
if (!skipPhoneticHandling && composer.inputValidityCheck(charStr: inputText)) || confirmCombination {
// macOS 調
//
proc: if [0, 1].contains(prefs.specifyIntonationKeyBehavior), composer.isEmpty, !input.isSpace {
// prevReading 調調
guard let prevReading = previousParsableReading, isIntonationKey(input) else { break proc }
var theComposer = composer
prevReading.0.map(\.description).forEach { theComposer.receiveKey(fromPhonabet: $0) }
// 調調
let oldIntonation: Tekkon.Phonabet = theComposer.intonation
theComposer.receiveKey(fromString: inputText)
if theComposer.intonation == oldIntonation, prefs.specifyIntonationKeyBehavior == 1 { break proc }
theComposer.intonation.clear()
//
let temporaryReadingKey = theComposer.getComposition()
if currentLM.hasUnigramsFor(keyArray: [temporaryReadingKey]) {
compositor.dropKey(direction: .rear)
walk() // Walk walk
composer = theComposer
// generateStateOfInputting()調 generateStateOfInputting()
overrideHappened = true
} else {
delegate.callError("4B0DD2D4語彙庫內無「\(temporaryReadingKey)」的匹配記錄,放棄覆寫游標身後的內容。")
return true
}
}
// Enter (CR / LF)
composer.receiveKey(fromString: confirmCombination ? " " : inputText)
keyConsumedByReading = true
narrateTheComposer(when: !overrideHappened && prefs.readingNarrationCoverage >= 2, allowDuplicates: false)
// 調 setInlineDisplayWithCursor() return true
// 調
if !composer.hasIntonation() {
delegate.switchState(generateStateOfInputting())
return true
}
}
//
var composeReading = composer.hasIntonation() && composer.inputValidityCheck(charStr: inputText)
// Enter Space composer
// |=
composeReading = composeReading || (!composer.isEmpty && confirmCombination)
ifComposeReading: if composeReading {
if input.isControlHold, input.isCommandHold, input.isEnter,
!input.isOptionHold, !input.isShiftHold, compositor.isEmpty
{
return handleEnter(input: input, readingOnly: true)
}
//
let maybeKey = composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
guard let readingKey = maybeKey else { break ifComposeReading }
//
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
delegate.callError("B49C0979語彙庫內無「\(readingKey)」的匹配記錄。")
if prefs.keepReadingUponCompositionError {
composer.intonation.clear() // 調
delegate.switchState(generateStateOfInputting())
return true
}
composer.clear()
//
switch compositor.isEmpty {
case false: delegate.switchState(generateStateOfInputting())
case true: delegate.switchState(IMEState.ofAbortion())
}
return true // IMK
}
//
// Megrez
if input.isInvalid {
delegate.callError("22017F76: 不合規的按鍵輸入。")
return true
} else if !compositor.insertKey(readingKey) {
delegate.callError("3CF278C9: 得檢查對應的語言模組的 hasUnigramsFor() 是否有誤判之情形。")
return true
} else {
narrateTheComposer(with: readingKey, when: prefs.readingNarrationCoverage == 1)
}
//
walk()
// App
let textToCommit = commitOverflownComposition
//
retrieveUOMSuggestions(apply: true)
//
composer.clear()
// setInlineDisplayWithCursor()
var inputting = generateStateOfInputting()
inputting.textToCommit = textToCommit
if overrideHappened {
inputting.tooltip = "Previous intonation has been overridden.".localized
inputting.tooltipDuration = 2
inputting.data.tooltipColorState = .normal
}
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.keyArray
let text: String = firstCandidate.value
delegate.switchState(IMEState.ofCommitting(textToCommit: text))
if prefs.associatedPhrasesEnabled {
let associatedCandidates = generateArrayOfAssociates(withPairs: [.init(keyArray: reading, value: text)])
delegate.switchState(
associatedCandidates.isEmpty
? IMEState.ofEmpty()
: IMEState.ofAssociates(candidates: associatedCandidates)
)
}
default: break
}
}
// SessionCtl IMK
return true
}
/// 調
/// 調
if keyConsumedByReading {
// strict false
if composer.phonabetKeyForQuery(pronouncable: false) == nil {
// 調
if !composer.isPinyinMode, input.isSpace,
compositor.insertKey(existedIntonation.value)
{
walk()
var theInputting = generateStateOfInputting()
theInputting.textToCommit = commitOverflownComposition
composer.clear()
delegate.switchState(theInputting)
return true
}
composer.clear()
return nil
}
// setInlineDisplayWithCursor()
var resultState = generateStateOfInputting()
resultState.tooltip = tooltipForStandaloneIntonationMark
resultState.tooltipDuration = 0
resultState.data.tooltipColorState = .prompt
delegate.switchState(resultState)
return true
}
return nil
}
}
// MARK: -
private extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
func handleCassetteComposition(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
let state = delegate.state
// `%quick`
var handleQuickCandidate = true
if currentLM.areCassetteCandidateKeysShiftHeld { handleQuickCandidate = input.isShiftHold }
let hasQuickCandidates: Bool = state.type == .ofInputting && state.isCandidateContainer
// `%symboldef`
if handleCassetteSymbolTable(input: input) {
return true
} else if hasQuickCandidates, input.text != currentLM.cassetteWildcardKey {
// `%quick` `%symboldef`
guard !(handleQuickCandidate && handleCandidate(input: input, ignoringModifiers: true)) else { return true }
} else {
// `%quick`
guard !(hasQuickCandidates && handleQuickCandidate && handleCandidate(input: input)) else { return true }
}
//
var wildcardKey: String { currentLM.cassetteWildcardKey } //
let inputText = input.text
let isWildcardKeyInput: Bool = (inputText == wildcardKey && !wildcardKey.isEmpty)
let skipStrokeHandling =
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|| input.isControlHold || input.isOptionHold || input.isCommandHold // || input.isShiftHold
var confirmCombination = input.isSpace
var isLongestPossibleKeyFormed: Bool {
guard !isWildcardKeyInput, prefs.autoCompositeWithLongestPossibleCassetteKey else { return false }
return !currentLM.hasCassetteWildcardResultsFor(key: calligrapher) && !calligrapher.isEmpty
}
var isStrokesFull: Bool {
calligrapher.count >= currentLM.maxCassetteKeyLength || isLongestPossibleKeyFormed
}
prehandling: if !skipStrokeHandling && currentLM.isThisCassetteKeyAllowed(key: inputText) {
if calligrapher.isEmpty, isWildcardKeyInput {
delegate.callError("3606B9C0")
if input.beganWithLetter {
var newEmptyState = compositor.isEmpty ? IMEState.ofEmpty() : generateStateOfInputting()
newEmptyState.tooltip = NSLocalizedString("Wildcard key cannot be the initial key.", comment: "")
newEmptyState.data.tooltipColorState = .redAlert
newEmptyState.tooltipDuration = 1.0
delegate.switchState(newEmptyState)
return true
}
delegate.callNotification(NSLocalizedString("Wildcard key cannot be the initial key.", comment: ""))
return nil
}
if isStrokesFull {
delegate.callError("2268DD51: calligrapher is full, clearing calligrapher.")
calligrapher.removeAll()
} else {
calligrapher.append(inputText)
}
if isWildcardKeyInput {
break prehandling
}
if !isStrokesFull {
var result = generateStateOfInputting()
if !calligrapher.isEmpty, let fetched = currentLM.cassetteQuickSetsFor(key: calligrapher)?.split(separator: "\t") {
result.candidates = fetched.enumerated().map {
(keyArray: [($0.offset + 1).description], value: $0.element.description)
}
}
delegate.switchState(result)
return true
}
}
if !(state.type == .ofInputting && state.isCandidateContainer) {
confirmCombination = confirmCombination || input.isEnter
}
var combineStrokes =
(isStrokesFull && prefs.autoCompositeWithLongestPossibleCassetteKey)
|| (isWildcardKeyInput && !calligrapher.isEmpty)
// Enter Space calligrapher
// |=
combineStrokes = combineStrokes || (!calligrapher.isEmpty && confirmCombination)
ifCombineStrokes: if combineStrokes {
// calligrapher
guard !calligrapher.isEmpty else { break ifCombineStrokes }
if input.isControlHold, input.isCommandHold, input.isEnter,
!input.isOptionHold, !input.isShiftHold, composer.isEmpty
{
return handleEnter(input: input, readingOnly: true)
}
//
if !currentLM.hasUnigramsFor(keyArray: [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
}
//
// Megrez
if input.isInvalid {
delegate.callError("BFE387CC: 不合規的按鍵輸入。")
return true
} else if !compositor.insertKey(calligrapher) {
delegate.callError("61F6B11F: 得檢查對應的語言模組的 hasUnigramsFor() 是否有誤判之情形。")
return true
}
//
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.keyArray
let text: String = firstCandidate.value
delegate.switchState(IMEState.ofCommitting(textToCommit: text))
if prefs.associatedPhrasesEnabled {
let associatedCandidates = generateArrayOfAssociates(withPairs: [.init(keyArray: reading, value: text)])
delegate.switchState(
associatedCandidates.isEmpty
? IMEState.ofEmpty()
: IMEState.ofAssociates(candidates: associatedCandidates)
)
}
default: break
}
}
// SessionCtl IMK
return true
}
return nil
}
}
// MARK: - (Handle Code Point Input)
private extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
func handleCodePointComposition(input: InputSignalProtocol) -> Bool? {
guard !input.isReservedKey else { return nil }
guard let delegate = delegate, input.text.count == 1 else { return nil }
guard !input.text.compactMap(\.hexDigitValue).isEmpty else {
delegate.callError("05DD692C輸入的字元並非 ASCII 字元。。")
return true
}
switch strCodePointBuffer.count {
case 0 ..< 4:
if strCodePointBuffer.count < 3 {
strCodePointBuffer.append(input.text)
var updatedState = generateStateOfInputting(guarded: true)
updatedState.tooltipDuration = 0
updatedState.tooltip = TypingMethod.codePoint.getTooltip(vertical: delegate.isVerticalTyping)
delegate.switchState(updatedState)
return true
}
guard
var char = "\(strCodePointBuffer)\(input.text)"
.parsedAsHexLiteral(encoding: IMEApp.currentInputMode.nonUTFEncoding)?.first?.description
else {
delegate.callError("D220B880輸入的字碼沒有對應的字元。")
var updatedState = IMEState.ofAbortion()
updatedState.tooltipDuration = 0
updatedState.tooltip = "Invalid Code Point.".localized
delegate.switchState(updatedState)
currentTypingMethod = .codePoint
return true
}
// macOS
if char.count > 1 { char = char.map(\.description)[0] }
delegate.switchState(IMEState.ofCommitting(textToCommit: char))
var updatedState = generateStateOfInputting(guarded: true)
updatedState.tooltipDuration = 0
updatedState.tooltip = TypingMethod.codePoint.getTooltip(vertical: delegate.isVerticalTyping)
delegate.switchState(updatedState)
currentTypingMethod = .codePoint
return true
default:
delegate.switchState(generateStateOfInputting())
currentTypingMethod = .codePoint
return true
}
}
}
// MARK: - Handle Hanin Keyboard Symbol Inputs
private extension InputHandler {
///
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleHaninKeyboardSymbolModeInput(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
let charText = input.text.lowercased().applyingTransformFW2HW(reverse: false)
guard CandidateNode.mapHaninKeyboardSymbols.keys.contains(charText) else {
return revolveTypingMethod(to: .vChewingFactory)
}
guard
charText.count == 1, let symbols = CandidateNode.queryHaninKeyboardSymbols(char: charText)
else {
delegate.callError("C1A760C7")
return true
}
// commit buffer ESC
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
if symbols.members.count == 1 {
delegate.switchState(IMEState.ofCommitting(textToCommit: symbols.members.map(\.name).joined()))
} else {
delegate.switchState(IMEState.ofSymbolTable(node: symbols))
}
currentTypingMethod = .vChewingFactory // toggle
return true
}
}