vChewing-macOS/Source/Modules/InputHandler_HandleComposit...

368 lines
17 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Shared
import Tekkon
extension InputHandler {
/// InputHandler.HandleInput()
/// - Parameters:
/// - input:
/// - Returns: IMK
func handleComposition(input: InputSignalProtocol) -> Bool? {
if isCodePointInputMode { return handleCodePointComposition(input: input) }
if prefs.cassetteEnabled { return handleCassetteComposition(input: input) }
return handlePhonabetComposition(input: input)
}
// MARK: (Handle BPMF Keys)
/// InputHandler.HandleInput()
/// - Parameters:
/// - input:
/// - Returns: IMK
private func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
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(key: input.charCode)) || 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: input.text)
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()
} else {
delegate.callError("4B0DD2D4語彙庫內無「\(temporaryReadingKey)」的匹配記錄,放棄覆寫游標身後的內容。")
return true
}
}
// Enter (CR / LF)
composer.receiveKey(fromString: confirmCombination ? " " : input.text)
keyConsumedByReading = true
// 調 setInlineDisplayWithCursor() return true
// 調
if !composer.hasIntonation() {
delegate.switchState(generateStateOfInputting())
return true
}
}
let readingKey = composer.getComposition() //
//
var composeReading = composer.hasIntonation() && composer.inputValidityCheck(key: input.charCode) //
// Enter Space composer
// |=
// 使 composer.value.isEmpty composer 調
composeReading = composeReading || (!composer.isEmpty && confirmCombination)
// readingKey
composeReading = composeReading && !readingKey.isEmpty
if composeReading {
if input.isControlHold, input.isCommandHold, input.isEnter,
!input.isOptionHold, !input.isShiftHold, compositor.isEmpty
{
return handleEnter(input: input, readingOnly: true)
}
//
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, !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
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.joined(separator: compositor.separator)
let text: String = firstCandidate.1
delegate.switchState(IMEState.ofCommitting(textToCommit: text))
if prefs.associatedPhrasesEnabled {
let associatedPhrases = generateStateOfAssociates(withPair: .init(keyArray: [reading], value: text))
delegate.switchState(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases)
}
default: break
}
}
// SessionCtl IMK
return true
}
/// 調
/// 調
if keyConsumedByReading {
if readingKey.isEmpty {
composer.clear()
return nil
}
// setInlineDisplayWithCursor()
delegate.switchState(generateStateOfInputting())
return true
}
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 wildcardKey: String { currentLM.cassetteWildcardKey } //
let isWildcardKeyInput: Bool = (input.text == wildcardKey && !wildcardKey.isEmpty)
var keyConsumedByStrokes = false
let skipStrokeHandling =
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|| input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold
let confirmCombination = input.isSpace || input.isEnter
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: input.text) {
if calligrapher.isEmpty, isWildcardKeyInput {
delegate.callError("3606B9C0")
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
}
if isStrokesFull {
delegate.callError("2268DD51: calligrapher is full, clearing calligrapher.")
calligrapher.removeAll()
} else {
calligrapher.append(input.text)
}
if isWildcardKeyInput {
break prehandling
}
keyConsumedByStrokes = true
if !isStrokesFull {
delegate.switchState(generateStateOfInputting())
return true
}
}
var combineStrokes =
(isStrokesFull && prefs.autoCompositeWithLongestPossibleCassetteKey)
|| (isWildcardKeyInput && !calligrapher.isEmpty)
// Enter Space calligrapher
// |=
combineStrokes = combineStrokes || (!calligrapher.isEmpty && confirmCombination)
if combineStrokes {
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, !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.0.joined(separator: compositor.separator)
let text: String = firstCandidate.1
delegate.switchState(IMEState.ofCommitting(textToCommit: text))
if prefs.associatedPhrasesEnabled {
let associatedPhrases = generateStateOfAssociates(withPair: .init(keyArray: [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
}
// MARK: (Handle Code Point Input)
/// InputHandler.HandleInput()
/// - Parameters:
/// - 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()
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] }
var updatedState = IMEState.ofCommitting(textToCommit: char)
updatedState.tooltipDuration = 0
updatedState.tooltip = tooltipCodePointInputMode
delegate.switchState(updatedState)
isCodePointInputMode = true
return true
default:
delegate.switchState(generateStateOfInputting())
isCodePointInputMode = true
return true
}
}
}