vChewing-macOS/Source/Modules/InputHandler_Core.swift

637 lines
29 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 AppKit
import LangModelAssembly
import Megrez
import Shared
import Tekkon
/// 調
/// Megrez Tekkon
/// composer compositor
// MARK: - InputHandler (Protocol).
public protocol InputHandlerProtocol {
var currentLM: vChewingLM.LMInstantiator { get set }
var currentUOM: vChewingLM.LMUserOverride { get set }
var delegate: InputHandlerDelegate? { get set }
var composer: Tekkon.Composer { get set }
var keySeparator: String { get }
static var keySeparator: String { get }
var isCompositorEmpty: Bool { get }
var isComposerUsingPinyin: Bool { get }
func clear()
func clearComposerAndCalligrapher()
func ensureKeyboardParser()
func triageInput(event input: InputSignalProtocol) -> Bool
func generateStateOfCandidates() -> IMEStateProtocol
func generateStateOfInputting(sansReading: Bool, guarded: Bool) -> IMEStateProtocol
func generateStateOfAssociates(withPair pair: Megrez.KeyValuePaired) -> IMEStateProtocol
func consolidateNode(
candidate: (keyArray: [String], value: String), respectCursorPushing: Bool, preConsolidate: Bool, skipObservation: Bool
)
func updateUnigramData() -> Bool
func previewCompositionBufferForCandidate(at index: Int)
}
extension InputHandlerProtocol {
func generateStateOfInputting(sansReading: Bool = false, guarded: Bool = false) -> IMEStateProtocol {
generateStateOfInputting(sansReading: sansReading, guarded: guarded)
}
func consolidateNode(candidate: (keyArray: [String], value: String), respectCursorPushing: Bool, preConsolidate: Bool) {
consolidateNode(
candidate: candidate, respectCursorPushing: respectCursorPushing,
preConsolidate: preConsolidate, skipObservation: false
)
}
}
// MARK: - (Delegate).
/// InputHandler
public protocol InputHandlerDelegate {
var isASCIIMode: Bool { get }
var isVerticalTyping: Bool { get }
var selectionKeys: String { get }
var state: IMEStateProtocol { get set }
var clientBundleIdentifier: String { get }
var clientMitigationLevel: Int { get }
func callError(_ logMessage: String)
func callNotification(_ message: String)
func updateVerticalTypingStatus()
func switchState(_ newState: IMEStateProtocol)
func candidateController() -> CtlCandidateProtocol?
func candidateSelectionConfirmedByInputHandler(at index: Int)
func setInlineDisplayWithCursor()
func updatePopupDisplayWithCursor()
func performUserPhraseOperation(addToFilter: Bool) -> Bool
}
// MARK: - (Kernel).
/// InputHandler 調
public class InputHandler: InputHandlerProtocol {
/// (SessionCtl)便
public var delegate: InputHandlerDelegate?
public var prefs: PrefMgrProtocol
///
let kEpsilon: Double = 0.000_001
public var calligrapher = "" //
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()
}
public func clear() {
clearComposerAndCalligrapher()
compositor.clear()
isCodePointInputMode = false
isHaninKeyboardSymbolMode = false
}
/// /
var isConsideredEmptyForNow: Bool {
compositor.isEmpty && isComposerOrCalligrapherEmpty && !isCodePointInputMode && !isHaninKeyboardSymbolMode
}
// MARK: - Hanin Keyboard Symbol Mode.
var isHaninKeyboardSymbolMode = false
static let tooltipHaninKeyboardSymbolMode: String = "\("Hanin Keyboard Symbol Input.".localized)"
// MARK: - Codepoint Input Buffer.
var isCodePointInputMode = false {
willSet {
strCodePointBuffer.removeAll()
}
}
var strCodePointBuffer = ""
var tooltipCodePointInputMode: String {
let commonTerm = NSMutableString()
commonTerm.insert("Code Point Input.".localized, at: 0)
if !(delegate?.isVerticalTyping ?? false) {
switch IMEApp.currentInputMode {
case .imeModeCHS: commonTerm.insert("[GB] ", at: 0)
case .imeModeCHT: commonTerm.insert("[Big5] ", at: 0)
default: break
}
}
return commonTerm.description
}
// MARK: - Functions dealing with Megrez.
public var isCompositorEmpty: Bool { compositor.isEmpty }
///
/// - 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.length { isBound = true }
let rawResult = compositor.walkedNodes.findNode(at: index)?.isReadingMismatched ?? false
return !isBound && rawResult
}
/// Megrez 使
/// - Remark: Megrez v2.6.2 cursor
var actualNodeCursorPosition: Int {
compositor.cursor
- ((compositor.cursor == compositor.length || !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.KeyValuePaired) -> [(keyArray: [String], value: String)] {
var arrResult: [(keyArray: [String], value: String)] = []
if currentLM.hasAssociatedPhrasesFor(pair: pair) {
arrResult = currentLM.associatedPhrasesFor(pair: pair).map { ([""], $0) }
}
return arrResult
}
///
/// - Parameter direction:
/// - Returns:
func getStepsToNearbyNodeBorder(direction: Megrez.Compositor.TypingDirection) -> Int {
let currentCursor = compositor.cursor
var testCompositor = compositor // Compositor hardCopy
testCompositor.jumpCursorBySpan(to: direction)
return abs(testCompositor.cursor - currentCursor)
}
///
///
/// 使
/// **macOS **
/// OV Bug
///
///
/// - Remark:
///
///
///
/// v1.9.3 SP2 Bug v1.9.4
/// v2.0.2
/// - Parameter theCandidate:
func consolidateCursorContext(with theCandidate: Megrez.KeyValuePaired) {
var grid = compositor.hardCopy // Node hardCopy
var frontBoundaryEX = actualNodeCursorPosition + 1
var rearBoundaryEX = actualNodeCursorPosition
var debugIntelToPrint = ""
if grid.overrideCandidate(theCandidate, at: actualNodeCursorPosition) {
grid.walk()
let range = grid.walkedNodes.contextRange(ofGivenCursor: actualNodeCursorPosition)
rearBoundaryEX = range.lowerBound
frontBoundaryEX = range.upperBound
debugIntelToPrint.append("EX: \(rearBoundaryEX)..<\(frontBoundaryEX), ")
}
let range = compositor.walkedNodes.contextRange(ofGivenCursor: actualNodeCursorPosition)
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.length)
compositor.cursor = cursorBackup //
debugIntelToPrint.append("FIN: \(rearBoundary)..<\(frontBoundary)")
vCLog(debugIntelToPrint)
//
var nodeIndices = [Int]() //
var position = rearBoundary //
while position < frontBoundary {
guard let regionIndex = compositor.walkedNodes.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.map(\.description)
for (subPosition, key) in currentNode.keyArray.enumerated() {
guard values.count > subPosition else { break } //
let thePair = Megrez.KeyValuePaired(
keyArray: [key], value: values[subPosition]
)
compositor.overrideCandidate(thePair, at: position)
position += 1
}
continue
}
position += 1
}
}
///
///
/// - Parameters:
/// - value:
/// - respectCursorPushing: true
/// - preConsolidate:
/// - skipObservation:
public func consolidateNode(
candidate: (keyArray: [String], value: String), respectCursorPushing: Bool = true,
preConsolidate: Bool = false, skipObservation: Bool = false
) {
let theCandidate: Megrez.KeyValuePaired = .init(candidate)
///
if preConsolidate { consolidateCursorContext(with: theCandidate) }
//
if !compositor.overrideCandidate(theCandidate, at: actualNodeCursorPosition) { return }
let previousWalk = compositor.walkedNodes
//
walk()
let currentWalk = compositor.walkedNodes
// 使
var accumulatedCursor = 0
let currentNode = currentWalk.findNode(at: actualNodeCursorPosition, target: &accumulatedCursor)
guard let currentNode = currentNode else { return }
uom: if currentNode.currentUnigram.score > -12, prefs.fetchSuggestionsFromUserOverrideModel {
if skipObservation { break uom }
vCLog("UOM: Start Observation.")
// 使
//
// AppDelegate
prefs.failureFlagForUOMObservation = true
//
//
currentUOM.performObservation(
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualNodeCursorPosition,
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) -> [(keyArray: [String], value: String)] {
/// 使 nodesCrossing macOS
/// nodeCrossing
var arrCandidates: [Megrez.KeyValuePaired] = {
switch prefs.useRearCursorMode {
case false: return compositor.fetchCandidates(filter: .endAt)
case true: return compositor.fetchCandidates(filter: .beginAt)
}
}()
/// nodes
///
///
if arrCandidates.isEmpty { return .init() }
// 調
if !prefs.fetchSuggestionsFromUserOverrideModel || prefs.useSCPCTypingMode || fixOrder {
return arrCandidates.map { ($0.keyArray, $0.value) }
}
let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = retrieveUOMSuggestions(apply: false)
let arrSuggestedCandidates: [Megrez.KeyValuePaired] = arrSuggestedUnigrams.map {
Megrez.KeyValuePaired(key: $0.0, value: $0.1.value)
}
arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates
arrCandidates = arrCandidates.deduplicated
arrCandidates = arrCandidates.stableSort { $0.keyArray.count > $1.keyArray.count }
return arrCandidates.map { ($0.keyArray, $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: actualNodeCursorPosition, timestamp: Date().timeIntervalSince1970
)
arrResult.append(contentsOf: suggestion.candidates)
if apply {
///
if !suggestion.isEmpty, let newestSuggestedCandidate = suggestion.candidates.last {
let overrideBehavior: Megrez.Node.OverrideType =
suggestion.forceHighScoreOverride ? .withHighScore : .withTopUnigramScore
let suggestedPair: Megrez.KeyValuePaired = .init(
key: newestSuggestedCandidate.0, value: newestSuggestedCandidate.1.value
)
vCLog(
"UOM: Suggestion received, overriding the node score of the selected candidate: \(suggestedPair.toNGramKey)")
if !compositor.overrideCandidate(suggestedPair, at: actualNodeCursorPosition, overrideType: overrideBehavior) {
compositor.overrideCandidateLiteral(
newestSuggestedCandidate.1.value, at: actualNodeCursorPosition, overrideType: overrideBehavior
)
}
walk()
}
}
arrResult = arrResult.stableSort { $0.1.score > $1.1.score }
return arrResult
}
public func previewCompositionBufferForCandidate(at index: Int) {
guard var delegate = delegate, delegate.state.type == .ofCandidates,
(0 ..< delegate.state.candidates.count).contains(index)
else {
return
}
let gridBackup = compositor.hardCopy
defer { compositor = gridBackup }
var theState = delegate.state
let highlightedPair = theState.candidates[index]
consolidateNode(
candidate: highlightedPair, respectCursorPushing: false,
preConsolidate: PrefMgr.shared.consolidateContextOnCandidateSelection,
skipObservation: true
)
theState.data.displayTextSegments = compositor.walkedNodes.values
theState.data.cursor = convertCursorForDisplay(compositor.cursor)
let markerBackup = compositor.marker
if compositor.cursor == compositor.length {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
} else if compositor.cursor == 0 {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
} else {
compositor.jumpCursorBySpan(to: prefs.useRearCursorMode ? .front : .rear, isMarker: true)
}
theState.data.marker = compositor.marker
compositor.marker = markerBackup
delegate.state = theState // switchState
delegate.setInlineDisplayWithCursor()
delegate.updatePopupDisplayWithCursor()
}
// MARK: - Extracted methods and functions (Tekkon).
var isComposerOrCalligrapherEmpty: Bool {
if !strCodePointBuffer.isEmpty { return false }
return prefs.cassetteEnabled ? calligrapher.isEmpty : composer.isEmpty
}
/// _
var currentKeyboardParser: String { currentKeyboardParserType.name + "_" }
var currentKeyboardParserType: KeyboardParser { .init(rawValue: prefs.keyboardParser) ?? .ofStandard }
///
public func ensureKeyboardParser() {
switch currentKeyboardParserType {
case .ofStandard: composer.ensureParser(arrange: .ofDachen)
case .ofDachen26: composer.ensureParser(arrange: .ofDachen26)
case .ofETen: composer.ensureParser(arrange: .ofETen)
case .ofHsu: composer.ensureParser(arrange: .ofHsu)
case .ofETen26: composer.ensureParser(arrange: .ofETen26)
case .ofIBM: composer.ensureParser(arrange: .ofIBM)
case .ofMiTAC: composer.ensureParser(arrange: .ofMiTAC)
case .ofFakeSeigyou: composer.ensureParser(arrange: .ofFakeSeigyou)
case .ofSeigyou: composer.ensureParser(arrange: .ofSeigyou)
case .ofStarlight: composer.ensureParser(arrange: .ofStarlight)
case .ofAlvinLiu: composer.ensureParser(arrange: .ofAlvinLiu)
case .ofHanyuPinyin: composer.ensureParser(arrange: .ofHanyuPinyin)
case .ofSecondaryPinyin: composer.ensureParser(arrange: .ofSecondaryPinyin)
case .ofYalePinyin: composer.ensureParser(arrange: .ofYalePinyin)
case .ofHualuoPinyin: composer.ensureParser(arrange: .ofHualuoPinyin)
case .ofUniversalPinyin: composer.ensureParser(arrange: .ofUniversalPinyin)
case .ofWadeGilesPinyin: composer.ensureParser(arrange: .ofWadeGilesPinyin)
}
composer.clear()
composer.phonabetCombinationCorrectionEnabled = prefs.autoCorrectReadingCombination
}
public var isComposerUsingPinyin: Bool { composer.isPinyinMode }
public func clearComposerAndCalligrapher() {
calligrapher.removeAll()
composer.clear()
strCodePointBuffer.removeAll()
}
func letComposerAndCalligrapherDoBackSpace() {
_ = prefs.cassetteEnabled ? calligrapher = String(calligrapher.dropLast(1)) : composer.doBackSpace()
}
///
/// 調調
var previousParsableCalligraph: String? {
if compositor.cursor == 0 { return nil }
let cursorPrevious = max(compositor.cursor - 1, 0)
return compositor.keys[cursorPrevious]
}
///
/// 調調
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.map(\.description)
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.hasIntonation(withNothingElse: true)
}
var readingForDisplay: String {
if !prefs.cassetteEnabled {
return composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer)
}
if !prefs.showTranslatedStrokesInCompositionBuffer { return calligrapher }
return calligrapher.map(\.description).map {
currentLM.convertCassetteKeyToDisplay(char: $0)
}.joined()
}
// MARK: - Extracted methods and functions (Megrez).
public var keySeparator: String { compositor.separator }
public static var keySeparator: String { Megrez.Compositor.theSeparator }
///
public func updateUnigramData() -> Bool {
let result = compositor.update(updateExisting: true)
defer { walk() }
return result > 0
}
///
/// - Parameter input:
/// - Returns:
func generatePunctuationNamePrefix(withKeyCondition input: InputSignalProtocol) -> String {
if prefs.halfWidthPunctuationEnabled { return "_half_punctuation_" }
// SHIFT+ALT+
// input.isMainAreaNumKey Shift
if input.isMainAreaNumKey, input.keyModifierFlags == [.option, .shift] { return "_shift_alt_punctuation_" }
var result = ""
switch (input.isControlHold, input.isOptionHold) {
case (true, true): result.append("_alt_ctrl_punctuation_")
case (true, false): result.append("_ctrl_punctuation_")
case (false, true): result.append("_alt_punctuation_")
case (false, false): result.append("_punctuation_")
}
return result
}
///
/// - Parameter input:
/// - Returns:
func punctuationQueryStrings(input: InputSignalProtocol) -> [String] {
///
/// - /
/// -
var result: [String] = []
let inputText = input.text
let punctuationNamePrefix: String = generatePunctuationNamePrefix(withKeyCondition: input)
let parser = currentKeyboardParser
let arrCustomPunctuations: [String] = [punctuationNamePrefix, parser, inputText]
let customPunctuation: String = arrCustomPunctuations.joined()
result.append(customPunctuation)
///
let arrPunctuations: [String] = [punctuationNamePrefix, inputText]
let punctuation: String = arrPunctuations.joined()
result.append(punctuation)
return result
}
}
// 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.length > compositorWidthLimit,
let delegate = delegate,
delegate.clientMitigationLevel >= 2
else { return "" }
// Steam Client Identifier
var textToCommit = ""
while compositor.length > compositorWidthLimit {
var delta = compositor.length - 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.map(\.description)[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
}
}