diff --git a/Source/Modules/ControllerModules/InputState.swift b/Source/Modules/ControllerModules/InputState.swift index 1e10759b..a2cd6052 100644 --- a/Source/Modules/ControllerModules/InputState.swift +++ b/Source/Modules/ControllerModules/InputState.swift @@ -96,15 +96,15 @@ class InputState { /// .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 class Committing: InputState { - private(set) var poppedText: String = "" + private(set) var textToCommit: String = "" - convenience init(poppedText: String) { + convenience init(textToCommit: String) { self.init() - self.poppedText = poppedText + self.textToCommit = textToCommit } var description: String { - "" + "" } } @@ -164,7 +164,7 @@ class InputState { /// .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 class Inputting: NotEmpty { - var poppedText: String = "" + var textToCommit: String = "" var tooltip: String = "" override init(composingBuffer: String, cursorIndex: Int) { @@ -172,7 +172,7 @@ class InputState { } override var description: String { - ", poppedText:\(poppedText)>" + ", textToCommit:\(textToCommit)>" } } diff --git a/Source/Modules/ControllerModules/KeyHandler_Core.swift b/Source/Modules/ControllerModules/KeyHandler_Core.swift index b82c8ceb..604d7845 100644 --- a/Source/Modules/ControllerModules/KeyHandler_Core.swift +++ b/Source/Modules/ControllerModules/KeyHandler_Core.swift @@ -24,10 +24,15 @@ 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. */ +/// 該檔案乃按鍵調度模組的核心部分,主要承接型別初期化內容、協定內容、以及 +/// 被封裝的「與 Megrez 組字引擎和 Tekkon 注拼引擎對接的」各種工具函數。 +/// 注意:不要把 composer 注拼槽與 compositor 組字器這兩個概念搞混。 + import Cocoa -// MARK: - Delegate. +// MARK: - 委任協定 (Delegate). +/// KeyHandler 委任協定 protocol KeyHandlerDelegate { func ctlCandidate() -> ctlCandidate func keyHandler( @@ -38,45 +43,52 @@ protocol KeyHandlerDelegate { -> Bool } -// MARK: - Kernel. +// MARK: - 核心 (Kernel). +/// KeyHandler 按鍵調度模組。 class KeyHandler { + /// 半衰模組的衰減指數 let kEpsilon: Double = 0.000001 - let kMaxComposingBufferNeedsToWalkSize = Int(max(12, ceil(Double(mgrPrefs.composingBufferSize) / 2))) - var composer: Tekkon.Composer = .init() - var compositor: Megrez.Compositor - var currentLM: vChewing.LMInstantiator = .init() - var currentUOM: vChewing.LMUserOverride = .init() - var walkedAnchors: [Megrez.NodeAnchor] = [] + /// 規定最大動態爬軌範圍。組字器內超出該範圍的節錨都會被自動標記為「已經手動選字過」,減少爬軌運算負擔。 + let kMaxComposingBufferNeedsToWalkSize = Int(max(12, ceil(Double(mgrPrefs.composingBufferSize) / 2))) + var composer: Tekkon.Composer = .init() // 注拼槽 + var compositor: Megrez.Compositor // 組字器 + var currentLM: vChewing.LMInstantiator = .init() // 當前主語言模組 + var currentUOM: vChewing.LMUserOverride = .init() // 當前半衰記憶模組 + var walkedAnchors: [Megrez.NodeAnchor] = [] // 用以記錄爬過的節錨的陣列 + /// 委任物件 (ctlInputMethod),以便呼叫其中的函數。 var delegate: KeyHandlerDelegate? + /// InputMode 需要在每次出現內容變更的時候都連帶重設組字器與各項語言模組, + /// 順帶更新 IME 模組及 UserPrefs 當中對於當前語言模式的記載。 var inputMode: InputMode = IME.currentInputMode { willSet { - // 將新的簡繁輸入模式提報給 ctlInputMethod: + // 這個標籤在下文會用到。 + let isCHS: Bool = (newValue == InputMode.imeModeCHS) + /// 將新的簡繁輸入模式提報給 ctlInputMethod 與 IME 模組。 IME.currentInputMode = newValue mgrPrefs.mostRecentInputMode = IME.currentInputMode.rawValue - - let isCHS: Bool = (newValue == InputMode.imeModeCHS) - // Reinitiate language models if necessary + /// 重設所有語言模組。這裡不需要做按需重設,因為對運算量沒有影響。 currentLM = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT currentUOM = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT - - // Synchronize the sub-languageModel state settings to the new LM. + /// 將與主語言模組有關的選項同步至主語言模組內。 syncBaseLMPrefs() - - // Create new compositor and clear the composer. - // When it recreates, it adapts to the latest imeMode settings. - // This allows it to work with correct LMs. - reinitCompositor() + /// 重建新的組字器,且清空注拼槽+同步最新的注拼槽排列設定。 + /// 組字器只能藉由重建才可以與當前新指派的語言模組對接。 + ensureCompositor() ensureParser() } } + /// 初期化。 public init() { + /// 組字器初期化。因為是首次初期化變數,所以這裡不能用 ensureCompositor() 代勞。 compositor = Megrez.Compositor(lm: currentLM, separator: "-") + /// 注拼槽初期化。 ensureParser() - // 下面這句必須用 defer,否則不會觸發其 willSet 部分的內容。 + /// 讀取最近的簡繁體模式、且將該屬性內容塞到 inputMode 當中。 + /// 這句必須用 defer 來處理,否則不會觸發其 willSet 部分的內容。 defer { inputMode = IME.currentInputMode } } @@ -88,18 +100,22 @@ class KeyHandler { // MARK: - Functions dealing with Megrez. + /// 實際上要拿給 Megrez 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。 + /// + /// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。 var actualCandidateCursorIndex: Int { mgrPrefs.useRearCursorMode ? min(compositorCursorIndex, compositorLength - 1) : max(compositorCursorIndex, 1) } + /// 利用給定的讀音鏈來試圖爬取最接近的組字結果(最大相似度估算)。 + /// + /// 該過程讀取的權重資料是經過 Viterbi 演算法計算得到的結果。 + /// + /// 該函數的爬取順序是從頭到尾。 func walk() { - // Retrieve the most likely grid, i.e. a Maximum Likelihood Estimation - // of the best possible Mandarin characters given the input syllables, - // using the Viterbi algorithm implemented in the Megrez library. - // The walk() traces the grid to the end. walkedAnchors = compositor.walk() - // if DEBUG mode is enabled, a GraphViz file is written to kGraphVizOutputfile. + // 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。 if mgrPrefs.isDebugModeEnabled { let result = compositor.grid.dumpDOT do { @@ -113,29 +129,31 @@ class KeyHandler { } } + /// 在爬取組字結果之前,先將即將從組字區溢出的內容遞交出去。 + /// + /// 在理想狀況之下,組字區多長都無所謂。但是,Viterbi 演算法使用 O(N^2), + /// 會使得運算壓力隨著節錨數量的增加而增大。於是,有必要限定組字區的長度。 + /// 超過該長度的內容會在爬軌之前先遞交出去,使其不再記入最大相似度估算的 + /// 估算對象範圍。用比較形象且生動卻有點噁心的解釋的話,蒼蠅一邊吃一邊屙。 var popOverflowComposingTextAndWalk: String { - // In ideal situations we can allow users to type infinitely in a buffer. - // However, Viberti algorithm has a complexity of O(N^2), the walk will - // become slower as the number of nodes increase. Therefore, we need to - // auto-commit overflown texts which usually lose their influence over - // the whole MLE anyway -- so that when the user type along, the already - // composed text in the rear side of the buffer will be committed out. - // (i.e. popped out.) - - var poppedText = "" + var textToCommit = "" if compositor.grid.width > mgrPrefs.composingBufferSize { if !walkedAnchors.isEmpty { let anchor: Megrez.NodeAnchor = walkedAnchors[0] if let theNode = anchor.node { - poppedText = theNode.currentKeyValue.value + textToCommit = theNode.currentKeyValue.value } compositor.removeHeadReadings(count: anchor.spanningLength) } } walk() - return poppedText + return textToCommit } + /// 用以組建聯想詞陣列的函式。 + /// - Parameter key: 給定的聯想詞的開頭字。 + /// - Returns: 抓取到的聯想詞陣列。 + /// 不會是 nil,但那些負責接收結果的函數會對空白陣列結果做出正確的處理。 func buildAssociatePhraseArray(withKey key: String) -> [String] { var arrResult: [String] = [] if currentLM.hasAssociatedPhrasesForKey(key) { @@ -144,6 +162,11 @@ class KeyHandler { return arrResult } + /// 在組字器內,以給定之候選字字串、來試圖在給定游標位置所在之處指定選字處理過程。 + /// 然後再將對應的節錨內的節點標記為「已經手動選字過」。 + /// - Parameters: + /// - value: 給定之候選字字串。 + /// - respectCursorPushing: 若該選項為 true,則會在選字之後始終將游標推送至選字厚的節錨的前方。 func fixNode(value: String, respectCursorPushing: Bool = true) { let cursorIndex = min(actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), compositorLength) compositor.grid.fixNodeSelectedCandidate(location: cursorIndex, value: value) @@ -153,9 +176,7 @@ class KeyHandler { // ) // // 不要針對逐字選字模式啟用臨時半衰記憶模型。 // if !mgrPrefs.useSCPCTypingMode { - // // If the length of the readings and the characters do not match, - // // it often means it is a special symbol and it should not be stored - // // in the user override model. + // // 所有讀音數與字符數不匹配的情況均不得塞入半衰記憶模組。 // var addToUserOverrideModel = true // if selectedNode.spanningLength != value.count { // IME.prtDebugIntel("UOM: SpanningLength != value.count, dismissing.") @@ -163,7 +184,7 @@ class KeyHandler { // } // if addToUserOverrideModel { // if let theNode = selectedNode.node { - // // 威注音的 SymbolLM 的 Score 是 -12。 + // // 威注音的 SymbolLM 的 Score 是 -12,符合該條件的內容不得塞入半衰記憶模組。 // if theNode.scoreFor(candidate: value) <= -12 { // IME.prtDebugIntel("UOM: Score <= -12, dismissing.") // addToUserOverrideModel = false @@ -172,6 +193,8 @@ class KeyHandler { // } // if addToUserOverrideModel { // IME.prtDebugIntel("UOM: Start Observation.") + // // 令半衰記憶模組觀測給定的 trigram。 + // // 這個過程會讓半衰引擎根據當前上下文生成 trigram 索引鍵。 // currentUOM.observe( // walkedNodes: walkedAnchors, cursorIndex: cursorIndex, candidate: value, // timestamp: NSDate().timeIntervalSince1970 @@ -180,6 +203,7 @@ class KeyHandler { // } walk() + /// 若偏好設定內啟用了相關選項,則會在選字之後始終將游標推送至選字厚的節錨的前方。 if mgrPrefs.moveCursorAfterSelectingCandidate, respectCursorPushing { var nextPosition = 0 for node in walkedAnchors { @@ -192,23 +216,26 @@ class KeyHandler { } } + /// 組字器內超出最大動態爬軌範圍的節錨都會被自動標記為「已經手動選字過」,減少爬軌運算負擔。 func markNodesFixedIfNecessary() { let width = compositor.grid.width if width <= kMaxComposingBufferNeedsToWalkSize { return } - var index: Int = 0 + var index = 0 for anchor in walkedAnchors { guard let node = anchor.node else { break } if index >= width - kMaxComposingBufferNeedsToWalkSize { break } if node.score < node.kSelectedCandidateScore { compositor.grid.fixNodeSelectedCandidate( - location: index + anchor.spanningLength, value: node.currentKeyValue.value) + location: index + anchor.spanningLength, value: node.currentKeyValue.value + ) } index += anchor.spanningLength } } + /// 獲取候選字詞陣列資料內容。 var candidatesArray: [String] { var arrCandidates: [String] = [] var arrNodes: [Megrez.NodeAnchor] = [] @@ -234,7 +261,9 @@ class KeyHandler { return arrCandidates } + /// 向半衰引擎詢問可能的選字建議。 func dealWithOverrideModelSuggestions() { + /// 先就當前上下文讓半衰引擎重新生成 trigram 索引鍵。 let overrideValue = mgrPrefs.useSCPCTypingMode ? "" @@ -243,6 +272,7 @@ class KeyHandler { timestamp: NSDate().timeIntervalSince1970 ) + /// 再拿著索引鍵去問半衰模組有沒有選字建議。有的話就遵循之、讓天權星引擎對指定節錨下的節點複寫權重。 if !overrideValue.isEmpty { IME.prtDebugIntel( "UOM: Suggestion retrieved, overriding the node score of the selected candidate.") @@ -256,6 +286,11 @@ class KeyHandler { } } + /// 就給定的節錨陣列,根據半衰模組的衰減指數,來找出最高權重數值。 + /// - Parameters: + /// - nodes: 給定的節錨陣列。 + /// - epsilon: 半衰模組的衰減指數。 + /// - Returns: 尋獲的最高權重數值。 func findHighestScore(nodes: [Megrez.NodeAnchor], epsilon: Double) -> Double { var highestScore: Double = 0 for currentAnchor in nodes { @@ -271,10 +306,12 @@ class KeyHandler { // MARK: - Extracted methods and functions (Tekkon). + /// 獲取與當前注音排列或拼音輸入種類有關的標點索引鍵,以英數下畫線「_」結尾。 var currentMandarinParser: String { mgrPrefs.mandarinParserName + "_" } + /// 給注拼槽指定注音排列或拼音輸入種類之後,將注拼槽內容清空。 func ensureParser() { switch mgrPrefs.mandarinParser { case MandarinParser.ofStandard.rawValue: @@ -310,7 +347,11 @@ class KeyHandler { composer.clear() } - /// 用於網頁 Ruby 的注音需要按照教科書印刷的方式來顯示輕聲,所以這裡處理一下。 + /// 用於網頁 Ruby 的注音需要按照教科書印刷的方式來顯示輕聲。該函數負責這種轉換。 + /// - Parameters: + /// - target: 要拿來做轉換處理的讀音鏈。 + /// - newSeparator: 新的讀音分隔符。 + /// - Returns: 經過轉換處理的讀音鏈。 func cnvZhuyinKeyToTextbookReading(target: String, newSeparator: String = "-") -> String { var arrReturn: [String] = [] for neta in target.split(separator: "-") { @@ -324,7 +365,11 @@ class KeyHandler { return arrReturn.joined(separator: newSeparator) } - // 用於網頁 Ruby 的拼音的陰平必須顯示,這裡處理一下。 + /// 用於網頁 Ruby 的拼音的陰平必須顯示,這裡處理一下。 + /// - Parameters: + /// - target: 要拿來做轉換處理的讀音鏈。 + /// - newSeparator: 新的讀音分隔符。 + /// - Returns: 經過轉換處理的讀音鏈。 func restoreToneOneInZhuyinKey(target: String, newSeparator: String = "-") -> String { var arrReturn: [String] = [] for neta in target.split(separator: "-") { @@ -339,8 +384,10 @@ class KeyHandler { // MARK: - Extracted methods and functions (Megrez). - var isCompositorEmpty: Bool { compositor.grid.width == 0 } + /// 組字器是否為空。 + var isCompositorEmpty: Bool { compositor.isEmpty } + /// 獲取原始節錨資料陣列。 var rawNodes: [Megrez.NodeAnchor] { /// 警告:不要對游標前置風格使用 nodesCrossing,否則會導致游標行為與 macOS 內建注音輸入法不一致。 /// 微軟新注音輸入法的游標後置風格也是不允許 nodeCrossing 的。 @@ -349,45 +396,54 @@ class KeyHandler { : compositor.grid.nodesEndingAt(location: actualCandidateCursorIndex) } + /// 將輸入法偏好設定同步至語言模組內。 func syncBaseLMPrefs() { currentLM.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled currentLM.isCNSEnabled = mgrPrefs.cns11643Enabled currentLM.isSymbolEnabled = mgrPrefs.symbolInputEnabled } - func reinitCompositor() { - // Each Mandarin syllable is separated by a hyphen. + /// 令組字器重新初期化,使其與被重新指派過的主語言模組對接。 + func ensureCompositor() { + // 每個漢字讀音都由一個西文半形減號分隔開。 compositor = Megrez.Compositor(lm: currentLM, separator: "-") } + /// 自組字器獲取目前的讀音陣列。 var currentReadings: [String] { compositor.readings } + /// 以給定的(讀音)索引鍵,來檢測當前主語言模型內是否有對應的資料在庫。 func ifLangModelHasUnigrams(forKey reading: String) -> Bool { currentLM.hasUnigramsFor(key: reading) } + /// 在組字器的給定游標位置內插入讀音。 func insertToCompositorAtCursor(reading: String) { compositor.insertReadingAtCursor(reading: reading) } + /// 組字器的游標位置。 var compositorCursorIndex: Int { get { compositor.cursorIndex } set { compositor.cursorIndex = newValue } } + /// 組字器的目前的長度。 var compositorLength: Int { compositor.length } - func deleteBuilderReadingInFrontOfCursor() { + /// 在組字器內,朝著與文字輸入方向相反的方向、砍掉一個與游標相鄰的讀音。 + /// + /// 在威注音的術語體系當中,「與文字輸入方向相反的方向」為向後(Rear)。 + func deleteCompositorReadingAtTheRearOfCursor() { compositor.deleteReadingAtTheRearOfCursor() } - func deleteBuilderReadingToTheFrontOfCursor() { + /// 在組字器內,朝著往文字輸入方向、砍掉一個與游標相鄰的讀音。 + /// + /// 在威注音的術語體系當中,「文字輸入方向」為向前(Front)。 + func deleteCompositorReadingToTheFrontOfCursor() { compositor.deleteReadingToTheFrontOfCursor() } - - var keyLengthAtIndexZero: Int { - walkedAnchors[0].node?.currentKeyValue.value.count ?? 0 - } } diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift index affc11f8..1f8462fb 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift @@ -24,9 +24,11 @@ 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 -// MARK: - § Handle Candidate State. +// MARK: - § 對選字狀態進行調度 (Handle Candidate State). extension KeyHandler { func handleCandidate( @@ -43,7 +45,7 @@ extension KeyHandler { return true } - // MARK: Cancel Candidate + // MARK: 取消選字 (Cancel Candidate) let cancelCandidateKey = input.isBackSpace || input.isESC || input.isDelete @@ -286,7 +288,7 @@ extension KeyHandler { } } - // MARK: - Associated Phrases + // MARK: 聯想詞處理 (Associated Phrases) if state is InputState.AssociatedPhrases { if !input.isShiftHold { return false } @@ -322,9 +324,13 @@ extension KeyHandler { if state is InputState.AssociatedPhrases { return false } - // MARK: SCPC Mode Processing + // MARK: 逐字選字模式的處理 (SCPC Mode Processing) if mgrPrefs.useSCPCTypingMode { + /// 檢查: + /// - 是否是針對當前注音排列/拼音輸入種類專門提供的標點符號。 + /// - 是否是需要摁修飾鍵才可以輸入的那種標點符號。 + var punctuationNamePrefix = "" if input.isOptionHold && !input.isControlHold { @@ -346,6 +352,8 @@ extension KeyHandler { ] let customPunctuation: String = arrCustomPunctuations.joined(separator: "") + /// 如果仍無匹配結果的話,看看這個輸入是否是不需要修飾鍵的那種標點鍵輸入。 + let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))] let punctuation: String = arrPunctuations.joined(separator: "") diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift index 724d10c4..ae4337bf 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift @@ -24,9 +24,12 @@ 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. */ +/// 該檔案乃按鍵調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給按鍵調度模組處理時、 +/// 按鍵調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。 + import Cocoa -// MARK: - § Handle Input with States. +// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) extension KeyHandler { func handle( @@ -36,10 +39,9 @@ extension KeyHandler { errorCallback: @escaping () -> Void ) -> Bool { let charCode: UniChar = input.charCode - var state = state // Turn this incoming constant into variable. + var state = state // 常數轉變數。 - // Ignore the input if its inputText is empty. - // Reason: such inputs may be functional key combinations. + // 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。 guard let inputText: String = input.inputText, !inputText.isEmpty else { return false } @@ -52,8 +54,7 @@ extension KeyHandler { return true } - // Ignore the input if the composing buffer is empty with no reading - // and there is some function key combination. + // 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。 let isFunctionKey: Bool = input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNumericPad) if !(state is InputState.NotEmpty) && !(state is InputState.AssociatedPhrases) && isFunctionKey { @@ -62,37 +63,40 @@ extension KeyHandler { // MARK: Caps Lock processing. - // If Caps Lock is ON, temporarily disable phonetic reading. - // Note: Alphanumerical mode processing. + /// 若 Caps Lock 被啟用的話,則暫停對注音輸入的處理。 + /// 這裡的處理原先是給威注音曾經有過的 Shift 切換英數模式來用的,但因為採 Chromium 核 + /// 心的瀏覽器會讓 IMK 無法徹底攔截對 Shift 鍵的單擊行為、導致這個模式的使用體驗非常糟 + /// 糕,故僅保留以 Caps Lock 驅動的英數模式。 if input.isBackSpace || input.isEnter || input.isAbsorbedArrowKey || input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse || input.isCursorForward || input.isCursorBackward { - // Do nothing if backspace is pressed -- we ignore the key + // 略過對 BackSpace 的處理。 } else if input.isCapsLockOn { - // Process all possible combination, we hope. + // 但願能夠處理這種情況下所有可能的案件組合。 clear() stateCallback(InputState.Empty()) - // When shift is pressed, don't do further processing... - // ...since it outputs capital letter anyway. + // 摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。 if input.isShiftHold { return false } - // If ASCII but not printable, don't use insertText:replacementRange: - // Certain apps don't handle non-ASCII char insertions. + /// 如果是 ASCII 當中的不可列印的字元的話,不使用「insertText:replacementRange:」。 + /// 某些應用無法正常處理非 ASCII 字符的輸入。 + /// 注意:這裡一定要用 Objective-C 的 isPrintable() 函數來處理,否則無效。 + /// 這個函數已經包裝在 CTools.h 裡面了,這樣就可以拿給 Swift 用。 if charCode < 0x80, !CTools.isPrintable(charCode) { return false } - // Commit the entire input buffer. - stateCallback(InputState.Committing(poppedText: inputText.lowercased())) + // 將整個組字區的內容遞交給客體應用。 + stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) stateCallback(InputState.Empty()) return true } - // MARK: Numeric Pad Processing. + // MARK: 處理數字小鍵盤 (Numeric Pad Processing) if input.isNumericPad { if !input.isLeft, !input.isRight, !input.isDown, @@ -100,13 +104,13 @@ extension KeyHandler { { clear() stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(poppedText: inputText.lowercased())) + stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) stateCallback(InputState.Empty()) return true } } - // MARK: Handle Candidates. + // MARK: 處理候選字詞 (Handle Candidates) if state is InputState.ChoosingCandidate { return handleCandidate( @@ -114,7 +118,7 @@ extension KeyHandler { ) } - // MARK: Handle Associated Phrases. + // MARK: 處理聯想詞 (Handle Associated Phrases) if state is InputState.AssociatedPhrases { if handleCandidate( @@ -126,7 +130,7 @@ extension KeyHandler { } } - // MARK: Handle Marking. + // MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking) if let marking = state as? InputState.Marking { if handleMarkingState( @@ -139,19 +143,20 @@ extension KeyHandler { stateCallback(state) } - // MARK: Handle BPMF Keys. + // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) var keyConsumedByReading = false let skipPhoneticHandling = input.isReservedKey || input.isControlHold || input.isOptionHold - // See if Phonetic reading is valid. + // 這裡 inputValidityCheck() 是讓注拼槽檢查 charCode 這個 UniChar 是否是合法的注音輸入。 + // 如果是的話,就將這次傳入的這個按鍵訊號塞入注拼槽內且標記為「keyConsumedByReading」。 + // 函數 composer.receiveKey() 可以既接收 String 又接收 UniChar。 if !skipPhoneticHandling && composer.inputValidityCheck(key: charCode) { composer.receiveKey(fromCharCode: charCode) keyConsumedByReading = true - // If we have a tone marker, we have to insert the reading to the - // compositor in other words, if we don't have a tone marker, we just - // update the composing buffer. + // 沒有調號的話,只需要 updateClientComposingBuffer() 且終止處理(return true)即可。 + // 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。 let composeReading = composer.hasToneMarker() if !composeReading { stateCallback(buildInputtingState) @@ -161,44 +166,49 @@ extension KeyHandler { var composeReading = composer.hasToneMarker() // 這裡不需要做排他性判斷。 - // See if we have composition if Enter/Space is hit and buffer is not empty. - // We use "|=" conditioning so that the tone marker key is also taken into account. - // However, Swift does not support "|=". + // 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 _composer 內的注音來做檢查了。 + // 來看看詞庫內到底有沒有對應的讀音索引。這裡用了類似「|=」的判斷處理方式。 composeReading = composeReading || (!composer.isEmpty && (input.isSpace || input.isEnter)) if composeReading { if input.isSpace, !composer.hasToneMarker() { - composer.receiveKey(fromString: " ") // 補上空格。 + // 補上空格,否則倚天忘形與許氏排列某些音無法響應不了陰平聲調。 + // 小麥注音因為使用 OVMandarin,所以不需要這樣補。但鐵恨引擎對所有聲調一視同仁。 + composer.receiveKey(fromString: " ") } - let reading = composer.getComposition() + let reading = composer.getComposition() // 拿取用來進行索引檢索用的注音 + // 如果輸入法的辭典索引是漢語拼音的話,要注意上一行拿到的內容得是漢語拼音。 - // See whether we have a unigram for this... + // 向語言模型詢問是否有對應的記錄 if !ifLangModelHasUnigrams(forKey: reading) { IME.prtDebugIntel("B49C0979:語彙庫內無「\(reading)」的匹配記錄。") errorCallback() composer.clear() + // 根據「組字器是否為空」來判定回呼哪一種狀態 stateCallback((compositorLength == 0) ? InputState.EmptyIgnoringPreviousState() : buildInputtingState) - return true + return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了 } - // ... and insert it into the grid... + // 將該讀音插入至組字器內的軌格當中 insertToCompositorAtCursor(reading: reading) - // ... then walk the grid... - let poppedText = popOverflowComposingTextAndWalk + // 讓組字器反爬軌格 + let textToCommit = popOverflowComposingTextAndWalk - // ... get and tweak override model suggestion if possible... + // 看看半衰記憶模組是否會對目前的狀態給出自動選字建議 // dealWithOverrideModelSuggestions() // 暫時禁用,因為無法使其生效。 - // ... fix nodes if necessary... + // 將組字器內超出最大動態爬軌範圍的節錨都標記為「已經手動選字過」,減少之後的爬軌運算負擔。 markNodesFixedIfNecessary() - // ... then update the text. + // 之後就是更新組字區了。先清空注拼槽的內容。 composer.clear() + // 再以回呼組字狀態的方式來執行 updateClientComposingBuffer()。 let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) + /// 逐字選字模式的處理。 if mgrPrefs.useSCPCTypingMode { let choosingCandidates: InputState.ChoosingCandidate = buildCandidate( state: inputting, @@ -207,7 +217,7 @@ extension KeyHandler { if choosingCandidates.candidates.count == 1 { clear() let text: String = choosingCandidates.candidates.first ?? "" - stateCallback(InputState.Committing(poppedText: text)) + stateCallback(InputState.Committing(textToCommit: text)) if !mgrPrefs.associatedPhrasesEnabled { stateCallback(InputState.Empty()) @@ -227,42 +237,44 @@ extension KeyHandler { stateCallback(choosingCandidates) } } - return true // Telling the client that the key is consumed. + // 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。 + return true } - // The only possibility for this to be true is that the Phonetic reading - // already has a tone marker but the last key is *not* a tone marker key. An - // example is the sequence "6u" with the Standard layout, which produces "ㄧˊ" - // but does not compose. Only sequences such as "ㄧˊ", "ˊㄧˊ", "ˊㄧˇ", or "ˊㄧ " - // would compose. + /// 如果此時這個選項是 true 的話,可知當前注拼槽輸入了聲調、且上一次按鍵不是聲調按鍵。 + /// 比方說大千傳統佈局敲「6j」會出現「ˊㄨ」但並不會被認為是「ㄨˊ」,因為先輸入的調號 + /// 並非用來確認這個注音的調號。除非是:「ㄨˊ」「ˊㄨˊ」「ˊㄨˇ」「ˊㄨ 」等。 if keyConsumedByReading { + // 以回呼組字狀態的方式來執行 updateClientComposingBuffer()。 stateCallback(buildInputtingState) return true } // MARK: Calling candidate window using Up / Down or PageUp / PageDn. + // 用上下左右鍵呼叫選字窗。 + if let currentState = state as? InputState.NotEmpty, composer.isEmpty, input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse || input.isSpace || input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior) || (input.isTypingVertical && (input.isverticalTypingOnlyChooseCandidateKey)) { if input.isSpace { - // If the Space key is NOT set to be a selection key + /// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話……… if !mgrPrefs.chooseCandidateUsingSpace { if compositorCursorIndex >= compositorLength { let composingBuffer = currentState.composingBuffer if !composingBuffer.isEmpty { - stateCallback(InputState.Committing(poppedText: composingBuffer)) + stateCallback(InputState.Committing(textToCommit: composingBuffer)) } clear() - stateCallback(InputState.Committing(poppedText: " ")) + stateCallback(InputState.Committing(textToCommit: " ")) stateCallback(InputState.Empty()) } else if ifLangModelHasUnigrams(forKey: " ") { insertToCompositorAtCursor(reading: " ") - let poppedText = popOverflowComposingTextAndWalk + let textToCommit = popOverflowComposingTextAndWalk let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) } return true @@ -371,9 +383,9 @@ extension KeyHandler { if ifLangModelHasUnigrams(forKey: "_punctuation_list") { if composer.isEmpty { insertToCompositorAtCursor(reading: "_punctuation_list") - let poppedText: String! = popOverflowComposingTextAndWalk + let textToCommit: String! = popOverflowComposingTextAndWalk let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) stateCallback(buildCandidate(state: inputting, isTypingVertical: input.isTypingVertical)) } else { // If there is still unfinished bpmf reading, ignore the punctuation @@ -394,7 +406,9 @@ extension KeyHandler { // MARK: Punctuation - // If nothing is matched, see if it's a punctuation key for current layout. + /// 如果仍無匹配結果的話,先看一下: + /// - 是否是針對當前注音排列/拼音輸入種類專門提供的標點符號。 + /// - 是否是需要摁修飾鍵才可以輸入的那種標點符號。 var punctuationNamePrefix = "" @@ -425,7 +439,8 @@ extension KeyHandler { return true } - // if nothing is matched, see if it's a punctuation key. + /// 如果仍無匹配結果的話,看看這個輸入是否是不需要修飾鍵的那種標點鍵輸入。 + let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))] let punctuation: String = arrPunctuations.joined(separator: "") @@ -453,13 +468,12 @@ extension KeyHandler { } } - // MARK: - Still Nothing. + // MARK: - 終末處理 (Still Nothing) - // Still nothing? Then we update the composing buffer. - // Note that some app has strange behavior if we don't do this, - // "thinking" that the key is not actually consumed. - // 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 - // 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 + /// 對剩下的漏網之魚做攔截處理、直接將當前狀態繼續回呼給 ctlInputMethod。 + /// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 + /// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 + /// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 if (state is InputState.NotEmpty) || !composer.isEmpty { IME.prtDebugIntel( "Blocked data: charCode: \(charCode), keyCode: \(input.keyCode)") diff --git a/Source/Modules/ControllerModules/KeyHandler_States.swift b/Source/Modules/ControllerModules/KeyHandler_States.swift index 715a6c31..a391c663 100644 --- a/Source/Modules/ControllerModules/KeyHandler_States.swift +++ b/Source/Modules/ControllerModules/KeyHandler_States.swift @@ -24,35 +24,35 @@ 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 -// MARK: - § State managements. +// MARK: - § 根據按鍵行為來調控模式的函數 (Functions Interact With States). extension KeyHandler { // MARK: - 構築狀態(State Building) + /// 生成「正在輸入」狀態。 var buildInputtingState: InputState.Inputting { - // "Updating the composing buffer" means to request the client - // to "refresh" the text input buffer with our "composing text" + /// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容 + /// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。 var tooltipParameterRef: [String] = ["", ""] var composingBuffer = "" var composedStringCursorIndex = 0 var readingCursorIndex = 0 - // We must do some Unicode codepoint counting to find the actual cursor location for the client - // i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars - // locations. Since we are using Swift, we use .utf16 as the equivalent of NSString.length(). + /// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度, + /// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。 + /// 這樣就可以免除不必要的類型轉換。 for walkedNode in walkedAnchors { if let theNode = walkedNode.node { let strNodeValue = theNode.currentKeyValue.value composingBuffer += strNodeValue let arrSplit: [String] = Array(strNodeValue).map { String($0) } let codepointCount = arrSplit.count - // This re-aligns the cursor index in the composed string - // (the actual cursor on the screen) with the compositor's logical - // cursor (reading) cursor; each built node has a "spanning length" - // (e.g. two reading blocks has a spanning length of 2), and we - // accumulate those lengths to calculate the displayed cursor - // index. + /// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。 + /// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來 + /// 累加、以此為依據,來校正「可見游標位置」。 let spanningLength: Int = walkedNode.spanningLength if readingCursorIndex + spanningLength <= compositorCursorIndex { composedStringCursorIndex += strNodeValue.utf16.count @@ -69,14 +69,12 @@ extension KeyHandler { if readingCursorIndex < compositorCursorIndex { composedStringCursorIndex += strNodeValue.utf16.count readingCursorIndex += spanningLength - if readingCursorIndex > compositorCursorIndex { - readingCursorIndex = compositorCursorIndex - } - // Now we start preparing the contents of the tooltips used - // in cases of moving cursors across certain emojis which emoji - // char count is inequal to the reading count. - // Example in McBopomofo: Typing 王建民 (3 readings) gets a tree emoji. - // Example in vChewing: Typing 義麵 (2 readings) gets a pasta emoji. + readingCursorIndex = min(readingCursorIndex, compositorCursorIndex) + /// 接下來再處理這麼一種情況: + /// 某些錨點內的當前候選字詞長度與讀音長度不相等。 + /// 但此時游標還是按照每個讀音單位來移動的, + /// 所以需要上下文工具提示來顯示游標的相對位置。 + /// 這裡先計算一下要用在工具提示當中的顯示參數的內容。 switch compositorCursorIndex { case compositor.readings.count...: tooltipParameterRef[0] = compositor.readings[compositor.readings.count - 1] @@ -94,9 +92,8 @@ extension KeyHandler { } } - // Now, we gather all the intel, separate the composing buffer to two parts (head and tail), - // and insert the reading text (the Mandarin syllable) in between them. - // The reading text is what the user is typing. + /// 再接下來,藉由已經計算成功的「可見游標位置」,咱們計算一下在這個游標之前與之後的 + /// 組字區內容,以便之後在這之間插入正在輸入的漢字讀音(藉由鐵恨 composer 注拼槽取得)。 var arrHead = [String.UTF16View.Element]() var arrTail = [String.UTF16View.Element]() @@ -108,34 +105,38 @@ extension KeyHandler { } } + /// 現在呢,咱們拿到了游標前後的 stringview 資料,準備著手生成要在組字區內顯示用的內容。 + /// 在這對前後資料當中插入目前正在輸入的讀音資料即可。 let head = String(utf16CodeUnits: arrHead, count: arrHead.count) let reading = composer.getInlineCompositionForIMK(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer) let tail = String(utf16CodeUnits: arrTail, count: arrTail.count) let composedText = head + reading + tail let cursorIndex = composedStringCursorIndex + reading.utf16.count + /// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。 let stateResult = InputState.Inputting(composingBuffer: composedText, cursorIndex: cursorIndex) - // Now we start weaving the contents of the tooltip. - if tooltipParameterRef[0].isEmpty, tooltipParameterRef[1].isEmpty { - stateResult.tooltip = "" - } else if tooltipParameterRef[0].isEmpty { - stateResult.tooltip = String( - format: NSLocalizedString("Cursor is to the rear of \"%@\".", comment: ""), - tooltipParameterRef[1] - ) - } else if tooltipParameterRef[1].isEmpty { - stateResult.tooltip = String( - format: NSLocalizedString("Cursor is in front of \"%@\".", comment: ""), - tooltipParameterRef[0] - ) - } else { - stateResult.tooltip = String( - format: NSLocalizedString("Cursor is between \"%@\" and \"%@\".", comment: ""), - tooltipParameterRef[0], tooltipParameterRef[1] - ) + /// 根據上文的參數結果來決定生成怎樣的工具提示。 + switch (tooltipParameterRef[0].isEmpty, tooltipParameterRef[1].isEmpty) { + case (true, true): stateResult.tooltip.removeAll() + case (true, false): + stateResult.tooltip = String( + format: NSLocalizedString("Cursor is to the rear of \"%@\".", comment: ""), + tooltipParameterRef[1] + ) + case (false, true): + stateResult.tooltip = String( + format: NSLocalizedString("Cursor is in front of \"%@\".", comment: ""), + tooltipParameterRef[0] + ) + case (false, false): + stateResult.tooltip = String( + format: NSLocalizedString("Cursor is between \"%@\" and \"%@\".", comment: ""), + tooltipParameterRef[0], tooltipParameterRef[1] + ) } + /// 給工具提示設定提示配色。 if !stateResult.tooltip.isEmpty { ctlInputMethod.tooltipController.setColor(state: .denialOverflow) } @@ -145,6 +146,11 @@ extension KeyHandler { // MARK: - 用以生成候選詞陣列及狀態 + /// 拿著給定的候選字詞陣列資料內容,切換至選字狀態。 + /// - Parameters: + /// - currentState: 當前狀態。 + /// - isTypingVertical: 是否縱排輸入? + /// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。 func buildCandidate( state currentState: InputState.NotEmpty, isTypingVertical: Bool = false @@ -159,13 +165,19 @@ extension KeyHandler { // MARK: - 用以接收聯想詞陣列且生成狀態 - // 這次重寫時,針對「buildAssociatePhraseStateWithKey」這個(用以生成帶有 - // 聯想詞候選清單的結果的狀態回呼的)函數進行了小幅度的重構處理,使其始終 - // 可以從 Core 部分的「buildAssociatePhraseArray」函數獲取到一個內容類型 - // 為「String」的標準 Swift 陣列。這樣一來,該聯想詞狀態回呼函數將始終能 - // 夠傳回正確的結果形態、永遠也無法傳回 nil。於是,所有在用到該函數時以 - // 回傳結果類型判斷作為合法性判斷依據的函數,全都將依據改為檢查傳回的陣列 - // 是否為空:如果陣列為空的話,直接回呼一個空狀態。 + /// 拿著給定的聯想詞陣列資料內容,切換至聯想詞狀態。 + /// + /// 這次重寫時,針對「buildAssociatePhraseStateWithKey」這個(用以生成帶有 + /// 聯想詞候選清單的結果的狀態回呼的)函數進行了小幅度的重構處理,使其始終 + /// 可以從 Core 部分的「buildAssociatePhraseArray」函數獲取到一個內容類型 + /// 為「String」的標準 Swift 陣列。這樣一來,該聯想詞狀態回呼函數將始終能 + /// 夠傳回正確的結果形態、永遠也無法傳回 nil。於是,所有在用到該函數時以 + /// 回傳結果類型判斷作為合法性判斷依據的函數,全都將依據改為檢查傳回的陣列 + /// 是否為空:如果陣列為空的話,直接回呼一個空狀態。 + /// - Parameters: + /// - key: 給定的索引鍵(也就是給定的聯想詞的開頭字)。 + /// - isTypingVertical: 是否縱排輸入? + /// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。 func buildAssociatePhraseState( withKey key: String!, isTypingVertical: Bool @@ -178,6 +190,13 @@ extension KeyHandler { // MARK: - 用以處理就地新增自訂語彙時的行為 + /// 用以處理就地新增自訂語彙時的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - input: 輸入按鍵訊號。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleMarkingState( _ state: InputState.Marking, input: InputSignal, @@ -246,8 +265,16 @@ extension KeyHandler { return false } - // MARK: - 標點輸入處理 + // MARK: - 標點輸入的處理 + /// 標點輸入的處理。 + /// - Parameters: + /// - customPunctuation: 自訂標點索引鍵頭。 + /// - state: 當前狀態。 + /// - isTypingVertical: 是否縱排輸入? + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handlePunctuation( _ customPunctuation: String, state: InputState, @@ -261,9 +288,9 @@ extension KeyHandler { if composer.isEmpty { insertToCompositorAtCursor(reading: customPunctuation) - let poppedText = popOverflowComposingTextAndWalk + let textToCommit = popOverflowComposingTextAndWalk let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) if mgrPrefs.useSCPCTypingMode, composer.isEmpty { @@ -273,8 +300,8 @@ extension KeyHandler { ) if candidateState.candidates.count == 1 { clear() - if let strPoppedText: String = candidateState.candidates.first { - stateCallback(InputState.Committing(poppedText: strPoppedText) as InputState.Committing) + if let strtextToCommit: String = candidateState.candidates.first { + stateCallback(InputState.Committing(textToCommit: strtextToCommit) as InputState.Committing) stateCallback(InputState.Empty()) } else { stateCallback(candidateState) @@ -293,8 +320,13 @@ extension KeyHandler { } } - // MARK: - Enter 鍵處理 + // MARK: - Enter 鍵的處理 + /// Enter 鍵的處理。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnter( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -303,13 +335,18 @@ extension KeyHandler { guard let currentState = state as? InputState.Inputting else { return false } clear() - stateCallback(InputState.Committing(poppedText: currentState.composingBuffer)) + stateCallback(InputState.Committing(textToCommit: currentState.composingBuffer)) stateCallback(InputState.Empty()) return true } - // MARK: - CMD+Enter 鍵處理(注音文) + // MARK: - CMD+Enter 鍵的處理(注音文) + /// CMD+Enter 鍵的處理(注音文)。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleCtrlCommandEnter( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -329,13 +366,18 @@ extension KeyHandler { clear() - stateCallback(InputState.Committing(poppedText: composingBuffer)) + stateCallback(InputState.Committing(textToCommit: composingBuffer)) stateCallback(InputState.Empty()) return true } - // MARK: - CMD+Alt+Enter 鍵處理(網頁 Ruby 注音文標記) + // MARK: - CMD+Alt+Enter 鍵的處理(網頁 Ruby 注音文標記) + /// CMD+Alt+Enter 鍵的處理(網頁 Ruby 注音文標記)。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleCtrlOptionCommandEnter( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -368,13 +410,19 @@ extension KeyHandler { clear() - stateCallback(InputState.Committing(poppedText: composed)) + stateCallback(InputState.Committing(textToCommit: composed)) stateCallback(InputState.Empty()) return true } // MARK: - 處理 Backspace (macOS Delete) 按鍵行為 + /// 處理 Backspace (macOS Delete) 按鍵行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackspace( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -386,7 +434,7 @@ extension KeyHandler { composer.clear() } else if composer.isEmpty { if compositorCursorIndex >= 0 { - deleteBuilderReadingInFrontOfCursor() + deleteCompositorReadingAtTheRearOfCursor() walk() } else { IME.prtDebugIntel("9D69908D") @@ -408,6 +456,12 @@ extension KeyHandler { // MARK: - 處理 PC Delete (macOS Fn+BackSpace) 按鍵行為 + /// 處理 PC Delete (macOS Fn+BackSpace) 按鍵行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleDelete( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -417,7 +471,7 @@ extension KeyHandler { if composer.isEmpty { if compositorCursorIndex != compositorLength { - deleteBuilderReadingToTheFrontOfCursor() + deleteCompositorReadingToTheFrontOfCursor() walk() let inputting = buildInputtingState // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 @@ -442,6 +496,12 @@ extension KeyHandler { // MARK: - 處理與當前文字輸入排版前後方向呈 90 度的那兩個方向鍵的按鍵行為 + /// 處理與當前文字輸入排版前後方向呈 90 度的那兩個方向鍵的按鍵行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleAbsorbedArrowKey( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -456,8 +516,14 @@ extension KeyHandler { return true } - // MARK: - 處理 Home 鍵行為 + // MARK: - 處理 Home 鍵的行為 + /// 處理 Home 鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleHome( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -484,8 +550,14 @@ extension KeyHandler { return true } - // MARK: - 處理 End 鍵行為 + // MARK: - 處理 End 鍵的行為 + /// 處理 End 鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnd( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -512,8 +584,13 @@ extension KeyHandler { return true } - // MARK: - 處理 Esc 鍵行為 + // MARK: - 處理 Esc 鍵的行為 + /// 處理 Esc 鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEsc( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -521,17 +598,13 @@ extension KeyHandler { ) -> Bool { guard state is InputState.Inputting else { return false } - let escToClearInputBufferEnabled: Bool = mgrPrefs.escToCleanInputBuffer - - if escToClearInputBufferEnabled { - // If the option is enabled, we clear everything in the buffer. - // This includes walked nodes and the reading. Note that this convention - // is by default in macOS 10.0-10.5 built-in Panasonic Hanin and later macOS Zhuyin. - // Some Windows users hate this design, hence the option here to disable it. + if mgrPrefs.escToCleanInputBuffer { + /// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。 + /// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。 clear() stateCallback(InputState.EmptyIgnoringPreviousState()) } else { - // If reading is not empty, we cancel the reading. + /// 如果注拼槽不是空的話,則清空之。 if !composer.isEmpty { composer.clear() if compositorLength == 0 { @@ -546,6 +619,13 @@ extension KeyHandler { // MARK: - 處理向前方向鍵的行為 + /// 處理向前方向鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - input: 輸入按鍵訊號。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleForward( state: InputState, input: InputSignal, @@ -595,6 +675,13 @@ extension KeyHandler { // MARK: - 處理向後方向鍵的行為 + /// 處理向後方向鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - input: 輸入按鍵訊號。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackward( state: InputState, input: InputSignal, @@ -644,6 +731,13 @@ extension KeyHandler { // MARK: - 處理上下文候選字詞輪替(Tab 按鍵,或者 Shift+Space) + /// 以給定之參數來處理上下文候選字詞之輪替。 + /// - Parameters: + /// - state: 當前狀態。 + /// - reverseModifier: 是否有控制輪替方向的修飾鍵輸入。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleInlineCandidateRotation( state: InputState, reverseModifier: Bool, @@ -697,18 +791,17 @@ extension KeyHandler { var currentIndex = 0 if currentNode.score < currentNode.kSelectedCandidateScore { - // Once the user never select a candidate for the node, - // we start from the first candidate, so the user has a - // chance to use the unigram with two or more characters - // when type the tab key for the first time. - // - // In other words, if a user type two BPMF readings, - // but the score of seeing them as two unigrams is higher - // than a phrase with two characters, the user can just - // use the longer phrase by tapping the tab key. + /// 只要是沒有被使用者手動選字過的(節錨下的)節點, + /// 就從第一個候選字詞開始,這樣使用者在敲字時就會優先匹配 + /// 那些字詞長度不小於 2 的單元圖。換言之,如果使用者敲了兩個 + /// 注音讀音、卻發現這兩個注音讀音各自的單字權重遠高於由這兩個 + /// 讀音組成的雙字詞的權重、導致這個雙字詞並未在爬軌時被自動 + /// 選中的話,則使用者可以直接摁下本函數對應的按鍵來輪替候選字即可。 + /// (預設情況下是 (Shift+)Tab 來做正 (反) 向切換,但也可以用 + /// Shift(+CMD)+Space 來切換、以應對臉書綁架 Tab 鍵的情況。 if candidates[0] == currentValue { - // If the first candidate is the value of the - // current node, we use next one. + /// 如果第一個候選字詞是當前節點的候選字詞的值的話, + /// 那就切到下一個(或上一個,也就是最後一個)候選字詞。 if reverseModifier { currentIndex = candidates.count - 1 } else { diff --git a/Source/Modules/IMEModules/ctlInputMethod.swift b/Source/Modules/IMEModules/ctlInputMethod.swift index 83717931..8bf39a06 100644 --- a/Source/Modules/IMEModules/ctlInputMethod.swift +++ b/Source/Modules/IMEModules/ctlInputMethod.swift @@ -178,7 +178,7 @@ class ctlInputMethod: IMKInputController { override func commitComposition(_ sender: Any!) { _ = sender // Stop clang-format from ruining the parameters of this function. if let state = state as? InputState.NotEmpty { - handle(state: InputState.Committing(poppedText: state.composingBuffer)) + handle(state: InputState.Committing(textToCommit: state.composingBuffer)) } resetKeyHandler() } @@ -306,9 +306,9 @@ extension ctlInputMethod { ctlCandidateCurrent.visible = false hideTooltip() - let poppedText = state.poppedText - if !poppedText.isEmpty { - commit(text: poppedText) + let textToCommit = state.textToCommit + if !textToCommit.isEmpty { + commit(text: textToCommit) } client().setMarkedText( "", selectionRange: NSRange(location: 0, length: 0), @@ -321,9 +321,9 @@ extension ctlInputMethod { ctlCandidateCurrent.visible = false hideTooltip() - let poppedText = state.poppedText - if !poppedText.isEmpty { - commit(text: poppedText) + let textToCommit = state.textToCommit + if !textToCommit.isEmpty { + commit(text: textToCommit) } // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, @@ -622,7 +622,7 @@ extension ctlInputMethod: ctlCandidateDelegate { state: .SymbolTable(node: node, isTypingVertical: state.isTypingVertical) ) } else { - handle(state: .Committing(poppedText: node.title)) + handle(state: .Committing(textToCommit: node.title)) handle(state: .Empty()) } return @@ -637,7 +637,7 @@ extension ctlInputMethod: ctlCandidateDelegate { if mgrPrefs.useSCPCTypingMode { keyHandler.clear() let composingBuffer = inputting.composingBuffer - handle(state: .Committing(poppedText: composingBuffer)) + handle(state: .Committing(textToCommit: composingBuffer)) if mgrPrefs.associatedPhrasesEnabled, let associatePhrases = keyHandler.buildAssociatePhraseState( withKey: composingBuffer, isTypingVertical: state.isTypingVertical @@ -655,7 +655,7 @@ extension ctlInputMethod: ctlCandidateDelegate { if let state = state as? InputState.AssociatedPhrases { let selectedValue = state.candidates[index] - handle(state: .Committing(poppedText: selectedValue)) + handle(state: .Committing(textToCommit: selectedValue)) if mgrPrefs.associatedPhrasesEnabled, let associatePhrases = keyHandler.buildAssociatePhraseState( withKey: selectedValue, isTypingVertical: state.isTypingVertical