ctlIME // Tear ctlInputMethod into parts.
This commit is contained in:
parent
38b0478d39
commit
42cc923425
|
@ -41,7 +41,7 @@ class ctlInputMethod: IMKInputController {
|
|||
static var areWeDeleting = false
|
||||
|
||||
/// 目前在用的的選字窗副本。
|
||||
private var ctlCandidateCurrent = ctlCandidateUniversal.init(.horizontal)
|
||||
var ctlCandidateCurrent = ctlCandidateUniversal.init(.horizontal)
|
||||
|
||||
/// 工具提示視窗的副本。
|
||||
static let tooltipController = TooltipController()
|
||||
|
@ -49,9 +49,9 @@ class ctlInputMethod: IMKInputController {
|
|||
// MARK: -
|
||||
|
||||
/// 按鍵調度模組的副本。
|
||||
private var keyHandler: KeyHandler = .init()
|
||||
var keyHandler: KeyHandler = .init()
|
||||
/// 用以記錄當前輸入法狀態的變數。
|
||||
private var state: InputStateProtocol = InputState.Empty()
|
||||
var state: InputStateProtocol = InputState.Empty()
|
||||
|
||||
// MARK: - 工具函式
|
||||
|
||||
|
@ -233,474 +233,3 @@ class ctlInputMethod: IMKInputController {
|
|||
resetKeyHandler()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 狀態調度 (State Handling)
|
||||
|
||||
extension ctlInputMethod {
|
||||
/// 針對傳入的新狀態進行調度。
|
||||
///
|
||||
/// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數,
|
||||
/// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。
|
||||
/// - Parameter newState: 新狀態。
|
||||
private func handle(state newState: InputStateProtocol) {
|
||||
let prevState = state
|
||||
state = newState
|
||||
|
||||
switch newState {
|
||||
case let newState as InputState.Deactivated:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Empty:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.EmptyIgnoringPreviousState:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Committing:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Inputting:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Marking:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.ChoosingCandidate:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.AssociatedPhrases:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.SymbolTable:
|
||||
handle(state: newState, previous: prevState)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
||||
private func setInlineDisplayWithCursor() {
|
||||
guard let state = state as? InputState.NotEmpty else {
|
||||
clearInlineDisplay()
|
||||
return
|
||||
}
|
||||
|
||||
var identifier: AnyObject {
|
||||
switch IME.currentInputMode {
|
||||
case InputMode.imeModeCHS:
|
||||
if #available(macOS 12.0, *) {
|
||||
return "zh-Hans" as AnyObject
|
||||
}
|
||||
case InputMode.imeModeCHT:
|
||||
if #available(macOS 12.0, *) {
|
||||
return (mgrPrefs.shiftJISShinjitaiOutputEnabled || mgrPrefs.chineseConversionEnabled)
|
||||
? "ja" as AnyObject : "zh-Hant" as AnyObject
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return "" as AnyObject
|
||||
}
|
||||
|
||||
// [Shiki's Note] This might needs to be bug-reported to Apple:
|
||||
// The LanguageIdentifier attribute of an NSAttributeString designated to
|
||||
// IMK Client().SetMarkedText won't let the actual font respect your languageIdentifier
|
||||
// settings. Still, this might behaves as Apple's current expectation, I'm afraid.
|
||||
if #available(macOS 12.0, *) {
|
||||
state.attributedString.setAttributes(
|
||||
[.languageIdentifier: identifier],
|
||||
range: NSRange(
|
||||
location: 0,
|
||||
length: state.composingBuffer.utf16.count
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度
|
||||
/// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
||||
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
||||
client().setMarkedText(
|
||||
state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0),
|
||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
/// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。
|
||||
/// 當 setInlineDisplayWithCursor() 在錯誤的狀態下被呼叫時,也會觸發這個函式。
|
||||
private func clearInlineDisplay() {
|
||||
client().setMarkedText(
|
||||
"", selectionRange: NSRange(location: 0, length: 0),
|
||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
/// 遞交組字區內容。
|
||||
/// 注意:必須在 IMK 的 commitComposition 函式當中也間接或者直接執行這個處理。
|
||||
private func commit(text: String) {
|
||||
let buffer = IME.kanjiConversionIfRequired(text)
|
||||
if buffer.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
client().insertText(
|
||||
buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Deactivated, previous: InputStateProtocol) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.delegate = nil
|
||||
ctlCandidateCurrent.visible = false
|
||||
hideTooltip()
|
||||
if let previous = previous as? InputState.NotEmpty {
|
||||
commit(text: previous.composingBuffer)
|
||||
}
|
||||
clearInlineDisplay()
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Empty, previous: InputStateProtocol) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
hideTooltip()
|
||||
// 全專案用以判斷「.EmptyIgnoringPreviousState」的地方僅此一處。
|
||||
if let previous = previous as? InputState.NotEmpty,
|
||||
!(state is InputState.EmptyIgnoringPreviousState)
|
||||
{
|
||||
commit(text: previous.composingBuffer)
|
||||
}
|
||||
clearInlineDisplay()
|
||||
}
|
||||
|
||||
private func handle(
|
||||
state: InputState.EmptyIgnoringPreviousState, previous: InputStateProtocol
|
||||
) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
// 這個函式就是去掉 previous state 使得沒有任何東西可以 commit。
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Committing, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
hideTooltip()
|
||||
let textToCommit = state.textToCommit
|
||||
if !textToCommit.isEmpty {
|
||||
commit(text: textToCommit)
|
||||
}
|
||||
clearInlineDisplay()
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Inputting, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
hideTooltip()
|
||||
let textToCommit = state.textToCommit
|
||||
if !textToCommit.isEmpty {
|
||||
commit(text: textToCommit)
|
||||
}
|
||||
setInlineDisplayWithCursor()
|
||||
if !state.tooltip.isEmpty {
|
||||
show(
|
||||
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
|
||||
cursorIndex: state.cursorIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Marking, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
setInlineDisplayWithCursor()
|
||||
if state.tooltip.isEmpty {
|
||||
hideTooltip()
|
||||
} else {
|
||||
show(
|
||||
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
|
||||
cursorIndex: state.markerIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(state: InputState.ChoosingCandidate, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
hideTooltip()
|
||||
setInlineDisplayWithCursor()
|
||||
show(candidateWindowWith: state)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.SymbolTable, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
hideTooltip()
|
||||
setInlineDisplayWithCursor()
|
||||
show(candidateWindowWith: state)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.AssociatedPhrases, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
hideTooltip()
|
||||
clearInlineDisplay()
|
||||
show(candidateWindowWith: state)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ctlInputMethod {
|
||||
private func show(candidateWindowWith state: InputStateProtocol) {
|
||||
var isTypingVertical: Bool {
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.isTypingVertical
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.isTypingVertical
|
||||
}
|
||||
return false
|
||||
}
|
||||
var isCandidateWindowVertical: Bool {
|
||||
var candidates: [String] = []
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
candidates = state.candidates
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
candidates = state.candidates
|
||||
}
|
||||
if isTypingVertical { return true }
|
||||
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
||||
candidates.sort {
|
||||
$0.count > $1.count
|
||||
}
|
||||
// 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。
|
||||
// 範例:「屬實牛逼」(會有一大串各種各樣的「鼠食牛Beer」的 emoji)。
|
||||
let maxCandidatesPerPage = mgrPrefs.candidateKeys.count
|
||||
let firstPageCandidates = candidates[0..<min(maxCandidatesPerPage, candidates.count)]
|
||||
return firstPageCandidates.joined().count > Int(round(Double(maxCandidatesPerPage) * 1.8))
|
||||
// 上面這句如果是 true 的話,就會是縱排;反之則為橫排。
|
||||
}
|
||||
|
||||
ctlCandidateCurrent.delegate = nil
|
||||
|
||||
/// 下面這一段本可直接指定 currentLayout,但這樣的話翻頁按鈕位置無法精準地重新繪製。
|
||||
/// 所以只能重新初期化。壞處就是得在 ctlCandidate() 當中與 SymbolTable 控制有關的地方
|
||||
/// 新增一個空狀態請求、防止縱排與橫排選字窗同時出現。
|
||||
/// layoutCandidateView 在這裡無法起到糾正作用。
|
||||
/// 該問題徹底解決的價值並不大,直接等到 macOS 10.x 全線淘汰之後用 SwiftUI 重寫選字窗吧。
|
||||
|
||||
if isCandidateWindowVertical { // 縱排輸入時強制使用縱排選字窗
|
||||
ctlCandidateCurrent = .init(.vertical)
|
||||
} else if mgrPrefs.useHorizontalCandidateList {
|
||||
ctlCandidateCurrent = .init(.horizontal)
|
||||
} else {
|
||||
ctlCandidateCurrent = .init(.vertical)
|
||||
}
|
||||
|
||||
// set the attributes for the candidate panel (which uses NSAttributedString)
|
||||
let textSize = mgrPrefs.candidateListTextSize
|
||||
let keyLabelSize = max(textSize / 2, mgrPrefs.minKeyLabelSize)
|
||||
|
||||
func labelFont(name: String?, size: CGFloat) -> NSFont {
|
||||
if let name = name {
|
||||
return NSFont(name: name, size: size) ?? NSFont.systemFont(ofSize: size)
|
||||
}
|
||||
return NSFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
func candidateFont(name: String?, size: CGFloat) -> NSFont {
|
||||
let currentMUIFont =
|
||||
(keyHandler.inputMode == InputMode.imeModeCHS)
|
||||
? "Sarasa Term Slab SC" : "Sarasa Term Slab TC"
|
||||
var finalReturnFont =
|
||||
NSFont(name: currentMUIFont, size: size) ?? NSFont.systemFont(ofSize: size)
|
||||
// 對更紗黑體的依賴到 macOS 11 Big Sur 為止。macOS 12 Monterey 開始則依賴系統內建的函式使用蘋方來處理。
|
||||
if #available(macOS 12.0, *) { finalReturnFont = NSFont.systemFont(ofSize: size) }
|
||||
if let name = name {
|
||||
return NSFont(name: name, size: size) ?? finalReturnFont
|
||||
}
|
||||
return finalReturnFont
|
||||
}
|
||||
|
||||
ctlCandidateCurrent.keyLabelFont = labelFont(
|
||||
name: mgrPrefs.candidateKeyLabelFontName, size: keyLabelSize
|
||||
)
|
||||
ctlCandidateCurrent.candidateFont = candidateFont(
|
||||
name: mgrPrefs.candidateTextFontName, size: textSize
|
||||
)
|
||||
|
||||
let candidateKeys = mgrPrefs.candidateKeys
|
||||
let keyLabels =
|
||||
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
||||
let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : ""
|
||||
ctlCandidateCurrent.keyLabels = keyLabels.map {
|
||||
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
||||
}
|
||||
|
||||
ctlCandidateCurrent.delegate = self
|
||||
ctlCandidateCurrent.reloadData()
|
||||
|
||||
ctlCandidateCurrent.visible = true
|
||||
|
||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||
var cursor = 0
|
||||
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
cursor = state.cursorIndex
|
||||
if cursor == state.composingBuffer.count, cursor != 0 {
|
||||
cursor -= 1
|
||||
}
|
||||
}
|
||||
|
||||
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
||||
client().attributes(
|
||||
forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect
|
||||
)
|
||||
cursor -= 1
|
||||
}
|
||||
|
||||
if isTypingVertical {
|
||||
ctlCandidateCurrent.set(
|
||||
windowTopLeftPoint: NSPoint(
|
||||
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0
|
||||
),
|
||||
bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0
|
||||
)
|
||||
} else {
|
||||
ctlCandidateCurrent.set(
|
||||
windowTopLeftPoint: NSPoint(x: lineHeightRect.origin.x, y: lineHeightRect.origin.y - 4.0),
|
||||
bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func show(tooltip: String, composingBuffer: String, cursorIndex: Int) {
|
||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||
var cursor = cursorIndex
|
||||
if cursor == composingBuffer.count, cursor != 0 {
|
||||
cursor -= 1
|
||||
}
|
||||
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
||||
client().attributes(
|
||||
forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect
|
||||
)
|
||||
cursor -= 1
|
||||
}
|
||||
ctlInputMethod.tooltipController.show(tooltip: tooltip, at: lineHeightRect.origin)
|
||||
}
|
||||
|
||||
private func hideTooltip() {
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ctlInputMethod: KeyHandlerDelegate {
|
||||
func ctlCandidate() -> ctlCandidate { ctlCandidateCurrent }
|
||||
|
||||
func keyHandler(
|
||||
_: KeyHandler, didSelectCandidateAt index: Int,
|
||||
ctlCandidate controller: ctlCandidate
|
||||
) {
|
||||
ctlCandidate(controller, didSelectCandidateAtIndex: index)
|
||||
}
|
||||
|
||||
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol)
|
||||
-> Bool
|
||||
{
|
||||
guard let state = state as? InputState.Marking else {
|
||||
return false
|
||||
}
|
||||
if !state.validToWrite {
|
||||
return false
|
||||
}
|
||||
let refInputModeReversed: InputMode =
|
||||
(keyHandler.inputMode == InputMode.imeModeCHT)
|
||||
? InputMode.imeModeCHS : InputMode.imeModeCHT
|
||||
if !mgrLangModel.writeUserPhrase(
|
||||
state.userPhrase, inputMode: keyHandler.inputMode,
|
||||
areWeDuplicating: state.chkIfUserPhraseExists,
|
||||
areWeDeleting: ctlInputMethod.areWeDeleting
|
||||
)
|
||||
|| !mgrLangModel.writeUserPhrase(
|
||||
state.userPhraseConverted, inputMode: refInputModeReversed,
|
||||
areWeDuplicating: false,
|
||||
areWeDeleting: ctlInputMethod.areWeDeleting
|
||||
)
|
||||
{
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ctlInputMethod: ctlCandidateDelegate {
|
||||
func candidateCountForController(_ controller: ctlCandidate) -> Int {
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates.count
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.candidates.count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ctlCandidate(_ controller: ctlCandidate, candidateAtIndex index: Int)
|
||||
-> String
|
||||
{
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates[index]
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.candidates[index]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ctlCandidate(_ controller: ctlCandidate, didSelectCandidateAtIndex index: Int) {
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
|
||||
if let state = state as? InputState.SymbolTable,
|
||||
let node = state.node.children?[index]
|
||||
{
|
||||
if let children = node.children, !children.isEmpty {
|
||||
handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現
|
||||
handle(
|
||||
state: InputState.SymbolTable(node: node, isTypingVertical: state.isTypingVertical)
|
||||
)
|
||||
} else {
|
||||
handle(state: InputState.Committing(textToCommit: node.title))
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
let selectedValue = state.candidates[index]
|
||||
keyHandler.fixNode(value: selectedValue, respectCursorPushing: true)
|
||||
|
||||
let inputting = keyHandler.buildInputtingState
|
||||
|
||||
if mgrPrefs.useSCPCTypingMode {
|
||||
keyHandler.clear()
|
||||
let composingBuffer = inputting.composingBuffer
|
||||
handle(state: InputState.Committing(textToCommit: composingBuffer))
|
||||
if mgrPrefs.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
||||
withKey: composingBuffer, isTypingVertical: state.isTypingVertical
|
||||
), !associatePhrases.candidates.isEmpty
|
||||
{
|
||||
handle(state: associatePhrases)
|
||||
} else {
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
} else {
|
||||
handle(state: inputting)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
let selectedValue = state.candidates[index]
|
||||
handle(state: InputState.Committing(textToCommit: selectedValue))
|
||||
if mgrPrefs.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
||||
withKey: selectedValue, isTypingVertical: state.isTypingVertical
|
||||
), !associatePhrases.candidates.isEmpty
|
||||
{
|
||||
handle(state: associatePhrases)
|
||||
} else {
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
|
||||
// All possible vChewing-specific modifications are of:
|
||||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
/*
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
1. The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
2. 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 above.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - KeyHandler Delegate
|
||||
|
||||
extension ctlInputMethod: KeyHandlerDelegate {
|
||||
func ctlCandidate() -> ctlCandidate { ctlCandidateCurrent }
|
||||
|
||||
func keyHandler(
|
||||
_: KeyHandler, didSelectCandidateAt index: Int,
|
||||
ctlCandidate controller: ctlCandidate
|
||||
) {
|
||||
ctlCandidate(controller, didSelectCandidateAtIndex: index)
|
||||
}
|
||||
|
||||
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol)
|
||||
-> Bool
|
||||
{
|
||||
guard let state = state as? InputState.Marking else {
|
||||
return false
|
||||
}
|
||||
if !state.validToWrite {
|
||||
return false
|
||||
}
|
||||
let refInputModeReversed: InputMode =
|
||||
(keyHandler.inputMode == InputMode.imeModeCHT)
|
||||
? InputMode.imeModeCHS : InputMode.imeModeCHT
|
||||
if !mgrLangModel.writeUserPhrase(
|
||||
state.userPhrase, inputMode: keyHandler.inputMode,
|
||||
areWeDuplicating: state.chkIfUserPhraseExists,
|
||||
areWeDeleting: ctlInputMethod.areWeDeleting
|
||||
)
|
||||
|| !mgrLangModel.writeUserPhrase(
|
||||
state.userPhraseConverted, inputMode: refInputModeReversed,
|
||||
areWeDuplicating: false,
|
||||
areWeDeleting: ctlInputMethod.areWeDeleting
|
||||
)
|
||||
{
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Candidate Controller Delegate
|
||||
|
||||
extension ctlInputMethod: ctlCandidateDelegate {
|
||||
func candidateCountForController(_ controller: ctlCandidate) -> Int {
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates.count
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.candidates.count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func ctlCandidate(_ controller: ctlCandidate, candidateAtIndex index: Int)
|
||||
-> String
|
||||
{
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.candidates[index]
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.candidates[index]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ctlCandidate(_ controller: ctlCandidate, didSelectCandidateAtIndex index: Int) {
|
||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||
|
||||
if let state = state as? InputState.SymbolTable,
|
||||
let node = state.node.children?[index]
|
||||
{
|
||||
if let children = node.children, !children.isEmpty {
|
||||
handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現
|
||||
handle(
|
||||
state: InputState.SymbolTable(node: node, isTypingVertical: state.isTypingVertical)
|
||||
)
|
||||
} else {
|
||||
handle(state: InputState.Committing(textToCommit: node.title))
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
let selectedValue = state.candidates[index]
|
||||
keyHandler.fixNode(value: selectedValue, respectCursorPushing: true)
|
||||
|
||||
let inputting = keyHandler.buildInputtingState
|
||||
|
||||
if mgrPrefs.useSCPCTypingMode {
|
||||
keyHandler.clear()
|
||||
let composingBuffer = inputting.composingBuffer
|
||||
handle(state: InputState.Committing(textToCommit: composingBuffer))
|
||||
if mgrPrefs.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
||||
withKey: composingBuffer, isTypingVertical: state.isTypingVertical
|
||||
), !associatePhrases.candidates.isEmpty
|
||||
{
|
||||
handle(state: associatePhrases)
|
||||
} else {
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
} else {
|
||||
handle(state: inputting)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let state = state as? InputState.AssociatedPhrases {
|
||||
let selectedValue = state.candidates[index]
|
||||
handle(state: InputState.Committing(textToCommit: selectedValue))
|
||||
if mgrPrefs.associatedPhrasesEnabled,
|
||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
||||
withKey: selectedValue, isTypingVertical: state.isTypingVertical
|
||||
), !associatePhrases.candidates.isEmpty
|
||||
{
|
||||
handle(state: associatePhrases)
|
||||
} else {
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
|
||||
// All possible vChewing-specific modifications are of:
|
||||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
/*
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
1. The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
2. 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 above.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
// MARK: - Tooltip Display and Candidate Display Methods
|
||||
|
||||
extension ctlInputMethod {
|
||||
func show(tooltip: String, composingBuffer: String, cursorIndex: Int) {
|
||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||
var cursor = cursorIndex
|
||||
if cursor == composingBuffer.count, cursor != 0 {
|
||||
cursor -= 1
|
||||
}
|
||||
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
||||
client().attributes(
|
||||
forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect
|
||||
)
|
||||
cursor -= 1
|
||||
}
|
||||
ctlInputMethod.tooltipController.show(tooltip: tooltip, at: lineHeightRect.origin)
|
||||
}
|
||||
|
||||
func show(candidateWindowWith state: InputStateProtocol) {
|
||||
var isTypingVertical: Bool {
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
return state.isTypingVertical
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
return state.isTypingVertical
|
||||
}
|
||||
return false
|
||||
}
|
||||
var isCandidateWindowVertical: Bool {
|
||||
var candidates: [String] = []
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
candidates = state.candidates
|
||||
} else if let state = state as? InputState.AssociatedPhrases {
|
||||
candidates = state.candidates
|
||||
}
|
||||
if isTypingVertical { return true }
|
||||
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
||||
candidates.sort {
|
||||
$0.count > $1.count
|
||||
}
|
||||
// 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。
|
||||
// 範例:「屬實牛逼」(會有一大串各種各樣的「鼠食牛Beer」的 emoji)。
|
||||
let maxCandidatesPerPage = mgrPrefs.candidateKeys.count
|
||||
let firstPageCandidates = candidates[0..<min(maxCandidatesPerPage, candidates.count)]
|
||||
return firstPageCandidates.joined().count > Int(round(Double(maxCandidatesPerPage) * 1.8))
|
||||
// 上面這句如果是 true 的話,就會是縱排;反之則為橫排。
|
||||
}
|
||||
|
||||
ctlCandidateCurrent.delegate = nil
|
||||
|
||||
/// 下面這一段本可直接指定 currentLayout,但這樣的話翻頁按鈕位置無法精準地重新繪製。
|
||||
/// 所以只能重新初期化。壞處就是得在 ctlCandidate() 當中與 SymbolTable 控制有關的地方
|
||||
/// 新增一個空狀態請求、防止縱排與橫排選字窗同時出現。
|
||||
/// layoutCandidateView 在這裡無法起到糾正作用。
|
||||
/// 該問題徹底解決的價值並不大,直接等到 macOS 10.x 全線淘汰之後用 SwiftUI 重寫選字窗吧。
|
||||
|
||||
if isCandidateWindowVertical { // 縱排輸入時強制使用縱排選字窗
|
||||
ctlCandidateCurrent = .init(.vertical)
|
||||
} else if mgrPrefs.useHorizontalCandidateList {
|
||||
ctlCandidateCurrent = .init(.horizontal)
|
||||
} else {
|
||||
ctlCandidateCurrent = .init(.vertical)
|
||||
}
|
||||
|
||||
// set the attributes for the candidate panel (which uses NSAttributedString)
|
||||
let textSize = mgrPrefs.candidateListTextSize
|
||||
let keyLabelSize = max(textSize / 2, mgrPrefs.minKeyLabelSize)
|
||||
|
||||
func labelFont(name: String?, size: CGFloat) -> NSFont {
|
||||
if let name = name {
|
||||
return NSFont(name: name, size: size) ?? NSFont.systemFont(ofSize: size)
|
||||
}
|
||||
return NSFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
func candidateFont(name: String?, size: CGFloat) -> NSFont {
|
||||
let currentMUIFont =
|
||||
(keyHandler.inputMode == InputMode.imeModeCHS)
|
||||
? "Sarasa Term Slab SC" : "Sarasa Term Slab TC"
|
||||
var finalReturnFont =
|
||||
NSFont(name: currentMUIFont, size: size) ?? NSFont.systemFont(ofSize: size)
|
||||
// 對更紗黑體的依賴到 macOS 11 Big Sur 為止。macOS 12 Monterey 開始則依賴系統內建的函式使用蘋方來處理。
|
||||
if #available(macOS 12.0, *) { finalReturnFont = NSFont.systemFont(ofSize: size) }
|
||||
if let name = name {
|
||||
return NSFont(name: name, size: size) ?? finalReturnFont
|
||||
}
|
||||
return finalReturnFont
|
||||
}
|
||||
|
||||
ctlCandidateCurrent.keyLabelFont = labelFont(
|
||||
name: mgrPrefs.candidateKeyLabelFontName, size: keyLabelSize
|
||||
)
|
||||
ctlCandidateCurrent.candidateFont = candidateFont(
|
||||
name: mgrPrefs.candidateTextFontName, size: textSize
|
||||
)
|
||||
|
||||
let candidateKeys = mgrPrefs.candidateKeys
|
||||
let keyLabels =
|
||||
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
||||
let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : ""
|
||||
ctlCandidateCurrent.keyLabels = keyLabels.map {
|
||||
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
||||
}
|
||||
|
||||
ctlCandidateCurrent.delegate = self
|
||||
ctlCandidateCurrent.reloadData()
|
||||
|
||||
ctlCandidateCurrent.visible = true
|
||||
|
||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||
var cursor = 0
|
||||
|
||||
if let state = state as? InputState.ChoosingCandidate {
|
||||
cursor = state.cursorIndex
|
||||
if cursor == state.composingBuffer.count, cursor != 0 {
|
||||
cursor -= 1
|
||||
}
|
||||
}
|
||||
|
||||
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
||||
client().attributes(
|
||||
forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect
|
||||
)
|
||||
cursor -= 1
|
||||
}
|
||||
|
||||
if isTypingVertical {
|
||||
ctlCandidateCurrent.set(
|
||||
windowTopLeftPoint: NSPoint(
|
||||
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0
|
||||
),
|
||||
bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0
|
||||
)
|
||||
} else {
|
||||
ctlCandidateCurrent.set(
|
||||
windowTopLeftPoint: NSPoint(x: lineHeightRect.origin.x, y: lineHeightRect.origin.y - 4.0),
|
||||
bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
|
||||
// All possible vChewing-specific modifications are of:
|
||||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
/*
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
1. The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
2. 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 above.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - 狀態調度 (State Handling)
|
||||
|
||||
extension ctlInputMethod {
|
||||
/// 針對傳入的新狀態進行調度。
|
||||
///
|
||||
/// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數,
|
||||
/// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。
|
||||
/// - Parameter newState: 新狀態。
|
||||
func handle(state newState: InputStateProtocol) {
|
||||
let prevState = state
|
||||
state = newState
|
||||
|
||||
switch newState {
|
||||
case let newState as InputState.Deactivated:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Empty:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.EmptyIgnoringPreviousState:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Committing:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Inputting:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.Marking:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.ChoosingCandidate:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.AssociatedPhrases:
|
||||
handle(state: newState, previous: prevState)
|
||||
case let newState as InputState.SymbolTable:
|
||||
handle(state: newState, previous: prevState)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
||||
private func setInlineDisplayWithCursor() {
|
||||
guard let state = state as? InputState.NotEmpty else {
|
||||
clearInlineDisplay()
|
||||
return
|
||||
}
|
||||
|
||||
var identifier: AnyObject {
|
||||
switch IME.currentInputMode {
|
||||
case InputMode.imeModeCHS:
|
||||
if #available(macOS 12.0, *) {
|
||||
return "zh-Hans" as AnyObject
|
||||
}
|
||||
case InputMode.imeModeCHT:
|
||||
if #available(macOS 12.0, *) {
|
||||
return (mgrPrefs.shiftJISShinjitaiOutputEnabled || mgrPrefs.chineseConversionEnabled)
|
||||
? "ja" as AnyObject : "zh-Hant" as AnyObject
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return "" as AnyObject
|
||||
}
|
||||
|
||||
// [Shiki's Note] This might needs to be bug-reported to Apple:
|
||||
// The LanguageIdentifier attribute of an NSAttributeString designated to
|
||||
// IMK Client().SetMarkedText won't let the actual font respect your languageIdentifier
|
||||
// settings. Still, this might behaves as Apple's current expectation, I'm afraid.
|
||||
if #available(macOS 12.0, *) {
|
||||
state.attributedString.setAttributes(
|
||||
[.languageIdentifier: identifier],
|
||||
range: NSRange(
|
||||
location: 0,
|
||||
length: state.composingBuffer.utf16.count
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度
|
||||
/// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
||||
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
||||
client().setMarkedText(
|
||||
state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0),
|
||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
/// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。
|
||||
/// 當 setInlineDisplayWithCursor() 在錯誤的狀態下被呼叫時,也會觸發這個函式。
|
||||
private func clearInlineDisplay() {
|
||||
client().setMarkedText(
|
||||
"", selectionRange: NSRange(location: 0, length: 0),
|
||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
/// 遞交組字區內容。
|
||||
/// 注意:必須在 IMK 的 commitComposition 函式當中也間接或者直接執行這個處理。
|
||||
private func commit(text: String) {
|
||||
let buffer = IME.kanjiConversionIfRequired(text)
|
||||
if buffer.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
client().insertText(
|
||||
buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||
)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Deactivated, previous: InputStateProtocol) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.delegate = nil
|
||||
ctlCandidateCurrent.visible = false
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
if let previous = previous as? InputState.NotEmpty {
|
||||
commit(text: previous.composingBuffer)
|
||||
}
|
||||
clearInlineDisplay()
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Empty, previous: InputStateProtocol) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
// 全專案用以判斷「.EmptyIgnoringPreviousState」的地方僅此一處。
|
||||
if let previous = previous as? InputState.NotEmpty,
|
||||
!(state is InputState.EmptyIgnoringPreviousState)
|
||||
{
|
||||
commit(text: previous.composingBuffer)
|
||||
}
|
||||
clearInlineDisplay()
|
||||
}
|
||||
|
||||
private func handle(
|
||||
state: InputState.EmptyIgnoringPreviousState, previous: InputStateProtocol
|
||||
) {
|
||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
// 這個函式就是去掉 previous state 使得沒有任何東西可以 commit。
|
||||
handle(state: InputState.Empty())
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Committing, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
let textToCommit = state.textToCommit
|
||||
if !textToCommit.isEmpty {
|
||||
commit(text: textToCommit)
|
||||
}
|
||||
clearInlineDisplay()
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Inputting, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
let textToCommit = state.textToCommit
|
||||
if !textToCommit.isEmpty {
|
||||
commit(text: textToCommit)
|
||||
}
|
||||
setInlineDisplayWithCursor()
|
||||
if !state.tooltip.isEmpty {
|
||||
show(
|
||||
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
|
||||
cursorIndex: state.cursorIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(state: InputState.Marking, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlCandidateCurrent.visible = false
|
||||
setInlineDisplayWithCursor()
|
||||
if state.tooltip.isEmpty {
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
} else {
|
||||
show(
|
||||
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
|
||||
cursorIndex: state.markerIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(state: InputState.ChoosingCandidate, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
setInlineDisplayWithCursor()
|
||||
show(candidateWindowWith: state)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.SymbolTable, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
setInlineDisplayWithCursor()
|
||||
show(candidateWindowWith: state)
|
||||
}
|
||||
|
||||
private func handle(state: InputState.AssociatedPhrases, previous: InputStateProtocol) {
|
||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
||||
ctlInputMethod.tooltipController.hide()
|
||||
clearInlineDisplay()
|
||||
show(candidateWindowWith: state)
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
5B0AF8B527B2C8290096FE54 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0AF8B427B2C8290096FE54 /* StringExtension.swift */; };
|
||||
5B11328927B94CFB00E58451 /* AppleKeyboardConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */; };
|
||||
5B21176C287539BB000443A9 /* ctlInputMethod_HandleStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */; };
|
||||
5B21176E28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */; };
|
||||
5B21177028753B9D000443A9 /* ctlInputMethod_Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B21176F28753B9D000443A9 /* ctlInputMethod_Delegates.swift */; };
|
||||
5B242403284B0D6500520FE4 /* ctlCandidateUniversal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B242402284B0D6500520FE4 /* ctlCandidateUniversal.swift */; };
|
||||
5B3133BF280B229700A4A505 /* KeyHandler_States.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3133BE280B229700A4A505 /* KeyHandler_States.swift */; };
|
||||
5B38F59A281E2E49007D5F5D /* 6_Unigram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1D15FC0EB100ABF4B3 /* 6_Unigram.swift */; };
|
||||
|
@ -199,6 +202,9 @@
|
|||
5B18BA7227C7BD8B0056EB19 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
|
||||
5B18BA7327C7BD8C0056EB19 /* LICENSE-JPN.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "LICENSE-JPN.txt"; sourceTree = "<group>"; };
|
||||
5B18BA7427C7BD8C0056EB19 /* LICENSE-CHT.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "LICENSE-CHT.txt"; sourceTree = "<group>"; };
|
||||
5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_HandleStates.swift; sourceTree = "<group>"; };
|
||||
5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_HandleDisplay.swift; sourceTree = "<group>"; };
|
||||
5B21176F28753B9D000443A9 /* ctlInputMethod_Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_Delegates.swift; sourceTree = "<group>"; };
|
||||
5B242402284B0D6500520FE4 /* ctlCandidateUniversal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlCandidateUniversal.swift; sourceTree = "<group>"; };
|
||||
5B2DB17127AF8771006D874E /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; name = Makefile; path = Data/Makefile; sourceTree = "<group>"; };
|
||||
5B2F2BB3286216A500B8557B /* vChewingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = vChewingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -446,6 +452,9 @@
|
|||
children = (
|
||||
5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */,
|
||||
D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */,
|
||||
5B21176F28753B9D000443A9 /* ctlInputMethod_Delegates.swift */,
|
||||
5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */,
|
||||
5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */,
|
||||
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
||||
D4E569DA27A34CC100AC2CEF /* CTools.h */,
|
||||
D4E569DB27A34CC100AC2CEF /* CTools.m */,
|
||||
|
@ -1141,6 +1150,8 @@
|
|||
5BA9FD4827FEF3C9002DE248 /* PreferencesWindowController.swift in Sources */,
|
||||
5BD0113B28180D6100609769 /* LMInstantiator.swift in Sources */,
|
||||
D4E569DC27A34D0E00AC2CEF /* CTools.m in Sources */,
|
||||
5B21177028753B9D000443A9 /* ctlInputMethod_Delegates.swift in Sources */,
|
||||
5B21176E28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift in Sources */,
|
||||
5B84579F2871AD2200C93B01 /* HotenkaChineseConverter.swift in Sources */,
|
||||
5B887F302826AEA400B6651E /* lmCoreEX.swift in Sources */,
|
||||
5BA9FD4627FEF3C9002DE248 /* Container.swift in Sources */,
|
||||
|
@ -1163,6 +1174,7 @@
|
|||
5B11328927B94CFB00E58451 /* AppleKeyboardConverter.swift in Sources */,
|
||||
5B54E743283A7D89001ECBDC /* lmCoreNS.swift in Sources */,
|
||||
5B62A32927AE77D100A19448 /* FSEventStreamHelper.swift in Sources */,
|
||||
5B21176C287539BB000443A9 /* ctlInputMethod_HandleStates.swift in Sources */,
|
||||
5B38F59B281E2E49007D5F5D /* 7_KeyValuePaired.swift in Sources */,
|
||||
5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */,
|
||||
5B38F5A4281E2E49007D5F5D /* 5_LanguageModel.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue