vChewing-macOS/Packages/vChewing_MainAssembly/Sources/MainAssembly/InputHandler_HandleStates.s...

1164 lines
46 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).
// ====================
// 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 Megrez
import Shared
import Tekkon
// MARK: - § 調 (Functions Interact With States).
extension InputHandler {
// MARK: - State Building
///
/// - Parameters:
/// - sansReading: /
/// - guarded: InputMethodKit
/// - Returns:
public func generateStateOfInputting(sansReading: Bool = false, guarded: Bool = false) -> IMEStateProtocol {
if isConsideredEmptyForNow, !guarded { return IMEState.ofAbortion() }
var segHighlightedAt: Int?
let cpInput = isCodePointInputMode && !sansReading
/// (Update the composing buffer)
/// IMEStateData NSAttributeString
var displayTextSegments: [String] = cpInput
? [strCodePointBuffer]
: compositor.walkedNodes.values
var cursor = cpInput
? displayTextSegments.joined().count
: convertCursorForDisplay(compositor.cursor)
let cursorSansReading = cursor
let reading: String = (sansReading || isCodePointInputMode) ? "" : readingForDisplay //
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 = ""
//
segHighlightedAt = newDisplayTextSegments.count
newDisplayTextSegments.append(reading)
}
temporaryNode += String(char)
charCounter += 1
}
newDisplayTextSegments.append(temporaryNode)
temporaryNode = ""
}
if newDisplayTextSegments == displayTextSegments {
//
segHighlightedAt = newDisplayTextSegments.count
newDisplayTextSegments.append(reading)
}
displayTextSegments = newDisplayTextSegments
cursor += reading.count
}
for i in 0 ..< displayTextSegments.count {
displayTextSegments[i] = displayTextSegments[i].trimmingCharacters(in: .newlines)
}
///
var result = IMEState.ofInputting(
displayTextSegments: displayTextSegments,
cursor: cursor, highlightAt: segHighlightedAt
)
result.marker = cursorSansReading
///
if guarded, result.displayTextSegments.joined().isEmpty {
result.data.displayTextSegments = [" "]
result.cursor = 0
result.marker = 0
}
return result
}
/// 調
var tooltipForStandaloneIntonationMark: String {
guard !isComposerUsingPinyin else { return "" }
guard composer.hasIntonation(withNothingElse: true) else { return "" }
guard composer.intonation.value != " " else { return "" }
let result = NSMutableString()
result.append("Intonation mark. ENTER to commit.\nSPACE to insert into composition buffer.".localized)
if prefs.acceptLeadingIntonations {
result.append("\n")
result.append("It will attempt to combine with the incoming phonabet input.".localized)
}
return result.description
}
///
/// - Parameter rawCursor:
/// - Returns:
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 {
strNodeValue.forEach { _ in
if readingCursorIndex < rawCursor {
composedStringCursorIndex += 1
readingCursorIndex += 1
}
}
continue
}
guard readingCursorIndex < rawCursor else { continue }
composedStringCursorIndex += strNodeValue.count
readingCursorIndex += spanningLength
readingCursorIndex = min(readingCursorIndex, rawCursor)
}
return composedStringCursorIndex
}
// MARK: -
///
/// - Returns:
public func generateStateOfCandidates() -> IMEStateProtocol {
var result = IMEState.ofCandidates(
candidates: generateArrayOfCandidates(fixOrder: prefs.useFixedCandidateOrderOnSelection),
displayTextSegments: compositor.walkedNodes.values,
cursor: delegate?.state.cursor ?? generateStateOfInputting().cursor
)
if !prefs.useRearCursorMode {
let markerBackup = compositor.marker
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
result.marker = compositor.marker
compositor.marker = markerBackup
}
return result
}
// MARK: -
///
///
/// generateStateOfAssociates
/// 使
/// Core generateArrayOfAssociates
/// String Swift
/// nil
///
///
///
/// SessionCtl InputHandler generateArrayOfAssociates().
/// - Parameters:
/// - key:
/// - Returns:
public func generateStateOfAssociates(withPair pair: Megrez.KeyValuePaired) -> IMEStateProtocol {
IMEState.ofAssociates(candidates: generateArrayOfAssociates(withPair: pair))
}
// MARK: -
///
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleMarkingState(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
if input.isEsc {
delegate.switchState(generateStateOfInputting())
return true
}
//
if input.isControlHold, input.isCommandHold, input.isEnter {
delegate.callError("1198E3E5")
return true
}
// Enter
if input.isEnter {
var tooltipMessage = "+ Succeeded in adding / boosting a user phrase."
var tooltipColorState: TooltipColorState = .normal
//
if input.isShiftHold, input.isCommandHold {
tooltipMessage = "- Succeeded in nerfing a user phrase."
tooltipColorState = .succeeded
}
if !state.isMarkedLengthValid {
delegate.callError("9AAFAC00")
return true
}
let areWeUnfiltering = state.markedTargetIsCurrentlyFiltered
if !delegate.performUserPhraseOperation(addToFilter: false) {
delegate.callError("5B69CC8D")
return true
}
if areWeUnfiltering {
tooltipMessage = "- Succeeded in unfiltering a phrase."
tooltipColorState = .succeeded
}
var newState = generateStateOfInputting()
newState.tooltip = NSLocalizedString(tooltipMessage, comment: "")
newState.data.tooltipColorState = tooltipColorState
newState.tooltipDuration = 1.85
delegate.switchState(newState)
return true
}
// BackSpace & Delete
if input.isBackSpace || input.isDelete {
let tooltipMessage = "! Succeeded in filtering a user phrase."
if !state.isFilterable {
delegate.callError("1F88B191")
return true
}
if !delegate.performUserPhraseOperation(addToFilter: true) {
delegate.callError("68D3C6C8")
return true
}
var newState = generateStateOfInputting()
newState.tooltip = NSLocalizedString(tooltipMessage, comment: "")
newState.data.tooltipColorState = .warning
newState.tooltipDuration = 1.85
delegate.switchState(newState)
return true
}
// Shift + Left
if input.isCursorBackward, input.isShiftHold {
if compositor.marker > 0 {
if input.isCommandHold || input.isOptionHold {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
} else {
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
delegate.switchState(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
delegate.callError("1149908D")
}
return true
}
// Shift + Right
if input.isCursorForward, input.isShiftHold {
if compositor.marker < compositor.length {
if input.isCommandHold || input.isOptionHold {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
} else {
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
delegate.switchState(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
delegate.callError("9B51408D")
}
return true
}
return false
}
// MARK: -
///
/// - Parameters:
/// - customPunctuation:
/// - Returns: SessionCtl IMK
func handlePunctuation(_ customPunctuation: String) -> Bool {
guard let delegate = delegate else { return false }
if !currentLM.hasUnigramsFor(keyArray: [customPunctuation]) {
return false
}
guard isComposerOrCalligrapherEmpty else {
//
delegate.callError("A9B69908D")
return true
}
guard compositor.insertKey(customPunctuation) else {
delegate.callError("C0793A6D: 得檢查對應的語言模組的 hasUnigramsFor() 是否有誤判之情形。")
return true
}
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = generateStateOfInputting()
inputting.textToCommit = textToCommit
delegate.switchState(inputting)
//
guard prefs.useSCPCTypingMode, isComposerOrCalligrapherEmpty else { return true }
let candidateState = generateStateOfCandidates()
switch candidateState.candidates.count {
case 2...: delegate.switchState(candidateState)
case 1:
clear() // candidateState
if let strToCommit = candidateState.candidates.first?.value, !strToCommit.isEmpty {
delegate.switchState(IMEState.ofCommitting(textToCommit: strToCommit))
} else {
delegate.switchState(candidateState)
}
default: delegate.callError("8DA4096E")
}
return true
}
// MARK: - Enter
/// Enter
/// - Parameters:
/// - input:
/// - readingOnly:
/// - associatesData:
/// .ofInputting()
/// - Returns: SessionCtl IMK
@discardableResult func handleEnter(
input: InputSignalProtocol, readingOnly: Bool = false,
associatesData: @escaping () -> ([(keyArray: [String], value: String)]) = { [] }
) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
if isHaninKeyboardSymbolMode { return handleHaninKeyboardSymbolModeToggle() }
if isCodePointInputMode { return handleCodePointInputToggle() }
guard state.type == .ofInputting else { return false }
var displayedText = state.displayedText
if input.keyModifierFlags == [.option, .shift] {
displayedText = displayedText.map(\.description).joined(separator: " ")
} else if readingOnly {
displayedText = commissionByCtrlCommandEnter()
} else if input.isCommandHold, input.isControlHold {
displayedText = input.isOptionHold
? commissionByCtrlOptionCommandEnter(isShiftPressed: input.isShiftHold)
: commissionByCtrlCommandEnter(isShiftPressed: input.isShiftHold)
}
delegate.switchState(IMEState.ofCommitting(textToCommit: displayedText))
associatedPhrases: if !prefs.useSCPCTypingMode, prefs.associatedPhrasesEnabled {
guard input.keyModifierFlags == .shift else { break associatedPhrases }
guard isComposerOrCalligrapherEmpty else { break associatedPhrases }
let associatedCandidates = associatesData()
guard !associatedCandidates.isEmpty else { break associatedPhrases }
delegate.switchState(IMEState.ofAssociates(candidates: associatedCandidates))
}
return true
}
// MARK: - Command+Enter
/// Command+Enter
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
private func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
var displayedText = compositor.keys.joined(separator: "\t")
if compositor.isEmpty {
displayedText = readingForDisplay
}
if !prefs.cassetteEnabled {
if prefs.inlineDumpPinyinInLieuOfZhuyin {
if !compositor.isEmpty {
var arrDisplayedTextElements = [String]()
compositor.keys.forEach { key in
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) //
}
displayedText = arrDisplayedTextElements.joined(separator: "\t")
}
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) //
}
if prefs.showHanyuPinyinInCompositionBuffer {
if compositor.isEmpty {
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
}
}
}
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
return displayedText
}
// MARK: - Command+Option+Enter Ruby
/// Command+Option+Enter Ruby
/// - Parameter isShiftPressed: Shift
/// - Returns: SessionCtl IMK
private func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
var composed = ""
compositor.walkedNodes.smashedPairs.forEach { key, value in
var key = key
if !prefs.cassetteEnabled {
key =
prefs.inlineDumpPinyinInLieuOfZhuyin
? Tekkon.restoreToneOneInPhona(target: key) //
: Tekkon.cnvPhonaToTextbookReading(target: key) //
if prefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 調
}
}
key = key.replacingOccurrences(of: "\t", with: " ")
if isShiftPressed {
if !composed.isEmpty { composed += " " }
composed += key.contains("_") ? "??" : key
return
}
//
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
}
return composed
}
// MARK: - BackSpace (macOS Delete)
/// BackSpace (macOS Delete)
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleBackSpace(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else {
isCodePointInputMode = false
return false
}
if isCodePointInputMode {
if !strCodePointBuffer.isEmpty {
func refreshState() {
var updatedState = generateStateOfInputting(guarded: true)
updatedState.tooltipDuration = 0
updatedState.tooltip = tooltipCodePointInputMode
delegate.switchState(updatedState)
}
strCodePointBuffer = strCodePointBuffer.dropLast(1).description
if input.keyModifierFlags == .option {
strCodePointBuffer.removeAll()
refreshState()
isCodePointInputMode = true
return true
}
if !strCodePointBuffer.isEmpty {
refreshState()
return true
}
}
return handleCodePointInputToggle()
}
// macOS Shift+BackSpace
shiftBksp: switch prefs.specifyShiftBackSpaceKeyBehavior {
case 0:
if prefs.cassetteEnabled {
guard input.isShiftHold, calligrapher.isEmpty else { break shiftBksp }
guard let prevReading = previousParsableCalligraph else { break shiftBksp }
compositor.dropKey(direction: .rear)
walk() // Walk walk
calligrapher = prevReading
} else {
guard input.isShiftHold, isComposerOrCalligrapherEmpty else { break shiftBksp }
guard let prevReading = previousParsableReading else { break shiftBksp }
// prevReading 調調
compositor.dropKey(direction: .rear)
walk() // Walk walk
prevReading.1.map(\.description).forEach { composer.receiveKey(fromPhonabet: $0) }
}
delegate.switchState(generateStateOfInputting())
return true
case 1:
delegate.switchState(IMEState.ofAbortion())
return true
default: break
}
let steps = getStepsToNearbyNodeBorder(direction: .rear)
var actualSteps = 1
switch input.keyModifierFlags {
case .shift:
delegate.switchState(IMEState.ofAbortion())
return true
case .option:
actualSteps = steps
default: break
}
if isComposerOrCalligrapherEmpty {
if compositor.cursor <= 0 || actualSteps <= 0 {
delegate.callError("9D69908D")
return true
}
for _ in 0 ..< actualSteps {
compositor.dropKey(direction: .rear)
}
walk()
} else {
_ = input.keyModifierFlags == .option
? clearComposerAndCalligrapher()
: letComposerAndCalligrapherDoBackSpace()
}
switch isConsideredEmptyForNow {
case false:
var result = generateStateOfInputting()
if prefs.cassetteEnabled, let fetched = currentLM.cassetteQuickSetsFor(key: calligrapher)?.split(separator: "\t") {
result.candidates = fetched.enumerated().map {
(keyArray: [($0.offset + 1).description], value: $0.element.description)
}
}
delegate.switchState(result)
case true: delegate.switchState(IMEState.ofAbortion())
}
return true
}
// MARK: - PC Delete (macOS Fn+BackSpace)
/// PC Delete (macOS Fn+BackSpace)
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleDelete(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
if isHaninKeyboardSymbolMode { return handleHaninKeyboardSymbolModeToggle() }
if isCodePointInputMode { return handleCodePointInputToggle() }
guard state.type == .ofInputting else { return false }
let steps = getStepsToNearbyNodeBorder(direction: .front)
var actualSteps = 1
// macOS PC Delete .function
//
switch input.keyModifierFlags {
case _ where input.isShiftHold && !input.isOptionHold && !input.isControlHold:
delegate.switchState(IMEState.ofAbortion())
return true
case _ where !input.isShiftHold && input.isOptionHold && !input.isControlHold:
actualSteps = steps
default: break
}
if isComposerOrCalligrapherEmpty {
if compositor.cursor >= compositor.length || actualSteps <= 0 {
delegate.callError("9B69938D")
return true
}
for _ in 0 ..< actualSteps {
compositor.dropKey(direction: .front)
}
walk()
} else {
clearComposerAndCalligrapher()
}
let inputting = generateStateOfInputting()
// count > 0!isEmpty滿
switch inputting.displayedText.isEmpty {
case false: delegate.switchState(inputting)
case true: delegate.switchState(IMEState.ofAbortion())
}
return true
}
// MARK: - 90
/// 90
/// - Returns: SessionCtl IMK
func handleClockKey() -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !isComposerOrCalligrapherEmpty { delegate.callError("9B6F908D") }
return true
}
// MARK: - Home
/// Home
/// - Returns: SessionCtl IMK
func handleHome() -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !isComposerOrCalligrapherEmpty {
delegate.callError("ABC44080")
return true
}
if compositor.cursor != 0 {
compositor.cursor = 0
delegate.switchState(generateStateOfInputting())
} else {
delegate.callError("66D97F90")
}
return true
}
// MARK: - End
/// End
/// - Returns: SessionCtl IMK
func handleEnd() -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !isComposerOrCalligrapherEmpty {
delegate.callError("9B69908D")
return true
}
if compositor.cursor != compositor.length {
compositor.cursor = compositor.length
delegate.switchState(generateStateOfInputting())
} else {
delegate.callError("9B69908E")
}
return true
}
// MARK: - Esc
/// Esc
/// - Returns: SessionCtl IMK
func handleEsc() -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
if isHaninKeyboardSymbolMode { return handleHaninKeyboardSymbolModeToggle() }
if isCodePointInputMode { return handleCodePointInputToggle() }
guard state.type == .ofInputting else { return false }
if prefs.escToCleanInputBuffer {
///
/// macOS Windows 使
delegate.switchState(IMEState.ofAbortion())
} else {
if isComposerOrCalligrapherEmpty {
let commitText = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: commitText))
return true
}
///
clearComposerAndCalligrapher()
switch compositor.isEmpty {
case false: delegate.switchState(generateStateOfInputting())
case true: delegate.switchState(IMEState.ofAbortion())
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleForward(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !isComposerOrCalligrapherEmpty {
delegate.callError("B3BA5257")
return true
}
if input.isShiftHold {
// Shift + Right
if compositor.cursor < compositor.length {
compositor.marker = compositor.cursor
if input.isCommandHold || input.isOptionHold {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
} else {
compositor.marker += 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
delegate.switchState(marking)
} else {
delegate.callError("BB7F6DB9")
}
} else if input.isOptionHold, !input.isShiftHold {
if input.isControlHold {
return handleEnd()
}
//
if !compositor.jumpCursorBySpan(to: .front) {
delegate.callError("33C3B580")
return true
}
delegate.switchState(generateStateOfInputting())
} else {
if compositor.cursor < compositor.length {
compositor.cursor += 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .front)
}
delegate.switchState(generateStateOfInputting())
} else {
delegate.callError("A96AAD58")
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleBackward(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
guard state.type == .ofInputting else { return false }
if !isComposerOrCalligrapherEmpty {
delegate.callError("6ED95318")
return true
}
if input.isShiftHold {
// Shift + left
if compositor.cursor > 0 {
compositor.marker = compositor.cursor
if input.isCommandHold || input.isOptionHold {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
} else {
compositor.marker -= 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
delegate.switchState(marking)
} else {
delegate.callError("D326DEA3")
}
} else if input.isOptionHold, !input.isShiftHold {
if input.isControlHold { return handleHome() }
//
if !compositor.jumpCursorBySpan(to: .rear) {
delegate.callError("8D50DD9E")
return true
}
delegate.switchState(generateStateOfInputting())
} else {
if compositor.cursor > 0 {
compositor.cursor -= 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .rear)
}
delegate.switchState(generateStateOfInputting())
} else {
delegate.callError("7045E6F3")
}
}
return true
}
// MARK: - Tab Shift+Space
///
/// - Parameters:
/// - reverseOrder:
/// - Returns: SessionCtl IMK
func revolveCandidate(reverseOrder: Bool) -> Bool {
guard let delegate = delegate else { return false }
let state = delegate.state
if isComposerOrCalligrapherEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
guard state.type == .ofInputting else {
guard state.type == .ofEmpty else {
delegate.callError("6044F081")
return true
}
// 使 Tab
return false
}
guard isComposerOrCalligrapherEmpty else {
delegate.callError("A2DAF7BC")
return true
}
let candidates = generateArrayOfCandidates(fixOrder: prefs.useFixedCandidateOrderOnSelection)
guard !candidates.isEmpty else {
delegate.callError("3378A6DF")
return true
}
guard let region = compositor.walkedNodes.cursorRegionMap[actualNodeCursorPosition],
compositor.walkedNodes.count > region
else {
delegate.callError("1CE6FFBD")
return true
}
let currentNode = compositor.walkedNodes[region]
let currentPaired = (currentNode.keyArray, currentNode.value)
//
let newIndex: Int = {
if candidates.count == 1 { return 0 }
var result = 0
theLoop: for candidate in candidates {
if !currentNode.isOverridden {
if candidates[0] == currentPaired { result = reverseOrder ? candidates.count - 1 : 1 }
break theLoop
}
result.revolveAsIndex(with: candidates, clockwise: !(candidate == currentPaired && reverseOrder))
if candidate == currentPaired { break }
}
return (0 ..< candidates.count).contains(result) ? result : 0
}()
if candidates.count > 1 {
consolidateNode(
candidate: candidates[newIndex], respectCursorPushing: false,
preConsolidate: false, skipObservation: true
)
}
//
func isContextVertical() -> Bool {
delegate.updateVerticalTypingStatus()
return delegate.isVerticalTyping
}
var newState = generateStateOfInputting()
let locID = Bundle.main.preferredLocalizations[0]
let newTooltip = NSMutableString()
newTooltip.insert(" " + candidates[newIndex].value, at: 0)
if #available(macOS 10.13, *), isContextVertical(), locID != "en" {
newTooltip.insert((newIndex + 1).i18n(loc: locID) + "" + candidates.count.i18n(loc: locID), at: 0)
} else {
newTooltip.insert((newIndex + 1).description + " / " + candidates.count.description, at: 0)
}
newState.tooltip = newTooltip.description
vCLog(newState.tooltip)
newState.tooltipDuration = 0
delegate.switchState(newState)
return true
}
// MARK: - CodePoint Input Toggle
@discardableResult func handleCodePointInputToggle() -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
if isCodePointInputMode {
isCodePointInputMode = false
delegate.switchState(IMEState.ofAbortion())
return true
}
var updatedState = generateStateOfInputting(sansReading: true)
delegate.switchState(IMEState.ofCommitting(textToCommit: updatedState.displayedText))
updatedState = generateStateOfInputting(guarded: true)
updatedState.tooltipDuration = 0
updatedState.tooltip = tooltipCodePointInputMode
delegate.switchState(updatedState)
isCodePointInputMode = true
return true
}
// MARK: - Hanin Pallete
@discardableResult func handleHaninKeyboardSymbolModeToggle() -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
if isCodePointInputMode { isCodePointInputMode = false }
if isHaninKeyboardSymbolMode {
isHaninKeyboardSymbolMode = false
delegate.switchState(IMEState.ofAbortion())
return true
}
var updatedState = generateStateOfInputting(sansReading: true)
delegate.switchState(IMEState.ofCommitting(textToCommit: updatedState.displayedText))
updatedState = generateStateOfInputting(guarded: true)
updatedState.tooltipDuration = 0
updatedState.tooltip = Self.tooltipHaninKeyboardSymbolMode
delegate.switchState(updatedState)
isHaninKeyboardSymbolMode = true
return true
}
///
/// - Parameters:
/// - input:
/// - Returns: SessionCtl IMK
func handleHaninKeyboardSymbolModeInput(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
let charText = input.text.lowercased().applyingTransformFW2HW(reverse: false)
guard CandidateNode.mapHaninKeyboardSymbols.keys.contains(charText) else {
return handleHaninKeyboardSymbolModeToggle()
}
guard
charText.count == 1, let symbols = CandidateNode.queryHaninKeyboardSymbols(char: charText)
else {
delegate.callError("C1A760C7")
return true
}
// commit buffer ESC
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
if symbols.members.count == 1 {
delegate.switchState(IMEState.ofCommitting(textToCommit: symbols.members.map(\.name).joined()))
} else {
delegate.switchState(IMEState.ofSymbolTable(node: symbols))
}
isHaninKeyboardSymbolMode = false // toggle
return true
}
// MARK: - Symbol Menu Input
///
/// - Parameters:
/// - alternative: 使
/// - JIS: JIS
/// - Returns: SessionCtl IMK
func handlePunctuationList(alternative: Bool, isJIS: Bool = false) -> Bool {
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
if alternative {
if currentLM.hasUnigramsFor(keyArray: ["_punctuation_list"]) {
if isComposerOrCalligrapherEmpty, compositor.insertKey("_punctuation_list") {
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = generateStateOfInputting()
inputting.textToCommit = textToCommit
delegate.switchState(inputting)
//
let newState = generateStateOfCandidates()
_ = newState.candidates.isEmpty ? delegate.callError("B5127D8A") : delegate.switchState(newState)
} else { //
delegate.callError("17446655")
}
return true
} else {
let errorMessage =
NSLocalizedString(
"Please manually implement the symbols of this menu \nin the user phrase file with “_punctuation_list” key.",
comment: ""
)
vCLog("8EB3FB1A: " + errorMessage)
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
delegate.switchState(IMEState.ofCommitting(textToCommit: isJIS ? "_" : "`"))
return true
}
} else {
// commit buffer ESC
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
delegate.switchState(IMEState.ofSymbolTable(node: CandidateNode.root))
return true
}
}
// MARK: - Caps Lock Caps Lock and Alphanumerical mode
/// CapsLock
/// - Remark: Caps Lock
/// Caps Lock
/// - Parameter input:
/// - Returns: IMK
func handleCapsLockAndAlphanumericalMode(input: InputSignalProtocol) -> Bool? {
guard let delegate = delegate else { return nil }
let handleCapsLock = !PrefMgr.shared.bypassNonAppleCapsLockHandling && input.isCapsLockOn
guard handleCapsLock || delegate.isASCIIMode else { return nil }
// macOS 12 CapsLock
// .ofEmpty()
delegate.switchState(IMEState.ofEmpty())
// Shift
if (input.isUpperCaseASCIILetterKey && delegate.isASCIIMode)
|| (handleCapsLock && input.isShiftHold)
{
return false
}
// Shift
if delegate.isASCIIMode, !handleCapsLock { return false }
/// ASCII
/// 使insertText:replacementRange:
/// ASCII
if input.isASCII, !input.charCode.isPrintableASCII { return false }
//
delegate.switchState(IMEState.ofCommitting(textToCommit: input.text.lowercased()))
return true
}
// MARK: - Intentionally Call Candidate Window
///
/// - Parameter input:
/// - Returns: IMK
func callCandidateState(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
var state: IMEStateProtocol { delegate.state }
//
// state.hasComposition
// !input.isFunctionKeyHold
//
let notEmpty = state.hasComposition && !compositor.isEmpty && isComposerOrCalligrapherEmpty
let bannedModifiers: NSEvent.ModifierFlags = [.option, .shift, .command, .control]
let noBannedModifiers = bannedModifiers.intersection(input.keyModifierFlags).isEmpty
var triggered = input.isCursorClockLeft || input.isCursorClockRight
triggered = triggered || (input.isSpace && prefs.chooseCandidateUsingSpace)
triggered = triggered || input.isPageDown || input.isPageUp
triggered = triggered || (input.isTab && prefs.specifyShiftTabKeyBehavior)
guard notEmpty, noBannedModifiers, triggered else { return false }
//
let candidateState: IMEStateProtocol = generateStateOfCandidates()
_ = candidateState.candidates.isEmpty ? delegate.callError("3572F238") : delegate.switchState(candidateState)
return true
}
// MARK: - /FW/HW Arabic Numerals
/// /
/// - Parameter input:
/// - Returns: IMK
func handleArabicNumeralInputs(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
guard delegate.state.type == .ofEmpty, input.isMainAreaNumKey else { return false }
guard input.isOptionHold, !input.isCommandHold, !input.isControlHold else { return false }
guard let strRAW = input.mainAreaNumKeyChar else { return false }
let newString: String = {
if input.isShiftHold {
return strRAW.applyingTransformFW2HW(reverse: !prefs.halfWidthPunctuationEnabled)
}
return strRAW.applyingTransformFW2HW(reverse: false)
}()
delegate.switchState(IMEState.ofCommitting(textToCommit: newString))
return true
}
// MARK: - SHIFT Shift + Letter keys
/// SHIFT
/// - Parameter input:
/// - Returns: IMK
func handleLettersWithShiftHold(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
let inputText = input.text
if input.isUpperCaseASCIILetterKey, !input.isCommandHold, !input.isControlHold {
if input.isShiftHold { // isOptionHold
switch prefs.upperCaseLetterKeyBehavior {
case 1, 3:
if prefs.upperCaseLetterKeyBehavior == 3, !isConsideredEmptyForNow { break }
let commitText = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: commitText + inputText.lowercased()))
return true
case 2, 4:
if prefs.upperCaseLetterKeyBehavior == 4, !isConsideredEmptyForNow { break }
let commitText = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: commitText + inputText.uppercased()))
return true
default: // case 0
break
}
//
let letter = "_letter_\(inputText)"
if handlePunctuation(letter) {
return true
}
}
}
return false
}
// MARK: -
func handleCassetteSymbolTable(input: InputSignalProtocol) -> Bool {
guard let delegate = delegate else { return false }
guard prefs.cassetteEnabled else { return false }
let inputText = input.text
guard !inputText.isEmpty else { return false }
let queryString = calligrapher + inputText
let maybeResult = currentLM.cassetteSymbolDataFor(key: queryString)
guard let result = maybeResult else { return false }
let root = CandidateNode(name: queryString, symbols: result)
// commit buffer ESC
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
delegate.switchState(IMEState.ofSymbolTable(node: root))
return true
}
}