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

441 lines
20 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? {
guard let delegate = delegate else { return nil }
//
guard !input.text.isEmpty, input.charCode.isPrintable else { return nil }
if isCodePointInputMode { return handleCodePointComposition(input: input) }
if prefs.cassetteEnabled {
// `%quick`
var handleQuickCandidate = true
if currentLM.areCassetteCandidateKeysShiftHeld { handleQuickCandidate = input.isShiftHold }
let hasQuickCandidates: Bool = delegate.state.type == .ofInputting && delegate.state.isCandidateContainer
// `%symboldef`
if handleCassetteSymbolTable(input: input) {
return true
} else if hasQuickCandidates, input.text != currentLM.cassetteWildcardKey,
let itim = input.inputTextIgnoringModifiers,
let newEv = (input as? NSEvent)?.reinitiate(characters: itim)
{
// `%quick` `%symboldef`
guard !(handleQuickCandidate && handleCandidate(input: newEv)) else { return true }
} else {
// `%quick`
guard !(hasQuickCandidates && handleQuickCandidate && handleCandidate(input: input)) else { return true }
}
return handleCassetteComposition(input: input)
}
return handlePhonabetComposition(input: input)
}
// MARK: (Handle BPMF Keys)
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
private 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
// 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
// 調 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
}
//
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: -
extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
private func handleCassetteComposition(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
let state = delegate.state
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)
/// InputHandler.HandleInput()
/// - Parameter input:
/// - Returns: IMK
private 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 = tooltipCodePointInputMode
delegate.switchState(updatedState)
return true
}
let encoding: CFStringEncodings? = {
switch IMEApp.currentInputMode {
case .imeModeCHS: return .GB_18030_2000
case .imeModeCHT: return .big5_HKSCS_1999
default: return nil
}
}()
guard
var char = "\(strCodePointBuffer)\(input.text)"
.parsedAsHexLiteral(encoding: encoding)?.first?.description
else {
delegate.callError("D220B880輸入的字碼沒有對應的字元。")
var updatedState = IMEState.ofAbortion()
updatedState.tooltipDuration = 0
updatedState.tooltip = "Invalid Code Point.".localized
delegate.switchState(updatedState)
isCodePointInputMode = true
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 = tooltipCodePointInputMode
delegate.switchState(updatedState)
isCodePointInputMode = true
return true
default:
delegate.switchState(generateStateOfInputting())
isCodePointInputMode = true
return true
}
}
}