vChewing-macOS/Source/Modules/ControllerModules/KeyHandler_Core.swift

490 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).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT 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.
/// 調
/// Megrez Tekkon
/// composer compositor
import Foundation
// MARK: - (Delegate).
/// KeyHandler
protocol KeyHandlerDelegate {
var clientBundleIdentifier: String { get }
var isVerticalTyping: Bool { get }
func ctlCandidate() -> ctlCandidateProtocol
func keyHandler(
_: KeyHandler, didSelectCandidateAt index: Int,
ctlCandidate controller: ctlCandidateProtocol
)
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
-> Bool
}
// MARK: - (Kernel).
/// KeyHandler 調
public class KeyHandler {
///
let kEpsilon: Double = 0.000001
///
var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty }
var composer: Tekkon.Composer = .init() //
var compositor: Megrez.Compositor //
var currentLM: vChewing.LMInstantiator = .init() //
var currentUOM: vChewing.LMUserOverride = .init() //
/// (ctlInputMethod)便
var delegate: KeyHandlerDelegate?
/// InputMode
/// IME UserPrefs
var inputMode: InputMode = IME.currentInputMode {
willSet {
//
let isCHS: Bool = (newValue == InputMode.imeModeCHS)
/// Prefs IME
IME.currentInputMode = newValue
mgrPrefs.mostRecentInputMode = IME.currentInputMode.rawValue
///
currentLM = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT
currentUOM = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT
///
syncBaseLMPrefs()
///
///
ensureCompositor()
ensureParser()
}
}
///
public init() {
///
Megrez.Compositor.maxSpanLength = mgrPrefs.maxCandidateLength
/// ensureCompositor()
compositor = Megrez.Compositor(with: currentLM, separator: "-")
///
ensureParser()
/// inputMode
/// defer willSet
defer { inputMode = IME.currentInputMode }
}
func clear() {
composer.clear()
compositor.clear()
}
// MARK: - Functions dealing with Megrez.
///
/// - Returns:
func currentMarkedRange() -> Range<Int> {
min(compositor.cursor, compositor.marker)..<max(compositor.cursor, compositor.marker)
}
///
func isCursorCuttingChar(isMarker: Bool = false) -> Bool {
let index = isMarker ? compositor.marker : compositor.cursor
var isBound = (index == compositor.walkedNodes.contextRange(ofGivenCursor: index).lowerBound)
if index == compositor.width { isBound = true }
let rawResult = compositor.walkedNodes.findNode(at: index)?.isReadingMismatched ?? false
return !isBound && rawResult
}
/// Megrez 使便
///
/// 使 Node Crossing
var actualCandidateCursor: Int {
compositor.cursor
- ((compositor.cursor == compositor.width || !mgrPrefs.useRearCursorMode) && compositor.cursor > 0 ? 1 : 0)
}
///
///
/// Viterbi
///
///
func walk() {
compositor.walk()
// GraphViz
if mgrPrefs.isDebugModeEnabled {
let result = compositor.dumpDOT
let appSupportPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending(
"/vChewing-visualization.dot")
do {
try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8)
} catch {
IME.prtDebugIntel("Failed from writing dumpDOT results.")
}
}
}
///
/// - Parameter key:
/// - Returns:
/// nil
func buildAssociatePhraseArray(withPair pair: Megrez.Compositor.KeyValuePaired) -> [(String, String)] {
var arrResult: [(String, String)] = []
if currentLM.hasAssociatedPhrasesFor(pair: pair) {
arrResult = currentLM.associatedPhrasesFor(pair: pair).map { ("", $0) }
}
return arrResult
}
///
///
/// 使
/// **macOS **
/// OV Bug
///
///
/// - Remark:
///
///
///
/// v1.9.3 SP2 Bug v1.9.4
/// v2.0.2
/// - Parameter theCandidate:
func consolidateCursorContext(with theCandidate: Megrez.Compositor.KeyValuePaired) {
var grid = compositor
var frontBoundaryEX = actualCandidateCursor + 1
var rearBoundaryEX = actualCandidateCursor
var debugIntelToPrint = ""
if grid.overrideCandidate(theCandidate, at: actualCandidateCursor) {
grid.walk()
let range = grid.walkedNodes.contextRange(ofGivenCursor: actualCandidateCursor)
rearBoundaryEX = range.lowerBound
frontBoundaryEX = range.upperBound
debugIntelToPrint.append("EX: \(rearBoundaryEX)..<\(frontBoundaryEX), ")
}
let range = compositor.walkedNodes.contextRange(ofGivenCursor: actualCandidateCursor)
var rearBoundary = min(range.lowerBound, rearBoundaryEX)
var frontBoundary = max(range.upperBound, frontBoundaryEX)
debugIntelToPrint.append("INI: \(rearBoundary)..<\(frontBoundary), ")
let cursorBackup = compositor.cursor
while compositor.cursor > rearBoundary { compositor.jumpCursorBySpan(to: .rear) }
rearBoundary = min(compositor.cursor, rearBoundary)
compositor.cursor = cursorBackup //
while compositor.cursor < frontBoundary { compositor.jumpCursorBySpan(to: .front) }
frontBoundary = min(max(compositor.cursor, frontBoundary), compositor.width)
compositor.cursor = cursorBackup //
debugIntelToPrint.append("FIN: \(rearBoundary)..<\(frontBoundary)")
IME.prtDebugIntel(debugIntelToPrint)
//
var nodeIndices = [Int]() //
var position = rearBoundary //
while position < frontBoundary {
guard let regionIndex = compositor.cursorRegionMap[position] else {
position += 1
continue
}
if !nodeIndices.contains(regionIndex) {
nodeIndices.append(regionIndex) //
guard compositor.walkedNodes.count > regionIndex else { break } //
let currentNode = compositor.walkedNodes[regionIndex]
guard currentNode.keyArray.count == currentNode.value.count else {
compositor.overrideCandidate(currentNode.currentPair, at: position)
position += currentNode.keyArray.count
continue
}
let values = currentNode.currentPair.value.charComponents
for (subPosition, key) in currentNode.keyArray.enumerated() {
guard values.count > subPosition else { break } //
let thePair = Megrez.Compositor.KeyValuePaired(
key: key, value: values[subPosition]
)
compositor.overrideCandidate(thePair, at: position)
position += 1
}
continue
}
position += 1
}
}
///
///
/// - Parameters:
/// - value:
/// - respectCursorPushing: true
/// - consolidate:
func fixNode(candidate: (String, String), respectCursorPushing: Bool = true, preConsolidate: Bool = false) {
let theCandidate: Megrez.Compositor.KeyValuePaired = .init(key: candidate.0, value: candidate.1)
///
if preConsolidate { consolidateCursorContext(with: theCandidate) }
//
if !compositor.overrideCandidate(theCandidate, at: actualCandidateCursor) { return }
let previousWalk = compositor.walkedNodes
//
walk()
let currentWalk = compositor.walkedNodes
// 使
var accumulatedCursor = 0
let currentNode = currentWalk.findNode(at: actualCandidateCursor, target: &accumulatedCursor)
guard let currentNode = currentNode else { return }
if currentNode.currentUnigram.score > -12, mgrPrefs.fetchSuggestionsFromUserOverrideModel {
IME.prtDebugIntel("UOM: Start Observation.")
// 使
//
// AppDelegate
mgrPrefs.failureFlagForUOMObservation = true
//
//
currentUOM.performObservation(
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualCandidateCursor,
timestamp: Date().timeIntervalSince1970, saveCallback: { mgrLangModel.saveUserOverrideModelData() }
)
//
mgrPrefs.failureFlagForUOMObservation = false
}
///
if mgrPrefs.moveCursorAfterSelectingCandidate, respectCursorPushing {
// compositor.cursor = accumulatedCursor
compositor.jumpCursorBySpan(to: .front)
}
}
///
func getCandidatesArray(fixOrder: Bool = true) -> [(String, String)] {
/// 使 nodesCrossing macOS
/// nodeCrossing
var arrCandidates: [Megrez.Compositor.KeyValuePaired] = {
switch mgrPrefs.useRearCursorMode {
case false:
return compositor.fetchCandidates(at: actualCandidateCursor, filter: .endAt)
case true:
return compositor.fetchCandidates(at: actualCandidateCursor, filter: .beginAt)
}
}()
/// nodes
///
///
if arrCandidates.isEmpty { return .init() }
// 調
if !mgrPrefs.fetchSuggestionsFromUserOverrideModel || mgrPrefs.useSCPCTypingMode || fixOrder {
return arrCandidates.map { ($0.key, $0.value) }
}
let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = fetchSuggestionsFromUOM(apply: false)
let arrSuggestedCandidates: [Megrez.Compositor.KeyValuePaired] = arrSuggestedUnigrams.map {
Megrez.Compositor.KeyValuePaired(key: $0.0, value: $0.1.value)
}
arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates
arrCandidates = arrCandidates.deduplicate
arrCandidates = arrCandidates.stableSort { $0.key.split(separator: "-").count > $1.key.split(separator: "-").count }
return arrCandidates.map { ($0.key, $0.value) }
}
///
@discardableResult func fetchSuggestionsFromUOM(apply: Bool) -> [(String, Megrez.Unigram)] {
var arrResult = [(String, Megrez.Unigram)]()
///
if mgrPrefs.useSCPCTypingMode { return arrResult }
///
if !mgrPrefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
///
let suggestion = currentUOM.fetchSuggestion(
currentWalk: compositor.walkedNodes, cursor: actualCandidateCursor, timestamp: Date().timeIntervalSince1970
)
arrResult.append(contentsOf: suggestion.candidates)
if apply {
///
if !suggestion.isEmpty, let newestSuggestedCandidate = suggestion.candidates.last {
let overrideBehavior: Megrez.Compositor.Node.OverrideType =
suggestion.forceHighScoreOverride ? .withHighScore : .withTopUnigramScore
let suggestedPair: Megrez.Compositor.KeyValuePaired = .init(
key: newestSuggestedCandidate.0, value: newestSuggestedCandidate.1.value
)
IME.prtDebugIntel(
"UOM: Suggestion retrieved, overriding the node score of the selected candidate: \(suggestedPair.toNGramKey)")
if !compositor.overrideCandidate(suggestedPair, at: actualCandidateCursor, overrideType: overrideBehavior) {
compositor.overrideCandidateLiteral(
newestSuggestedCandidate.1.value, at: actualCandidateCursor, overrideType: overrideBehavior
)
}
walk()
}
}
arrResult = arrResult.stableSort { $0.1.score > $1.1.score }
return arrResult
}
// MARK: - Extracted methods and functions (Tekkon).
/// _
var currentMandarinParser: String {
mgrPrefs.mandarinParserName + "_"
}
///
func ensureParser() {
switch mgrPrefs.mandarinParser {
case MandarinParser.ofStandard.rawValue:
composer.ensureParser(arrange: .ofDachen)
case MandarinParser.ofDachen26.rawValue:
composer.ensureParser(arrange: .ofDachen26)
case MandarinParser.ofETen.rawValue:
composer.ensureParser(arrange: .ofETen)
case MandarinParser.ofHsu.rawValue:
composer.ensureParser(arrange: .ofHsu)
case MandarinParser.ofETen26.rawValue:
composer.ensureParser(arrange: .ofETen26)
case MandarinParser.ofIBM.rawValue:
composer.ensureParser(arrange: .ofIBM)
case MandarinParser.ofMiTAC.rawValue:
composer.ensureParser(arrange: .ofMiTAC)
case MandarinParser.ofFakeSeigyou.rawValue:
composer.ensureParser(arrange: .ofFakeSeigyou)
case MandarinParser.ofSeigyou.rawValue:
composer.ensureParser(arrange: .ofSeigyou)
case MandarinParser.ofStarlight.rawValue:
composer.ensureParser(arrange: .ofStarlight)
case MandarinParser.ofHanyuPinyin.rawValue:
composer.ensureParser(arrange: .ofHanyuPinyin)
case MandarinParser.ofSecondaryPinyin.rawValue:
composer.ensureParser(arrange: .ofSecondaryPinyin)
case MandarinParser.ofYalePinyin.rawValue:
composer.ensureParser(arrange: .ofYalePinyin)
case MandarinParser.ofHualuoPinyin.rawValue:
composer.ensureParser(arrange: .ofHualuoPinyin)
case MandarinParser.ofUniversalPinyin.rawValue:
composer.ensureParser(arrange: .ofUniversalPinyin)
default:
composer.ensureParser(arrange: .ofDachen)
mgrPrefs.mandarinParser = MandarinParser.ofStandard.rawValue
}
composer.clear()
composer.phonabetCombinationCorrectionEnabled = mgrPrefs.autoCorrectReadingCombination
}
///
/// 調調
var previousParsableReading: (String, String, Bool)? {
if compositor.cursor == 0 { return nil }
let cursorPrevious = max(compositor.cursor - 1, 0)
let rawData = compositor.keys[cursorPrevious]
let components = rawData.charComponents
var hasIntonation = false
for neta in components {
if !Tekkon.allowedPhonabets.contains(neta) || neta == " " { return nil }
if Tekkon.allowedIntonations.contains(neta) { hasIntonation = true }
}
if hasIntonation, components.count == 1 { return nil } // 調
let rawDataSansIntonation = hasIntonation ? components.dropLast(1).joined() : rawData
return (rawData, rawDataSansIntonation, hasIntonation)
}
/// 調
/// - Parameter input:
/// - Returns: 調
func isIntonationKey(_ input: InputSignalProtocol) -> Bool {
var theComposer = composer //
theComposer.clear() //
theComposer.receiveKey(fromString: input.text)
return theComposer.hasToneMarker(withNothingElse: true)
}
// MARK: - Extracted methods and functions (Megrez).
///
func syncBaseLMPrefs() {
currentLM.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled
currentLM.isCNSEnabled = mgrPrefs.cns11643Enabled
currentLM.isSymbolEnabled = mgrPrefs.symbolInputEnabled
}
/// 使
func ensureCompositor() {
// 西
compositor = Megrez.Compositor(with: currentLM, separator: "-")
}
///
/// - Parameter input:
/// - Returns:
func generatePunctuationNamePrefix(withKeyCondition input: InputSignalProtocol) -> String {
if mgrPrefs.halfWidthPunctuationEnabled {
return "_half_punctuation_"
}
switch (input.isControlHold, input.isOptionHold) {
case (true, true): return "_alt_ctrl_punctuation_"
case (true, false): return "_ctrl_punctuation_"
case (false, true): return "_alt_punctuation_"
case (false, false): return "_punctuation_"
}
}
}
// MARK: - Components for Popup Composition Buffer (PCB) Window.
///
/// - Remark: IMKTextInput mgrPrefs
private let compositorWidthLimit = 20
extension KeyHandler {
///
///
///
///
/// 使
///
var commitOverflownComposition: String {
guard !compositor.walkedNodes.isEmpty,
compositor.width > compositorWidthLimit,
let identifier = delegate?.clientBundleIdentifier,
mgrPrefs.clientsIMKTextInputIncapable.contains(identifier)
else {
return ""
}
// Steam Client Identifier
var textToCommit = ""
while compositor.width > compositorWidthLimit {
var delta = compositor.width - compositorWidthLimit
let node = compositor.walkedNodes[0]
if node.isReadingMismatched {
delta = node.keyArray.count
textToCommit += node.currentPair.value
} else {
delta = min(delta, node.keyArray.count)
textToCommit += node.currentPair.value.charComponents[0..<delta].joined()
}
let newCursor = max(compositor.cursor - delta, 0)
compositor.cursor = 0
if !node.isReadingMismatched {
consolidateCursorContext(with: node.currentPair)
}
// Bigram
for _ in 0..<delta {
compositor.dropKey(direction: .front)
}
compositor.cursor = newCursor
walk()
}
return textToCommit
}
}