diff --git a/Packages/vChewing_Shared/Sources/Shared/Shared.swift b/Packages/vChewing_Shared/Sources/Shared/Shared.swift index 7ff0876b..910f841d 100644 --- a/Packages/vChewing_Shared/Sources/Shared/Shared.swift +++ b/Packages/vChewing_Shared/Sources/Shared/Shared.swift @@ -123,17 +123,33 @@ public enum TooltipColorState { // MARK: - IMEState types. -// 用以讓每個狀態自描述的 enum。 +/// 用以讓每個狀態自描述的 enum。 public enum StateType: String { + /// **失活狀態 .ofDeactivated**: 使用者沒在使用輸入法、或者使用者已經切換到另一個客體應用來敲字。 case ofDeactivated = "Deactivated" + /// **空狀態 .ofEmpty**: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。 + /// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。 + /// 威注音輸入法在「組字區與組音區/組筆區同時為空」、 + /// 且客體軟體正在準備接收使用者文字輸入行為的時候,會處於空狀態。 + /// 有時,威注音會利用呼叫空狀態的方式,讓組字區內已經顯示出來的內容遞交出去。 case ofEmpty = "Empty" - case ofAbortion = "Abortion" // 該狀態會自動轉為 Empty + /// **中絕狀態 .ofAbortion**: 與 .ofEmpty() 類似,但會扔掉上一個狀態的內容、 + /// 不將這些內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .ofEmpty()。 + case ofAbortion = "Abortion" + /// **遞交狀態 .ofCommitting**: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 + /// 該狀態在處理完畢之後會被立刻切換至 .ofEmpty()。如果直接呼叫處理該狀態的話, + /// 在呼叫處理之前的組字區的內容會消失,除非你事先呼叫處理過 .ofEmpty()。 case ofCommitting = "Committing" + /// **聯想詞狀態 .ofAssociates**: 逐字選字模式內的聯想詞輸入狀態。 case ofAssociates = "Associates" - case ofNotEmpty = "NotEmpty" + /// **輸入狀態 .ofInputting**: 使用者輸入了內容。此時會出現組字區(Compositor)。 case ofInputting = "Inputting" + /// **標記狀態 .ofMarking**: 使用者在組字區內標記某段範圍, + /// 可以決定是添入新詞、還是將這個範圍的詞音組合放入語彙濾除清單。 case ofMarking = "Marking" + /// **選字狀態 .ofCandidates**: 叫出選字窗、允許使用者選字。 case ofCandidates = "Candidates" + /// **分類分層符號表狀態 .ofSymbolTable**: 分類分層符號表選單專用的狀態,有自身的特殊處理。 case ofSymbolTable = "SymbolTable" } diff --git a/Source/Modules/IMEState.swift b/Source/Modules/IMEState.swift index c55b71a9..289cc483 100644 --- a/Source/Modules/IMEState.swift +++ b/Source/Modules/IMEState.swift @@ -26,20 +26,23 @@ import Shared /// /// 輸入法控制器持下述狀態: /// -/// - .Deactivated: 使用者沒在使用輸入法。 -/// - .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。因為逐字選字模式不需要在 -/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 -/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給 -/// 客體應用、準備新的輸入行為。 -/// - .Abortion: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些 -/// 內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .Empty()。 -/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 -/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。 -/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 -/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、還是將這個範圍的 -/// 詞音組合放入語彙濾除清單。 -/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。 -/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 +/// - **失活狀態 .ofDeactivated**: 使用者沒在使用輸入法、或者使用者已經切換到另一個客體應用來敲字。 +/// - **空狀態 .ofEmpty**: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。 +/// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。 +/// 威注音輸入法在「組字區與組音區/組筆區同時為空」、 +/// 且客體軟體正在準備接收使用者文字輸入行為的時候,會處於空狀態。 +/// 有時,威注音會利用呼叫空狀態的方式,讓組字區內已經顯示出來的內容遞交出去。 +/// - **聯想詞狀態 .ofAssociates**: 逐字選字模式內的聯想詞輸入狀態。 +/// - **中絕狀態 .ofAbortion**: 與 .ofEmpty() 類似,但會扔掉上一個狀態的內容、 +/// 不將這些內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .ofEmpty()。 +/// - **遞交狀態 .ofCommitting**: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 +/// 該狀態在處理完畢之後會被立刻切換至 .ofEmpty()。如果直接呼叫處理該狀態的話, +/// 在呼叫處理之前的組字區的內容會消失,除非你事先呼叫處理過 .ofEmpty()。 +/// - **輸入狀態 .ofInputting**: 使用者輸入了內容。此時會出現組字區(Compositor)。 +/// - **標記狀態 .ofMarking**: 使用者在組字區內標記某段範圍, +/// 可以決定是添入新詞、還是將這個範圍的詞音組合放入語彙濾除清單。 +/// - **選字狀態 .ofCandidates**: 叫出選字窗、允許使用者選字。 +/// - **分類分層符號表狀態 .ofSymbolTable**: 分類分層符號表選單專用的狀態,有自身的特殊處理。 public struct IMEState: IMEStateProtocol { public var type: StateType = .ofEmpty public var data: IMEStateDataProtocol = IMEStateData() as IMEStateDataProtocol @@ -52,6 +55,28 @@ public struct IMEState: IMEStateProtocol { isVerticalTyping = SessionCtl.isVerticalTyping } + /// 內部專用初期化函式,僅用於生成「有輸入內容」的狀態。 + /// - Parameters: + /// - displayTextSegments: 用以顯示的文本的字詞字串陣列,其中包含正在輸入的讀音或字根。 + /// - cursor: 要顯示的游標(UTF8)。 + fileprivate init(displayTextSegments: [String], cursor: Int) { + // 注意資料的設定順序,一定得先設定 displayTextSegments。 + data.displayTextSegments = displayTextSegments.map { + if !SessionCtl.isVerticalTyping { return $0 } + guard PrefMgr.shared.hardenVerticalPunctuations else { return $0 } + var neta = $0 + ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: SessionCtl.isVerticalTyping) + return neta + } + data.cursor = cursor + data.marker = cursor + } + + /// 泛用初期化函式。 + /// - Parameters: + /// - data: 資料載體。 + /// - type: 狀態類型。 + /// - node: 節點。 init( _ data: IMEStateDataProtocol = IMEStateData() as IMEStateDataProtocol, type: StateType = .ofEmpty, node: CandidateNode @@ -88,24 +113,8 @@ extension IMEState { return result } - public static func ofNotEmpty(displayTextSegments: [String], cursor: Int) -> IMEState { - var result = IMEState(type: .ofNotEmpty) - // 注意資料的設定順序,一定得先設定 displayTextSegments。 - result.data.displayTextSegments = displayTextSegments.map { - if !SessionCtl.isVerticalTyping { return $0 } - guard PrefMgr.shared.hardenVerticalPunctuations else { return $0 } - var neta = $0 - ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: SessionCtl.isVerticalTyping) - return neta - } - - result.data.cursor = cursor - result.data.marker = cursor - return result - } - public static func ofInputting(displayTextSegments: [String], cursor: Int) -> IMEState { - var result = Self.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) + var result = IMEState(displayTextSegments: displayTextSegments, cursor: cursor) result.type = .ofInputting return result } @@ -115,7 +124,7 @@ extension IMEState { ) -> IMEState { - var result = Self.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) + var result = IMEState(displayTextSegments: displayTextSegments, cursor: cursor) result.type = .ofMarking result.data.marker = marker result.data.markedReadings = markedReadings @@ -126,14 +135,14 @@ extension IMEState { public static func ofCandidates(candidates: [(String, String)], displayTextSegments: [String], cursor: Int) -> IMEState { - var result = Self.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) + var result = IMEState(displayTextSegments: displayTextSegments, cursor: cursor) result.type = .ofCandidates result.data.candidates = candidates return result } public static func ofSymbolTable(node: CandidateNode) -> IMEState { - var result = IMEState(type: .ofNotEmpty, node: node) + var result = IMEState(node: node) result.type = .ofSymbolTable return result } @@ -195,7 +204,7 @@ extension IMEState { /// 該參數僅用作輔助判斷。在 InputHandler 內使用的話,必須再檢查 !compositor.isEmpty。 public var hasComposition: Bool { switch type { - case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true + case .ofInputting, .ofMarking, .ofCandidates: return true default: return false } } diff --git a/Source/Modules/SessionCtl_HandleStates.swift b/Source/Modules/SessionCtl_HandleStates.swift index 739841d7..7bcd022e 100644 --- a/Source/Modules/SessionCtl_HandleStates.swift +++ b/Source/Modules/SessionCtl_HandleStates.swift @@ -95,7 +95,6 @@ extension SessionCtl { tooltipInstance.hide() setInlineDisplayWithCursor() showCandidates() - default: break } // 浮動組字窗的顯示判定 if newState.hasComposition, PrefMgr.shared.clientsIMKTextInputIncapable.contains(clientBundleIdentifier) { @@ -108,33 +107,18 @@ extension SessionCtl { } } - /// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。 + /// 如果當前狀態含有「組字結果內容」、或者有選字窗內容、或者存在正在輸入的字根/讀音,則在組字區內顯示游標。 public func setInlineDisplayWithCursor() { - if state.type == .ofAssociates { - doSetMarkedText( - state.data.attributedStringPlaceholder, selectionRange: NSRange(location: 0, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) - return - } - - if state.hasComposition || state.isCandidateContainer { - /// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度 - /// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。 - /// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。 - doSetMarkedText( - attributedStringSecured.0, selectionRange: attributedStringSecured.1, - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) - return - } - - // 其它情形。 - clearInlineDisplay() + /// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度 + /// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。 + /// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。 + doSetMarkedText( + attributedStringSecured.0, selectionRange: attributedStringSecured.1, + replacementRange: NSRange(location: NSNotFound, length: NSNotFound) + ) } - /// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。 - /// 當 setInlineDisplayWithCursor() 在錯誤的狀態下被呼叫時,也會觸發這個函式。 + /// 在處理某些「沒有組字區內容顯示」且「不需要攔截某些按鍵處理」的狀態時使用的函式,會清空螢幕上顯示的組字區。 private func clearInlineDisplay() { doSetMarkedText( "", selectionRange: NSRange(location: 0, length: 0),