Repo // Refactoring InputFSM - preparation.
This commit is contained in:
parent
205e07e03a
commit
97062ab731
|
@ -0,0 +1,182 @@
|
|||
// (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 Foundation
|
||||
|
||||
// 用以讓每個狀態自描述的 enum。
|
||||
public enum StateType {
|
||||
case ofDeactivated
|
||||
case ofEmpty
|
||||
case ofAbortion // 該狀態會自動轉為 Empty
|
||||
case ofCommitting
|
||||
case ofAssociates
|
||||
case ofNotEmpty
|
||||
case ofInputting
|
||||
case ofMarking
|
||||
case ofCandidates
|
||||
case ofSymbolTable
|
||||
}
|
||||
|
||||
// 所有 InputState 均遵守該協定:
|
||||
public protocol InputStateProtocol {
|
||||
var type: StateType { get }
|
||||
var data: StateData { get }
|
||||
var hasBuffer: Bool { get }
|
||||
var isCandidateContainer: Bool { get }
|
||||
var displayedText: String { get }
|
||||
var textToCommit: String { get set }
|
||||
var tooltip: String { get set }
|
||||
var attributedString: NSAttributedString { get }
|
||||
var node: SymbolNode { get set }
|
||||
}
|
||||
|
||||
public struct IMEState {
|
||||
public var type: StateType = .ofEmpty
|
||||
public var data: StateData = .init()
|
||||
init(_ data: StateData = .init(), type: StateType = .ofEmpty) {
|
||||
self.data = data
|
||||
self.type = type
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 針對不同的狀態,規定不同的構造器
|
||||
|
||||
extension IMEState {
|
||||
public static func Deactivated() -> IMEState { .init(type: .ofDeactivated) }
|
||||
public static func Empty() -> IMEState { .init(type: .ofEmpty) }
|
||||
public static func Abortion() -> IMEState { .init(type: .ofAbortion) }
|
||||
public static func Committing(textToCommit: String) -> IMEState {
|
||||
var result = IMEState(type: .ofCommitting)
|
||||
result.data.textToCommit = textToCommit
|
||||
ChineseConverter.ensureCurrencyNumerals(target: &result.data.textToCommit)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func Associates(candidates: [(String, String)]) -> IMEState {
|
||||
var result = IMEState(type: .ofAssociates)
|
||||
result.data.candidates = candidates
|
||||
return result
|
||||
}
|
||||
|
||||
public static func NotEmpty(nodeValues: [String], reading: String = "", cursor: Int) -> IMEState {
|
||||
var result = IMEState(type: .ofNotEmpty)
|
||||
// 注意資料的設定順序:nodeValuesArray 必須比 reading 先設定。
|
||||
result.data.nodeValuesArray = nodeValues
|
||||
if !reading.isEmpty {
|
||||
result.data.reading = reading // 會在被寫入資料值後自動更新 nodeValuesArray
|
||||
}
|
||||
// 此時 nodeValuesArray 已經被塞上讀音,直接使用即可。
|
||||
result.data.displayedText = result.data.nodeValuesArray.joined()
|
||||
result.data.cursor = cursor
|
||||
return result
|
||||
}
|
||||
|
||||
public static func Inputting(nodeValues: [String], reading: String = "", cursor: Int) -> IMEState {
|
||||
var result = IMEState.NotEmpty(nodeValues: nodeValues, reading: reading, cursor: cursor)
|
||||
result.type = .ofInputting
|
||||
return result
|
||||
}
|
||||
|
||||
public static func Marking(nodeValues: [String], nodeReadings: [String], cursor: Int, marker: Int) -> IMEState {
|
||||
var result = IMEState.NotEmpty(nodeValues: nodeValues, cursor: cursor)
|
||||
result.type = .ofMarking
|
||||
result.data.nodeReadingsArray = nodeReadings
|
||||
result.data.marker = marker
|
||||
StateData.Marking.updateParameters(&result.data)
|
||||
return result
|
||||
}
|
||||
|
||||
public static func Candidates(candidates: [(String, String)], nodeValues: [String], cursor: Int) -> IMEState {
|
||||
var result = IMEState.NotEmpty(nodeValues: nodeValues, cursor: cursor)
|
||||
result.type = .ofCandidates
|
||||
result.data.candidates = candidates
|
||||
return result
|
||||
}
|
||||
|
||||
public static func SymbolTable(node: SymbolNode, previous: SymbolNode? = nil) -> IMEState {
|
||||
let candidates = { node.children?.map(\.title) ?? [String]() }().map { ("", $0) }
|
||||
var result = IMEState.Candidates(candidates: candidates, nodeValues: [], cursor: 0)
|
||||
result.type = .ofSymbolTable
|
||||
result.data.node = node
|
||||
if let previous = previous {
|
||||
result.data.node.previous = previous
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 規定一個狀態該怎樣返回自己的資料值
|
||||
|
||||
extension IMEState: InputStateProtocol {
|
||||
public var convertedToInputting: IMEState {
|
||||
if type == .ofInputting { return self }
|
||||
var result = IMEState.Inputting(nodeValues: data.nodeValuesArray, reading: data.reading, cursor: data.cursor)
|
||||
result.tooltip = data.tooltipBackupForInputting
|
||||
return result
|
||||
}
|
||||
|
||||
public var textToCommit: String {
|
||||
get {
|
||||
data.textToCommit
|
||||
}
|
||||
set {
|
||||
data.textToCommit = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var tooltip: String {
|
||||
get {
|
||||
data.tooltip
|
||||
}
|
||||
set {
|
||||
data.tooltip = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var attributedString: NSAttributedString {
|
||||
switch type {
|
||||
case .ofMarking: return data.attributedStringMarking
|
||||
case .ofAssociates, .ofSymbolTable: return data.attributedStringPlaceholder
|
||||
default: return data.attributedStringNormal
|
||||
}
|
||||
}
|
||||
|
||||
public var node: SymbolNode {
|
||||
get {
|
||||
data.node
|
||||
}
|
||||
set {
|
||||
data.node = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var tooltipBackupForInputting: String {
|
||||
get {
|
||||
data.tooltipBackupForInputting
|
||||
}
|
||||
set {
|
||||
data.tooltipBackupForInputting = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var hasBuffer: Bool {
|
||||
switch type {
|
||||
case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
public var isCandidateContainer: Bool {
|
||||
switch type {
|
||||
case .ofCandidates, .ofAssociates, .ofSymbolTable: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
public var displayedText: String { data.displayedText }
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
// (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 Foundation
|
||||
|
||||
public struct StateData {
|
||||
var displayedText: String = "" {
|
||||
didSet {
|
||||
let result = IME.kanjiConversionIfRequired(displayedText)
|
||||
if result.utf16.count == displayedText.utf16.count, result.count == displayedText.count {
|
||||
displayedText = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Cursor & Marker & Range for UTF8
|
||||
|
||||
var cursor: Int = 0 {
|
||||
didSet {
|
||||
cursor = min(max(cursor, 0), displayedText.count)
|
||||
}
|
||||
}
|
||||
|
||||
var marker: Int = 0 {
|
||||
didSet {
|
||||
marker = min(max(marker, 0), displayedText.count)
|
||||
}
|
||||
}
|
||||
|
||||
var markedRange: Range<Int> {
|
||||
min(cursor, marker)..<max(cursor, marker)
|
||||
}
|
||||
|
||||
// MARK: Cursor & Marker & Range for UTF16 (Read-Only)
|
||||
|
||||
var u16Cursor: Int {
|
||||
displayedText.charComponents[0..<cursor].joined().utf16.count
|
||||
}
|
||||
|
||||
var u16Marker: Int {
|
||||
displayedText.charComponents[0..<marker].joined().utf16.count
|
||||
}
|
||||
|
||||
var u16MarkedRange: Range<Int> {
|
||||
min(u16Cursor, u16Marker)..<max(u16Cursor, u16Marker)
|
||||
}
|
||||
|
||||
// MARK: Other data for non-empty states.
|
||||
|
||||
var markedTargetExists: Bool = false
|
||||
var nodeReadingsArray = [String]()
|
||||
var nodeValuesArray = [String]()
|
||||
var reading: String = "" {
|
||||
didSet {
|
||||
if !reading.isEmpty {
|
||||
var newNodeValuesArray = [String]()
|
||||
var temporaryNode = ""
|
||||
var charCounter = 0
|
||||
for node in nodeValuesArray {
|
||||
for char in node {
|
||||
if charCounter == cursor - reading.count {
|
||||
newNodeValuesArray.append(temporaryNode)
|
||||
temporaryNode = ""
|
||||
newNodeValuesArray.append(reading)
|
||||
}
|
||||
temporaryNode += String(char)
|
||||
charCounter += 1
|
||||
}
|
||||
newNodeValuesArray.append(temporaryNode)
|
||||
temporaryNode = ""
|
||||
}
|
||||
nodeValuesArray = newNodeValuesArray.isEmpty ? [reading] : newNodeValuesArray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var candidates = [(String, String)]()
|
||||
var textToCommit: String = ""
|
||||
var tooltip: String = ""
|
||||
var tooltipBackupForInputting: String = ""
|
||||
var attributedStringPlaceholder: NSAttributedString = .init(
|
||||
string: " ",
|
||||
attributes: [
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.markedClauseSegment: 0,
|
||||
]
|
||||
)
|
||||
var isFilterable: Bool {
|
||||
markedTargetExists ? mgrPrefs.allowedMarkRange.contains(markedRange.count) : false
|
||||
}
|
||||
|
||||
var readingCountMismatched: Bool { displayedText.count != nodeReadingsArray.count }
|
||||
var attributedStringNormal: NSAttributedString {
|
||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||
let attributedString = NSMutableAttributedString(string: displayedText)
|
||||
var newBegin = 0
|
||||
for (i, neta) in nodeValuesArray.enumerated() {
|
||||
attributedString.setAttributes(
|
||||
[
|
||||
/// 不能用 .thick,否則會看不到游標。
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.markedClauseSegment: i,
|
||||
], range: NSRange(location: newBegin, length: neta.utf16.count)
|
||||
)
|
||||
newBegin += neta.utf16.count
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
var attributedStringMarking: NSAttributedString {
|
||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||
let attributedString = NSMutableAttributedString(string: displayedText)
|
||||
let end = u16MarkedRange.upperBound
|
||||
|
||||
attributedString.setAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.markedClauseSegment: 0,
|
||||
], range: NSRange(location: 0, length: u16MarkedRange.lowerBound)
|
||||
)
|
||||
attributedString.setAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.thick.rawValue,
|
||||
.markedClauseSegment: 1,
|
||||
],
|
||||
range: NSRange(
|
||||
location: u16MarkedRange.lowerBound,
|
||||
length: u16MarkedRange.upperBound - u16MarkedRange.lowerBound
|
||||
)
|
||||
)
|
||||
attributedString.setAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.markedClauseSegment: 2,
|
||||
],
|
||||
range: NSRange(
|
||||
location: end,
|
||||
length: displayedText.utf16.count - end
|
||||
)
|
||||
)
|
||||
return attributedString
|
||||
}
|
||||
|
||||
var node: SymbolNode = .init("")
|
||||
}
|
||||
|
||||
// MARK: - InputState 工具函式
|
||||
|
||||
extension StateData {
|
||||
var chkIfUserPhraseExists: Bool {
|
||||
let text = displayedText.charComponents[markedRange].joined()
|
||||
let selectedReadings = nodeReadingsArray[markedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
return mgrLangModel.checkIfUserPhraseExist(
|
||||
userPhrase: text, mode: IME.currentInputMode, key: joined
|
||||
)
|
||||
}
|
||||
|
||||
var userPhrase: String {
|
||||
let text = displayedText.charComponents[markedRange].joined()
|
||||
let selectedReadings = nodeReadingsArray[markedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||
return "\(text) \(joined)\(nerfedScore)"
|
||||
}
|
||||
|
||||
var userPhraseConverted: String {
|
||||
let text =
|
||||
ChineseConverter.crossConvert(displayedText.charComponents[markedRange].joined()) ?? ""
|
||||
let selectedReadings = nodeReadingsArray[markedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
||||
return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)"
|
||||
}
|
||||
|
||||
enum Marking {
|
||||
private static func generateReadingThread(_ data: StateData) -> String {
|
||||
var arrOutput = [String]()
|
||||
for neta in data.nodeReadingsArray[data.markedRange] {
|
||||
var neta = neta
|
||||
if neta.isEmpty { continue }
|
||||
if neta.contains("_") {
|
||||
arrOutput.append("??")
|
||||
continue
|
||||
}
|
||||
if mgrPrefs.showHanyuPinyinInCompositionBuffer { // 恢復陰平標記->注音轉拼音->轉教科書式標調
|
||||
neta = Tekkon.restoreToneOneInZhuyinKey(target: neta)
|
||||
neta = Tekkon.cnvPhonaToHanyuPinyin(target: neta)
|
||||
neta = Tekkon.cnvHanyuPinyinToTextbookStyle(target: neta)
|
||||
} else {
|
||||
neta = Tekkon.cnvZhuyinChainToTextbookReading(target: neta)
|
||||
}
|
||||
arrOutput.append(neta)
|
||||
}
|
||||
return arrOutput.joined(separator: " ")
|
||||
}
|
||||
|
||||
/// 更新工具提示內容、以及對應配對是否在庫。
|
||||
/// - Parameter data: 要處理的狀態資料包。
|
||||
public static func updateParameters(_ data: inout StateData) {
|
||||
var tooltipGenerated: String {
|
||||
if data.displayedText.count != data.nodeReadingsArray.count {
|
||||
ctlInputMethod.tooltipController.setColor(state: .redAlert)
|
||||
return NSLocalizedString(
|
||||
"⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: ""
|
||||
)
|
||||
}
|
||||
if mgrPrefs.phraseReplacementEnabled {
|
||||
ctlInputMethod.tooltipController.setColor(state: .warning)
|
||||
return NSLocalizedString(
|
||||
"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: ""
|
||||
)
|
||||
}
|
||||
if data.markedRange.isEmpty {
|
||||
return ""
|
||||
}
|
||||
|
||||
let text = data.displayedText.charComponents[data.markedRange].joined()
|
||||
if data.markedRange.count < mgrPrefs.allowedMarkRange.lowerBound {
|
||||
ctlInputMethod.tooltipController.setColor(state: .denialInsufficiency)
|
||||
return String(
|
||||
format: NSLocalizedString(
|
||||
"\"%@\" length must ≥ 2 for a user phrase.", comment: ""
|
||||
) + "\n// " + generateReadingThread(data), text
|
||||
)
|
||||
} else if data.markedRange.count > mgrPrefs.allowedMarkRange.upperBound {
|
||||
ctlInputMethod.tooltipController.setColor(state: .denialOverflow)
|
||||
return String(
|
||||
format: NSLocalizedString(
|
||||
"\"%@\" length should ≤ %d for a user phrase.", comment: ""
|
||||
) + "\n// " + generateReadingThread(data), text, mgrPrefs.allowedMarkRange.upperBound
|
||||
)
|
||||
}
|
||||
|
||||
let selectedReadings = data.nodeReadingsArray[data.markedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
let exist = mgrLangModel.checkIfUserPhraseExist(
|
||||
userPhrase: text, mode: IME.currentInputMode, key: joined
|
||||
)
|
||||
if exist {
|
||||
data.markedTargetExists = exist
|
||||
ctlInputMethod.tooltipController.setColor(state: .prompt)
|
||||
return String(
|
||||
format: NSLocalizedString(
|
||||
"\"%@\" already exists: ENTER to boost, SHIFT+COMMAND+ENTER to nerf, \n BackSpace or Delete key to exclude.",
|
||||
comment: ""
|
||||
) + "\n// " + generateReadingThread(data), text
|
||||
)
|
||||
}
|
||||
ctlInputMethod.tooltipController.resetColor()
|
||||
return String(
|
||||
format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n// "
|
||||
+ generateReadingThread(data),
|
||||
text
|
||||
)
|
||||
}
|
||||
data.tooltip = tooltipGenerated
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,27 +12,6 @@ import Foundation
|
|||
|
||||
// 註:所有 InputState 型別均不適合使用 Struct,因為 Struct 無法相互繼承派生。
|
||||
|
||||
// 用以讓每個狀態自描述的 enum。
|
||||
public enum StateType {
|
||||
case ofDeactivated
|
||||
case ofAssociatedPhrases
|
||||
case ofEmpty
|
||||
case ofEmptyIgnoringPreviousState
|
||||
case ofCommitting
|
||||
case ofNotEmpty
|
||||
case ofInputting
|
||||
case ofMarking
|
||||
case ofChoosingCandidate
|
||||
case ofSymbolTable
|
||||
}
|
||||
|
||||
// 所有 InputState 均遵守該協定:
|
||||
public protocol InputStateProtocol {
|
||||
var type: StateType { get }
|
||||
var hasBuffer: Bool { get }
|
||||
var isCandidateContainer: Bool { get }
|
||||
}
|
||||
|
||||
/// 此型別用以呈現輸入法控制器(ctlInputMethod)的各種狀態。
|
||||
///
|
||||
/// 從實際角度來看,輸入法屬於有限態械(Finite State Machine)。其藉由滑鼠/鍵盤
|
||||
|
@ -56,7 +35,7 @@ public protocol InputStateProtocol {
|
|||
/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。
|
||||
/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給
|
||||
/// 客體應用、準備新的輸入行為。
|
||||
/// - .EmptyIgnoringPreviousState: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些
|
||||
/// - .Abortion: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些
|
||||
/// 內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .Empty()。
|
||||
/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。
|
||||
/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。
|
||||
|
@ -68,6 +47,12 @@ public protocol InputStateProtocol {
|
|||
public enum InputState {
|
||||
/// .Deactivated: 使用者沒在使用輸入法。
|
||||
class Deactivated: InputStateProtocol {
|
||||
var node: SymbolNode = .init("")
|
||||
var attributedString: NSAttributedString = .init()
|
||||
var data: StateData = .init()
|
||||
var textToCommit: String = ""
|
||||
var tooltip: String = ""
|
||||
let displayedText: String = ""
|
||||
let hasBuffer: Bool = false
|
||||
let isCandidateContainer: Bool = false
|
||||
public var type: StateType { .ofDeactivated }
|
||||
|
@ -78,32 +63,41 @@ public enum InputState {
|
|||
/// .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。
|
||||
/// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。
|
||||
class Empty: InputStateProtocol {
|
||||
var node: SymbolNode = .init("")
|
||||
var attributedString: NSAttributedString = .init()
|
||||
var data: StateData = .init()
|
||||
var textToCommit: String = ""
|
||||
var tooltip: String = ""
|
||||
let hasBuffer: Bool = false
|
||||
let isCandidateContainer: Bool = false
|
||||
public var type: StateType { .ofEmpty }
|
||||
let composingBuffer: String = ""
|
||||
let displayedText: String = ""
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// .EmptyIgnoringPreviousState: 與 Empty 類似,
|
||||
/// .Abortion: 與 Empty 類似,
|
||||
/// 但會扔掉上一個狀態的內容、不將這些內容遞交給客體應用。
|
||||
/// 該狀態在處理完畢之後會被立刻切換至 .Empty()。
|
||||
class EmptyIgnoringPreviousState: Empty {
|
||||
override public var type: StateType { .ofEmptyIgnoringPreviousState }
|
||||
class Abortion: Empty {
|
||||
override public var type: StateType { .ofAbortion }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。
|
||||
class Committing: InputStateProtocol {
|
||||
var node: SymbolNode = .init("")
|
||||
var attributedString: NSAttributedString = .init()
|
||||
var data: StateData = .init()
|
||||
var tooltip: String = ""
|
||||
var textToCommit: String = ""
|
||||
let displayedText: String = ""
|
||||
let hasBuffer: Bool = false
|
||||
let isCandidateContainer: Bool = false
|
||||
public var type: StateType { .ofCommitting }
|
||||
private(set) var textToCommit: String = ""
|
||||
|
||||
convenience init(textToCommit: String) {
|
||||
self.init()
|
||||
init(textToCommit: String) {
|
||||
self.textToCommit = textToCommit
|
||||
ChineseConverter.ensureCurrencyNumerals(target: &self.textToCommit)
|
||||
}
|
||||
|
@ -113,18 +107,20 @@ public enum InputState {
|
|||
|
||||
/// .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。
|
||||
/// 因為逐字選字模式不需要在組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。
|
||||
class AssociatedPhrases: InputStateProtocol {
|
||||
class Associates: InputStateProtocol {
|
||||
var node: SymbolNode = .init("")
|
||||
var attributedString: NSAttributedString = .init()
|
||||
var data: StateData = .init()
|
||||
var textToCommit: String = ""
|
||||
var tooltip: String = ""
|
||||
let displayedText: String = ""
|
||||
let hasBuffer: Bool = false
|
||||
let isCandidateContainer: Bool = true
|
||||
public var type: StateType { .ofAssociatedPhrases }
|
||||
private(set) var candidates: [(String, String)] = []
|
||||
private(set) var isTypingVertical: Bool = false
|
||||
init(candidates: [(String, String)], isTypingVertical: Bool) {
|
||||
self.candidates = candidates
|
||||
self.isTypingVertical = isTypingVertical
|
||||
}
|
||||
|
||||
var attributedString: NSMutableAttributedString {
|
||||
public var type: StateType { .ofAssociates }
|
||||
var candidates: [(String, String)] { data.candidates }
|
||||
init(candidates: [(String, String)]) {
|
||||
data.candidates = candidates
|
||||
attributedString = {
|
||||
let attributedString = NSMutableAttributedString(
|
||||
string: " ",
|
||||
attributes: [
|
||||
|
@ -133,6 +129,7 @@ public enum InputState {
|
|||
]
|
||||
)
|
||||
return attributedString
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,27 +142,32 @@ public enum InputState {
|
|||
/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。
|
||||
/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。
|
||||
class NotEmpty: InputStateProtocol {
|
||||
var node: SymbolNode = .init("")
|
||||
var attributedString: NSAttributedString = .init()
|
||||
var data: StateData = .init()
|
||||
var tooltip: String = ""
|
||||
var textToCommit: String = ""
|
||||
let hasBuffer: Bool = true
|
||||
var isCandidateContainer: Bool { false }
|
||||
public var type: StateType { .ofNotEmpty }
|
||||
private(set) var composingBuffer: String
|
||||
private(set) var displayedText: String
|
||||
private(set) var cursorIndex: Int = 0 { didSet { cursorIndex = max(cursorIndex, 0) } }
|
||||
private(set) var reading: String = ""
|
||||
private(set) var nodeValuesArray = [String]()
|
||||
public var composingBufferConverted: String {
|
||||
let converted = IME.kanjiConversionIfRequired(composingBuffer)
|
||||
if converted.utf16.count != composingBuffer.utf16.count
|
||||
|| converted.count != composingBuffer.count
|
||||
public var displayedTextConverted: String {
|
||||
let converted = IME.kanjiConversionIfRequired(displayedText)
|
||||
if converted.utf16.count != displayedText.utf16.count
|
||||
|| converted.count != displayedText.count
|
||||
{
|
||||
return composingBuffer
|
||||
return displayedText
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
public var committingBufferConverted: String { composingBufferConverted }
|
||||
public var committingBufferConverted: String { displayedTextConverted }
|
||||
|
||||
init(composingBuffer: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) {
|
||||
self.composingBuffer = composingBuffer
|
||||
init(displayedText: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) {
|
||||
self.displayedText = displayedText
|
||||
self.reading = reading
|
||||
// 為了簡化運算,將 reading 本身也變成一個字詞節點。
|
||||
if !reading.isEmpty {
|
||||
|
@ -189,13 +191,12 @@ public enum InputState {
|
|||
} else {
|
||||
self.nodeValuesArray = nodeValuesArray
|
||||
}
|
||||
defer { self.cursorIndex = cursorIndex }
|
||||
}
|
||||
|
||||
var attributedString: NSMutableAttributedString {
|
||||
defer {
|
||||
self.cursorIndex = cursorIndex
|
||||
self.attributedString = {
|
||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||
let attributedString = NSMutableAttributedString(string: composingBufferConverted)
|
||||
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
|
||||
var newBegin = 0
|
||||
for (i, neta) in nodeValuesArray.enumerated() {
|
||||
attributedString.setAttributes(
|
||||
|
@ -208,6 +209,8 @@ public enum InputState {
|
|||
newBegin += neta.utf16.count
|
||||
}
|
||||
return attributedString
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,23 +219,20 @@ public enum InputState {
|
|||
/// .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。
|
||||
class Inputting: NotEmpty {
|
||||
override public var type: StateType { .ofInputting }
|
||||
var textToCommit: String = ""
|
||||
var tooltip: String = ""
|
||||
|
||||
override public var committingBufferConverted: String {
|
||||
let committingBuffer = nodeValuesArray.joined()
|
||||
let converted = IME.kanjiConversionIfRequired(committingBuffer)
|
||||
if converted.utf16.count != composingBuffer.utf16.count
|
||||
|| converted.count != composingBuffer.count
|
||||
if converted.utf16.count != displayedText.utf16.count
|
||||
|| converted.count != displayedText.count
|
||||
{
|
||||
return composingBuffer
|
||||
return displayedText
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
override init(composingBuffer: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) {
|
||||
override init(displayedText: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) {
|
||||
super.init(
|
||||
composingBuffer: composingBuffer, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
||||
displayedText: displayedText, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -247,8 +247,8 @@ public enum InputState {
|
|||
private(set) var markerIndex: Int = 0 { didSet { markerIndex = max(markerIndex, 0) } }
|
||||
private(set) var markedRange: Range<Int>
|
||||
private var literalMarkedRange: Range<Int> {
|
||||
let lowerBoundLiteral = composingBuffer.charIndexLiteral(from: markedRange.lowerBound)
|
||||
let upperBoundLiteral = composingBuffer.charIndexLiteral(from: markedRange.upperBound)
|
||||
let lowerBoundLiteral = displayedText.charIndexLiteral(from: markedRange.lowerBound)
|
||||
let upperBoundLiteral = displayedText.charIndexLiteral(from: markedRange.upperBound)
|
||||
return lowerBoundLiteral..<upperBoundLiteral
|
||||
}
|
||||
|
||||
|
@ -274,8 +274,9 @@ public enum InputState {
|
|||
}
|
||||
|
||||
private var markedTargetExists = false
|
||||
var tooltip: String {
|
||||
if composingBuffer.count != readings.count {
|
||||
|
||||
var tooltipForMarking: String {
|
||||
if displayedText.count != readings.count {
|
||||
ctlInputMethod.tooltipController.setColor(state: .redAlert)
|
||||
return NSLocalizedString(
|
||||
"⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: ""
|
||||
|
@ -291,7 +292,7 @@ public enum InputState {
|
|||
return ""
|
||||
}
|
||||
|
||||
let text = composingBuffer.utf16SubString(with: markedRange)
|
||||
let text = displayedText.utf16SubString(with: markedRange)
|
||||
if literalMarkedRange.count < allowedMarkRange.lowerBound {
|
||||
ctlInputMethod.tooltipController.setColor(state: .denialInsufficiency)
|
||||
return String(
|
||||
|
@ -331,26 +332,26 @@ public enum InputState {
|
|||
)
|
||||
}
|
||||
|
||||
var tooltipForInputting: String = ""
|
||||
var tooltipBackupForInputting: String = ""
|
||||
private(set) var readings: [String]
|
||||
|
||||
init(
|
||||
composingBuffer: String, cursorIndex: Int, markerIndex: Int, readings: [String], nodeValuesArray: [String] = []
|
||||
displayedText: String, cursorIndex: Int, markerIndex: Int, readings: [String], nodeValuesArray: [String] = []
|
||||
) {
|
||||
let begin = min(cursorIndex, markerIndex)
|
||||
let end = max(cursorIndex, markerIndex)
|
||||
markedRange = begin..<end
|
||||
self.readings = readings
|
||||
super.init(
|
||||
composingBuffer: composingBuffer, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray
|
||||
displayedText: displayedText, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray
|
||||
)
|
||||
defer { self.markerIndex = markerIndex }
|
||||
}
|
||||
|
||||
override var attributedString: NSMutableAttributedString {
|
||||
defer {
|
||||
self.markerIndex = markerIndex
|
||||
tooltip = tooltipForMarking
|
||||
attributedString = {
|
||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||
let attributedString = NSMutableAttributedString(string: composingBufferConverted)
|
||||
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
|
||||
let end = markedRange.upperBound
|
||||
|
||||
attributedString.setAttributes(
|
||||
|
@ -376,26 +377,28 @@ public enum InputState {
|
|||
],
|
||||
range: NSRange(
|
||||
location: end,
|
||||
length: composingBuffer.utf16.count - end
|
||||
length: displayedText.utf16.count - end
|
||||
)
|
||||
)
|
||||
return attributedString
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var convertedToInputting: Inputting {
|
||||
let state = Inputting(
|
||||
composingBuffer: composingBuffer, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
||||
displayedText: displayedText, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
||||
)
|
||||
state.tooltip = tooltipForInputting
|
||||
state.tooltip = tooltipBackupForInputting
|
||||
return state
|
||||
}
|
||||
|
||||
var validToFilter: Bool { markedTargetExists ? allowedMarkRange.contains(literalMarkedRange.count) : false }
|
||||
var isFilterable: Bool { markedTargetExists ? allowedMarkRange.contains(literalMarkedRange.count) : false }
|
||||
|
||||
var bufferReadingCountMisMatch: Bool { composingBuffer.count != readings.count }
|
||||
var bufferReadingCountMisMatch: Bool { displayedText.count != readings.count }
|
||||
|
||||
var chkIfUserPhraseExists: Bool {
|
||||
let text = composingBuffer.utf16SubString(with: markedRange)
|
||||
let text = displayedText.utf16SubString(with: markedRange)
|
||||
let selectedReadings = readings[literalMarkedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
return mgrLangModel.checkIfUserPhraseExist(
|
||||
|
@ -404,7 +407,7 @@ public enum InputState {
|
|||
}
|
||||
|
||||
var userPhrase: String {
|
||||
let text = composingBuffer.utf16SubString(with: markedRange)
|
||||
let text = displayedText.utf16SubString(with: markedRange)
|
||||
let selectedReadings = readings[literalMarkedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||
|
@ -413,7 +416,7 @@ public enum InputState {
|
|||
|
||||
var userPhraseConverted: String {
|
||||
let text =
|
||||
ChineseConverter.crossConvert(composingBuffer.utf16SubString(with: markedRange)) ?? ""
|
||||
ChineseConverter.crossConvert(displayedText.utf16SubString(with: markedRange)) ?? ""
|
||||
let selectedReadings = readings[literalMarkedRange]
|
||||
let joined = selectedReadings.joined(separator: "-")
|
||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||
|
@ -427,9 +430,8 @@ public enum InputState {
|
|||
/// .ChoosingCandidate: 叫出選字窗、允許使用者選字。
|
||||
class ChoosingCandidate: NotEmpty {
|
||||
override var isCandidateContainer: Bool { true }
|
||||
override public var type: StateType { .ofChoosingCandidate }
|
||||
private(set) var candidates: [(String, String)]
|
||||
private(set) var isTypingVertical: Bool
|
||||
override public var type: StateType { .ofCandidates }
|
||||
var candidates: [(String, String)]
|
||||
// 該變數改為可以隨時更改的內容,不然的話 ctlInputMethod.candidateSelectionChanged() 會上演俄羅斯套娃(崩潰)。
|
||||
public var chosenCandidateString: String = "" {
|
||||
didSet {
|
||||
|
@ -443,76 +445,11 @@ public enum InputState {
|
|||
}
|
||||
|
||||
init(
|
||||
composingBuffer: String, cursorIndex: Int, candidates: [(String, String)], isTypingVertical: Bool,
|
||||
displayedText: String, cursorIndex: Int, candidates: [(String, String)],
|
||||
nodeValuesArray: [String] = []
|
||||
) {
|
||||
self.candidates = candidates
|
||||
self.isTypingVertical = isTypingVertical
|
||||
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray)
|
||||
}
|
||||
|
||||
// 這個函式尚未經過嚴格的單元測試。請在使用時確保 chosenCandidateString 為空。
|
||||
// 不為空的話,該參數的返回值就會有對應的影響、顯示成類似 macOS 內建注音輸入法那樣子。
|
||||
// 本來想給輸入法拓展這方面的功能的,奈何 ctlInputMethod.candidateSelectionChanged() 這函式太氣人。
|
||||
// 想要講的幹話已經在那邊講完了,感興趣的可以去看看。
|
||||
override var attributedString: NSMutableAttributedString {
|
||||
guard !chosenCandidateString.isEmpty else { return super.attributedString }
|
||||
let bufferTextRear = composingBuffer.utf16SubString(with: 0..<cursorIndex)
|
||||
let bufferTextFront = composingBuffer.utf16SubString(with: cursorIndex..<(composingBuffer.utf16.count))
|
||||
let cursorIndexU8 = bufferTextRear.count - 1
|
||||
// 排除一些不應該出現的情形。
|
||||
if (mgrPrefs.useRearCursorMode && bufferTextFront.count < chosenCandidateString.count)
|
||||
|| (!mgrPrefs.useRearCursorMode && bufferTextRear.count < chosenCandidateString.count)
|
||||
{
|
||||
return super.attributedString
|
||||
}
|
||||
// u16Range 是用來畫線的,因為 NSAttributedString 只認 NSRange。
|
||||
let u16Range: Range<Int> = {
|
||||
switch mgrPrefs.useRearCursorMode {
|
||||
case false: return (max(0, cursorIndex - chosenCandidateString.utf16.count))..<cursorIndex
|
||||
case true:
|
||||
return
|
||||
cursorIndex..<min(cursorIndex + chosenCandidateString.utf16.count, composingBuffer.utf16.count - 1)
|
||||
}
|
||||
}()
|
||||
// u8Range 是用來計算字串的。
|
||||
let u8Range: Range<Int> = {
|
||||
switch mgrPrefs.useRearCursorMode {
|
||||
case false: return (max(0, cursorIndexU8 - chosenCandidateString.count))..<cursorIndexU8
|
||||
case true:
|
||||
return cursorIndexU8..<min(cursorIndexU8 + chosenCandidateString.count, composingBuffer.count - 1)
|
||||
}
|
||||
}()
|
||||
let strSegmentedRear = composingBuffer.charComponents[0..<u8Range.lowerBound].joined()
|
||||
let strSegmentedFront = composingBuffer.charComponents[u8Range.upperBound...].joined()
|
||||
let newBufferConverted: String = NotEmpty(
|
||||
composingBuffer: strSegmentedRear + chosenCandidateString + strSegmentedFront, cursorIndex: 0
|
||||
).composingBufferConverted
|
||||
guard newBufferConverted.count == composingBuffer.count else { return super.attributedString }
|
||||
|
||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||
let attributedStringResult = NSMutableAttributedString(string: newBufferConverted)
|
||||
attributedStringResult.setAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.markedClauseSegment: 0,
|
||||
], range: NSRange(location: 0, length: u16Range.lowerBound)
|
||||
)
|
||||
attributedStringResult.setAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.thick.rawValue,
|
||||
.markedClauseSegment: 1,
|
||||
], range: NSRange(location: u16Range.lowerBound, length: u16Range.count)
|
||||
)
|
||||
attributedStringResult.setAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.markedClauseSegment: 2,
|
||||
], range: NSRange(location: u16Range.upperBound, length: newBufferConverted.utf16.count)
|
||||
)
|
||||
|
||||
return attributedStringResult
|
||||
super.init(displayedText: displayedText, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,26 +458,22 @@ public enum InputState {
|
|||
/// .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。
|
||||
class SymbolTable: ChoosingCandidate {
|
||||
override public var type: StateType { .ofSymbolTable }
|
||||
var node: SymbolNode
|
||||
|
||||
init(node: SymbolNode, previous: SymbolNode? = nil, isTypingVertical: Bool) {
|
||||
init(node: SymbolNode, previous: SymbolNode? = nil) {
|
||||
super.init(displayedText: "", cursorIndex: 0, candidates: [])
|
||||
self.node = node
|
||||
if let previous = previous {
|
||||
self.node.previous = previous
|
||||
}
|
||||
let candidates = node.children?.map(\.title) ?? [String]()
|
||||
super.init(
|
||||
composingBuffer: "", cursorIndex: 0, candidates: candidates.map { ("", $0) },
|
||||
isTypingVertical: isTypingVertical
|
||||
)
|
||||
}
|
||||
self.candidates = candidates.map { ("", $0) }
|
||||
|
||||
// InputState.SymbolTable 這個狀態比較特殊,不能把真空組字區交出去。
|
||||
// 不然的話,在絕大多數終端機類應用當中、以及在 MS Word 等軟體當中
|
||||
// 會出現符號選字窗無法響應方向鍵的問題。
|
||||
// 如有誰要修奇摩注音的一點通選單的話,修復原理也是一樣的。
|
||||
// Crediting Qwertyyb: https://github.com/qwertyyb/Fire/issues/55#issuecomment-1133497700
|
||||
override var attributedString: NSMutableAttributedString {
|
||||
attributedString = {
|
||||
let attributedString = NSMutableAttributedString(
|
||||
string: " ",
|
||||
attributes: [
|
||||
|
@ -549,6 +482,7 @@ public enum InputState {
|
|||
]
|
||||
)
|
||||
return attributedString
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ extension KeyHandler {
|
|||
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
|
||||
|
||||
if cancelCandidateKey {
|
||||
if state is InputState.AssociatedPhrases
|
||||
if state is InputState.Associates
|
||||
|| mgrPrefs.useSCPCTypingMode
|
||||
|| compositor.isEmpty
|
||||
{
|
||||
|
@ -49,13 +49,13 @@ extension KeyHandler {
|
|||
// 就將當前的組字緩衝區析構處理、強制重設輸入狀態。
|
||||
// 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。
|
||||
// 所以這裡需要對 compositor.isEmpty 做判定。
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
} else {
|
||||
stateCallback(buildInputtingState)
|
||||
}
|
||||
if let state = state as? InputState.SymbolTable, let nodePrevious = state.node.previous {
|
||||
stateCallback(InputState.SymbolTable(node: nodePrevious, isTypingVertical: state.isTypingVertical))
|
||||
stateCallback(InputState.SymbolTable(node: nodePrevious))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -63,8 +63,8 @@ extension KeyHandler {
|
|||
// MARK: Enter
|
||||
|
||||
if input.isEnter {
|
||||
if state is InputState.AssociatedPhrases, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
if state is InputState.Associates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
return true
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ extension KeyHandler {
|
|||
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
candidates = state.candidates
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
} else if let state = state as? InputState.Associates {
|
||||
candidates = state.candidates
|
||||
}
|
||||
|
||||
|
@ -267,13 +267,13 @@ extension KeyHandler {
|
|||
|
||||
// MARK: 聯想詞處理 (Associated Phrases)
|
||||
|
||||
if state is InputState.AssociatedPhrases {
|
||||
if state is InputState.Associates {
|
||||
if !input.isShiftHold { return false }
|
||||
}
|
||||
|
||||
var index: Int = NSNotFound
|
||||
let match: String =
|
||||
(state is InputState.AssociatedPhrases) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
||||
(state is InputState.Associates) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
||||
|
||||
for j in 0..<ctlCandidateCurrent.keyLabels.count {
|
||||
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
|
||||
|
@ -293,7 +293,7 @@ extension KeyHandler {
|
|||
}
|
||||
}
|
||||
|
||||
if state is InputState.AssociatedPhrases { return false }
|
||||
if state is InputState.Associates { return false }
|
||||
|
||||
// MARK: 逐字選字模式的處理 (SCPC Mode Processing)
|
||||
|
||||
|
@ -333,7 +333,7 @@ extension KeyHandler {
|
|||
didSelectCandidateAt: candidateIndex,
|
||||
ctlCandidate: ctlCandidateCurrent
|
||||
)
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
return handle(
|
||||
input: input, state: InputState.Empty(), stateCallback: stateCallback, errorCallback: errorCallback
|
||||
|
|
|
@ -62,7 +62,7 @@ extension KeyHandler {
|
|||
composer.receiveKey(fromString: input.text)
|
||||
keyConsumedByReading = true
|
||||
|
||||
// 沒有調號的話,只需要 updateClientComposingBuffer() 且終止處理(return true)即可。
|
||||
// 沒有調號的話,只需要 updateClientdisplayedText() 且終止處理(return true)即可。
|
||||
// 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。
|
||||
if !composer.hasToneMarker() {
|
||||
stateCallback(buildInputtingState)
|
||||
|
@ -100,7 +100,7 @@ extension KeyHandler {
|
|||
switch compositor.isEmpty {
|
||||
case false: stateCallback(buildInputtingState)
|
||||
case true:
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
}
|
||||
return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。
|
||||
|
@ -118,7 +118,7 @@ extension KeyHandler {
|
|||
// 之後就是更新組字區了。先清空注拼槽的內容。
|
||||
composer.clear()
|
||||
|
||||
// 再以回呼組字狀態的方式來執行 updateClientComposingBuffer()。
|
||||
// 再以回呼組字狀態的方式來執行 updateClientdisplayedText()。
|
||||
let inputting = buildInputtingState
|
||||
stateCallback(inputting)
|
||||
|
||||
|
@ -157,7 +157,7 @@ extension KeyHandler {
|
|||
|
||||
/// 是說此時注拼槽並非為空、卻還沒組音。這種情況下只可能是「注拼槽內只有聲調」。
|
||||
if keyConsumedByReading {
|
||||
// 以回呼組字狀態的方式來執行 updateClientComposingBuffer()。
|
||||
// 以回呼組字狀態的方式來執行 updateClientdisplayedText()。
|
||||
stateCallback(buildInputtingState)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ extension KeyHandler {
|
|||
// 提前過濾掉一些不合規的按鍵訊號輸入,免得相關按鍵訊號被送給 Megrez 引發輸入法崩潰。
|
||||
if input.isInvalid {
|
||||
// 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。
|
||||
// 因為「.EmptyIgnoringPreviousState」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。
|
||||
// 因為「.Abortion」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。
|
||||
if state is InputState.Empty || state is InputState.Deactivated {
|
||||
return false
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ extension KeyHandler {
|
|||
// 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。
|
||||
let isFunctionKey: Bool =
|
||||
input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey)
|
||||
if !(state is InputState.NotEmpty) && !(state is InputState.AssociatedPhrases) && isFunctionKey {
|
||||
if !(state is InputState.NotEmpty) && !(state is InputState.Associates) && isFunctionKey {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,7 @@ extension KeyHandler {
|
|||
// 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。
|
||||
// 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。
|
||||
if input.isNumericPadKey {
|
||||
if !(state is InputState.ChoosingCandidate || state is InputState.AssociatedPhrases
|
||||
if !(state is InputState.ChoosingCandidate || state is InputState.Associates
|
||||
|| state is InputState.SymbolTable)
|
||||
{
|
||||
stateCallback(InputState.Empty())
|
||||
|
@ -114,7 +114,7 @@ extension KeyHandler {
|
|||
|
||||
// MARK: 處理聯想詞 (Handle Associated Phrases)
|
||||
|
||||
if state is InputState.AssociatedPhrases {
|
||||
if state is InputState.Associates {
|
||||
if handleCandidate(
|
||||
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
||||
) {
|
||||
|
@ -155,9 +155,9 @@ extension KeyHandler {
|
|||
/// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話………
|
||||
if !mgrPrefs.chooseCandidateUsingSpace {
|
||||
if compositor.cursor >= compositor.length {
|
||||
let composingBuffer = currentState.composingBuffer
|
||||
if !composingBuffer.isEmpty {
|
||||
stateCallback(InputState.Committing(textToCommit: composingBuffer))
|
||||
let displayedText = currentState.displayedText
|
||||
if !displayedText.isEmpty {
|
||||
stateCallback(InputState.Committing(textToCommit: displayedText))
|
||||
}
|
||||
stateCallback(InputState.Committing(textToCommit: " "))
|
||||
stateCallback(InputState.Empty())
|
||||
|
|
|
@ -110,7 +110,7 @@ extension KeyHandler {
|
|||
|
||||
/// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。
|
||||
return InputState.Inputting(
|
||||
composingBuffer: cleanedComposition, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
||||
displayedText: cleanedComposition, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -123,13 +123,12 @@ extension KeyHandler {
|
|||
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
||||
func buildCandidate(
|
||||
state currentState: InputState.NotEmpty,
|
||||
isTypingVertical: Bool = false
|
||||
isTypingVertical _: Bool = false
|
||||
) -> InputState.ChoosingCandidate {
|
||||
InputState.ChoosingCandidate(
|
||||
composingBuffer: currentState.composingBuffer,
|
||||
displayedText: currentState.displayedText,
|
||||
cursorIndex: currentState.cursorIndex,
|
||||
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
|
||||
isTypingVertical: isTypingVertical,
|
||||
nodeValuesArray: compositor.walkedNodes.values
|
||||
)
|
||||
}
|
||||
|
@ -147,16 +146,13 @@ extension KeyHandler {
|
|||
/// 是否為空:如果陣列為空的話,直接回呼一個空狀態。
|
||||
/// - Parameters:
|
||||
/// - key: 給定的索引鍵(也就是給定的聯想詞的開頭字)。
|
||||
/// - isTypingVertical: 是否縱排輸入?
|
||||
/// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。
|
||||
func buildAssociatePhraseState(
|
||||
withPair pair: Megrez.Compositor.Candidate,
|
||||
isTypingVertical: Bool
|
||||
) -> InputState.AssociatedPhrases! {
|
||||
withPair pair: Megrez.Compositor.Candidate
|
||||
) -> InputState.Associates! {
|
||||
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
||||
InputState.AssociatedPhrases(
|
||||
candidates: buildAssociatePhraseArray(withPair: pair), isTypingVertical: isTypingVertical
|
||||
)
|
||||
InputState.Associates(
|
||||
candidates: buildAssociatePhraseArray(withPair: pair))
|
||||
}
|
||||
|
||||
// MARK: - 用以處理就地新增自訂語彙時的行為
|
||||
|
@ -190,7 +186,7 @@ extension KeyHandler {
|
|||
if input.isEnter {
|
||||
if let keyHandlerDelegate = delegate {
|
||||
// 先判斷是否是在摁了降權組合鍵的時候目標不在庫。
|
||||
if input.isShiftHold, input.isCommandHold, !state.validToFilter {
|
||||
if input.isShiftHold, input.isCommandHold, !state.isFilterable {
|
||||
IME.prtDebugIntel("2EAC1F7A")
|
||||
errorCallback()
|
||||
return true
|
||||
|
@ -207,7 +203,7 @@ extension KeyHandler {
|
|||
// BackSpace & Delete
|
||||
if input.isBackSpace || input.isDelete {
|
||||
if let keyHandlerDelegate = delegate {
|
||||
if !state.validToFilter {
|
||||
if !state.isFilterable {
|
||||
IME.prtDebugIntel("1F88B191")
|
||||
errorCallback()
|
||||
return true
|
||||
|
@ -226,15 +222,15 @@ extension KeyHandler {
|
|||
if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold {
|
||||
var index = state.markerIndex
|
||||
if index > 0 {
|
||||
index = state.composingBuffer.utf16PreviousPosition(for: index)
|
||||
index = state.displayedText.utf16PreviousPosition(for: index)
|
||||
let marking = InputState.Marking(
|
||||
composingBuffer: state.composingBuffer,
|
||||
displayedText: state.displayedText,
|
||||
cursorIndex: state.cursorIndex,
|
||||
markerIndex: index,
|
||||
readings: state.readings,
|
||||
nodeValuesArray: compositor.walkedNodes.values
|
||||
)
|
||||
marking.tooltipForInputting = state.tooltipForInputting
|
||||
marking.tooltipBackupForInputting = state.tooltipBackupForInputting
|
||||
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
||||
} else {
|
||||
IME.prtDebugIntel("1149908D")
|
||||
|
@ -247,16 +243,16 @@ extension KeyHandler {
|
|||
// Shift + Right
|
||||
if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold {
|
||||
var index = state.markerIndex
|
||||
if index < (state.composingBuffer.utf16.count) {
|
||||
index = state.composingBuffer.utf16NextPosition(for: index)
|
||||
if index < (state.displayedText.utf16.count) {
|
||||
index = state.displayedText.utf16NextPosition(for: index)
|
||||
let marking = InputState.Marking(
|
||||
composingBuffer: state.composingBuffer,
|
||||
displayedText: state.displayedText,
|
||||
cursorIndex: state.cursorIndex,
|
||||
markerIndex: index,
|
||||
readings: state.readings,
|
||||
nodeValuesArray: compositor.walkedNodes.values
|
||||
)
|
||||
marking.tooltipForInputting = state.tooltipForInputting
|
||||
marking.tooltipBackupForInputting = state.tooltipBackupForInputting
|
||||
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
||||
} else {
|
||||
IME.prtDebugIntel("9B51408D")
|
||||
|
@ -336,7 +332,7 @@ extension KeyHandler {
|
|||
) -> Bool {
|
||||
guard let currentState = state as? InputState.Inputting else { return false }
|
||||
|
||||
stateCallback(InputState.Committing(textToCommit: currentState.composingBuffer))
|
||||
stateCallback(InputState.Committing(textToCommit: currentState.displayedText))
|
||||
stateCallback(InputState.Empty())
|
||||
return true
|
||||
}
|
||||
|
@ -354,17 +350,17 @@ extension KeyHandler {
|
|||
) -> Bool {
|
||||
guard state is InputState.Inputting else { return false }
|
||||
|
||||
var composingBuffer = compositor.keys.joined(separator: "-")
|
||||
var displayedText = compositor.keys.joined(separator: "-")
|
||||
if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||
composingBuffer = Tekkon.restoreToneOneInZhuyinKey(target: composingBuffer) // 恢復陰平標記
|
||||
composingBuffer = Tekkon.cnvPhonaToHanyuPinyin(target: composingBuffer) // 注音轉拼音
|
||||
displayedText = Tekkon.restoreToneOneInZhuyinKey(target: displayedText) // 恢復陰平標記
|
||||
displayedText = Tekkon.cnvPhonaToHanyuPinyin(target: displayedText) // 注音轉拼音
|
||||
}
|
||||
|
||||
if let delegate = delegate, !delegate.clientBundleIdentifier.contains("vChewingPhraseEditor") {
|
||||
composingBuffer = composingBuffer.replacingOccurrences(of: "-", with: " ")
|
||||
displayedText = displayedText.replacingOccurrences(of: "-", with: " ")
|
||||
}
|
||||
|
||||
stateCallback(InputState.Committing(textToCommit: composingBuffer))
|
||||
stateCallback(InputState.Committing(textToCommit: displayedText))
|
||||
stateCallback(InputState.Empty())
|
||||
return true
|
||||
}
|
||||
|
@ -434,14 +430,14 @@ extension KeyHandler {
|
|||
stateCallback(buildInputtingState)
|
||||
return true
|
||||
case 1:
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
return true
|
||||
default: break
|
||||
}
|
||||
|
||||
if input.isShiftHold, input.isOptionHold {
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
return true
|
||||
}
|
||||
|
@ -465,7 +461,7 @@ extension KeyHandler {
|
|||
switch composer.isEmpty && compositor.isEmpty {
|
||||
case false: stateCallback(buildInputtingState)
|
||||
case true:
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
}
|
||||
return true
|
||||
|
@ -489,7 +485,7 @@ extension KeyHandler {
|
|||
guard state is InputState.Inputting else { return false }
|
||||
|
||||
if input.isShiftHold {
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
return true
|
||||
}
|
||||
|
@ -510,10 +506,10 @@ extension KeyHandler {
|
|||
|
||||
let inputting = buildInputtingState
|
||||
// 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
||||
switch inputting.composingBuffer.isEmpty {
|
||||
switch inputting.displayedText.isEmpty {
|
||||
case false: stateCallback(inputting)
|
||||
case true:
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
}
|
||||
return true
|
||||
|
@ -625,7 +621,7 @@ extension KeyHandler {
|
|||
if mgrPrefs.escToCleanInputBuffer {
|
||||
/// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。
|
||||
/// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
} else {
|
||||
if composer.isEmpty { return true }
|
||||
|
@ -634,7 +630,7 @@ extension KeyHandler {
|
|||
switch compositor.isEmpty {
|
||||
case false: stateCallback(buildInputtingState)
|
||||
case true:
|
||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
||||
stateCallback(InputState.Abortion())
|
||||
stateCallback(InputState.Empty())
|
||||
}
|
||||
}
|
||||
|
@ -667,16 +663,16 @@ extension KeyHandler {
|
|||
|
||||
if input.isShiftHold {
|
||||
// Shift + Right
|
||||
if currentState.cursorIndex < currentState.composingBuffer.utf16.count {
|
||||
let nextPosition = currentState.composingBuffer.utf16NextPosition(
|
||||
if currentState.cursorIndex < currentState.displayedText.utf16.count {
|
||||
let nextPosition = currentState.displayedText.utf16NextPosition(
|
||||
for: currentState.cursorIndex)
|
||||
let marking: InputState.Marking! = InputState.Marking(
|
||||
composingBuffer: currentState.composingBuffer,
|
||||
displayedText: currentState.displayedText,
|
||||
cursorIndex: currentState.cursorIndex,
|
||||
markerIndex: nextPosition,
|
||||
readings: compositor.keys
|
||||
)
|
||||
marking.tooltipForInputting = currentState.tooltip
|
||||
marking.tooltipBackupForInputting = currentState.tooltip
|
||||
stateCallback(marking)
|
||||
} else {
|
||||
IME.prtDebugIntel("BB7F6DB9")
|
||||
|
@ -742,15 +738,15 @@ extension KeyHandler {
|
|||
if input.isShiftHold {
|
||||
// Shift + left
|
||||
if currentState.cursorIndex > 0 {
|
||||
let previousPosition = currentState.composingBuffer.utf16PreviousPosition(
|
||||
let previousPosition = currentState.displayedText.utf16PreviousPosition(
|
||||
for: currentState.cursorIndex)
|
||||
let marking: InputState.Marking! = InputState.Marking(
|
||||
composingBuffer: currentState.composingBuffer,
|
||||
displayedText: currentState.displayedText,
|
||||
cursorIndex: currentState.cursorIndex,
|
||||
markerIndex: previousPosition,
|
||||
readings: compositor.keys
|
||||
)
|
||||
marking.tooltipForInputting = currentState.tooltip
|
||||
marking.tooltipBackupForInputting = currentState.tooltip
|
||||
stateCallback(marking)
|
||||
} else {
|
||||
IME.prtDebugIntel("D326DEA3")
|
||||
|
|
|
@ -31,11 +31,9 @@ extension ctlInputMethod {
|
|||
if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) {
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
||||
+ {
|
||||
toggleASCIIMode()
|
||||
+ toggleASCIIMode()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
if shouldUseHandle {
|
||||
|
|
|
@ -71,7 +71,7 @@ class ctlInputMethod: IMKInputController {
|
|||
}
|
||||
if let state = state as? InputState.NotEmpty {
|
||||
/// 將傳回的新狀態交給調度函式。
|
||||
handle(state: InputState.Committing(textToCommit: state.composingBufferConverted))
|
||||
handle(state: InputState.Committing(textToCommit: state.displayedTextConverted))
|
||||
}
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
|
@ -115,11 +115,9 @@ class ctlInputMethod: IMKInputController {
|
|||
} else {
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
||||
+ {
|
||||
isASCIIMode
|
||||
+ isASCIIMode
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +323,7 @@ class ctlInputMethod: IMKInputController {
|
|||
}
|
||||
}
|
||||
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
if let state = state as? InputState.Associates {
|
||||
handleCandidatesPrepared(state.candidates, prefix: "⇧")
|
||||
} else if let state = state as? InputState.SymbolTable {
|
||||
// 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。
|
||||
|
@ -363,9 +361,9 @@ class ctlInputMethod: IMKInputController {
|
|||
/// - Parameter candidateString: 已經確認的候選字詞內容。
|
||||
override open func candidateSelected(_ candidateString: NSAttributedString!) {
|
||||
let candidateString: NSAttributedString = candidateString ?? .init(string: "")
|
||||
if state is InputState.AssociatedPhrases {
|
||||
if state is InputState.Associates {
|
||||
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||
handle(state: InputState.EmptyIgnoringPreviousState())
|
||||
handle(state: InputState.Abortion())
|
||||
handle(state: InputState.Empty())
|
||||
return
|
||||
}
|
||||
|
@ -403,7 +401,7 @@ class ctlInputMethod: IMKInputController {
|
|||
}
|
||||
}
|
||||
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
if let state = state as? InputState.Associates {
|
||||
handleCandidatesSelected(state.candidates, prefix: "⇧")
|
||||
} else if let state = state as? InputState.SymbolTable {
|
||||
handleSymbolCandidatesSelected(state.candidates)
|
||||
|
|
|
@ -65,7 +65,7 @@ extension ctlInputMethod: KeyHandlerDelegate {
|
|||
// MARK: - Candidate Controller Delegate
|
||||
|
||||
extension ctlInputMethod: ctlCandidateDelegate {
|
||||
var isAssociatedPhrasesState: Bool { state is InputState.AssociatedPhrases }
|
||||
var isAssociatedPhrasesState: Bool { state is InputState.Associates }
|
||||
|
||||
/// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。
|
||||
/// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。
|
||||
|
@ -80,7 +80,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates.count
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
} else if let state = state as? InputState.Associates {
|
||||
return state.candidates.count
|
||||
}
|
||||
return 0
|
||||
|
@ -93,7 +93,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
} else if let state = state as? InputState.Associates {
|
||||
return state.candidates
|
||||
}
|
||||
return .init()
|
||||
|
@ -105,7 +105,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates[index]
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
} else if let state = state as? InputState.Associates {
|
||||
return state.candidates[index]
|
||||
}
|
||||
return ("", "")
|
||||
|
@ -119,9 +119,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
{
|
||||
if let children = node.children, !children.isEmpty {
|
||||
handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現
|
||||
handle(
|
||||
state: InputState.SymbolTable(node: node, previous: state.node, isTypingVertical: state.isTypingVertical)
|
||||
)
|
||||
handle(state: InputState.SymbolTable(node: node, previous: state.node))
|
||||
} else {
|
||||
handle(state: InputState.Committing(textToCommit: node.title))
|
||||
handle(state: InputState.Empty())
|
||||
|
@ -139,12 +137,11 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
let inputting = keyHandler.buildInputtingState
|
||||
|
||||
if mgrPrefs.useSCPCTypingMode {
|
||||
handle(state: InputState.Committing(textToCommit: inputting.composingBufferConverted))
|
||||
handle(state: InputState.Committing(textToCommit: inputting.displayedTextConverted))
|
||||
// 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。
|
||||
if mgrPrefs.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
||||
withPair: .init(key: selectedValue.0, value: selectedValue.1),
|
||||
isTypingVertical: state.isTypingVertical
|
||||
withPair: .init(key: selectedValue.0, value: selectedValue.1)
|
||||
), !associatePhrases.candidates.isEmpty
|
||||
{
|
||||
handle(state: associatePhrases)
|
||||
|
@ -157,7 +154,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
if let state = state as? InputState.Associates {
|
||||
let selectedValue = state.candidates[index]
|
||||
handle(state: InputState.Committing(textToCommit: selectedValue.1))
|
||||
// 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。
|
||||
|
@ -168,8 +165,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
|||
}
|
||||
if mgrPrefs.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
||||
withPair: .init(key: selectedValue.0, value: String(valueKept)),
|
||||
isTypingVertical: state.isTypingVertical
|
||||
withPair: .init(key: selectedValue.0, value: String(valueKept))
|
||||
), !associatePhrases.candidates.isEmpty
|
||||
{
|
||||
handle(state: associatePhrases)
|
||||
|
|
|
@ -13,11 +13,11 @@ import Cocoa
|
|||
// MARK: - Tooltip Display and Candidate Display Methods
|
||||
|
||||
extension ctlInputMethod {
|
||||
func show(tooltip: String, composingBuffer: String, cursorIndex: Int) {
|
||||
func show(tooltip: String, displayedText: String, cursorIndex: Int) {
|
||||
guard let client = client() else { return }
|
||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||
var cursor = cursorIndex
|
||||
if cursor == composingBuffer.count, cursor != 0 {
|
||||
if cursor == displayedText.count, cursor != 0 {
|
||||
cursor -= 1
|
||||
}
|
||||
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
||||
|
@ -41,10 +41,10 @@ extension ctlInputMethod {
|
|||
func show(candidateWindowWith state: InputStateProtocol) {
|
||||
guard let client = client() else { return }
|
||||
var isTypingVertical: Bool {
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.isTypingVertical
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.isTypingVertical
|
||||
if state.type == .ofCandidates {
|
||||
return ctlInputMethod.isVerticalTypingSituation
|
||||
} else if state.type == ..ofAssociates {
|
||||
return ctlInputMethod.isVerticalTypingSituation
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ extension ctlInputMethod {
|
|||
var candidates: [(String, String)] = .init()
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
candidates = state.candidates
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
} else if let state = state as? InputState.Associates {
|
||||
candidates = state.candidates
|
||||
}
|
||||
if isTypingVertical { return true }
|
||||
|
@ -106,7 +106,7 @@ extension ctlInputMethod {
|
|||
let candidateKeys = mgrPrefs.candidateKeys
|
||||
let keyLabels =
|
||||
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
||||
let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : ""
|
||||
let keyLabelSuffix = state is InputState.Associates ? "^" : ""
|
||||
ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
||||
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ extension ctlInputMethod {
|
|||
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
cursor = state.cursorIndex
|
||||
if cursor == state.composingBuffer.count, cursor != 0 {
|
||||
if cursor == state.displayedText.count, cursor != 0 {
|
||||
cursor -= 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ extension ctlInputMethod {
|
|||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Empty:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.EmptyIgnoringPreviousState:
|
||||
case let newState as InputState.Abortion:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Committing:
|
||||
handle(state: newState, previous: prevState)
|
||||
|
@ -37,7 +37,7 @@ extension ctlInputMethod {
|
|||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.ChoosingCandidate:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.AssociatedPhrases:
|
||||
case let newState as InputState.Associates:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.SymbolTable:
|
||||
handle(state: newState, previous: prevState)
|
||||
|
@ -48,7 +48,7 @@ extension ctlInputMethod {
|
|||
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
||||
func setInlineDisplayWithCursor() {
|
||||
guard let client = client() else { return }
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
if let state = state as? InputState.Associates {
|
||||
client.setMarkedText(
|
||||
state.attributedString, selectionRange: NSRange(location: 0, length: 0),
|
||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
|
@ -87,7 +87,7 @@ extension ctlInputMethod {
|
|||
[.languageIdentifier: identifier],
|
||||
range: NSRange(
|
||||
location: 0,
|
||||
length: state.composingBuffer.utf16.count
|
||||
length: state.displayedText.utf16.count
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -141,9 +141,9 @@ extension ctlInputMethod {
|
|||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
// 全專案用以判斷「.EmptyIgnoringPreviousState」的地方僅此一處。
|
||||
// 全專案用以判斷「.Abortion」的地方僅此一處。
|
||||
if let previous = previous as? InputState.NotEmpty,
|
||||
!(state is InputState.EmptyIgnoringPreviousState)
|
||||
!(state is InputState.Abortion)
|
||||
{
|
||||
commit(text: previous.committingBufferConverted)
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ extension ctlInputMethod {
|
|||
}
|
||||
|
||||
private func handle(
|
||||
state: InputState.EmptyIgnoringPreviousState, previous: InputStateProtocol
|
||||
state: InputState.Abortion, previous: InputStateProtocol
|
||||
) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
|
@ -188,7 +188,7 @@ extension ctlInputMethod {
|
|||
setInlineDisplayWithCursor()
|
||||
if !state.tooltip.isEmpty {
|
||||
show(
|
||||
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
|
||||
tooltip: state.tooltip, displayedText: state.displayedText,
|
||||
cursorIndex: state.cursorIndex
|
||||
)
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ extension ctlInputMethod {
|
|||
ctlInputMethod.tooltipController.hide()
|
||||
} else {
|
||||
show(
|
||||
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
|
||||
tooltip: state.tooltip, displayedText: state.displayedText,
|
||||
cursorIndex: state.markerIndex
|
||||
)
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ extension ctlInputMethod {
|
|||
show(candidateWindowWith: state)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.AssociatedPhrases, previous: InputStateProtocol) {
|
||||
private func handle(state: InputState.Associates, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
setInlineDisplayWithCursor()
|
||||
|
|
|
@ -212,11 +212,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleSCPCTypingModeEnabled()
|
||||
+ mgrPrefs.toggleSCPCTypingModeEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -224,11 +222,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleChineseConversionEnabled()
|
||||
+ mgrPrefs.toggleChineseConversionEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -236,11 +232,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
|
||||
+ mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -248,11 +242,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleCurrencyNumeralsEnabled()
|
||||
+ mgrPrefs.toggleCurrencyNumeralsEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -260,11 +252,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleHalfWidthPunctuationEnabled()
|
||||
+ mgrPrefs.toggleHalfWidthPunctuationEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -272,11 +262,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleCNS11643Enabled()
|
||||
+ mgrPrefs.toggleCNS11643Enabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -284,11 +272,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleSymbolInputEnabled()
|
||||
+ mgrPrefs.toggleSymbolInputEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -296,11 +282,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.toggleAssociatedPhrasesEnabled()
|
||||
+ mgrPrefs.toggleAssociatedPhrasesEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -308,11 +292,9 @@ extension ctlInputMethod {
|
|||
resetKeyHandler()
|
||||
NotifierController.notify(
|
||||
message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n"
|
||||
+ {
|
||||
mgrPrefs.togglePhraseReplacementEnabled()
|
||||
+ mgrPrefs.togglePhraseReplacementEnabled()
|
||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -438,6 +438,7 @@ public enum mgrPrefs {
|
|||
|
||||
@UserDefault(key: UserDef.kTrimUnfinishedReadingsOnCommit.rawValue, defaultValue: true)
|
||||
static var trimUnfinishedReadingsOnCommit: Bool
|
||||
|
||||
// MARK: - Settings (Tier 2)
|
||||
|
||||
@UserDefault(key: UserDef.kUseIMKCandidateWindow.rawValue, defaultValue: false)
|
||||
|
@ -458,6 +459,8 @@ public enum mgrPrefs {
|
|||
@UserDefault(key: UserDef.kMaxCandidateLength.rawValue, defaultValue: 10)
|
||||
static var maxCandidateLength: Int
|
||||
|
||||
static var allowedMarkRange: ClosedRange<Int> = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength
|
||||
|
||||
@UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep.rawValue, defaultValue: true)
|
||||
static var shouldNotFartInLieuOfBeep: Bool
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
class SymbolNode {
|
||||
public class SymbolNode {
|
||||
var title: String
|
||||
var children: [SymbolNode]?
|
||||
var previous: SymbolNode?
|
||||
|
|
|
@ -117,6 +117,8 @@
|
|||
5BEDB724283B4C250078EB25 /* data-symbols.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71E283B4AEA0078EB25 /* data-symbols.plist */; };
|
||||
5BEDB725283B4C250078EB25 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; };
|
||||
5BF0B84C28C070B000795FC6 /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */; };
|
||||
5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF56F9728C39A2700DD6839 /* IMEState.swift */; };
|
||||
5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF56F9928C39D1800DD6839 /* IMEStateData.swift */; };
|
||||
5BF9DA2728840E6200DBD48E /* template-usersymbolphrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2228840E6200DBD48E /* template-usersymbolphrases.txt */; };
|
||||
5BF9DA2828840E6200DBD48E /* template-exclusions.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2328840E6200DBD48E /* template-exclusions.txt */; };
|
||||
5BF9DA2928840E6200DBD48E /* template-associatedPhrases-chs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2428840E6200DBD48E /* template-associatedPhrases-chs.txt */; };
|
||||
|
@ -346,6 +348,8 @@
|
|||
5BEDB720283B4AEA0078EB25 /* data-cht.plist */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "data-cht.plist"; path = "Data/data-cht.plist"; sourceTree = "<group>"; };
|
||||
5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = "<group>"; };
|
||||
5BF255CD28B2694E003ECB60 /* vChewing-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "vChewing-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
5BF56F9728C39A2700DD6839 /* IMEState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMEState.swift; sourceTree = "<group>"; };
|
||||
5BF56F9928C39D1800DD6839 /* IMEStateData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMEStateData.swift; sourceTree = "<group>"; };
|
||||
5BF9DA2228840E6200DBD48E /* template-usersymbolphrases.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-usersymbolphrases.txt"; sourceTree = "<group>"; usesTabs = 0; };
|
||||
5BF9DA2328840E6200DBD48E /* template-exclusions.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-exclusions.txt"; sourceTree = "<group>"; usesTabs = 0; };
|
||||
5BF9DA2428840E6200DBD48E /* template-associatedPhrases-chs.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; name = "template-associatedPhrases-chs.txt"; path = "../Data/components/chs/template-associatedPhrases-chs.txt"; sourceTree = "<group>"; usesTabs = 0; };
|
||||
|
@ -498,6 +502,8 @@
|
|||
5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */,
|
||||
5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */,
|
||||
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
||||
5BF56F9728C39A2700DD6839 /* IMEState.swift */,
|
||||
5BF56F9928C39D1800DD6839 /* IMEStateData.swift */,
|
||||
D461B791279DAC010070E734 /* InputState.swift */,
|
||||
5BD0113C2818543900609769 /* KeyHandler_Core.swift */,
|
||||
5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */,
|
||||
|
@ -1206,6 +1212,7 @@
|
|||
5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */,
|
||||
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,
|
||||
D461B792279DAC010070E734 /* InputState.swift in Sources */,
|
||||
5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */,
|
||||
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */,
|
||||
D47B92C027972AD100458394 /* main.swift in Sources */,
|
||||
D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */,
|
||||
|
@ -1232,6 +1239,7 @@
|
|||
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */,
|
||||
5BD0113D2818543900609769 /* KeyHandler_Core.swift in Sources */,
|
||||
5B2170E4289FACAD00BE7304 /* 2_Walker.swift in Sources */,
|
||||
5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */,
|
||||
5BA9FD4227FEF3C8002DE248 /* PreferencePane.swift in Sources */,
|
||||
5BA0DF312817857D009E73BB /* lmUserOverride.swift in Sources */,
|
||||
5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue