vChewing-macOS/Source/Modules/KeyHandler_States.swift

822 lines
28 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 Megrez
import Shared
import Tekkon
// MARK: - § 調 (Functions Interact With States).
extension KeyHandler {
// MARK: - State Building
///
public var buildInputtingState: IMEState {
/// (Update the composing buffer)
/// NSAttributeString
var displayTextSegments: [String] = compositor.walkedNodes.values
var cursor = convertCursorForDisplay(compositor.cursor)
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer)
if !reading.isEmpty {
var newDisplayTextSegments = [String]()
var temporaryNode = ""
var charCounter = 0
for node in displayTextSegments {
for char in node {
if charCounter == cursor {
newDisplayTextSegments.append(temporaryNode)
temporaryNode = ""
newDisplayTextSegments.append(reading)
}
temporaryNode += String(char)
charCounter += 1
}
newDisplayTextSegments.append(temporaryNode)
temporaryNode = ""
}
if newDisplayTextSegments == displayTextSegments { newDisplayTextSegments.append(reading) }
displayTextSegments = newDisplayTextSegments
cursor += reading.count
}
///
return IMEState.ofInputting(displayTextSegments: displayTextSegments, cursor: cursor)
}
///
func convertCursorForDisplay(_ rawCursor: Int) -> Int {
var composedStringCursorIndex = 0
var readingCursorIndex = 0
for theNode in compositor.walkedNodes {
let strNodeValue = theNode.value
///
/// NodeAnchorspanningLength
///
let spanningLength: Int = theNode.keyArray.count
if readingCursorIndex + spanningLength <= rawCursor {
composedStringCursorIndex += strNodeValue.count
readingCursorIndex += spanningLength
continue
}
if !theNode.isReadingMismatched {
for _ in 0..<strNodeValue.count {
guard readingCursorIndex < rawCursor else { continue }
composedStringCursorIndex += 1
readingCursorIndex += 1
}
continue
}
guard readingCursorIndex < rawCursor else { continue }
composedStringCursorIndex += strNodeValue.count
readingCursorIndex += spanningLength
readingCursorIndex = min(readingCursorIndex, rawCursor)
}
return composedStringCursorIndex
}
// MARK: -
///
/// - Parameters:
/// - currentState:
/// - isTypingVertical:
/// - Returns:
func buildCandidate(
state currentState: IMEStateProtocol
) -> IMEState {
IMEState.ofCandidates(
candidates: getCandidatesArray(fixOrder: prefs.useFixecCandidateOrderOnSelection),
displayTextSegments: compositor.walkedNodes.values,
cursor: currentState.cursor
)
}
// MARK: -
///
///
/// buildAssociatePhraseStateWithKey
/// 使
/// Core buildAssociatePhraseArray
/// String Swift
/// nil
///
///
/// - Parameters:
/// - key:
/// - Returns:
func buildAssociatePhraseState(
withPair pair: Megrez.Compositor.KeyValuePaired
) -> IMEState {
IMEState.ofAssociates(
candidates: buildAssociatePhraseArray(withPair: pair))
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleMarkingState(
_ state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
if input.isEsc {
stateCallback(buildInputtingState)
return true
}
//
if input.isControlHold, input.isCommandHold, input.isEnter {
errorCallback("1198E3E5")
return true
}
// Enter
if input.isEnter {
if let ctlIME = delegate {
//
if input.isShiftHold, input.isCommandHold, !state.isFilterable {
errorCallback("2EAC1F7A")
return true
}
if !state.isMarkedLengthValid {
errorCallback("9AAFAC00")
return true
}
if !ctlIME.performUserPhraseOperation(with: state, addToFilter: false) {
errorCallback("5B69CC8D")
return true
}
}
stateCallback(buildInputtingState)
return true
}
// BackSpace & Delete
if input.isBackSpace || input.isDelete {
if let keyHandlerDelegate = delegate {
if !state.isFilterable {
errorCallback("1F88B191")
return true
}
if !keyHandlerDelegate.performUserPhraseOperation(with: state, addToFilter: true) {
errorCallback("68D3C6C8")
return true
}
}
stateCallback(buildInputtingState)
return true
}
// Shift + Left
if input.isCursorBackward, input.isShiftHold {
if compositor.marker > 0 {
compositor.marker -= 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: state.displayTextSegments,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.tooltipBackupForInputting = state.tooltipBackupForInputting
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
errorCallback("1149908D")
stateCallback(state)
}
return true
}
// Shift + Right
if input.isCursorForward, input.isShiftHold {
if compositor.marker < compositor.width {
compositor.marker += 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: state.displayTextSegments,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.tooltipBackupForInputting = state.tooltipBackupForInputting
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
errorCallback("9B51408D")
stateCallback(state)
}
return true
}
return false
}
// MARK: -
///
/// - Parameters:
/// - customPunctuation:
/// - isTypingVertical:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handlePunctuation(
_ customPunctuation: String,
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
if !currentLM.hasUnigramsFor(key: customPunctuation) {
return false
}
guard composer.isEmpty else {
//
errorCallback("A9B69908D")
stateCallback(state)
return true
}
compositor.insertKey(customPunctuation)
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = buildInputtingState
inputting.textToCommit = textToCommit
stateCallback(inputting)
//
guard prefs.useSCPCTypingMode, composer.isEmpty else { return true }
let candidateState = buildCandidate(state: inputting)
if candidateState.candidates.count == 1 {
clear() // candidateState
if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty {
stateCallback(IMEState.ofCommitting(textToCommit: candidateToCommit.1))
stateCallback(IMEState.ofEmpty())
} else {
stateCallback(candidateState)
}
} else {
stateCallback(candidateState)
}
return true
}
// MARK: - Enter
/// Enter
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: SessionCtl IMK
func handleEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
stateCallback(IMEState.ofCommitting(textToCommit: state.displayedText))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: - Command+Enter
/// Command+Enter
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: SessionCtl IMK
func handleCtrlCommandEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
var displayedText = compositor.keys.joined(separator: "-")
if prefs.inlineDumpPinyinInLieuOfZhuyin {
displayedText = Tekkon.restoreToneOneInZhuyinKey(target: displayedText) //
displayedText = Tekkon.cnvPhonaToHanyuPinyin(target: displayedText) //
}
if let delegate = delegate, !delegate.clientBundleIdentifier.contains("vChewingPhraseEditor") {
displayedText = displayedText.replacingOccurrences(of: "-", with: " ")
}
stateCallback(IMEState.ofCommitting(textToCommit: displayedText))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: - Command+Option+Enter Ruby
/// Command+Option+Enter Ruby
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: SessionCtl IMK
func handleCtrlOptionCommandEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
var composed = ""
for node in compositor.walkedNodes {
var key = node.key
if prefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.restoreToneOneInZhuyinKey(target: key) //
key = Tekkon.cnvPhonaToHanyuPinyin(target: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(target: key) // 調
key = key.replacingOccurrences(of: "-", with: " ")
} else {
key = Tekkon.cnvZhuyinChainToTextbookReading(target: key, newSeparator: " ")
}
let value = node.value
//
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
}
stateCallback(IMEState.ofCommitting(textToCommit: composed))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: - BackSpace (macOS Delete)
/// BackSpace (macOS Delete)
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleBackSpace(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
// macOS Shift+BackSpace
switch prefs.specifyShiftBackSpaceKeyBehavior {
case 0:
guard input.isShiftHold, composer.isEmpty else { break }
guard let prevReading = previousParsableReading else { break }
// prevReading 調調
compositor.dropKey(direction: .rear)
walk() // Walk walk
prevReading.1.charComponents.forEach { composer.receiveKey(fromPhonabet: $0) }
stateCallback(buildInputtingState)
return true
case 1:
stateCallback(IMEState.ofAbortion())
return true
default: break
}
if input.isShiftHold, input.isOptionHold {
stateCallback(IMEState.ofAbortion())
return true
}
if composer.hasToneMarker(withNothingElse: true) {
composer.clear()
} else if composer.isEmpty {
if compositor.cursor > 0 {
compositor.dropKey(direction: .rear)
walk()
} else {
errorCallback("9D69908D")
stateCallback(state)
return true
}
} else {
composer.doBackSpace()
}
switch composer.isEmpty && compositor.isEmpty {
case false: stateCallback(buildInputtingState)
case true:
stateCallback(IMEState.ofAbortion())
}
return true
}
// MARK: - PC Delete (macOS Fn+BackSpace)
/// PC Delete (macOS Fn+BackSpace)
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleDelete(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if input.isShiftHold {
stateCallback(IMEState.ofAbortion())
return true
}
if compositor.cursor == compositor.length, composer.isEmpty {
errorCallback("9B69938D")
stateCallback(state)
return true
}
if composer.isEmpty {
compositor.dropKey(direction: .front)
walk()
} else {
composer.clear()
}
let inputting = buildInputtingState
// count > 0!isEmpty滿
switch inputting.displayedText.isEmpty {
case false: stateCallback(inputting)
case true:
stateCallback(IMEState.ofAbortion())
}
return true
}
// MARK: - 90
/// 90
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleClockKey(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
errorCallback("9B6F908D")
}
stateCallback(state)
return true
}
// MARK: - Home
/// Home
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleHome(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
errorCallback("ABC44080")
stateCallback(state)
return true
}
if compositor.cursor != 0 {
compositor.cursor = 0
stateCallback(buildInputtingState)
} else {
errorCallback("66D97F90")
stateCallback(state)
}
return true
}
// MARK: - End
/// End
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleEnd(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
errorCallback("9B69908D")
stateCallback(state)
return true
}
if compositor.cursor != compositor.length {
compositor.cursor = compositor.length
stateCallback(buildInputtingState)
} else {
errorCallback("9B69908E")
stateCallback(state)
}
return true
}
// MARK: - Esc
/// Esc
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: SessionCtl IMK
func handleEsc(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if prefs.escToCleanInputBuffer {
///
/// macOS Windows 使
stateCallback(IMEState.ofAbortion())
} else {
if composer.isEmpty { return true }
///
composer.clear()
switch compositor.isEmpty {
case false: stateCallback(buildInputtingState)
case true:
stateCallback(IMEState.ofAbortion())
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleForward(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
errorCallback("B3BA5257")
stateCallback(state)
return true
}
if input.isShiftHold {
// Shift + Right
if compositor.cursor < compositor.width {
compositor.marker = compositor.cursor + 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: compositor.walkedNodes.values,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.tooltipBackupForInputting = state.tooltip
stateCallback(marking)
} else {
errorCallback("BB7F6DB9")
stateCallback(state)
}
} else if input.isOptionHold {
if input.isControlHold {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
//
if !compositor.jumpCursorBySpan(to: .front) {
errorCallback("33C3B580")
stateCallback(state)
return true
}
stateCallback(buildInputtingState)
} else {
if compositor.cursor < compositor.length {
compositor.cursor += 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .front)
}
stateCallback(buildInputtingState)
} else {
errorCallback("A96AAD58")
stateCallback(state)
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleBackward(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
errorCallback("6ED95318")
stateCallback(state)
return true
}
if input.isShiftHold {
// Shift + left
if compositor.cursor > 0 {
compositor.marker = compositor.cursor - 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: compositor.walkedNodes.values,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.tooltipBackupForInputting = state.tooltip
stateCallback(marking)
} else {
errorCallback("D326DEA3")
stateCallback(state)
}
} else if input.isOptionHold {
if input.isControlHold {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
//
if !compositor.jumpCursorBySpan(to: .rear) {
errorCallback("8D50DD9E")
stateCallback(state)
return true
}
stateCallback(buildInputtingState)
} else {
if compositor.cursor > 0 {
compositor.cursor -= 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .rear)
}
stateCallback(buildInputtingState)
} else {
errorCallback("7045E6F3")
stateCallback(state)
}
}
return true
}
// MARK: - Tab Shift+Space
///
/// - Parameters:
/// - state:
/// - reverseModifier:
/// - stateCallback:
/// - errorCallback:
/// - Returns: SessionCtl IMK
func handleInlineCandidateRotation(
state: IMEStateProtocol,
reverseModifier: Bool,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping (String) -> Void
) -> Bool {
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
guard state.type == .ofInputting else {
guard state.type == .ofEmpty else {
errorCallback("6044F081")
return true
}
// 使 Tab
return false
}
guard composer.isEmpty else {
errorCallback("A2DAF7BC")
return true
}
let candidates = getCandidatesArray(fixOrder: true)
guard !candidates.isEmpty else {
errorCallback("3378A6DF")
return true
}
var length = 0
var currentNode: Megrez.Compositor.Node?
let cursorIndex = actualCandidateCursor
for node in compositor.walkedNodes {
length += node.spanLength
if length > cursorIndex {
currentNode = node
break
}
}
guard let currentNode = currentNode else {
errorCallback("F58DEA95")
return true
}
let currentPaired = (currentNode.key, currentNode.value)
var currentIndex = 0
if !currentNode.isOverriden {
/// 使
/// 使
/// 2 使
///
///
/// 使
/// (Shift+)Tab ()
/// Shift(+Command)+Space Alt+/ Alt+/
/// Tab
if candidates[0] == currentPaired {
///
///
currentIndex = reverseModifier ? candidates.count - 1 : 1
}
} else {
for candidate in candidates {
if candidate == currentPaired {
if reverseModifier {
if currentIndex == 0 {
currentIndex = candidates.count - 1
} else {
currentIndex -= 1
}
} else {
currentIndex += 1
}
break
}
currentIndex += 1
}
}
if currentIndex >= candidates.count {
currentIndex = 0
}
fixNode(candidate: candidates[currentIndex], respectCursorPushing: false, preConsolidate: false)
stateCallback(buildInputtingState)
return true
}
}