vChewing-macOS/Source/Modules/InputHandler_Core.swift

460 lines
21 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.
import LangModelAssembly
import Megrez
import Shared
import Tekkon
/// 調
/// Megrez Tekkon
/// composer compositor
// MARK: - (Delegate).
/// InputHandler
public protocol InputHandlerDelegate {
var selectionKeys: String { get }
var clientBundleIdentifier: String { get }
func candidateController() -> CtlCandidateProtocol
func candidateSelectionCalledByInputHandler(at index: Int)
func performUserPhraseOperation(with state: IMEStateProtocol, addToFilter: Bool)
-> Bool
}
// MARK: - (Kernel).
/// InputHandler 調
public class InputHandler {
/// (SessionCtl)便
public var delegate: InputHandlerDelegate?
public var prefs: PrefMgrProtocol
///
let kEpsilon: Double = 0.000_001
public var composer: Tekkon.Composer = .init() //
public var compositor: Megrez.Compositor //
public var currentUOM: vChewingLM.LMUserOverride
public var currentLM: vChewingLM.LMInstantiator {
didSet {
compositor.langModel = .init(withLM: currentLM)
clear()
}
}
///
public init(lm: vChewingLM.LMInstantiator, uom: vChewingLM.LMUserOverride, pref: PrefMgrProtocol) {
prefs = pref
currentLM = lm
currentUOM = uom
///
Megrez.Compositor.maxSpanLength = prefs.maxCandidateLength
/// ensureCompositor()
compositor = Megrez.Compositor(with: currentLM, separator: "-")
///
ensureKeyboardParser()
}
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 cursorForCandidate: Int {
compositor.cursor
- ((compositor.cursor == compositor.width || !prefs.useRearCursorMode) && compositor.cursor > 0 ? 1 : 0)
}
///
///
/// Viterbi
///
///
func walk() {
compositor.walk()
// GraphViz
if prefs.isDebugModeEnabled {
let result = compositor.dumpDOT
let thePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending(
"/vChewing-visualization.dot")
do {
try result.write(toFile: thePath, atomically: true, encoding: .utf8)
} catch {
vCLog("Failed from writing dumpDOT results.")
}
}
}
///
/// - Parameter key:
/// - Returns:
/// nil
func generateArrayOfAssociates(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 = cursorForCandidate + 1
var rearBoundaryEX = cursorForCandidate
var debugIntelToPrint = ""
if grid.overrideCandidate(theCandidate, at: cursorForCandidate) {
grid.walk()
let range = grid.walkedNodes.contextRange(ofGivenCursor: cursorForCandidate)
rearBoundaryEX = range.lowerBound
frontBoundaryEX = range.upperBound
debugIntelToPrint.append("EX: \(rearBoundaryEX)..<\(frontBoundaryEX), ")
}
let range = compositor.walkedNodes.contextRange(ofGivenCursor: cursorForCandidate)
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)")
vCLog(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 consolidateNode(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: cursorForCandidate) { return }
let previousWalk = compositor.walkedNodes
//
walk()
let currentWalk = compositor.walkedNodes
// 使
var accumulatedCursor = 0
let currentNode = currentWalk.findNode(at: cursorForCandidate, target: &accumulatedCursor)
guard let currentNode = currentNode else { return }
if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
vCLog("UOM: Start Observation.")
// 使
//
// AppDelegate
prefs.failureFlagForUOMObservation = true
//
//
currentUOM.performObservation(
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: cursorForCandidate,
timestamp: Date().timeIntervalSince1970, saveCallback: { self.currentUOM.saveData() }
)
//
prefs.failureFlagForUOMObservation = false
}
///
if prefs.moveCursorAfterSelectingCandidate, respectCursorPushing {
// compositor.cursor = accumulatedCursor
compositor.jumpCursorBySpan(to: .front)
}
}
///
func generateArrayOfCandidates(fixOrder: Bool = true) -> [(String, String)] {
/// 使 nodesCrossing macOS
/// nodeCrossing
var arrCandidates: [Megrez.Compositor.KeyValuePaired] = {
switch prefs.useRearCursorMode {
case false:
return compositor.fetchCandidates(at: cursorForCandidate, filter: .endAt)
case true:
return compositor.fetchCandidates(at: cursorForCandidate, filter: .beginAt)
}
}()
/// nodes
///
///
if arrCandidates.isEmpty { return .init() }
// 調
if !prefs.fetchSuggestionsFromUserOverrideModel || prefs.useSCPCTypingMode || fixOrder {
return arrCandidates.map { ($0.key, $0.value) }
}
let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = retrieveUOMSuggestions(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.deduplicated
arrCandidates = arrCandidates.stableSort { $0.key.split(separator: "-").count > $1.key.split(separator: "-").count }
return arrCandidates.map { ($0.key, $0.value) }
}
///
@discardableResult func retrieveUOMSuggestions(apply: Bool) -> [(String, Megrez.Unigram)] {
var arrResult = [(String, Megrez.Unigram)]()
///
if prefs.useSCPCTypingMode { return arrResult }
///
if !prefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
///
let suggestion = currentUOM.fetchSuggestion(
currentWalk: compositor.walkedNodes, cursor: cursorForCandidate, 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
)
vCLog(
"UOM: Suggestion retrieved, overriding the node score of the selected candidate: \(suggestedPair.toNGramKey)")
if !compositor.overrideCandidate(suggestedPair, at: cursorForCandidate, overrideType: overrideBehavior) {
compositor.overrideCandidateLiteral(
newestSuggestedCandidate.1.value, at: cursorForCandidate, overrideType: overrideBehavior
)
}
walk()
}
}
arrResult = arrResult.stableSort { $0.1.score > $1.1.score }
return arrResult
}
// MARK: - Extracted methods and functions (Tekkon).
/// _
var currentKeyboardParser: String {
currentKeyboardParserType.name + "_"
}
var currentKeyboardParserType: KeyboardParser {
.init(rawValue: prefs.keyboardParser) ?? .ofStandard
}
///
func ensureKeyboardParser() {
switch currentKeyboardParserType {
case KeyboardParser.ofStandard:
composer.ensureParser(arrange: .ofDachen)
case KeyboardParser.ofDachen26:
composer.ensureParser(arrange: .ofDachen26)
case KeyboardParser.ofETen:
composer.ensureParser(arrange: .ofETen)
case KeyboardParser.ofHsu:
composer.ensureParser(arrange: .ofHsu)
case KeyboardParser.ofETen26:
composer.ensureParser(arrange: .ofETen26)
case KeyboardParser.ofIBM:
composer.ensureParser(arrange: .ofIBM)
case KeyboardParser.ofMiTAC:
composer.ensureParser(arrange: .ofMiTAC)
case KeyboardParser.ofFakeSeigyou:
composer.ensureParser(arrange: .ofFakeSeigyou)
case KeyboardParser.ofSeigyou:
composer.ensureParser(arrange: .ofSeigyou)
case KeyboardParser.ofStarlight:
composer.ensureParser(arrange: .ofStarlight)
case KeyboardParser.ofHanyuPinyin:
composer.ensureParser(arrange: .ofHanyuPinyin)
case KeyboardParser.ofSecondaryPinyin:
composer.ensureParser(arrange: .ofSecondaryPinyin)
case KeyboardParser.ofYalePinyin:
composer.ensureParser(arrange: .ofYalePinyin)
case KeyboardParser.ofHualuoPinyin:
composer.ensureParser(arrange: .ofHualuoPinyin)
case KeyboardParser.ofUniversalPinyin:
composer.ensureParser(arrange: .ofUniversalPinyin)
}
composer.clear()
composer.phonabetCombinationCorrectionEnabled = prefs.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).
///
/// - Parameter input:
/// - Returns:
func generatePunctuationNamePrefix(withKeyCondition input: InputSignalProtocol) -> String {
if prefs.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 PrefMgr
private let compositorWidthLimit = 20
extension InputHandler {
///
///
///
///
/// 使
///
var commitOverflownComposition: String {
guard !compositor.walkedNodes.isEmpty,
compositor.width > compositorWidthLimit,
let identifier = delegate?.clientBundleIdentifier,
prefs.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
}
}