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

863 lines
30 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).
// 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.
/// 調調
import Foundation
// MARK: - § 調 (Functions Interact With States).
extension KeyHandler {
// MARK: - State Building
///
var buildInputtingState: IMEState {
/// (Update the composing buffer)
/// NSAttributeString
var displayTextSegments: [String] = compositor.walkedNodes.values.map {
guard let delegate = delegate, delegate.isVerticalTyping else { return $0 }
guard mgrPrefs.hardenVerticalPunctuations else { return $0 }
var neta = $0
ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping)
return neta
}
var cursor = convertCursorForDisplay(compositor.cursor)
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: mgrPrefs.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,
isTypingVertical _: Bool = false
) -> IMEState {
IMEState.ofCandidates(
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
displayTextSegments: compositor.walkedNodes.values,
cursor: currentState.data.cursor
)
}
// MARK: -
///
///
/// buildAssociatePhraseStateWithKey
/// 使
/// Core buildAssociatePhraseArray
/// String Swift
/// nil
///
///
/// - Parameters:
/// - key:
/// - Returns:
func buildAssociatePhraseState(
withPair pair: Megrez.Compositor.KeyValuePaired
) -> IMEState {
//  Xcode
IMEState.ofAssociates(
candidates: buildAssociatePhraseArray(withPair: pair))
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleMarkingState(
_ state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
if input.isEsc {
stateCallback(buildInputtingState)
return true
}
//
if input.isControlHold, input.isCommandHold, input.isEnter {
IME.prtDebugIntel("1198E3E5")
errorCallback()
return true
}
// Enter
if input.isEnter {
if let keyHandlerDelegate = delegate {
//
if input.isShiftHold, input.isCommandHold, !state.isFilterable {
IME.prtDebugIntel("2EAC1F7A")
errorCallback()
return true
}
if !state.isMarkedLengthValid {
IME.prtDebugIntel("9AAFAC00")
errorCallback()
return true
}
if !keyHandlerDelegate.keyHandler(self, didRequestWriteUserPhraseWith: state, addToFilter: false) {
IME.prtDebugIntel("5B69CC8D")
errorCallback()
return true
}
}
stateCallback(buildInputtingState)
return true
}
// BackSpace & Delete
if input.isBackSpace || input.isDelete {
if let keyHandlerDelegate = delegate {
if !state.isFilterable {
IME.prtDebugIntel("1F88B191")
errorCallback()
return true
}
if !keyHandlerDelegate.keyHandler(self, didRequestWriteUserPhraseWith: state, addToFilter: true) {
IME.prtDebugIntel("68D3C6C8")
errorCallback()
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.data.displayTextSegments,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
IME.prtDebugIntel("1149908D")
errorCallback()
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.data.displayTextSegments,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
IME.prtDebugIntel("9B51408D")
errorCallback()
stateCallback(state)
}
return true
}
return false
}
// MARK: -
///
/// - Parameters:
/// - customPunctuation:
/// - state:
/// - isTypingVertical:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handlePunctuation(
_ customPunctuation: String,
state: IMEStateProtocol,
usingVerticalTyping isTypingVertical: Bool,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
if !currentLM.hasUnigramsFor(key: customPunctuation) {
return false
}
guard composer.isEmpty else {
//
IME.prtDebugIntel("A9B69908D")
errorCallback()
stateCallback(state)
return true
}
compositor.insertKey(customPunctuation)
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = buildInputtingState
inputting.textToCommit = textToCommit
stateCallback(inputting)
//
guard mgrPrefs.useSCPCTypingMode, composer.isEmpty else { return true }
let candidateState = buildCandidate(
state: inputting,
isTypingVertical: isTypingVertical
)
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: ctlInputMethod 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: ctlInputMethod IMK
func handleCtrlCommandEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
var displayedText = compositor.keys.joined(separator: "-")
if mgrPrefs.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: ctlInputMethod 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 mgrPrefs.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: ctlInputMethod IMK
func handleBackSpace(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
// macOS Shift+BackSpace
switch mgrPrefs.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 {
IME.prtDebugIntel("9D69908D")
errorCallback()
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: ctlInputMethod IMK
func handleDelete(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if input.isShiftHold {
stateCallback(IMEState.ofAbortion())
return true
}
if compositor.cursor == compositor.length, composer.isEmpty {
IME.prtDebugIntel("9B69938D")
errorCallback()
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: ctlInputMethod IMK
func handleClockKey(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("9B6F908D")
errorCallback()
}
stateCallback(state)
return true
}
// MARK: - Home
/// Home
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleHome(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("ABC44080")
errorCallback()
stateCallback(state)
return true
}
if compositor.cursor != 0 {
compositor.cursor = 0
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("66D97F90")
errorCallback()
stateCallback(state)
}
return true
}
// MARK: - End
/// End
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleEnd(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("9B69908D")
errorCallback()
stateCallback(state)
return true
}
if compositor.cursor != compositor.length {
compositor.cursor = compositor.length
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("9B69908E")
errorCallback()
stateCallback(state)
}
return true
}
// MARK: - Esc
/// Esc
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: ctlInputMethod IMK
func handleEsc(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if mgrPrefs.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: ctlInputMethod IMK
func handleForward(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("B3BA5257")
errorCallback()
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.data.tooltipBackupForInputting = state.tooltip
stateCallback(marking)
} else {
IME.prtDebugIntel("BB7F6DB9")
errorCallback()
stateCallback(state)
}
} else if input.isOptionHold {
if input.isControlHold {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
//
if !compositor.jumpCursorBySpan(to: .front) {
IME.prtDebugIntel("33C3B580")
errorCallback()
stateCallback(state)
return true
}
stateCallback(buildInputtingState)
} else {
if compositor.cursor < compositor.length {
compositor.cursor += 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .front)
}
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("A96AAD58")
errorCallback()
stateCallback(state)
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleBackward(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("6ED95318")
errorCallback()
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.data.tooltipBackupForInputting = state.tooltip
stateCallback(marking)
} else {
IME.prtDebugIntel("D326DEA3")
errorCallback()
stateCallback(state)
}
} else if input.isOptionHold {
if input.isControlHold {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
//
if !compositor.jumpCursorBySpan(to: .rear) {
IME.prtDebugIntel("8D50DD9E")
errorCallback()
stateCallback(state)
return true
}
stateCallback(buildInputtingState)
} else {
if compositor.cursor > 0 {
compositor.cursor -= 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .rear)
}
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("7045E6F3")
errorCallback()
stateCallback(state)
}
}
return true
}
// MARK: - Tab Shift+Space
///
/// - Parameters:
/// - state:
/// - reverseModifier:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleInlineCandidateRotation(
state: IMEStateProtocol,
reverseModifier: Bool,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
guard state.type == .ofInputting else {
guard state.type == .ofEmpty else {
IME.prtDebugIntel("6044F081")
errorCallback()
return true
}
// 使 Tab
return false
}
guard composer.isEmpty else {
IME.prtDebugIntel("A2DAF7BC")
errorCallback()
return true
}
let candidates = getCandidatesArray(fixOrder: true)
guard !candidates.isEmpty else {
IME.prtDebugIntel("3378A6DF")
errorCallback()
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 {
IME.prtDebugIntel("F58DEA95")
errorCallback()
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
}
}