diff --git a/DataCompiler/dataCompiler.swift b/DataCompiler/dataCompiler.swift index f929e557..f4dfd21e 100644 --- a/DataCompiler/dataCompiler.swift +++ b/DataCompiler/dataCompiler.swift @@ -57,7 +57,7 @@ extension String { } } -// MARK: - 引入小數點位數控制函數 +// MARK: - 引入小數點位數控制函式 // Ref: https://stackoverflow.com/a/32581409/4162914 extension Float { @@ -67,7 +67,7 @@ extension Float { } } -// MARK: - 引入冪乘函數 +// MARK: - 引入冪乘函式 // Ref: https://stackoverflow.com/a/41581695/4162914 precedencegroup ExponentiationPrecedence { diff --git a/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift b/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift index bf5d9f1c..47c82ce6 100644 --- a/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift +++ b/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift @@ -47,7 +47,7 @@ public class OpenCCBridge: NSObject { /// - Parameter string: Text in Original Script. /// - Returns: Text converted to Different Script. public static func crossConvert(_ string: String) -> String? { - switch ctlInputMethod.currentKeyHandler.inputMode { + switch IME.currentInputMode { case InputMode.imeModeCHS: return shared.traditionalize?.convert(string) case InputMode.imeModeCHT: diff --git a/Source/Data b/Source/Data index 0b755c73..1b67116c 160000 --- a/Source/Data +++ b/Source/Data @@ -1 +1 @@ -Subproject commit 0b755c7332fce88e6d1073164447fc451ec317be +Subproject commit 1b67116c77dd654f156c43754694ac3a17a19a3a diff --git a/Source/Modules/ControllerModules/InputState.swift b/Source/Modules/ControllerModules/InputState.swift index 248a81f2..ee285409 100644 --- a/Source/Modules/ControllerModules/InputState.swift +++ b/Source/Modules/ControllerModules/InputState.swift @@ -28,39 +28,40 @@ import Cocoa // 註:所有 InputState 型別均不適合使用 Struct,因為 Struct 無法相互繼承派生。 -/// Represents the states for the input method controller. +/// 此型別用以呈現輸入法控制器(ctlInputMethod)的各種狀態。 /// -/// An input method is actually a finite state machine. It receives the inputs -/// from hardware like keyboard and mouse, changes its state, updates user -/// interface by the state, and finally produces the text output and then them -/// to the client apps. It should be a one-way data flow, and the user interface -/// and text output should follow unconditionally one single data source. +/// 從實際角度來看,輸入法屬於有限態械(Finite State Machine)。其藉由滑鼠/鍵盤 +/// 等輸入裝置接收輸入訊號,據此切換至對應的狀態,再根據狀態更新使用者介面內容, +/// 最終生成文字輸出、遞交給接收文字輸入行為的客體應用。此乃單向資訊流序,且使用 +/// 者介面內容與文字輸出均無條件地遵循某一個指定的資料來源。 /// -/// The InputState class is for representing what the input controller is doing, -/// and the place to store the variables that could be used. For example, the -/// array for the candidate list is useful only when the user is choosing a -/// candidate, and the array should not exist when the input controller is in -/// another state. +/// InputState 型別用以呈現輸入法控制器正在做的事情,且分狀態儲存各種狀態限定的 +/// 常數與變數。對輸入法而言,使用狀態模式(而非策略模式)來做這種常數變數隔離, +/// 可能會讓新手覺得會有些牛鼎烹雞,卻實際上變相減少了在程式維護方面的管理難度、 +/// 不需要再在某個狀態下為了該狀態不需要的變數與常數的處置策略而煩惱。 /// -/// They are immutable objects. When the state changes, the controller should -/// create a new state object to replace the current state instead of modifying -/// the existing one. +/// 對 InputState 型別下的諸多狀態的切換,應以生成新副本來取代舊有副本的形式來完 +/// 成。唯一例外是 InputState.Marking、擁有可以將自身轉變為 InputState.Inputting +/// 的成員函式,但也只是生成副本、來交給輸入法控制器來處理而已。 /// -/// The input controller has following possible states: +/// 輸入法控制器持下述狀態: /// -/// - Deactivated: The user is not using the input method yet. -/// - Empty: The user has switched to this input method but inputted nothing yet, -/// or, he or she has committed text into the client apps and starts a new -/// input phase. -/// - Committing: The input controller is sending text to the client apps. -/// - Inputting: The user has inputted something and the input buffer is -/// visible. -/// - Marking: The user is creating a area in the input buffer and about to -/// create a new user phrase. -/// - Choosing Candidate: The candidate window is open to let the user to choose -/// one among the candidates. +/// - .Deactivated: 使用者沒在使用輸入法。 +/// - .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。因為逐字選字模式不需要在 +/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 +/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給 +/// 客體應用、準備新的輸入行為。 +/// - .EmptyIgnorePreviousState: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些 +/// 內容遞交給客體應用。 +/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 +/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。 +/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 +/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、還是將這個範圍的 +/// 詞音組合放入語彙濾除清單。 +/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。 +/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 class InputState { - /// Represents that the input controller is deactivated. + /// .Deactivated: 使用者沒在使用輸入法。 class Deactivated: InputState { var description: String { "" @@ -69,7 +70,8 @@ class InputState { // MARK: - - /// Represents that the composing buffer is empty. + /// .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。 + /// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。 class Empty: InputState { var composingBuffer: String { "" @@ -82,7 +84,8 @@ class InputState { // MARK: - - /// Represents that the composing buffer is empty. + /// .EmptyIgnorePreviousState: 與 Empty 類似, + /// 但會扔掉上一個狀態的內容、不將這些內容遞交給客體應用。 class EmptyIgnoringPreviousState: Empty { override var description: String { "" @@ -91,30 +94,76 @@ class InputState { // MARK: - - /// Represents that the input controller is committing text into client app. + /// .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 { - "" + "" } } // MARK: - - /// Represents that the composing buffer is not empty. + /// .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。 + /// 因為逐字選字模式不需要在組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 + class AssociatedPhrases: InputState { + private(set) var candidates: [String] = [] + private(set) var isTypingVertical: Bool = false + init(candidates: [String], isTypingVertical: Bool) { + self.candidates = candidates + self.isTypingVertical = isTypingVertical + super.init() + } + + var description: String { + "" + } + } + + // MARK: - + + /// .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。 + /// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 + /// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、 + /// 還是將這個範圍的詞音組合放入語彙濾除清單。 + /// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。 + /// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 class NotEmpty: InputState { private(set) var composingBuffer: String private(set) var cursorIndex: Int = 0 { didSet { cursorIndex = max(cursorIndex, 0) } } + var composingBufferConverted: String { + let converted = IME.kanjiConversionIfRequired(composingBuffer) + if converted.utf16.count != composingBuffer.utf16.count + || converted.count != composingBuffer.count + { + return composingBuffer + } + return converted + } init(composingBuffer: String, cursorIndex: Int) { self.composingBuffer = composingBuffer - self.cursorIndex = cursorIndex + super.init() + defer { self.cursorIndex = cursorIndex } + } + + var attributedString: NSAttributedString { + /// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況, + /// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。 + let attributedString = NSAttributedString( + string: composingBufferConverted, + attributes: [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 0, + ] + ) + return attributedString } var description: String { @@ -124,34 +173,24 @@ class InputState { // MARK: - - /// Represents that the user is inputting text. + /// .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 class Inputting: NotEmpty { - var poppedText: String = "" + var textToCommit: String = "" var tooltip: String = "" override init(composingBuffer: String, cursorIndex: Int) { super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) } - var attributedString: NSAttributedString { - let attributedString = NSAttributedString( - string: composingBuffer, - attributes: [ - .underlineStyle: NSUnderlineStyle.single.rawValue, - .markedClauseSegment: 0, - ] - ) - return attributedString - } - override var description: String { - ", poppedText:\(poppedText)>" + ", textToCommit:\(textToCommit)>" } } // MARK: - - /// Represents that the user is marking a range in the composing buffer. + /// .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、 + /// 還是將這個範圍的詞音組合放入語彙濾除清單。 class Marking: NotEmpty { private var allowedMarkRange: ClosedRange = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength private(set) var markerIndex: Int = 0 { didSet { markerIndex = max(markerIndex, 0) } } @@ -201,7 +240,7 @@ class InputState { let selectedReadings = readings[literalMarkedRange] let joined = selectedReadings.joined(separator: "-") let exist = mgrLangModel.checkIfUserPhraseExist( - userPhrase: text, mode: ctlInputMethod.currentKeyHandler.inputMode, key: joined + userPhrase: text, mode: IME.currentInputMode, key: joined ) if exist { deleteTargetExists = exist @@ -223,16 +262,18 @@ class InputState { private(set) var readings: [String] init(composingBuffer: String, cursorIndex: Int, markerIndex: Int, readings: [String]) { - self.markerIndex = markerIndex let begin = min(cursorIndex, markerIndex) let end = max(cursorIndex, markerIndex) markedRange = begin.." } @@ -344,22 +374,7 @@ class InputState { // MARK: - - /// Represents that the user is choosing in a candidates list - /// in the associated phrases mode. - class AssociatedPhrases: InputState { - private(set) var candidates: [String] = [] - private(set) var isTypingVertical: Bool = false - init(candidates: [String], isTypingVertical: Bool) { - self.candidates = candidates - self.isTypingVertical = isTypingVertical - super.init() - } - - var description: String { - "" - } - } - + /// .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 class SymbolTable: ChoosingCandidate { var node: SymbolNode @@ -393,75 +408,3 @@ class InputState { } } } - -class SymbolNode { - var title: String - var children: [SymbolNode]? - - init(_ title: String, _ children: [SymbolNode]? = nil) { - self.title = title - self.children = children - } - - init(_ title: String, symbols: String) { - self.title = title - children = Array(symbols).map { SymbolNode(String($0), nil) } - } - - static let catCommonSymbols = String( - format: NSLocalizedString("catCommonSymbols", comment: "")) - static let catHoriBrackets = String( - format: NSLocalizedString("catHoriBrackets", comment: "")) - static let catVertBrackets = String( - format: NSLocalizedString("catVertBrackets", comment: "")) - static let catGreekLetters = String( - format: NSLocalizedString("catGreekLetters", comment: "")) - static let catMathSymbols = String( - format: NSLocalizedString("catMathSymbols", comment: "")) - static let catCurrencyUnits = String( - format: NSLocalizedString("catCurrencyUnits", comment: "")) - static let catSpecialSymbols = String( - format: NSLocalizedString("catSpecialSymbols", comment: "")) - static let catUnicodeSymbols = String( - format: NSLocalizedString("catUnicodeSymbols", comment: "")) - static let catCircledKanjis = String( - format: NSLocalizedString("catCircledKanjis", comment: "")) - static let catCircledKataKana = String( - format: NSLocalizedString("catCircledKataKana", comment: "")) - static let catBracketKanjis = String( - format: NSLocalizedString("catBracketKanjis", comment: "")) - static let catSingleTableLines = String( - format: NSLocalizedString("catSingleTableLines", comment: "")) - static let catDoubleTableLines = String( - format: NSLocalizedString("catDoubleTableLines", comment: "")) - static let catFillingBlocks = String( - format: NSLocalizedString("catFillingBlocks", comment: "")) - static let catLineSegments = String( - format: NSLocalizedString("catLineSegments", comment: "")) - - static let root: SymbolNode = .init( - "/", - [ - SymbolNode("`"), - SymbolNode(catCommonSymbols, symbols: ",、。.?!;:‧‥﹐﹒˙·‘’“”〝〞‵′〃~$%@&#*"), - SymbolNode(catHoriBrackets, symbols: "()「」〔〕{}〈〉『』《》【】﹙﹚﹝﹞﹛﹜"), - SymbolNode(catVertBrackets, symbols: "︵︶﹁﹂︹︺︷︸︿﹀﹃﹄︽︾︻︼"), - SymbolNode( - catGreekLetters, symbols: "αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ" - ), - SymbolNode(catMathSymbols, symbols: "+-×÷=≠≒∞±√<>﹤﹥≦≧∩∪ˇ⊥∠∟⊿㏒㏑∫∮∵∴╳﹢"), - SymbolNode(catCurrencyUnits, symbols: "$€¥¢£₽₨₩฿₺₮₱₭₴₦৲৳૱௹﷼₹₲₪₡₫៛₵₢₸₤₳₥₠₣₰₧₯₶₷"), - SymbolNode(catSpecialSymbols, symbols: "↑↓←→↖↗↙↘↺⇧⇩⇦⇨⇄⇆⇅⇵↻◎○●⊕⊙※△▲☆★◇◆□■▽▼§¥〒¢£♀♂↯"), - SymbolNode(catUnicodeSymbols, symbols: "♨☀☁☂☃♠♥♣♦♩♪♫♬☺☻"), - SymbolNode(catCircledKanjis, symbols: "㊟㊞㊚㊛㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗︎㊘㊙︎㊜㊝㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰🈚︎🈯︎"), - SymbolNode( - catCircledKataKana, symbols: "㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋾" - ), - SymbolNode(catBracketKanjis, symbols: "㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃"), - SymbolNode(catSingleTableLines, symbols: "├─┼┴┬┤┌┐╞═╪╡│▕└┘╭╮╰╯"), - SymbolNode(catDoubleTableLines, symbols: "╔╦╗╠═╬╣╓╥╖╒╤╕║╚╩╝╟╫╢╙╨╜╞╪╡╘╧╛"), - SymbolNode(catFillingBlocks, symbols: "_ˍ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉◢◣◥◤"), - SymbolNode(catLineSegments, symbols: "﹣﹦≡|∣∥–︱—︳╴¯ ̄﹉﹊﹍﹎﹋﹌﹏︴∕﹨╱╲/\"), - ] - ) -} diff --git a/Source/Modules/ControllerModules/KeyHandler_Core.swift b/Source/Modules/ControllerModules/KeyHandler_Core.swift index 447da1a2..83e7a27f 100644 --- a/Source/Modules/ControllerModules/KeyHandler_Core.swift +++ b/Source/Modules/ControllerModules/KeyHandler_Core.swift @@ -24,16 +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 -public enum InputMode: String { - case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS" - case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT" - case imeModeNULL = "" -} - -// MARK: - Delegate. +// MARK: - 委任協定 (Delegate). +/// KeyHandler 委任協定 protocol KeyHandlerDelegate { func ctlCandidate() -> ctlCandidate func keyHandler( @@ -44,80 +43,81 @@ protocol KeyHandlerDelegate { -> Bool } -// MARK: - Kernel. +// MARK: - 核心 (Kernel). +/// KeyHandler 按鍵調度模組。 class KeyHandler { + /// 半衰模組的衰減指數 let kEpsilon: Double = 0.000001 - let kMaxComposingBufferNeedsToWalkSize: Int = 10 - var _composer: Tekkon.Composer = .init() - var _inputMode: String = "" - var _languageModel: vChewing.LMInstantiator = .init() - var _userOverrideModel: vChewing.LMUserOverride = .init() - var _builder: Megrez.BlockReadingBuilder - var _walkedNodes: [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? - var inputMode: InputMode { - get { - switch _inputMode { - case "org.atelierInmu.inputmethod.vChewing.IMECHS": - return InputMode.imeModeCHS - case "org.atelierInmu.inputmethod.vChewing.IMECHT": - return InputMode.imeModeCHT - default: - return InputMode.imeModeNULL - } - } - set { + /// InputMode 需要在每次出現內容變更的時候都連帶重設組字器與各項語言模組, + /// 順帶更新 IME 模組及 UserPrefs 當中對於當前語言模式的記載。 + var inputMode: InputMode = IME.currentInputMode { + willSet { + // 這個標籤在下文會用到。 let isCHS: Bool = (newValue == InputMode.imeModeCHS) - - // 緊接著將新的簡繁輸入模式提報給 ctlInputMethod: - ctlInputMethod.currentInputMode = isCHS ? InputMode.imeModeCHS.rawValue : InputMode.imeModeCHT.rawValue - mgrPrefs.mostRecentInputMode = ctlInputMethod.currentInputMode - - // 拿當前的 _inputMode 與 ctlInputMethod 的提報結果對比,不同的話則套用新設定: - if _inputMode != ctlInputMethod.currentInputMode { - // Reinitiate language models if necessary - _languageModel = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT - _userOverrideModel = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT - - // Synchronize the sub-languageModel state settings to the new LM. - syncBaseLMPrefs() - - // Create new grid builder and clear the composer. - createNewBuilder() - _composer.clear() - } - // 直接寫到衛星模組內,省得類型轉換 - _inputMode = ctlInputMethod.currentInputMode + /// 將新的簡繁輸入模式提報給 ctlInputMethod 與 IME 模組。 + IME.currentInputMode = newValue + mgrPrefs.mostRecentInputMode = IME.currentInputMode.rawValue + /// 重設所有語言模組。這裡不需要做按需重設,因為對運算量沒有影響。 + currentLM = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT + currentUOM = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT + /// 將與主語言模組有關的選項同步至主語言模組內。 + syncBaseLMPrefs() + /// 重建新的組字器,且清空注拼槽+同步最新的注拼槽排列設定。 + /// 組字器只能藉由重建才可以與當前新指派的語言模組對接。 + ensureCompositor() + ensureParser() } } + /// 初期化。 public init() { - _builder = Megrez.BlockReadingBuilder(lm: _languageModel, separator: "-") + /// 組字器初期化。因為是首次初期化變數,所以這裡不能用 ensureCompositor() 代勞。 + compositor = Megrez.Compositor(lm: currentLM, separator: "-") + /// 注拼槽初期化。 ensureParser() - inputMode = InputMode(rawValue: ctlInputMethod.currentInputMode) ?? InputMode.imeModeNULL + /// 讀取最近的簡繁體模式、且將該屬性內容塞到 inputMode 當中。 + /// 這句必須用 defer 來處理,否則不會觸發其 willSet 部分的內容。 + defer { inputMode = IME.currentInputMode } } func clear() { - _composer.clear() - _builder.clear() - _walkedNodes.removeAll() + composer.clear() + compositor.clear() + walkedAnchors.removeAll() } // MARK: - Functions dealing with Megrez. - 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. - _walkedNodes = _builder.walk() + /// 實際上要拿給 Megrez 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。 + /// + /// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。 + var actualCandidateCursorIndex: Int { + mgrPrefs.useRearCursorMode ? min(compositorCursorIndex, compositorLength - 1) : max(compositorCursorIndex, 1) + } - // if DEBUG mode is enabled, a GraphViz file is written to kGraphVizOutputfile. + /// 利用給定的讀音鏈來試圖爬取最接近的組字結果(最大相似度估算)。 + /// + /// 該過程讀取的權重資料是經過 Viterbi 演算法計算得到的結果。 + /// + /// 該函式的爬取順序是從頭到尾。 + func walk() { + walkedAnchors = compositor.walk() + + // 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。 if mgrPrefs.isDebugModeEnabled { - let result = _builder.grid.dumpDOT + let result = compositor.grid.dumpDOT do { try result.write( toFile: "/private/var/tmp/vChewing-visualization.dot", @@ -129,49 +129,54 @@ 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 = "" - if _builder.grid.width > mgrPrefs.composingBufferSize { - if !_walkedNodes.isEmpty { - let anchor: Megrez.NodeAnchor = _walkedNodes[0] + 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 } - _builder.removeHeadReadings(count: anchor.spanningLength) + compositor.removeHeadReadings(count: anchor.spanningLength) } } walk() - return poppedText + return textToCommit } + /// 用以組建聯想詞陣列的函式。 + /// - Parameter key: 給定的聯想詞的開頭字。 + /// - Returns: 抓取到的聯想詞陣列。 + /// 不會是 nil,但那些負責接收結果的函式會對空白陣列結果做出正確的處理。 func buildAssociatePhraseArray(withKey key: String) -> [String] { var arrResult: [String] = [] - if _languageModel.hasAssociatedPhrasesForKey(key) { - arrResult.append(contentsOf: _languageModel.associatedPhrasesForKey(key)) + if currentLM.hasAssociatedPhrasesForKey(key) { + arrResult.append(contentsOf: currentLM.associatedPhrasesForKey(key)) } return arrResult } + /// 在組字器內,以給定之候選字字串、來試圖在給定游標位置所在之處指定選字處理過程。 + /// 然後再將對應的節錨內的節點標記為「已經手動選字過」。 + /// - Parameters: + /// - value: 給定之候選字字串。 + /// - respectCursorPushing: 若該選項為 true,則會在選字之後始終將游標推送至選字厚的節錨的前方。 func fixNode(value: String, respectCursorPushing: Bool = true) { - let cursorIndex = min(actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), builderLength) - _builder.grid.fixNodeSelectedCandidate(location: cursorIndex, value: value) + let cursorIndex = min(actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), compositorLength) + compositor.grid.fixNodeSelectedCandidate(location: cursorIndex, value: value) // // 因半衰模組失能,故禁用之。 - // let selectedNode: Megrez.NodeAnchor = _builder.grid.fixNodeSelectedCandidate( + // let selectedNode: Megrez.NodeAnchor = compositor.grid.fixNodeSelectedCandidate( // location: cursorIndex, value: value // ) // // 不要針對逐字選字模式啟用臨時半衰記憶模型。 // 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.") @@ -179,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 @@ -188,26 +193,49 @@ class KeyHandler { // } // if addToUserOverrideModel { // IME.prtDebugIntel("UOM: Start Observation.") - // _userOverrideModel.observe( - // walkedNodes: _walkedNodes, cursorIndex: cursorIndex, candidate: value, + // // 令半衰記憶模組觀測給定的 trigram。 + // // 這個過程會讓半衰引擎根據當前上下文生成 trigram 索引鍵。 + // currentUOM.observe( + // walkedNodes: walkedAnchors, cursorIndex: cursorIndex, candidate: value, // timestamp: NSDate().timeIntervalSince1970 // ) // } // } walk() + /// 若偏好設定內啟用了相關選項,則會在選字之後始終將游標推送至選字厚的節錨的前方。 if mgrPrefs.moveCursorAfterSelectingCandidate, respectCursorPushing { var nextPosition = 0 - for node in _walkedNodes { + for node in walkedAnchors { if nextPosition >= cursorIndex { break } nextPosition += node.spanningLength } - if nextPosition <= builderLength { - builderCursorIndex = nextPosition + if nextPosition <= compositorLength { + compositorCursorIndex = nextPosition } } } + /// 組字器內超出最大動態爬軌範圍的節錨都會被自動標記為「已經手動選字過」,減少爬軌運算負擔。 + func markNodesFixedIfNecessary() { + let width = compositor.grid.width + if width <= kMaxComposingBufferNeedsToWalkSize { + return + } + 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 + ) + } + index += anchor.spanningLength + } + } + + /// 獲取候選字詞陣列資料內容。 var candidatesArray: [String] { var arrCandidates: [String] = [] var arrNodes: [Megrez.NodeAnchor] = [] @@ -233,20 +261,23 @@ class KeyHandler { return arrCandidates } + /// 向半衰引擎詢問可能的選字建議。 func dealWithOverrideModelSuggestions() { + /// 先就當前上下文讓半衰引擎重新生成 trigram 索引鍵。 let overrideValue = mgrPrefs.useSCPCTypingMode ? "" - : _userOverrideModel.suggest( - walkedNodes: _walkedNodes, cursorIndex: builderCursorIndex, + : currentUOM.suggest( + walkedNodes: walkedAnchors, cursorIndex: compositorCursorIndex, timestamp: NSDate().timeIntervalSince1970 ) + /// 再拿著索引鍵去問半衰模組有沒有選字建議。有的話就遵循之、讓天權星引擎對指定節錨下的節點複寫權重。 if !overrideValue.isEmpty { IME.prtDebugIntel( "UOM: Suggestion retrieved, overriding the node score of the selected candidate.") - _builder.grid.overrideNodeScoreForSelectedCandidate( - location: min(actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), builderLength), + compositor.grid.overrideNodeScoreForSelectedCandidate( + location: min(actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), compositorLength), value: overrideValue, overridingScore: findHighestScore(nodes: rawNodes, epsilon: kEpsilon) ) @@ -255,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 { @@ -270,92 +306,144 @@ class KeyHandler { // MARK: - Extracted methods and functions (Tekkon). + /// 獲取與當前注音排列或拼音輸入種類有關的標點索引鍵,以英數下畫線「_」結尾。 + var currentMandarinParser: String { + mgrPrefs.mandarinParserName + "_" + } + + /// 給注拼槽指定注音排列或拼音輸入種類之後,將注拼槽內容清空。 func ensureParser() { switch mgrPrefs.mandarinParser { case MandarinParser.ofStandard.rawValue: - _composer.ensureParser(arrange: .ofDachen) + composer.ensureParser(arrange: .ofDachen) case MandarinParser.ofDachen26.rawValue: - _composer.ensureParser(arrange: .ofDachen26) - case MandarinParser.ofEten.rawValue: - _composer.ensureParser(arrange: .ofEten) + composer.ensureParser(arrange: .ofDachen26) + case MandarinParser.ofETen.rawValue: + composer.ensureParser(arrange: .ofETen) case MandarinParser.ofHsu.rawValue: - _composer.ensureParser(arrange: .ofHsu) - case MandarinParser.ofEten26.rawValue: - _composer.ensureParser(arrange: .ofEten26) + composer.ensureParser(arrange: .ofHsu) + case MandarinParser.ofETen26.rawValue: + composer.ensureParser(arrange: .ofETen26) case MandarinParser.ofIBM.rawValue: - _composer.ensureParser(arrange: .ofIBM) + composer.ensureParser(arrange: .ofIBM) case MandarinParser.ofMiTAC.rawValue: - _composer.ensureParser(arrange: .ofMiTAC) + composer.ensureParser(arrange: .ofMiTAC) case MandarinParser.ofFakeSeigyou.rawValue: - _composer.ensureParser(arrange: .ofFakeSeigyou) + composer.ensureParser(arrange: .ofFakeSeigyou) case MandarinParser.ofHanyuPinyin.rawValue: - _composer.ensureParser(arrange: .ofHanyuPinyin) + composer.ensureParser(arrange: .ofHanyuPinyin) case MandarinParser.ofSecondaryPinyin.rawValue: - _composer.ensureParser(arrange: .ofSecondaryPinyin) + composer.ensureParser(arrange: .ofSecondaryPinyin) case MandarinParser.ofYalePinyin.rawValue: - _composer.ensureParser(arrange: .ofYalePinyin) + composer.ensureParser(arrange: .ofYalePinyin) case MandarinParser.ofHualuoPinyin.rawValue: - _composer.ensureParser(arrange: .ofHualuoPinyin) + composer.ensureParser(arrange: .ofHualuoPinyin) case MandarinParser.ofUniversalPinyin.rawValue: - _composer.ensureParser(arrange: .ofUniversalPinyin) + composer.ensureParser(arrange: .ofUniversalPinyin) default: - _composer.ensureParser(arrange: .ofDachen) + composer.ensureParser(arrange: .ofDachen) mgrPrefs.mandarinParser = MandarinParser.ofStandard.rawValue } - _composer.clear() + composer.clear() + } + + /// 用於網頁 Ruby 的注音需要按照教科書印刷的方式來顯示輕聲。該函式負責這種轉換。 + /// - Parameters: + /// - target: 要拿來做轉換處理的讀音鏈。 + /// - newSeparator: 新的讀音分隔符。 + /// - Returns: 經過轉換處理的讀音鏈。 + func cnvZhuyinKeyToTextbookReading(target: String, newSeparator: String = "-") -> String { + var arrReturn: [String] = [] + for neta in target.split(separator: "-") { + var newString = String(neta) + if String(neta.reversed()[0]) == "˙" { + newString = String(neta.dropLast()) + newString.insert("˙", at: newString.startIndex) + } + arrReturn.append(newString) + } + return arrReturn.joined(separator: newSeparator) + } + + /// 用於網頁 Ruby 的拼音的陰平必須顯示,這裡處理一下。 + /// - Parameters: + /// - target: 要拿來做轉換處理的讀音鏈。 + /// - newSeparator: 新的讀音分隔符。 + /// - Returns: 經過轉換處理的讀音鏈。 + func restoreToneOneInZhuyinKey(target: String, newSeparator: String = "-") -> String { + var arrReturn: [String] = [] + for neta in target.split(separator: "-") { + var newNeta = String(neta) + if !"ˊˇˋ˙".contains(String(neta.reversed()[0])), !neta.contains("_") { + newNeta += "1" + } + arrReturn.append(newNeta) + } + return arrReturn.joined(separator: newSeparator) } // MARK: - Extracted methods and functions (Megrez). - var isBuilderEmpty: Bool { _builder.grid.width == 0 } + /// 組字器是否為空。 + var isCompositorEmpty: Bool { compositor.isEmpty } + /// 獲取原始節錨資料陣列。 var rawNodes: [Megrez.NodeAnchor] { /// 警告:不要對游標前置風格使用 nodesCrossing,否則會導致游標行為與 macOS 內建注音輸入法不一致。 /// 微軟新注音輸入法的游標後置風格也是不允許 nodeCrossing 的。 mgrPrefs.useRearCursorMode - ? _builder.grid.nodesBeginningAt(location: actualCandidateCursorIndex) - : _builder.grid.nodesEndingAt(location: actualCandidateCursorIndex) + ? compositor.grid.nodesBeginningAt(location: actualCandidateCursorIndex) + : compositor.grid.nodesEndingAt(location: actualCandidateCursorIndex) } + /// 將輸入法偏好設定同步至語言模組內。 func syncBaseLMPrefs() { - _languageModel.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled - _languageModel.isCNSEnabled = mgrPrefs.cns11643Enabled - _languageModel.isSymbolEnabled = mgrPrefs.symbolInputEnabled + currentLM.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled + currentLM.isCNSEnabled = mgrPrefs.cns11643Enabled + currentLM.isSymbolEnabled = mgrPrefs.symbolInputEnabled } - func createNewBuilder() { - // Each Mandarin syllable is separated by a hyphen. - _builder = Megrez.BlockReadingBuilder(lm: _languageModel, separator: "-") + /// 令組字器重新初期化,使其與被重新指派過的主語言模組對接。 + func ensureCompositor() { + // 每個漢字讀音都由一個西文半形減號分隔開。 + compositor = Megrez.Compositor(lm: currentLM, separator: "-") } - var currentReadings: [String] { _builder.readings } + /// 自組字器獲取目前的讀音陣列。 + var currentReadings: [String] { compositor.readings } + /// 以給定的(讀音)索引鍵,來檢測當前主語言模型內是否有對應的資料在庫。 func ifLangModelHasUnigrams(forKey reading: String) -> Bool { - _languageModel.hasUnigramsFor(key: reading) + currentLM.hasUnigramsFor(key: reading) } - func insertReadingToBuilderAtCursor(reading: String) { - _builder.insertReadingAtCursor(reading: reading) + /// 在組字器的給定游標位置內插入讀音。 + func insertToCompositorAtCursor(reading: String) { + compositor.insertReadingAtCursor(reading: reading) } - var builderCursorIndex: Int { - get { _builder.cursorIndex } - set { _builder.cursorIndex = newValue } + /// 組字器的游標位置。 + var compositorCursorIndex: Int { + get { compositor.cursorIndex } + set { compositor.cursorIndex = newValue } } - var builderLength: Int { - _builder.length + /// 組字器的目前的長度。 + var compositorLength: Int { + compositor.length } - func deleteBuilderReadingInFrontOfCursor() { - _builder.deleteReadingAtTheRearOfCursor() + /// 在組字器內,朝著與文字輸入方向相反的方向、砍掉一個與游標相鄰的讀音。 + /// + /// 在威注音的術語體系當中,「與文字輸入方向相反的方向」為向後(Rear)。 + func deleteCompositorReadingAtTheRearOfCursor() { + compositor.deleteReadingAtTheRearOfCursor() } - func deleteBuilderReadingToTheFrontOfCursor() { - _builder.deleteReadingToTheFrontOfCursor() - } - - var keyLengthAtIndexZero: Int { - _walkedNodes[0].node?.currentKeyValue.value.count ?? 0 + /// 在組字器內,朝著往文字輸入方向、砍掉一個與游標相鄰的讀音。 + /// + /// 在威注音的術語體系當中,「文字輸入方向」為向前(Front)。 + func deleteCompositorReadingToTheFrontOfCursor() { + compositor.deleteReadingToTheFrontOfCursor() } } diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift index a57d3bc3..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 @@ -52,12 +54,12 @@ extension KeyHandler { if cancelCandidateKey { if (state is InputState.AssociatedPhrases) || mgrPrefs.useSCPCTypingMode - || isBuilderEmpty + || isCompositorEmpty { // 如果此時發現當前組字緩衝區為真空的情況的話, // 就將當前的組字緩衝區析構處理、強制重設輸入狀態。 // 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。 - // 所以這裡需要對 isBuilderEmpty 做判定。 + // 所以這裡需要對 isCompositorEmpty 做判定。 clear() stateCallback(InputState.EmptyIgnoringPreviousState()) } else { @@ -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,11 +352,13 @@ extension KeyHandler { ] let customPunctuation: String = arrCustomPunctuations.joined(separator: "") + /// 如果仍無匹配結果的話,看看這個輸入是否是不需要修飾鍵的那種標點鍵輸入。 + let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))] let punctuation: String = arrPunctuations.joined(separator: "") var shouldAutoSelectCandidate: Bool = - _composer.inputValidityCheck(key: charCode) || ifLangModelHasUnigrams(forKey: customPunctuation) + composer.inputValidityCheck(key: charCode) || ifLangModelHasUnigrams(forKey: customPunctuation) || ifLangModelHasUnigrams(forKey: punctuation) if !shouldAutoSelectCandidate, input.isUpperCaseASCIILetterKey { diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift index a0895d56..87c21d05 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift @@ -24,10 +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. */ -import Cocoa -import SwiftUI +/// 該檔案乃按鍵調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給按鍵調度模組處理時、 +/// 按鍵調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。 -// MARK: - § Handle Input with States. +import Cocoa + +// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) extension KeyHandler { func handle( @@ -37,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 } @@ -53,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 { @@ -63,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, @@ -101,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( @@ -115,7 +118,7 @@ extension KeyHandler { ) } - // MARK: Handle Associated Phrases. + // MARK: 處理聯想詞 (Handle Associated Phrases) if state is InputState.AssociatedPhrases { if handleCandidate( @@ -127,7 +130,7 @@ extension KeyHandler { } } - // MARK: Handle Marking. + // MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking) if let marking = state as? InputState.Marking { if handleMarkingState( @@ -140,63 +143,72 @@ 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. - if !skipPhoneticHandling && _composer.inputValidityCheck(key: charCode) { - _composer.receiveKey(fromCharCode: charCode) + // 這裡 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 - // builder in other words, if we don't have a tone marker, we just - // update the composing buffer. - let composeReading = _composer.hasToneMarker() + // 沒有調號的話,只需要 updateClientComposingBuffer() 且終止處理(return true)即可。 + // 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。 + let composeReading = composer.hasToneMarker() if !composeReading { stateCallback(buildInputtingState) return true } } - var composeReading = _composer.hasToneMarker() // 這裡不需要做排他性判斷。 + 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 "|=". - composeReading = composeReading || (!_composer.isEmpty && (input.isSpace || input.isEnter)) + // 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 _composer 內的注音來做檢查了。 + // 來看看詞庫內到底有沒有對應的讀音索引。這裡用了類似「|=」的判斷處理方式。 + composeReading = composeReading || (!composer.isEmpty && (input.isSpace || input.isEnter)) if composeReading { - if input.isSpace, !_composer.hasToneMarker() { - _composer.receiveKey(fromString: " ") // 補上空格。 + if input.isSpace, !composer.hasToneMarker() { + // 補上空格,否則倚天忘形與許氏排列某些音無法響應不了陰平聲調。 + // 小麥注音因為使用 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((builderLength == 0) ? InputState.EmptyIgnoringPreviousState() : buildInputtingState) - return true + composer.clear() + // 根據「組字器是否為空」來判定回呼哪一種狀態 + stateCallback((compositorLength == 0) ? InputState.EmptyIgnoringPreviousState() : buildInputtingState) + return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了 } - // ... and insert it into the grid... - insertReadingToBuilderAtCursor(reading: reading) + // 將該讀音插入至組字器內的軌格當中 + insertToCompositorAtCursor(reading: reading) - // ... then walk the grid... - let poppedText = popOverflowComposingTextAndWalk + // 讓組字器反爬軌格 + let textToCommit = popOverflowComposingTextAndWalk - // ... get and tweak override model suggestion if possible... + // 看看半衰記憶模組是否會對目前的狀態給出自動選字建議 // dealWithOverrideModelSuggestions() // 暫時禁用,因為無法使其生效。 - // ... then update the text. - _composer.clear() + // 將組字器內超出最大動態爬軌範圍的節錨都標記為「已經手動選字過」,減少之後的爬軌運算負擔。 + markNodesFixedIfNecessary() + // 之後就是更新組字區了。先清空注拼槽的內容。 + composer.clear() + + // 再以回呼組字狀態的方式來執行 updateClientComposingBuffer()。 let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) + /// 逐字選字模式的處理。 if mgrPrefs.useSCPCTypingMode { let choosingCandidates: InputState.ChoosingCandidate = buildCandidate( state: inputting, @@ -205,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()) @@ -225,45 +237,52 @@ 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, + // 用上下左右鍵呼叫選字窗。 + + 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 - if input.isShiftHold || !mgrPrefs.chooseCandidateUsingSpace { - if builderCursorIndex >= builderLength { + /// 倘若沒有在偏好設定內將 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: " ") { - insertReadingToBuilderAtCursor(reading: " ") - let poppedText = popOverflowComposingTextAndWalk + insertToCompositorAtCursor(reading: " ") + let textToCommit = popOverflowComposingTextAndWalk let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) } return true + } else if input.isShiftHold { // 臉書等網站會攔截 Tab 鍵,所以用 Shift+CMD+Space 對候選字詞做正向/反向輪替。 + return handleInlineCandidateRotation( + state: state, reverseModifier: input.isCommandHold, stateCallback: stateCallback, + errorCallback: errorCallback + ) } } stateCallback(buildCandidate(state: currentState, isTypingVertical: input.isTypingVertical)) @@ -279,8 +298,8 @@ extension KeyHandler { // MARK: Tab if input.isTab { - return handleTab( - state: state, isShiftHold: input.isShiftHold, stateCallback: stateCallback, errorCallback: errorCallback + return handleInlineCandidateRotation( + state: state, reverseModifier: input.isShiftHold, stateCallback: stateCallback, errorCallback: errorCallback ) } @@ -362,11 +381,11 @@ extension KeyHandler { if input.isSymbolMenuPhysicalKey && !input.isShiftHold { if input.isOptionHold { if ifLangModelHasUnigrams(forKey: "_punctuation_list") { - if _composer.isEmpty { - insertReadingToBuilderAtCursor(reading: "_punctuation_list") - let poppedText: String! = popOverflowComposingTextAndWalk + if composer.isEmpty { + insertToCompositorAtCursor(reading: "_punctuation_list") + 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 @@ -378,7 +397,7 @@ extension KeyHandler { } else { // 得在這裡先 commit buffer,不然會導致「在摁 ESC 離開符號選單時會重複輸入上一次的組字區的內容」的不當行為。 // 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。 - // 這裡不需要該函數所傳回的 bool 結果,所以用「_ =」解消掉。 + // 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。 _ = handleEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback) stateCallback(InputState.SymbolTable(node: SymbolNode.root, isTypingVertical: input.isTypingVertical)) return true @@ -387,7 +406,9 @@ extension KeyHandler { // MARK: Punctuation - // If nothing is matched, see if it's a punctuation key for current layout. + /// 如果仍無匹配結果的話,先看一下: + /// - 是否是針對當前注音排列/拼音輸入種類專門提供的標點符號。 + /// - 是否是需要摁修飾鍵才可以輸入的那種標點符號。 var punctuationNamePrefix = "" @@ -418,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: "") @@ -446,14 +468,13 @@ 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 按鍵干擾組字區」的問題。 - // 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 - if (state is InputState.NotEmpty) || !_composer.isEmpty { + /// 對剩下的漏網之魚做攔截處理、直接將當前狀態繼續回呼給 ctlInputMethod。 + /// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 + /// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 + /// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 + if (state is InputState.NotEmpty) || !composer.isEmpty { IME.prtDebugIntel( "Blocked data: charCode: \(charCode), keyCode: \(input.keyCode)") IME.prtDebugIntel("A9BFF20E") diff --git a/Source/Modules/ControllerModules/KeyHandler_Misc.swift b/Source/Modules/ControllerModules/KeyHandler_Misc.swift deleted file mode 100644 index e3e0b1a6..00000000 --- a/Source/Modules/ControllerModules/KeyHandler_Misc.swift +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). -// Refactored from the ObjCpp-version of this class by: -// (c) 2011 and onwards The OpenVanilla Project (MIT 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 - -// MARK: - § Misc functions. - -extension KeyHandler { - var currentMandarinParser: String { - mgrPrefs.mandarinParserName + "_" - } - - var actualCandidateCursorIndex: Int { - var cursorIndex = builderCursorIndex - switch mgrPrefs.useRearCursorMode { - case false: - do { - // macOS built-in Zhuyin style. - // (i.e. the cursor is always in front of the phrase.) - // No crossing. - switch cursorIndex { - case 0: cursorIndex = 1 - default: break - } - } - case true: - do { - // Microsoft new phonetics style. - // (i.e. the cursor is always at the rear of the phrase.) - // No crossing. - switch cursorIndex { - case builderLength: cursorIndex -= 1 - default: break - } - } - } - return cursorIndex - } - - // 用於網頁 Ruby 的注音需要按照教科書印刷的方式來顯示輕聲,所以這裡處理一下。 - func cnvZhuyinKeyToTextbookReading(target: String, newSeparator: String = "-") -> String { - var arrReturn: [String] = [] - for neta in target.split(separator: "-") { - var newString = String(neta) - if String(neta.reversed()[0]) == "˙" { - newString = String(neta.dropLast()) - newString.insert("˙", at: newString.startIndex) - } - arrReturn.append(newString) - } - return arrReturn.joined(separator: newSeparator) - } - - // 用於網頁 Ruby 的拼音的陰平必須顯示,這裡處理一下。 - func restoreToneOneInZhuyinKey(target: String, newSeparator: String = "-") -> String { - var arrReturn: [String] = [] - for neta in target.split(separator: "-") { - var newNeta = String(neta) - if !"ˊˇˋ˙".contains(String(neta.reversed()[0])), !neta.contains("_") { - newNeta += "1" - } - arrReturn.append(newNeta) - } - return arrReturn.joined(separator: newSeparator) - } -} diff --git a/Source/Modules/ControllerModules/KeyHandler_States.swift b/Source/Modules/ControllerModules/KeyHandler_States.swift index 6c516dbc..82b76b1d 100644 --- a/Source/Modules/ControllerModules/KeyHandler_States.swift +++ b/Source/Modules/ControllerModules/KeyHandler_States.swift @@ -24,68 +24,66 @@ 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(). - for walkedNode in _walkedNodes { + /// 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 builder'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 <= builderCursorIndex { + if readingCursorIndex + spanningLength <= compositorCursorIndex { composedStringCursorIndex += strNodeValue.utf16.count readingCursorIndex += spanningLength } else { if codepointCount == spanningLength { var i = 0 - while i < codepointCount, readingCursorIndex < builderCursorIndex { + while i < codepointCount, readingCursorIndex < compositorCursorIndex { composedStringCursorIndex += arrSplit[i].utf16.count readingCursorIndex += 1 i += 1 } } else { - if readingCursorIndex < builderCursorIndex { + if readingCursorIndex < compositorCursorIndex { composedStringCursorIndex += strNodeValue.utf16.count readingCursorIndex += spanningLength - if readingCursorIndex > builderCursorIndex { - readingCursorIndex = builderCursorIndex - } - // 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. - switch builderCursorIndex { - case _builder.readings.count...: - tooltipParameterRef[0] = _builder.readings[_builder.readings.count - 1] + readingCursorIndex = min(readingCursorIndex, compositorCursorIndex) + /// 接下來再處理這麼一種情況: + /// 某些錨點內的當前候選字詞長度與讀音長度不相等。 + /// 但此時游標還是按照每個讀音單位來移動的, + /// 所以需要上下文工具提示來顯示游標的相對位置。 + /// 這裡先計算一下要用在工具提示當中的顯示參數的內容。 + switch compositorCursorIndex { + case compositor.readings.count...: + tooltipParameterRef[0] = compositor.readings[compositor.readings.count - 1] case 0: - tooltipParameterRef[1] = _builder.readings[builderCursorIndex] + tooltipParameterRef[1] = compositor.readings[compositorCursorIndex] default: do { - tooltipParameterRef[0] = _builder.readings[builderCursorIndex - 1] - tooltipParameterRef[1] = _builder.readings[builderCursorIndex] + tooltipParameterRef[0] = compositor.readings[compositorCursorIndex - 1] + tooltipParameterRef[1] = compositor.readings[compositorCursorIndex] } } } @@ -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,49 @@ extension KeyHandler { } } + /// 現在呢,咱們拿到了游標前後的 stringview 資料,準備著手生成要在組字區內顯示用的內容。 + /// 在這對前後資料當中插入目前正在輸入的讀音資料即可。 let head = String(utf16CodeUnits: arrHead, count: arrHead.count) - let reading = _composer.getInlineCompositionForIMK(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer) + 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) + var cleanedComposition = "" - // 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] - ) + // 防止組字區內出現不可列印的字元。 + for theChar in composedText { + if let charCode = theChar.utf16.first { + if !(theChar.isASCII && !(charCode.isPrintable())) { + cleanedComposition += String(theChar) + } + } } + /// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。 + let stateResult = InputState.Inputting(composingBuffer: cleanedComposition, cursorIndex: cursorIndex) + + /// 根據上文的參數結果來決定生成怎樣的工具提示。 + 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 +157,11 @@ extension KeyHandler { // MARK: - 用以生成候選詞陣列及狀態 + /// 拿著給定的候選字詞陣列資料內容,切換至選字狀態。 + /// - Parameters: + /// - currentState: 當前狀態。 + /// - isTypingVertical: 是否縱排輸入? + /// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。 func buildCandidate( state currentState: InputState.NotEmpty, isTypingVertical: Bool = false @@ -159,13 +176,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 +201,13 @@ extension KeyHandler { // MARK: - 用以處理就地新增自訂語彙時的行為 + /// 用以處理就地新增自訂語彙時的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - input: 輸入按鍵訊號。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleMarkingState( _ state: InputState.Marking, input: InputSignal, @@ -246,8 +276,16 @@ extension KeyHandler { return false } - // MARK: - 標點輸入處理 + // MARK: - 標點輸入的處理 + /// 標點輸入的處理。 + /// - Parameters: + /// - customPunctuation: 自訂標點索引鍵頭。 + /// - state: 當前狀態。 + /// - isTypingVertical: 是否縱排輸入? + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handlePunctuation( _ customPunctuation: String, state: InputState, @@ -259,22 +297,22 @@ extension KeyHandler { return false } - if _composer.isEmpty { - insertReadingToBuilderAtCursor(reading: customPunctuation) - let poppedText = popOverflowComposingTextAndWalk + if composer.isEmpty { + insertToCompositorAtCursor(reading: customPunctuation) + let textToCommit = popOverflowComposingTextAndWalk let inputting = buildInputtingState - inputting.poppedText = poppedText + inputting.textToCommit = textToCommit stateCallback(inputting) - if mgrPrefs.useSCPCTypingMode, _composer.isEmpty { + if mgrPrefs.useSCPCTypingMode, composer.isEmpty { let candidateState = buildCandidate( state: inputting, isTypingVertical: isTypingVertical ) 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 +331,13 @@ extension KeyHandler { } } - // MARK: - Enter 鍵處理 + // MARK: - Enter 鍵的處理 + /// Enter 鍵的處理。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnter( state: InputState, stateCallback: @escaping (InputState) -> Void, @@ -303,13 +346,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 +377,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, @@ -345,7 +398,7 @@ extension KeyHandler { var composed = "" - for theAnchor in _walkedNodes { + for theAnchor in walkedAnchors { if let node = theAnchor.node { var key = node.currentKeyValue.key if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin { @@ -368,13 +421,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, @@ -382,11 +441,11 @@ extension KeyHandler { ) -> Bool { guard state is InputState.Inputting else { return false } - if _composer.hasToneMarker(withNothingElse: true) { - _composer.clear() - } else if _composer.isEmpty { - if builderCursorIndex >= 0 { - deleteBuilderReadingInFrontOfCursor() + if composer.hasToneMarker(withNothingElse: true) { + composer.clear() + } else if composer.isEmpty { + if compositorCursorIndex >= 0 { + deleteCompositorReadingAtTheRearOfCursor() walk() } else { IME.prtDebugIntel("9D69908D") @@ -395,10 +454,10 @@ extension KeyHandler { return true } } else { - _composer.doBackSpace() + composer.doBackSpace() } - if _composer.isEmpty, builderLength == 0 { + if composer.isEmpty, compositorLength == 0 { stateCallback(InputState.EmptyIgnoringPreviousState()) } else { stateCallback(buildInputtingState) @@ -408,6 +467,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, @@ -415,9 +480,9 @@ extension KeyHandler { ) -> Bool { guard state is InputState.Inputting else { return false } - if _composer.isEmpty { - if builderCursorIndex != builderLength { - deleteBuilderReadingToTheFrontOfCursor() + if composer.isEmpty { + if compositorCursorIndex != compositorLength { + deleteCompositorReadingToTheFrontOfCursor() walk() let inputting = buildInputtingState // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 @@ -442,13 +507,19 @@ extension KeyHandler { // MARK: - 處理與當前文字輸入排版前後方向呈 90 度的那兩個方向鍵的按鍵行為 + /// 處理與當前文字輸入排版前後方向呈 90 度的那兩個方向鍵的按鍵行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleAbsorbedArrowKey( state: InputState, stateCallback: @escaping (InputState) -> Void, errorCallback: @escaping () -> Void ) -> Bool { guard state is InputState.Inputting else { return false } - if !_composer.isEmpty { + if !composer.isEmpty { IME.prtDebugIntel("9B6F908D") errorCallback() } @@ -456,8 +527,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, @@ -465,15 +542,15 @@ extension KeyHandler { ) -> Bool { guard state is InputState.Inputting else { return false } - if !_composer.isEmpty { + if !composer.isEmpty { IME.prtDebugIntel("ABC44080") errorCallback() stateCallback(state) return true } - if builderCursorIndex != 0 { - builderCursorIndex = 0 + if compositorCursorIndex != 0 { + compositorCursorIndex = 0 stateCallback(buildInputtingState) } else { IME.prtDebugIntel("66D97F90") @@ -484,8 +561,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, @@ -493,15 +576,15 @@ extension KeyHandler { ) -> Bool { guard state is InputState.Inputting else { return false } - if !_composer.isEmpty { + if !composer.isEmpty { IME.prtDebugIntel("9B69908D") errorCallback() stateCallback(state) return true } - if builderCursorIndex != builderLength { - builderCursorIndex = builderLength + if compositorCursorIndex != compositorLength { + compositorCursorIndex = compositorLength stateCallback(buildInputtingState) } else { IME.prtDebugIntel("9B69908E") @@ -512,8 +595,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,20 +609,16 @@ 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 builderLength == 0 { + /// 如果注拼槽不是空的話,則清空之。 + if !composer.isEmpty { + composer.clear() + if compositorLength == 0 { stateCallback(InputState.EmptyIgnoringPreviousState()) } else { stateCallback(buildInputtingState) @@ -546,6 +630,13 @@ extension KeyHandler { // MARK: - 處理向前方向鍵的行為 + /// 處理向前方向鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - input: 輸入按鍵訊號。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleForward( state: InputState, input: InputSignal, @@ -554,7 +645,7 @@ extension KeyHandler { ) -> Bool { guard let currentState = state as? InputState.Inputting else { return false } - if !_composer.isEmpty { + if !composer.isEmpty { IME.prtDebugIntel("B3BA5257") errorCallback() stateCallback(state) @@ -580,8 +671,8 @@ extension KeyHandler { stateCallback(state) } } else { - if builderCursorIndex < builderLength { - builderCursorIndex += 1 + if compositorCursorIndex < compositorLength { + compositorCursorIndex += 1 stateCallback(buildInputtingState) } else { IME.prtDebugIntel("A96AAD58") @@ -595,6 +686,13 @@ extension KeyHandler { // MARK: - 處理向後方向鍵的行為 + /// 處理向後方向鍵的行為。 + /// - Parameters: + /// - state: 當前狀態。 + /// - input: 輸入按鍵訊號。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackward( state: InputState, input: InputSignal, @@ -603,7 +701,7 @@ extension KeyHandler { ) -> Bool { guard let currentState = state as? InputState.Inputting else { return false } - if !_composer.isEmpty { + if !composer.isEmpty { IME.prtDebugIntel("6ED95318") errorCallback() stateCallback(state) @@ -629,8 +727,8 @@ extension KeyHandler { stateCallback(state) } } else { - if builderCursorIndex > 0 { - builderCursorIndex -= 1 + if compositorCursorIndex > 0 { + compositorCursorIndex -= 1 stateCallback(buildInputtingState) } else { IME.prtDebugIntel("7045E6F3") @@ -642,11 +740,18 @@ extension KeyHandler { return true } - // MARK: - 處理 Tab 按鍵行為 + // MARK: - 處理上下文候選字詞輪替(Tab 按鍵,或者 Shift+Space) - func handleTab( + /// 以給定之參數來處理上下文候選字詞之輪替。 + /// - Parameters: + /// - state: 當前狀態。 + /// - reverseModifier: 是否有控制輪替方向的修飾鍵輸入。 + /// - stateCallback: 狀態回呼。 + /// - errorCallback: 錯誤回呼。 + /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 + func handleInlineCandidateRotation( state: InputState, - isShiftHold: Bool, + reverseModifier: Bool, stateCallback: @escaping (InputState) -> Void, errorCallback: @escaping () -> Void ) -> Bool { @@ -660,13 +765,13 @@ extension KeyHandler { return false } - guard _composer.isEmpty else { + guard composer.isEmpty else { IME.prtDebugIntel("A2DAF7BC") errorCallback() return true } - // 此處僅借用該函數生成結果內的某個物件,不用糾結「是否縱排輸入」。 + // 此處僅借用該函式生成結果內的某個物件,不用糾結「是否縱排輸入」。 let candidates = buildCandidate(state: state).candidates guard !candidates.isEmpty else { IME.prtDebugIntel("3378A6DF") @@ -677,9 +782,9 @@ extension KeyHandler { var length = 0 var currentAnchor = Megrez.NodeAnchor() let cursorIndex = min( - actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), builderLength + actualCandidateCursorIndex + (mgrPrefs.useRearCursorMode ? 1 : 0), compositorLength ) - for anchor in _walkedNodes { + for anchor in walkedAnchors { length += anchor.spanningLength if length >= cursorIndex { currentAnchor = anchor @@ -697,19 +802,18 @@ 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 isShiftHold { + /// 如果第一個候選字詞是當前節點的候選字詞的值的話, + /// 那就切到下一個(或上一個,也就是最後一個)候選字詞。 + if reverseModifier { currentIndex = candidates.count - 1 } else { currentIndex = 1 @@ -718,7 +822,7 @@ extension KeyHandler { } else { for candidate in candidates { if candidate == currentValue { - if isShiftHold { + if reverseModifier { if currentIndex == 0 { currentIndex = candidates.count - 1 } else { diff --git a/Source/Modules/ControllerModules/SyllableComposer.swift b/Source/Modules/ControllerModules/SyllableComposer.swift index 13758b40..6f9ff3ae 100644 --- a/Source/Modules/ControllerModules/SyllableComposer.swift +++ b/Source/Modules/ControllerModules/SyllableComposer.swift @@ -41,8 +41,8 @@ public struct Tekkon { public enum MandarinParser: Int { case ofDachen = 0 case ofDachen26 = 1 - case ofEten = 2 - case ofEten26 = 3 + case ofETen = 2 + case ofETen26 = 3 case ofHsu = 4 case ofIBM = 5 case ofMiTAC = 6 @@ -60,11 +60,11 @@ public struct Tekkon { return "Dachen" case .ofDachen26: return "Dachen26" - case .ofEten: + case .ofETen: return "ETen" case .ofHsu: return "Hsu" - case .ofEten26: + case .ofETen26: return "ETen26" case .ofIBM: return "IBM" @@ -140,6 +140,7 @@ public struct Tekkon { /// 自我清空內容。 public mutating func clear() { valueStorage = "" + type = .null } /// 自我變換資料值。 @@ -151,7 +152,7 @@ public struct Tekkon { ensureType() } - /// 用來自動更新自身的屬性值的函數。 + /// 用來自動更新自身的屬性值的函式。 mutating func ensureType() { if Tekkon.allowedConsonants.contains(value) { type = .consonant @@ -214,7 +215,7 @@ public struct Tekkon { /// 聲調。 public var intonation: Phonabet = "" - /// 為拉丁字母專用的組音區 + /// 為拉丁字母專用的組音區。 public var romajiBuffer: String = "" /// 注音排列種類。預設情況下是大千排列(Windows / macOS 預設注音排列)。 @@ -227,7 +228,7 @@ public struct Tekkon { consonant.value + semivowel.value + vowel.value + intonation.value } - /// 與 value 類似,這個函數就是用來決定輸入法組字區內顯示的注音/拼音內容, + /// 與 value 類似,這個函式就是用來決定輸入法組字區內顯示的注音/拼音內容, /// 但可以指定是否輸出教科書格式(拼音的調號在字母上方、注音的輕聲寫在左側)。 /// - Parameters: /// - isHanyuPinyin: 是否將輸出結果轉成漢語拼音。 @@ -250,7 +251,7 @@ public struct Tekkon { } } - // 該函數僅用來獲取給 macOS InputMethod Kit 的內文組字區使用的顯示字串。 + // 該函式僅用來獲取給 macOS InputMethod Kit 的內文組字區使用的顯示字串。 /// - Parameters: /// - isHanyuPinyin: 是否將輸出結果轉成漢語拼音。 public func getInlineCompositionForIMK(isHanyuPinyin: Bool = false) -> String { @@ -279,12 +280,12 @@ public struct Tekkon { } } - /// 注拼槽內容是否為空。 + /// 注拼槽內容是否可唸。 public var isPronouncable: Bool { !vowel.isEmpty || !semivowel.isEmpty || !consonant.isEmpty } - // MARK: 注拼槽對外處理函數 + // MARK: 注拼槽對外處理函式 /// 初期化一個新的注拼槽。可以藉由 @input 參數指定初期已經傳入的按鍵訊號。 /// 還可以在初期化時藉由 @arrange 參數來指定注音排列(預設為「.ofDachen」大千佈局)。 @@ -308,7 +309,7 @@ public struct Tekkon { // MARK: - Public Functions - /// 用於檢測「某個輸入字符訊號的合規性」的函數。 + /// 用於檢測「某個輸入字符訊號的合規性」的函式。 /// /// 注意:回傳結果會受到當前注音排列 parser 屬性的影響。 /// - Parameters: @@ -321,12 +322,12 @@ public struct Tekkon { return Tekkon.mapQwertyDachen[input] != nil case .ofDachen26: return Tekkon.mapDachenCP26StaticKeys[input] != nil - case .ofEten: - return Tekkon.mapQwertyEtenTraditional[input] != nil + case .ofETen: + return Tekkon.mapQwertyETenTraditional[input] != nil case .ofHsu: return Tekkon.mapHsuStaticKeys[input] != nil - case .ofEten26: - return Tekkon.mapEten26StaticKeys[input] != nil + case .ofETen26: + return Tekkon.mapETen26StaticKeys[input] != nil case .ofIBM: return Tekkon.mapQwertyIBM[input] != nil case .ofMiTAC: @@ -343,7 +344,7 @@ public struct Tekkon { } /// 接受傳入的按鍵訊號時的處理,處理對象為 String。 - /// 另有同名函數可處理 UniChar 訊號。 + /// 另有同名函式可處理 UniChar 訊號。 /// /// 如果是諸如複合型注音排列的話,翻譯結果有可能為空,但翻譯過程已經處理好聲介韻調分配了。 /// - Parameters: @@ -356,7 +357,7 @@ public struct Tekkon { intonation = Phonabet(theTone) } } else { - // 為了防止 romajiBuffer 越敲越長帶來算力負擔,這裡讓它在要溢出時自動丟掉先取音頭。 + // 為了防止 romajiBuffer 越敲越長帶來算力負擔,這裡讓它在要溢出時自動丟掉最早輸入的音頭。 if romajiBuffer.count > 5 { romajiBuffer = String(romajiBuffer.dropFirst()) } @@ -364,12 +365,12 @@ public struct Tekkon { receiveSequence(romajiBufferBackup, isRomaji: true) romajiBuffer = romajiBufferBackup } - default: receiveKey(fromPhonabet: translate(key: String(input))) + default: receiveKey(fromPhonabet: translate(key: input)) } } /// 接受傳入的按鍵訊號時的處理,處理對象為 UniChar。 - /// 其實也就是先將 UniChar 轉為 String 再交給某個同名異參的函數來處理而已。 + /// 其實也就是先將 UniChar 轉為 String 再交給某個同名異參的函式來處理而已。 /// /// 如果是諸如複合型注音排列的話,翻譯結果有可能為空,但翻譯過程已經處理好聲介韻調分配了。 /// - Parameters: @@ -474,7 +475,7 @@ public struct Tekkon { } } - /// 用來檢測是否有調號的函數,預設情況下不判定聲調以外的內容的存無。 + /// 用來檢測是否有調號的函式,預設情況下不判定聲調以外的內容的存無。 /// - Parameters: /// - withNothingElse: 追加判定「槽內是否僅有調號」。 public func hasToneMarker(withNothingElse: Bool = false) -> Bool { @@ -493,11 +494,11 @@ public struct Tekkon { // MARK: - Parser Processings - // 注拼槽對內處理用函數都在這一小節。 + // 注拼槽對內處理用函式都在這一小節。 /// 根據目前的注音排列設定來翻譯傳入的 String 訊號。 /// - /// 倚天或許氏鍵盤的處理函數會將分配過程代為處理過,此時回傳結果為空字串。 + /// 倚天或許氏鍵盤的處理函式會將分配過程代為處理過,此時回傳結果為空字串。 /// - Parameters: /// - key: 傳入的 String 訊號。 mutating func translate(key: String = "") -> String { @@ -506,12 +507,12 @@ public struct Tekkon { return Tekkon.mapQwertyDachen[key] ?? "" case .ofDachen26: return handleDachen26(key: key) - case .ofEten: - return Tekkon.mapQwertyEtenTraditional[key] ?? "" + case .ofETen: + return Tekkon.mapQwertyETenTraditional[key] ?? "" case .ofHsu: return handleHsu(key: key) - case .ofEten26: - return handleEten26(key: key) + case .ofETen26: + return handleETen26(key: key) case .ofIBM: return Tekkon.mapQwertyIBM[key] ?? "" case .ofMiTAC: @@ -521,19 +522,18 @@ public struct Tekkon { case .ofFakeSeigyou: return Tekkon.mapFakeSeigyou[key] ?? "" case .ofHanyuPinyin, .ofSecondaryPinyin, .ofYalePinyin, .ofHualuoPinyin, .ofUniversalPinyin: - break // 漢語拼音單獨用另外的函數處理 + break // 漢語拼音單獨用另外的函式處理 } return "" } /// 倚天忘形注音排列比較麻煩,需要單獨處理。 /// - /// 回傳結果是空字串的話,不要緊,因為該函數內部已經處理過分配過程了。 + /// 回傳結果是空字串的話,不要緊,因為該函式內部已經處理過分配過程了。 /// - Parameters: /// - key: 傳入的 String 訊號。 - mutating func handleEten26(key: String = "") -> String { - var strReturn = "" - strReturn = Tekkon.mapEten26StaticKeys[key] ?? "" + mutating func handleETen26(key: String = "") -> String { + var strReturn = Tekkon.mapETen26StaticKeys[key] ?? "" let incomingPhonabet = Phonabet(strReturn) switch key { @@ -612,16 +612,15 @@ public struct Tekkon { /// 許氏鍵盤與倚天忘形一樣同樣也比較麻煩,需要單獨處理。 /// - /// 回傳結果是空的話,不要緊,因為該函數內部已經處理過分配過程了。 + /// 回傳結果是空的話,不要緊,因為該函式內部已經處理過分配過程了。 /// - Parameters: /// - key: 傳入的 String 訊號。 mutating func handleHsu(key: String = "") -> String { - var strReturn = "" - strReturn = Tekkon.mapHsuStaticKeys[key] ?? "" + var strReturn = Tekkon.mapHsuStaticKeys[key] ?? "" let incomingPhonabet = Phonabet(strReturn) if key == " ", value == "ㄋ" { - consonant = "" + consonant.clear() vowel = "ㄣ" } @@ -719,7 +718,7 @@ public struct Tekkon { consonant.selfReplace("ㄒ", "ㄕ") } if consonant == "ㄏ", semivowel.isEmpty, vowel.isEmpty { - consonant = "" + consonant.clear() vowel = "ㄛ" } } @@ -736,12 +735,11 @@ public struct Tekkon { /// 大千忘形一樣同樣也比較麻煩,需要單獨處理。 /// - /// 回傳結果是空的話,不要緊,因為該函數內部已經處理過分配過程了。 + /// 回傳結果是空的話,不要緊,因為該函式內部已經處理過分配過程了。 /// - Parameters: /// - key: 傳入的 String 訊號。 mutating func handleDachen26(key: String = "") -> String { - var strReturn = "" - strReturn = Tekkon.mapDachenCP26StaticKeys[key] ?? "" + var strReturn = Tekkon.mapDachenCP26StaticKeys[key] ?? "" switch key { case "e": if isPronouncable { intonation = "ˊ" } else { consonant = "ㄍ" } @@ -759,11 +757,11 @@ public struct Tekkon { case "w": if consonant.isEmpty || consonant == "ㄉ" { consonant = "ㄊ" } else { consonant = "ㄉ" } case "m": if semivowel == "ㄩ", vowel != "ㄡ" { - semivowel = "" + semivowel.clear() vowel = "ㄡ" } else if semivowel != "ㄩ", vowel == "ㄡ" { semivowel = "ㄩ" - vowel = "" + vowel.clear() } else if !semivowel.isEmpty { vowel = "ㄡ" } else { @@ -771,13 +769,13 @@ public struct Tekkon { } case "u": if semivowel == "ㄧ", vowel != "ㄚ" { - semivowel = "" + semivowel.clear() vowel = "ㄚ" } else if semivowel != "ㄧ", vowel == "ㄚ" { semivowel = "ㄧ" } else if semivowel == "ㄧ", vowel == "ㄚ" { - semivowel = "" - vowel = "" + semivowel.clear() + vowel.clear() } else if !semivowel.isEmpty { vowel = "ㄚ" } else { @@ -834,6 +832,9 @@ public struct Tekkon { return targetConverted } + /// 漢語拼音數字標調式轉漢語拼音教科書格式,要求陰平必須是數字 1。 + /// - Parameters: + /// - target: 傳入的 String 對象物件。 static func cnvHanyuPinyinToTextbookStyle(target: String) -> String { var targetConverted = target for pair in arrHanyuPinyinTextbookStyleConversionTable { @@ -1296,14 +1297,14 @@ public struct Tekkon { /// /// 在這裡將二十六個字母寫全,也只是為了方便做 validity check。 /// 這裡提前對ㄓ/ㄍ/ㄕ做處理,然後再用程式判斷介母類型、據此判斷是否需要換成ㄒ/ㄑ/ㄐ。 - static let mapEten26StaticKeys: [String: String] = [ + static let mapETen26StaticKeys: [String: String] = [ "a": "ㄚ", "b": "ㄅ", "c": "ㄕ", "d": "ㄉ", "e": "ㄧ", "f": "ㄈ", "g": "ㄓ", "h": "ㄏ", "i": "ㄞ", "j": "ㄖ", "k": "ㄎ", "l": "ㄌ", "m": "ㄇ", "n": "ㄋ", "o": "ㄛ", "p": "ㄆ", "q": "ㄗ", "r": "ㄜ", "s": "ㄙ", "t": "ㄊ", "u": "ㄩ", "v": "ㄍ", "w": "ㄘ", "x": "ㄨ", "y": "ㄔ", "z": "ㄠ", " ": " ", ] /// 倚天傳統排列專用處理陣列。 - static let mapQwertyEtenTraditional: [String: String] = [ + static let mapQwertyETenTraditional: [String: String] = [ "'": "ㄘ", ",": "ㄓ", "-": "ㄥ", ".": "ㄔ", "/": "ㄕ", "0": "ㄤ", "1": "˙", "2": "ˊ", "3": "ˇ", "4": "ˋ", "7": "ㄑ", "8": "ㄢ", "9": "ㄣ", ";": "ㄗ", "=": "ㄦ", "a": "ㄚ", "b": "ㄅ", "c": "ㄒ", "d": "ㄉ", "e": "ㄧ", "f": "ㄈ", "g": "ㄐ", "h": "ㄏ", "i": "ㄞ", "j": "ㄖ", "k": "ㄎ", "l": "ㄌ", "m": "ㄇ", "n": "ㄋ", "o": "ㄛ", "p": "ㄆ", "q": "ㄟ", "r": "ㄜ", diff --git a/Source/Modules/IMEModules/ctlInputMethod.swift b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift similarity index 56% rename from Source/Modules/IMEModules/ctlInputMethod.swift rename to Source/Modules/ControllerModules/ctlInputMethod_Core.swift index a371cfb7..d4ab7717 100644 --- a/Source/Modules/IMEModules/ctlInputMethod.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift @@ -27,41 +27,58 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import Cocoa import InputMethodKit +/// 最小選字鍵字號。 private let kMinKeyLabelSize: CGFloat = 10 +/// 目前在用的的選字窗副本。 private var ctlCandidateCurrent = ctlCandidateUniversal.init(.horizontal) -@objc(ctlInputMethod) +/// 輸入法控制模組,乃在輸入法端用以控制輸入行為的基礎型別。 +/// +/// IMKInputController 完全實現了相關協定所定義的內容。 +/// 一般情況下,研發者不會複寫此型別,而是提供一個委任物件、 +/// 藉此實現研發者想製作的方法/函式。協定方法的 IMKInputController 版本 +/// 檢查委任物件是否實現了方法:若存在的話,就調用委任物件內的版本。 +/// - Remark: 在輸入法的主函式中分配的 IMKServer 型別為客體應用程式創建的每個 +/// 輸入會話創建一個控制器型別。因此,對於每個輸入會話,都有一個對應的 IMKInputController。 +@objc(ctlInputMethod) // 必須加上 ObjC,因為 IMK 是用 ObjC 寫的。 class ctlInputMethod: IMKInputController { - @objc static var areWeDeleting = false + /// 標記狀態來聲明目前是在新增使用者語彙、還是準備要濾除使用者語彙。 + static var areWeDeleting = false + /// 工具提示視窗的副本。 static let tooltipController = TooltipController() // MARK: - - private var currentClient: Any? - + /// 按鍵調度模組的副本。 private var keyHandler: KeyHandler = .init() + /// 用以記錄當前輸入法狀態的變數。 private var state: InputState = .Empty() - // 想讓 KeyHandler 能夠被外界調查狀態與參數的話,就得對 KeyHandler 做常態處理。 - // 這樣 InputState 可以藉由這個 ctlInputMethod 了解到當前的輸入模式是簡體中文還是繁體中文。 - // 然而,要是直接對 keyHandler 做常態處理的話,反而會導致 InputSignal 無法協同處理。 - // 所以才需要「currentKeyHandler」這個假 KeyHandler。 - // 這個「currentKeyHandler」僅用來讓其他模組知道當前的輸入模式是什麼模式,除此之外別無屌用。 - static var currentKeyHandler: KeyHandler = .init() - @objc static var currentInputMode = mgrPrefs.mostRecentInputMode + // MARK: - 工具函式 - // MARK: - Keyboard Layout Specifier - - @objc func setKeyLayout() { - if let client = currentClient { - (client as? IMKTextInput)?.overrideKeyboard(withKeyboardNamed: mgrPrefs.basicKeyboardLayout) - } + /// 指定鍵盤佈局。 + func setKeyLayout() { + client().overrideKeyboard(withKeyboardNamed: mgrPrefs.basicKeyboardLayout) } - // MARK: - IMKInputController methods + /// 重設按鍵調度模組。 + func resetKeyHandler() { + keyHandler.clear() + handle(state: InputState.Empty()) + } + // MARK: - IMKInputController 方法 + + /// 對用以設定委任物件的控制器型別進行初期化處理。 + /// + /// inputClient 參數是客體應用側存在的用以藉由 IMKServer 伺服器向輸入法傳訊的物件。該物件始終遵守 IMKTextInput 協定。 + /// - Remark: 所有由委任物件實裝的「被協定要求實裝的方法」都會有一個用來接受客體物件的參數。在 IMKInputController 內部的型別不需要接受這個參數,因為已經有「client()」這個參數存在了。 + /// - Parameters: + /// - server: IMKServer + /// - delegate: 客體物件 + /// - inputClient: 用以接受輸入的客體應用物件 override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { super.init(server: server, delegate: delegate, client: inputClient) keyHandler.delegate = self @@ -70,47 +87,44 @@ class ctlInputMethod: IMKInputController { resetKeyHandler() } - // MARK: - KeyHandler Reset Command + // MARK: - IMKStateSetting 協定規定的方法 - func resetKeyHandler(client sender: Any? = nil) { - keyHandler.clear() - if let client = sender as? IMKTextInput { - handle(state: InputState.Empty(), client: client) - } else if let currentClient = currentClient { - handle(state: InputState.Empty(), client: currentClient) - } - } - - // MARK: - IMKStateSetting protocol methods - - override func activateServer(_ client: Any!) { + /// 啟用輸入法時,會觸發該函式。 + /// - Parameter sender: 呼叫了該函式的客體(無須使用)。 + override func activateServer(_ sender: Any!) { + _ = sender // 防止格式整理工具毀掉與此對應的參數。 UserDefaults.standard.synchronize() - // reset the state - currentClient = client - keyHandler.clear() keyHandler.ensureParser() - if let bundleCheckID = (client as? IMKTextInput)?.bundleIdentifier() { - if bundleCheckID != Bundle.main.bundleIdentifier { - // Override the keyboard layout to the basic one. - setKeyLayout() - handle(state: .Empty(), client: client) - } - } + /// 必須加上下述條件,否則會在每次切換至輸入法本體的視窗(比如偏好設定視窗)時會卡死。 + /// 這是很多 macOS 副廠輸入法的常見失誤之處。 + if client().bundleIdentifier() != Bundle.main.bundleIdentifier { + // Override the keyboard layout to the basic one. + setKeyLayout() + handle(state: .Empty()) + } // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。 (NSApp.delegate as? AppDelegate)?.checkForUpdate() } - override func deactivateServer(_ client: Any!) { + /// 停用輸入法時,會觸發該函式。 + /// - Parameter sender: 呼叫了該函式的客體(無須使用)。 + override func deactivateServer(_ sender: Any!) { + _ = sender // 防止格式整理工具毀掉與此對應的參數。 keyHandler.clear() - currentClient = nil - handle(state: .Empty(), client: client) - handle(state: .Deactivated(), client: client) + handle(state: .Empty()) + handle(state: .Deactivated()) } - override func setValue(_ value: Any!, forTag tag: Int, client: Any!) { - _ = tag // Stop clang-format from ruining the parameters of this function. + /// 切換至某一個輸入法的某個副本時(比如威注音的簡體輸入法副本與繁體輸入法副本),會觸發該函式。 + /// - Parameters: + /// - value: 輸入法在系統偏好設定當中的副本的 identifier,與 bundle identifier 類似。在輸入法的 info.plist 內定義。 + /// - tag: 標記(無須使用)。 + /// - sender: 呼叫了該函式的客體(無須使用)。 + override func setValue(_ value: Any!, forTag tag: Int, client sender: Any!) { + _ = tag // 防止格式整理工具毀掉與此對應的參數。 + _ = sender // 防止格式整理工具毀掉與此對應的參數。 var newInputMode = InputMode(rawValue: value as? String ?? "") ?? InputMode.imeModeNULL switch newInputMode { case InputMode.imeModeCHS: @@ -126,32 +140,47 @@ class ctlInputMethod: IMKInputController { UserDefaults.standard.synchronize() keyHandler.clear() keyHandler.inputMode = newInputMode - if let bundleCheckID = (client as? IMKTextInput)?.bundleIdentifier() { - if bundleCheckID != Bundle.main.bundleIdentifier { - // Remember to override the keyboard layout again -- treat this as an activate event. - setKeyLayout() - handle(state: .Empty(), client: client) - } - } + /// 必須加上下述條件,否則會在每次切換至輸入法本體的視窗(比如偏好設定視窗)時會卡死。 + /// 這是很多 macOS 副廠輸入法的常見失誤之處。 + if client().bundleIdentifier() != Bundle.main.bundleIdentifier { + // Remember to override the keyboard layout again -- treat this as an activate event. + setKeyLayout() + handle(state: .Empty()) + } // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。 } // 讓外界知道目前的簡繁體輸入模式。 - ctlInputMethod.currentKeyHandler.inputMode = keyHandler.inputMode + IME.currentInputMode = keyHandler.inputMode } - // MARK: - IMKServerInput protocol methods + // MARK: - IMKServerInput 協定規定的方法 + /// 該函式的回饋結果決定了輸入法會攔截且捕捉哪些類型的輸入裝置操作事件。 + /// + /// 一個客體應用會與輸入法共同確認某個輸入裝置操作事件是否可以觸發輸入法內的某個方法。預設情況下, + /// 該函式僅響應 Swift 的「`NSEvent.EventTypeMask = [.keyDown]`」,也就是 ObjC 當中的「`NSKeyDownMask`」。 + /// 如果您的輸入法「僅攔截」鍵盤按鍵事件處理的話,IMK 會預設啟用這些對滑鼠的操作:當組字區存在時, + /// 如果使用者用滑鼠點擊了該文字輸入區內的組字區以外的區域的話,則該組字區的顯示內容會被直接藉由 + /// 「`commitComposition(_ message)`」遞交給客體。 + /// - Parameter sender: 呼叫了該函式的客體(無須使用)。 + /// - Returns: 返回一個 uint,其中承載了與系統 NSEvent 操作事件有關的掩碼集合(詳見 NSEvent.h)。 override func recognizedEvents(_ sender: Any!) -> Int { - _ = sender // Stop clang-format from ruining the parameters of this function. + _ = sender // 防止格式整理工具毀掉與此對應的參數。 let events: NSEvent.EventTypeMask = [.keyDown, .flagsChanged] return Int(events.rawValue) } + /// 接受所有鍵鼠事件為 NSEvent,讓輸入法判斷是否要處理、該怎樣處理。 + /// - Parameters: + /// - event: 裝置操作輸入事件。 + /// - sender: 呼叫了該函式的客體(無須使用)。 + /// - Returns: 回「`true`」以將該案件已攔截處理的訊息傳遞給 IMK;回「`false`」則放行、不作處理。 @objc(handleEvent:client:) override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { - // 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。 - // 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。 - // 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false, - // 否則,每次處理這種判斷時都會觸發 NSInternalInconsistencyException。 + _ = sender // 防止格式整理工具毀掉與此對應的參數。 + /// 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。 + /// 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。 + /// 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false, + /// 否則,每次處理這種判斷時都會觸發 NSInternalInconsistencyException。 if event.type == .flagsChanged { return false } @@ -160,18 +189,15 @@ class ctlInputMethod: IMKInputController { ctlInputMethod.areWeDeleting = event.modifierFlags.contains([.shift, .command]) var textFrame = NSRect.zero - guard let client = sender as? IMKTextInput else { - return false - } - let attributes: [AnyHashable: Any]? = client.attributes( + let attributes: [AnyHashable: Any]? = client().attributes( forCharacterIndex: 0, lineHeightRectangle: &textFrame ) let isTypingVertical = (attributes?["IMKTextOrientation"] as? NSNumber)?.intValue == 0 || false - if client.bundleIdentifier() + if client().bundleIdentifier() == "org.atelierInmu.vChewing.vChewingPhraseEditor" { IME.areWeUsingOurOwnPhraseEditor = true @@ -187,276 +213,202 @@ class ctlInputMethod: IMKInputController { return false } + /// 將按鍵行為與當前輸入法狀態結合起來、交給按鍵調度模組來處理。 + /// 再根據返回的 result bool 數值來告知 IMK「這個按鍵事件是被處理了還是被放行了」。 let result = keyHandler.handle(input: input, state: state) { newState in - self.handle(state: newState, client: client) + self.handle(state: newState) } errorCallback: { clsSFX.beep() } return result } - // 有時會出現某些 App 攔截輸入法的 Ctrl+Enter / Shift+Enter 熱鍵的情況。 - // 也就是說 handle(event:) 完全抓不到這個 Event。 - // 這時需要在 commitComposition 這一關做一些收尾處理。 + /// 有時會出現某些 App 攔截輸入法的 Ctrl+Enter / Shift+Enter 熱鍵的情況。 + /// 也就是說 handle(event:) 完全抓不到這個 Event。 + /// 這時需要在 commitComposition 這一關做一些收尾處理。 + /// - Parameter sender: 呼叫了該函式的客體(無須使用)。 override func commitComposition(_ sender: Any!) { - resetKeyHandler(client: sender) - } - - // 這個函數必須得在對應的狀態下給出對應的內容。 - override func composedString(_ sender: Any!) -> Any! { - _ = sender // Stop clang-format from ruining the parameters of this function. - return (state as? InputState.NotEmpty)?.composingBuffer ?? "" + _ = sender // 防止格式整理工具毀掉與此對應的參數。 + if let state = state as? InputState.NotEmpty { + /// 將傳回的新狀態交給調度函式。 + handle(state: InputState.Committing(textToCommit: state.composingBuffer)) + } + resetKeyHandler() } } -// MARK: - State Handling +// MARK: - 狀態調度 (State Handling) extension ctlInputMethod { - private func handle(state newState: InputState, client: Any?) { - let previous = state + /// 針對傳入的新狀態進行調度。 + /// + /// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數, + /// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。 + /// - Parameter newState: 新狀態。 + private func handle(state newState: InputState) { + let prevState = state state = newState - if let newState = newState as? InputState.Deactivated { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Empty { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.EmptyIgnoringPreviousState { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Committing { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Inputting { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Marking { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.ChoosingCandidate { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.AssociatedPhrases { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.SymbolTable { - handle(state: newState, previous: previous, client: client) + 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 } } - private func commit(text: String, client: Any!) { - func kanjiConversionIfRequired(_ text: String) -> String { - if keyHandler.inputMode == InputMode.imeModeCHT { - if !mgrPrefs.chineseConversionEnabled, mgrPrefs.shiftJISShinjitaiOutputEnabled { - return vChewingKanjiConverter.cnvTradToJIS(text) - } - if mgrPrefs.chineseConversionEnabled, !mgrPrefs.shiftJISShinjitaiOutputEnabled { - return vChewingKanjiConverter.cnvTradToKangXi(text) - } - // 本來這兩個開關不該同時開啟的,但萬一被開啟了的話就這樣處理: - if mgrPrefs.chineseConversionEnabled, mgrPrefs.shiftJISShinjitaiOutputEnabled { - return vChewingKanjiConverter.cnvTradToJIS(text) - } - // if (!mgrPrefs.chineseConversionEnabled && !mgrPrefs.shiftJISShinjitaiOutputEnabled) || (keyHandler.inputMode != InputMode.imeModeCHT); - return text - } - return text + /// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。 + private func setInlineDisplayWithCursor() { + guard let state = state as? InputState.NotEmpty else { + clearInlineDisplay() + return } + /// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度 + /// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。 + /// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。 + client().setMarkedText( + state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0), + replacementRange: NSRange(location: NSNotFound, length: NSNotFound) + ) + } - let buffer = kanjiConversionIfRequired(text) + /// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。 + /// 當 setInlineDisplayWithCursor() 在錯誤的狀態下被呼叫時,也會觸發這個函式。 + private func clearInlineDisplay() { + client().setMarkedText( + "", selectionRange: NSRange(location: 0, length: 0), + replacementRange: NSRange(location: NSNotFound, length: NSNotFound) + ) + } + + /// 遞交組字區內容。 + private func commit(text: String) { + let buffer = IME.kanjiConversionIfRequired(text) if buffer.isEmpty { return } - var bufferOutput = "" - - // 防止輸入法輸出不可列印的字元。 - for theChar in buffer { - if let charCode = theChar.utf16.first { - if !(theChar.isASCII && !(charCode.isPrintable())) { - bufferOutput += String(theChar) - } - } - } - - (client as? IMKTextInput)?.insertText( - bufferOutput, replacementRange: NSRange(location: NSNotFound, length: NSNotFound) + client().insertText( + buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound) ) } - private func handle(state: InputState.Deactivated, previous: InputState, client: Any?) { - _ = state // Stop clang-format from ruining the parameters of this function. - currentClient = nil - + private func handle(state: InputState.Deactivated, previous: InputState) { + _ = state // 防止格式整理工具毀掉與此對應的參數。 ctlCandidateCurrent.delegate = nil ctlCandidateCurrent.visible = false hideTooltip() - if let previous = previous as? InputState.NotEmpty { - commit(text: previous.composingBuffer, client: client) + commit(text: previous.composingBuffer) } - (client as? IMKTextInput)?.setMarkedText( - "", selectionRange: NSRange(location: 0, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) + clearInlineDisplay() } - private func handle(state: InputState.Empty, previous: InputState, client: Any?) { - _ = state // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.Empty, previous: InputState) { + _ = state // 防止格式整理工具毀掉與此對應的參數。 ctlCandidateCurrent.visible = false hideTooltip() - - guard let client = client as? IMKTextInput else { - return - } - if let previous = previous as? InputState.NotEmpty, !(state is InputState.EmptyIgnoringPreviousState) { - commit(text: previous.composingBuffer, client: client) + commit(text: previous.composingBuffer) } - client.setMarkedText( - "", selectionRange: NSRange(location: 0, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) + clearInlineDisplay() } private func handle( - state: InputState.EmptyIgnoringPreviousState, previous: InputState, client: Any! + state: InputState.EmptyIgnoringPreviousState, previous: InputState ) { - _ = state // Stop clang-format from ruining the parameters of this function. - _ = previous // Stop clang-format from ruining the parameters of this function. + _ = state // 防止格式整理工具毀掉與此對應的參數。 + _ = previous // 防止格式整理工具毀掉與此對應的參數。 ctlCandidateCurrent.visible = false hideTooltip() - - guard let client = client as? IMKTextInput else { - return - } - - client.setMarkedText( - "", selectionRange: NSRange(location: 0, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) + clearInlineDisplay() } - private func handle(state: InputState.Committing, previous: InputState, client: Any?) { - _ = previous // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.Committing, previous: InputState) { + _ = previous // 防止格式整理工具毀掉與此對應的參數。 ctlCandidateCurrent.visible = false hideTooltip() - - guard let client = client as? IMKTextInput else { - return + let textToCommit = state.textToCommit + if !textToCommit.isEmpty { + commit(text: textToCommit) } - - let poppedText = state.poppedText - if !poppedText.isEmpty { - commit(text: poppedText, client: client) - } - client.setMarkedText( - "", selectionRange: NSRange(location: 0, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) + clearInlineDisplay() } - private func handle(state: InputState.Inputting, previous: InputState, client: Any?) { - _ = previous // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.Inputting, previous: InputState) { + _ = previous // 防止格式整理工具毀掉與此對應的參數。 ctlCandidateCurrent.visible = false hideTooltip() - - guard let client = client as? IMKTextInput else { - return + let textToCommit = state.textToCommit + if !textToCommit.isEmpty { + commit(text: textToCommit) } - - let poppedText = state.poppedText - if !poppedText.isEmpty { - commit(text: poppedText, client: client) - } - - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText( - state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) + setInlineDisplayWithCursor() if !state.tooltip.isEmpty { show( tooltip: state.tooltip, composingBuffer: state.composingBuffer, - cursorIndex: state.cursorIndex, client: client + cursorIndex: state.cursorIndex ) } } - private func handle(state: InputState.Marking, previous: InputState, client: Any?) { - _ = previous // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.Marking, previous: InputState) { + _ = previous // 防止格式整理工具毀掉與此對應的參數。 ctlCandidateCurrent.visible = false - guard let client = client as? IMKTextInput else { - hideTooltip() - return - } - - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText( - state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) - + setInlineDisplayWithCursor() if state.tooltip.isEmpty { hideTooltip() } else { show( tooltip: state.tooltip, composingBuffer: state.composingBuffer, - cursorIndex: state.markerIndex, client: client + cursorIndex: state.markerIndex ) } } - private func handle(state: InputState.ChoosingCandidate, previous: InputState, client: Any?) { - _ = previous // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.ChoosingCandidate, previous: InputState) { + _ = previous // 防止格式整理工具毀掉與此對應的參數。 hideTooltip() - guard let client = client as? IMKTextInput else { - ctlCandidateCurrent.visible = false - return - } - - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText( - state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) - show(candidateWindowWith: state, client: client) + setInlineDisplayWithCursor() + show(candidateWindowWith: state) } - private func handle(state: InputState.SymbolTable, previous: InputState, client: Any?) { - _ = previous // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.SymbolTable, previous: InputState) { + _ = previous // 防止格式整理工具毀掉與此對應的參數。 hideTooltip() - guard let client = client as? IMKTextInput else { - ctlCandidateCurrent.visible = false - return - } - - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText( - state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) - show(candidateWindowWith: state, client: client) + setInlineDisplayWithCursor() + show(candidateWindowWith: state) } - private func handle(state: InputState.AssociatedPhrases, previous: InputState, client: Any?) { - _ = previous // Stop clang-format from ruining the parameters of this function. + private func handle(state: InputState.AssociatedPhrases, previous: InputState) { + _ = previous // 防止格式整理工具毀掉與此對應的參數。 hideTooltip() - guard let client = client as? IMKTextInput else { - ctlCandidateCurrent.visible = false - return - } - client.setMarkedText( - "", selectionRange: NSRange(location: 0, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) - show(candidateWindowWith: state, client: client) + clearInlineDisplay() + show(candidateWindowWith: state) } } // MARK: - extension ctlInputMethod { - private func show(candidateWindowWith state: InputState, client: Any!) { + private func show(candidateWindowWith state: InputState) { var isTypingVertical: Bool { if let state = state as? InputState.ChoosingCandidate { return state.isTypingVertical @@ -518,7 +470,7 @@ extension ctlInputMethod { ? "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 開始則依賴系統內建的函數使用蘋方來處理。 + // 對更紗黑體的依賴到 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 @@ -543,7 +495,6 @@ extension ctlInputMethod { ctlCandidateCurrent.delegate = self ctlCandidateCurrent.reloadData() - currentClient = client ctlCandidateCurrent.visible = true @@ -558,7 +509,7 @@ extension ctlInputMethod { } while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 { - (client as? IMKTextInput)?.attributes( + client().attributes( forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect ) cursor -= 1 @@ -579,14 +530,14 @@ extension ctlInputMethod { } } - private func show(tooltip: String, composingBuffer: String, cursorIndex: Int, client: Any!) { + 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 as? IMKTextInput)?.attributes( + client().attributes( forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect ) cursor -= 1 @@ -644,7 +595,7 @@ extension ctlInputMethod: KeyHandlerDelegate { extension ctlInputMethod: ctlCandidateDelegate { func candidateCountForController(_ controller: ctlCandidate) -> Int { - _ = controller // Stop clang-format from ruining the parameters of this function. + _ = controller // 防止格式整理工具毀掉與此對應的參數。 if let state = state as? InputState.ChoosingCandidate { return state.candidates.count } else if let state = state as? InputState.AssociatedPhrases { @@ -656,7 +607,7 @@ extension ctlInputMethod: ctlCandidateDelegate { func ctlCandidate(_ controller: ctlCandidate, candidateAtIndex index: Int) -> String { - _ = controller // Stop clang-format from ruining the parameters of this function. + _ = controller // 防止格式整理工具毀掉與此對應的參數。 if let state = state as? InputState.ChoosingCandidate { return state.candidates[index] } else if let state = state as? InputState.AssociatedPhrases { @@ -666,21 +617,19 @@ extension ctlInputMethod: ctlCandidateDelegate { } func ctlCandidate(_ controller: ctlCandidate, didSelectCandidateAtIndex index: Int) { - _ = controller // Stop clang-format from ruining the parameters of this function. - let client = currentClient + _ = controller // 防止格式整理工具毀掉與此對應的參數。 if let state = state as? InputState.SymbolTable, let node = state.node.children?[index] { if let children = node.children, !children.isEmpty { - handle(state: .Empty(), client: client) // 防止縱橫排選字窗同時出現 + handle(state: .Empty()) // 防止縱橫排選字窗同時出現 handle( - state: .SymbolTable(node: node, isTypingVertical: state.isTypingVertical), - client: currentClient + state: .SymbolTable(node: node, isTypingVertical: state.isTypingVertical) ) } else { - handle(state: .Committing(poppedText: node.title), client: client) - handle(state: .Empty(), client: client) + handle(state: .Committing(textToCommit: node.title)) + handle(state: .Empty()) } return } @@ -694,33 +643,33 @@ extension ctlInputMethod: ctlCandidateDelegate { if mgrPrefs.useSCPCTypingMode { keyHandler.clear() let composingBuffer = inputting.composingBuffer - handle(state: .Committing(poppedText: composingBuffer), client: client) + handle(state: .Committing(textToCommit: composingBuffer)) if mgrPrefs.associatedPhrasesEnabled, let associatePhrases = keyHandler.buildAssociatePhraseState( withKey: composingBuffer, isTypingVertical: state.isTypingVertical ), !associatePhrases.candidates.isEmpty { - handle(state: associatePhrases, client: client) + handle(state: associatePhrases) } else { - handle(state: .Empty(), client: client) + handle(state: .Empty()) } } else { - handle(state: inputting, client: client) + handle(state: inputting) } return } if let state = state as? InputState.AssociatedPhrases { let selectedValue = state.candidates[index] - handle(state: .Committing(poppedText: selectedValue), client: currentClient) + handle(state: .Committing(textToCommit: selectedValue)) if mgrPrefs.associatedPhrasesEnabled, let associatePhrases = keyHandler.buildAssociatePhraseState( withKey: selectedValue, isTypingVertical: state.isTypingVertical ), !associatePhrases.candidates.isEmpty { - handle(state: associatePhrases, client: client) + handle(state: associatePhrases) } else { - handle(state: .Empty(), client: client) + handle(state: .Empty()) } } } diff --git a/Source/Modules/IMEModules/ctlInputMethod_Menu.swift b/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift similarity index 99% rename from Source/Modules/IMEModules/ctlInputMethod_Menu.swift rename to Source/Modules/ControllerModules/ctlInputMethod_Menu.swift index 9cee83ab..a35664f9 100644 --- a/Source/Modules/IMEModules/ctlInputMethod_Menu.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift @@ -171,7 +171,7 @@ extension ctlInputMethod { ) } - // NSMenu 會阻止任何 modified key 相關的訊號傳回輸入法,所以咱們在此重設鍵盤佈局 + // NSMenu 會阻止任何修飾鍵狀態切換訊號傳回輸入法,所以咱們在此重設鍵盤佈局。 setKeyLayout() return menu diff --git a/Source/Modules/IMEModules/IME.swift b/Source/Modules/IMEModules/IME.swift index e6648177..043befa1 100644 --- a/Source/Modules/IMEModules/IME.swift +++ b/Source/Modules/IMEModules/IME.swift @@ -28,10 +28,34 @@ import Cocoa // The namespace of this input method. public enum vChewing {} +// The type of input modes. +public enum InputMode: String { + case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS" + case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT" + case imeModeNULL = "" +} + public enum IME { static let arrSupportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"] static let dlgOpenPath = NSOpenPanel() + // MARK: - 輸入法的當前的簡繁體中文模式是? + + static var currentInputMode: InputMode = .init(rawValue: mgrPrefs.mostRecentInputMode) ?? .imeModeNULL + + static func kanjiConversionIfRequired(_ text: String) -> String { + if currentInputMode == InputMode.imeModeCHT { + switch (mgrPrefs.chineseConversionEnabled, mgrPrefs.shiftJISShinjitaiOutputEnabled) { + case (false, true): return vChewingKanjiConverter.cnvTradToJIS(text) + case (true, false): return vChewingKanjiConverter.cnvTradToKangXi(text) + // 本來這兩個開關不該同時開啟的,但萬一被開啟了的話就這樣處理: + case (true, true): return vChewingKanjiConverter.cnvTradToJIS(text) + case (false, false): return text + } + } + return text + } + // MARK: - 開關判定當前應用究竟是? static var areWeUsingOurOwnPhraseEditor: Bool = false @@ -40,10 +64,10 @@ public enum IME { static func getInputMode(isReversed: Bool = false) -> InputMode { if isReversed { - return (ctlInputMethod.currentKeyHandler.inputMode == InputMode.imeModeCHT) + return (IME.currentInputMode == InputMode.imeModeCHT) ? InputMode.imeModeCHS : InputMode.imeModeCHT } else { - return ctlInputMethod.currentKeyHandler.inputMode + return IME.currentInputMode } } @@ -64,7 +88,7 @@ public enum IME { // MARK: - Initializing Language Models. static func initLangModels(userOnly: Bool) { - // mgrLangModel 的 loadUserPhrases 等函數在自動讀取 dataFolderPath 時, + // mgrLangModel 的 loadUserPhrases 等函式在自動讀取 dataFolderPath 時, // 如果發現自訂目錄不可用,則會自動抹去自訂目錄設定、改採預設目錄。 // 所以這裡不需要特別處理。 mgrLangModel.loadUserAssociatesData() diff --git a/Source/Modules/IMEModules/mgrPrefs.swift b/Source/Modules/IMEModules/mgrPrefs.swift index 1105d627..28c7adc1 100644 --- a/Source/Modules/IMEModules/mgrPrefs.swift +++ b/Source/Modules/IMEModules/mgrPrefs.swift @@ -78,10 +78,10 @@ private let kMinCandidateListTextSize: CGFloat = 12 private let kMaxCandidateListTextSize: CGFloat = 196 // default, min and max composing buffer size (in codepoints) -// modern Macs can usually work up to 16 codepoints when the builder still +// modern Macs can usually work up to 16 codepoints when the compositor still // walks the grid with good performance slower Macs (like old PowerBooks) // will start to sputter beyond 12 such is the algorithmatic complexity -// of the Viterbi algorithm used in the builder library (at O(N^2)) +// of the Viterbi algorithm used in the Megrez library (at O(N^2)) private let kDefaultComposingBufferSize = 20 private let kMinComposingBufferSize = 10 private let kMaxComposingBufferSize = 40 @@ -176,9 +176,9 @@ struct ComposingBufferSize { enum MandarinParser: Int { case ofStandard = 0 - case ofEten = 1 + case ofETen = 1 case ofHsu = 2 - case ofEten26 = 3 + case ofETen26 = 3 case ofIBM = 4 case ofMiTAC = 5 case ofFakeSeigyou = 6 @@ -193,11 +193,11 @@ enum MandarinParser: Int { switch self { case .ofStandard: return "Standard" - case .ofEten: + case .ofETen: return "ETen" case .ofHsu: return "Hsu" - case .ofEten26: + case .ofETen26: return "ETen26" case .ofIBM: return "IBM" diff --git a/Source/Modules/LangModelRelated/LMInstantiator.swift b/Source/Modules/LangModelRelated/LMInstantiator.swift index 6e3bfa41..6ea47680 100644 --- a/Source/Modules/LangModelRelated/LMInstantiator.swift +++ b/Source/Modules/LangModelRelated/LMInstantiator.swift @@ -24,9 +24,6 @@ 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. */ -// NOTE: We still keep some of the comments left by Zonble, -// regardless that he is not in charge of this Swift module。 - import Foundation // 簡體中文模式與繁體中文模式共用全字庫擴展模組,故單獨處理。 @@ -39,29 +36,25 @@ private var lmSymbols = vChewing.LMCoreNS( ) extension vChewing { - /// LMInstantiator is a facade for managing a set of models including - /// the input method language model, user phrases and excluded phrases. + /// 語言模組副本化模組(LMInstantiator,下稱「LMI」)自身為符合天權星組字引擎內 + /// 的 LanguageModel 協定的模組、統籌且整理來自其它子模組的資料(包括使用者語彙、 + /// 繪文字模組、語彙濾除表、原廠語言模組等)。 /// - /// It is the primary model class that the input controller and grammar builder - /// of vChewing talks to. When the grammar builder starts to build a sentence - /// from a series of BPMF readings, it passes the readings to the model to see - /// if there are valid unigrams, and use returned unigrams to produce the final - /// results. + /// LMI 型別為與輸入法按鍵調度模組直接溝通之唯一語言模組。當組字器開始根據給定的 + /// 讀音鏈構築語句時,LMI 會接收來自組字器的讀音、輪流檢查自身是否有可以匹配到的 + /// 單元圖結果,然後將結果整理為陣列、再回饋給組字器。 /// - /// LMInstantiator combine and transform the unigrams from the primary language - /// model and user phrases. The process is + /// LMI 還會在將單元圖結果整理成陣列時做出下述處理轉換步驟: /// - /// 1) Get the original unigrams. - /// 2) Drop the unigrams whose value is contained in the exclusion map. - /// 3) Replace the values of the unigrams using the phrase replacement map. - /// 4) Drop the duplicated phrases from the generated unigram array. + /// 1. 獲取原始結果陣列。 + /// 2. 如果有原始結果也出現在濾除表當中的話,則自結果陣列丟棄這類結果。 + /// 3. 如果啟用了語彙置換的話,則對目前經過處理的結果陣列套用語彙置換。 + /// 4. 擁有相同讀音與詞語資料值的單元圖只會留下權重最大的那一筆,其餘重複值會被丟棄。 /// - /// The controller can ask the model to load the primary input method language - /// model while launching and to load the user phrases anytime if the custom - /// files are modified. It does not keep the reference of the data pathes but - /// you have to pass the paths when you ask it to load. + /// LMI 會根據需要分別載入原廠語言模組和其他個別的子語言模組。LMI 本身不會記錄這些 + /// 語言模組的相關資料的存放位置,僅藉由參數來讀取相關訊息。 public class LMInstantiator: Megrez.LanguageModel { - // 在函數內部用以記錄狀態的開關。 + // 在函式內部用以記錄狀態的開關。 public var isPhraseReplacementEnabled = false public var isCNSEnabled = false public var isSymbolEnabled = false @@ -76,9 +69,9 @@ extension vChewing { /// 但是,LMCoreEX 對 2010-2013 年等舊 mac 機種而言,讀取速度異常緩慢。 /// 於是 LMCoreNS 就出場了,專門用來讀取原廠的 plist 格式的辭典。 - // 聲明原廠語言模組 - /// Reverse 的話,第一欄是注音,第二欄是對應的漢字,第三欄是可能的權重。 - /// 不 Reverse 的話,第一欄是漢字,第二欄是對應的注音,第三欄是可能的權重。 + // 聲明原廠語言模組: + // Reverse 的話,第一欄是注音,第二欄是對應的漢字,第三欄是可能的權重。 + // 不 Reverse 的話,第一欄是漢字,第二欄是對應的注音,第三欄是可能的權重。 var lmCore = LMCoreNS( reverse: false, consolidate: false, defaultScore: -9.9, forceDefaultScore: false ) @@ -100,10 +93,10 @@ extension vChewing { var lmReplacements = LMReplacments() var lmAssociates = LMAssociates() - // 初期化的函數先保留 + // 初期化的函式先保留 override init() {} - // 以下這些函數命名暫時保持原樣,等弒神行動徹底結束了再調整。 + // 以下這些函式命名暫時保持原樣,等弒神行動徹底結束了再調整。 public var isLanguageModelLoaded: Bool { lmCore.isLoaded() } public func loadLanguageModel(path: String) { @@ -194,12 +187,12 @@ extension vChewing { // MARK: - Core Functions (Public) - /// Not implemented since we do not have data to provide bigram function. + /// 威注音輸入法目前尚未具備對雙元圖的處理能力,故停用該函式。 // public func bigramsForKeys(preceedingKey: String, key: String) -> [Megrez.Bigram] { } - /// Returns a list of available unigram for the given key. - /// @param key:String represents the BPMF reading or a symbol key. - /// For instance, it you pass "ㄉㄨㄟˇ", it returns "㨃" and other possible candidates. + /// 給定讀音字串,讓 LMI 給出對應的經過處理的單元圖陣列。 + /// - Parameter key: 給定的讀音字串。 + /// - Returns: 對應的經過處理的單元圖陣列。 override open func unigramsFor(key: String) -> [Megrez.Unigram] { if key == " " { /// 給空格鍵指定輸出值。 @@ -267,6 +260,11 @@ extension vChewing { // MARK: - Core Functions (Private) + /// 給定單元圖原始結果陣列,經過語彙過濾處理+置換處理+去重複處理之後,給出單元圖結果陣列。 + /// - Parameters: + /// - unigrams: 傳入的單元圖原始結果陣列 + /// - filteredPairs: 傳入的要過濾掉的鍵值配對陣列 + /// - Returns: 經過語彙過濾處理+置換處理+去重複處理的單元圖結果陣列 func filterAndTransform( unigrams: [Megrez.Unigram], filter filteredPairs: Set diff --git a/Source/Modules/LangModelRelated/SubLMs/lmCoreEX.swift b/Source/Modules/LangModelRelated/SubLMs/lmCoreEX.swift index f3d60bb4..534d83a2 100644 --- a/Source/Modules/LangModelRelated/SubLMs/lmCoreEX.swift +++ b/Source/Modules/LangModelRelated/SubLMs/lmCoreEX.swift @@ -132,12 +132,12 @@ extension vChewing { /// 【該功能無法使用】根據給定的前述讀音索引鍵與當前讀音索引鍵,來獲取資料庫陣列內的對應資料陣列的字串首尾範圍資料、據此自 strData 取得字串形式的資料、生成雙元圖陣列。 /// - /// 威注音輸入法尚未引入雙元圖支援,所以該函數並未擴充相關功能,自然不會起作用。 + /// 威注音輸入法尚未引入雙元圖支援,所以該函式並未擴充相關功能,自然不會起作用。 /// - parameters: /// - precedingKey: 前述讀音索引鍵 /// - key: 當前讀音索引鍵 public func bigramsForKeys(precedingKey: String, key: String) -> [Megrez.Bigram] { - // 這裡用了點廢話處理,不然函數構建體會被 Swift 格式整理工具給毀掉。 + // 這裡用了點廢話處理,不然函式構建體會被 Swift 格式整理工具給毀掉。 // 其實只要一句「[Megrez.Bigram]()」就夠了。 precedingKey == key ? [Megrez.Bigram]() : [Megrez.Bigram]() } diff --git a/Source/Modules/LangModelRelated/SubLMs/lmCoreNS.swift b/Source/Modules/LangModelRelated/SubLMs/lmCoreNS.swift index 7db56652..926bf7fc 100644 --- a/Source/Modules/LangModelRelated/SubLMs/lmCoreNS.swift +++ b/Source/Modules/LangModelRelated/SubLMs/lmCoreNS.swift @@ -126,12 +126,12 @@ extension vChewing { /// 【該功能無法使用】根據給定的前述讀音索引鍵與當前讀音索引鍵,來獲取資料庫陣列內的對應資料陣列的 UTF8 資料、就地分析、生成雙元圖陣列。 /// - /// 威注音輸入法尚未引入雙元圖支援,所以該函數並未擴充相關功能,自然不會起作用。 + /// 威注音輸入法尚未引入雙元圖支援,所以該函式並未擴充相關功能,自然不會起作用。 /// - parameters: /// - precedingKey: 前述讀音索引鍵 /// - key: 當前讀音索引鍵 public func bigramsForKeys(precedingKey: String, key: String) -> [Megrez.Bigram] { - // 這裡用了點廢話處理,不然函數構建體會被 Swift 格式整理工具給毀掉。 + // 這裡用了點廢話處理,不然函式構建體會被 Swift 格式整理工具給毀掉。 // 其實只要一句「[Megrez.Bigram]()」就夠了。 precedingKey == key ? [Megrez.Bigram]() : [Megrez.Bigram]() } @@ -167,7 +167,7 @@ extension vChewing { rangeMap[cnvPhonabetToASCII(key)] != nil } - /// 內部函數,用以將注音讀音索引鍵進行加密。 + /// 內部函式,用以將注音讀音索引鍵進行加密。 /// /// 使用這種加密字串作為索引鍵,可以增加對 plist 資料庫的存取速度。 /// @@ -190,7 +190,7 @@ extension vChewing { return strOutput } - /// 內部函數,用以將被加密的注音讀音索引鍵進行解密。 + /// 內部函式,用以將被加密的注音讀音索引鍵進行解密。 /// /// 如果傳入的字串當中包含 ASCII 下畫線符號的話,則表明該字串並非注音讀音字串,會被忽略處理。 /// - parameters: diff --git a/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift b/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift index cedc44f2..5a3a2da3 100644 --- a/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift +++ b/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift @@ -29,27 +29,34 @@ extension vChewing { public class LMUserOverride { // MARK: - Private Structures - struct Override { + // 這些型別必須得用 class,不然會導致拿不到有效建議。 + + class Override { var count: Int = 0 var timestamp: Double = 0.0 } - struct Observation { + class Observation { var count: Int = 0 var overrides: [String: Override] = [:] - mutating func update(candidate: String, timestamp: Double) { + func update(candidate: String, timestamp: Double) { count += 1 - if var neta = overrides[candidate] { + if let neta = overrides[candidate] { neta.timestamp = timestamp neta.count += 1 + overrides[candidate] = neta } } } - struct KeyObservationPair { + class KeyObservationPair { var key: String var observation: Observation + init(key: String, observation: Observation) { + self.key = key + self.observation = observation + } } // MARK: - Main @@ -74,7 +81,7 @@ extension vChewing { let key = convertKeyFrom(walkedNodes: walkedNodes, cursorIndex: cursorIndex) guard mutLRUMap[key] != nil else { - var observation: Observation = .init() + let observation: Observation = .init() observation.update(candidate: candidate, timestamp: timestamp) let koPair = KeyObservationPair(key: key, observation: observation) mutLRUMap[key] = koPair @@ -87,7 +94,7 @@ extension vChewing { IME.prtDebugIntel("UOM: Observation finished with new observation: \(key)") return } - if var theNeta = mutLRUMap[key] { + if let theNeta = mutLRUMap[key] { theNeta.observation.update(candidate: candidate, timestamp: timestamp) mutLRUList.insert(theNeta, at: 0) mutLRUMap[key] = theNeta diff --git a/Source/Modules/LangModelRelated/SymbolNode.swift b/Source/Modules/LangModelRelated/SymbolNode.swift new file mode 100644 index 00000000..7226b28e --- /dev/null +++ b/Source/Modules/LangModelRelated/SymbolNode.swift @@ -0,0 +1,99 @@ +// 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 + +class SymbolNode { + var title: String + var children: [SymbolNode]? + + init(_ title: String, _ children: [SymbolNode]? = nil) { + self.title = title + self.children = children + } + + init(_ title: String, symbols: String) { + self.title = title + children = Array(symbols).map { SymbolNode(String($0), nil) } + } + + static let catCommonSymbols = String( + format: NSLocalizedString("catCommonSymbols", comment: "")) + static let catHoriBrackets = String( + format: NSLocalizedString("catHoriBrackets", comment: "")) + static let catVertBrackets = String( + format: NSLocalizedString("catVertBrackets", comment: "")) + static let catGreekLetters = String( + format: NSLocalizedString("catGreekLetters", comment: "")) + static let catMathSymbols = String( + format: NSLocalizedString("catMathSymbols", comment: "")) + static let catCurrencyUnits = String( + format: NSLocalizedString("catCurrencyUnits", comment: "")) + static let catSpecialSymbols = String( + format: NSLocalizedString("catSpecialSymbols", comment: "")) + static let catUnicodeSymbols = String( + format: NSLocalizedString("catUnicodeSymbols", comment: "")) + static let catCircledKanjis = String( + format: NSLocalizedString("catCircledKanjis", comment: "")) + static let catCircledKataKana = String( + format: NSLocalizedString("catCircledKataKana", comment: "")) + static let catBracketKanjis = String( + format: NSLocalizedString("catBracketKanjis", comment: "")) + static let catSingleTableLines = String( + format: NSLocalizedString("catSingleTableLines", comment: "")) + static let catDoubleTableLines = String( + format: NSLocalizedString("catDoubleTableLines", comment: "")) + static let catFillingBlocks = String( + format: NSLocalizedString("catFillingBlocks", comment: "")) + static let catLineSegments = String( + format: NSLocalizedString("catLineSegments", comment: "")) + + static let root: SymbolNode = .init( + "/", + [ + SymbolNode("`"), + SymbolNode(catCommonSymbols, symbols: ",、。.?!;:‧‥﹐﹒˙·‘’“”〝〞‵′〃~$%@&#*"), + SymbolNode(catHoriBrackets, symbols: "()「」〔〕{}〈〉『』《》【】﹙﹚﹝﹞﹛﹜"), + SymbolNode(catVertBrackets, symbols: "︵︶﹁﹂︹︺︷︸︿﹀﹃﹄︽︾︻︼"), + SymbolNode( + catGreekLetters, symbols: "αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ" + ), + SymbolNode(catMathSymbols, symbols: "+-×÷=≠≒∞±√<>﹤﹥≦≧∩∪ˇ⊥∠∟⊿㏒㏑∫∮∵∴╳﹢"), + SymbolNode(catCurrencyUnits, symbols: "$€¥¢£₽₨₩฿₺₮₱₭₴₦৲৳૱௹﷼₹₲₪₡₫៛₵₢₸₤₳₥₠₣₰₧₯₶₷"), + SymbolNode(catSpecialSymbols, symbols: "↑↓←→↖↗↙↘↺⇧⇩⇦⇨⇄⇆⇅⇵↻◎○●⊕⊙※△▲☆★◇◆□■▽▼§¥〒¢£♀♂↯"), + SymbolNode(catUnicodeSymbols, symbols: "♨☀☁☂☃♠♥♣♦♩♪♫♬☺☻"), + SymbolNode(catCircledKanjis, symbols: "㊟㊞㊚㊛㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗︎㊘㊙︎㊜㊝㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰🈚︎🈯︎"), + SymbolNode( + catCircledKataKana, symbols: "㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋾" + ), + SymbolNode(catBracketKanjis, symbols: "㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃"), + SymbolNode(catSingleTableLines, symbols: "├─┼┴┬┤┌┐╞═╪╡│▕└┘╭╮╰╯"), + SymbolNode(catDoubleTableLines, symbols: "╔╦╗╠═╬╣╓╥╖╒╤╕║╚╩╝╟╫╢╙╨╜╞╪╡╘╧╛"), + SymbolNode(catFillingBlocks, symbols: "_ˍ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉◢◣◥◤"), + SymbolNode(catLineSegments, symbols: "﹣﹦≡|∣∥–︱—︳╴¯ ̄﹉﹊﹍﹎﹋﹌﹏︴∕﹨╱╲/\"), + ] + ) +} diff --git a/Source/Modules/LangModelRelated/mgrLangModel.swift b/Source/Modules/LangModelRelated/mgrLangModel.swift index 770b4182..08f74d5d 100644 --- a/Source/Modules/LangModelRelated/mgrLangModel.swift +++ b/Source/Modules/LangModelRelated/mgrLangModel.swift @@ -29,7 +29,7 @@ import Cocoa /// 我們不能讓 mgrLangModel 這個靜態管理器來承載下面這些副本變數。 /// 所以,這些副本變數只能放在 mgrLangModel 的外部。 /// 同時,這些變數不對外開放任意存取權限。 -/// 我們只在 mgrLangModel 內部寫幾個回傳函數、供其餘控制模組來讀取。 +/// 我們只在 mgrLangModel 內部寫幾個回傳函式、供其餘控制模組來讀取。 private var gLangModelCHS = vChewing.LMInstantiator() private var gLangModelCHT = vChewing.LMInstantiator() @@ -37,7 +37,7 @@ private var gUserOverrideModelCHS = vChewing.LMUserOverride() private var gUserOverrideModelCHT = vChewing.LMUserOverride() enum mgrLangModel { - /// 寫幾個回傳函數、供其餘控制模組來讀取那些被設為 fileprivate 的器外變數。 + /// 寫幾個回傳函式、供其餘控制模組來讀取那些被設為 fileprivate 的器外變數。 public static var lmCHS: vChewing.LMInstantiator { gLangModelCHS } public static var lmCHT: vChewing.LMInstantiator { gLangModelCHT } public static var uomCHS: vChewing.LMUserOverride { gUserOverrideModelCHS } @@ -295,7 +295,7 @@ enum mgrLangModel { // The above "&" mutates the "isFolder" value to the real one received by the "folderExist". // 路徑沒有結尾斜槓的話,會導致目錄合規性判定失準。 - // 出於每個型別每個函數的自我責任原則,這裡多檢查一遍也不壞。 + // 出於每個型別每個函式的自我責任原則,這裡多檢查一遍也不壞。 var folderPath = folderPath // Convert the incoming constant to a variable. if isFolder.boolValue { folderPath?.ensureTrailingSlash() @@ -351,7 +351,7 @@ enum mgrLangModel { return true } - // MARK: - 用以讀取使用者語彙檔案目錄的函數,會自動對 mgrPrefs 當中的參數糾偏。 + // MARK: - 用以讀取使用者語彙檔案目錄的函式,會自動對 mgrPrefs 當中的參數糾偏。 // 當且僅當 mgrPrefs 當中的參數不合規(比如非實在路徑、或者無權限寫入)時,才會糾偏。 diff --git a/Source/Modules/LanguageParsers/Megrez/1_BlockReadingBuilder.swift b/Source/Modules/LanguageParsers/Megrez/1_Compositor.swift similarity index 82% rename from Source/Modules/LanguageParsers/Megrez/1_BlockReadingBuilder.swift rename to Source/Modules/LanguageParsers/Megrez/1_Compositor.swift index c8f5f1e4..3ba9a524 100644 --- a/Source/Modules/LanguageParsers/Megrez/1_BlockReadingBuilder.swift +++ b/Source/Modules/LanguageParsers/Megrez/1_Compositor.swift @@ -24,40 +24,43 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ extension Megrez { - /// 分節讀音槽。 - public class BlockReadingBuilder { + /// 組字器。 + public class Compositor { /// 給被丟掉的節點路徑施加的負權重。 private let kDroppedPathScore: Double = -999 - /// 該分節讀音槽的游標位置。 + /// 該組字器的游標位置。 private var mutCursorIndex: Int = 0 - /// 該分節讀音槽的讀音陣列。 + /// 該組字器的讀音陣列。 private var mutReadings: [String] = [] - /// 該分節讀音槽的軌格。 + /// 該組字器的軌格。 private var mutGrid: Grid = .init() - /// 該分節讀音槽所使用的語言模型。 + /// 該組字器所使用的語言模型。 private var mutLM: LanguageModel - /// 公開該分節讀音槽內可以允許的最大詞長。 + /// 公開該組字器內可以允許的最大詞長。 public var maxBuildSpanLength: Int { mutGrid.maxBuildSpanLength } /// 公開:多字讀音鍵當中用以分割漢字讀音的記號,預設為空。 public var joinSeparator: String = "" - /// 公開:該分節讀音槽的游標位置。 + /// 公開:該組字器的游標位置。 public var cursorIndex: Int { get { mutCursorIndex } set { mutCursorIndex = (newValue < 0) ? 0 : min(newValue, mutReadings.count) } } - /// 公開:該分節讀音槽的軌格(唯讀)。 + /// 公開:該組字器是否為空。 + public var isEmpty: Bool { grid.isEmpty } + + /// 公開:該組字器的軌格(唯讀)。 public var grid: Grid { mutGrid } - /// 公開:該分節讀音槽的長度,也就是內建漢字讀音的數量(唯讀)。 + /// 公開:該組字器的長度,也就是內建漢字讀音的數量(唯讀)。 public var length: Int { mutReadings.count } - /// 公開:該分節讀音槽的讀音陣列(唯讀)。 + /// 公開:該組字器的讀音陣列(唯讀)。 public var readings: [String] { mutReadings } - /// 分節讀音槽。 + /// 組字器。 /// - Parameters: /// - lm: 語言模型。可以是任何基於 Megrez.LanguageModel 的衍生型別。 - /// - length: 指定該分節讀音槽內可以允許的最大詞長,預設為 10 字。 + /// - length: 指定該組字器內可以允許的最大詞長,預設為 10 字。 /// - separator: 多字讀音鍵當中用以分割漢字讀音的記號,預設為空。 public init(lm: LanguageModel, length: Int = 10, separator: String = "") { mutLM = lm @@ -65,7 +68,7 @@ extension Megrez { joinSeparator = separator } - /// 分節讀音槽自我清空專用函數。 + /// 組字器自我清空專用函式。 public func clear() { mutCursorIndex = 0 mutReadings.removeAll() @@ -109,10 +112,10 @@ extension Megrez { return true } - /// 移除該分節讀音槽的第一個讀音單元。 + /// 移除該組字器的第一個讀音單元。 /// /// 用於輸入法組字區長度上限處理: - /// 將該位置要溢出的敲字內容遞交之後、再執行這個函數。 + /// 將該位置要溢出的敲字內容遞交之後、再執行這個函式。 @discardableResult public func removeHeadReadings(count: Int) -> Bool { let count = abs(count) // 防呆 if count > length { @@ -179,12 +182,14 @@ extension Megrez { $0.scoreForSort > $1.scoreForSort } - if let nodeOfNodeZero = nodes[0].node, nodeOfNodeZero.score >= nodeOfNodeZero.kSelectedCandidateScore { + if let nodeZero = nodes[0].node, nodeZero.score >= nodeZero.kSelectedCandidateScore { // 在使用者有選過候選字詞的情況下,摒棄非依此據而成的節點路徑。 - var nodeZero = nodes[0] - nodeZero.accumulatedScore = accumulatedScore + nodeOfNodeZero.score - var path: [NodeAnchor] = reverseWalk(at: location - nodeZero.spanningLength, score: nodeZero.accumulatedScore) - path.insert(nodeZero, at: 0) + var anchorZero = nodes[0] + anchorZero.accumulatedScore = accumulatedScore + nodeZero.score + var path: [NodeAnchor] = reverseWalk( + at: location - anchorZero.spanningLength, score: anchorZero.accumulatedScore + ) + path.insert(anchorZero, at: 0) paths.append(path) } else if !longPhrases.isEmpty { var path = [NodeAnchor]() @@ -202,17 +207,12 @@ extension Megrez { continue } theAnchor.accumulatedScore = accumulatedScore + theNode.score - if joinedValue.count >= longPhrases[0].count { - path = reverseWalk( - at: location - theAnchor.spanningLength, score: theAnchor.accumulatedScore, joinedPhrase: "", - longPhrases: .init() - ) - } else { - path = reverseWalk( - at: location - theAnchor.spanningLength, score: theAnchor.accumulatedScore, joinedPhrase: joinedValue, - longPhrases: longPhrases - ) - } + path = reverseWalk( + at: location - theAnchor.spanningLength, + score: theAnchor.accumulatedScore, + joinedPhrase: (joinedValue.count >= longPhrases[0].count) ? "" : joinedValue, + longPhrases: .init() + ) path.insert(theAnchor, at: 0) paths.append(path) } @@ -234,17 +234,11 @@ extension Megrez { guard let theNode = theAnchor.node else { continue } theAnchor.accumulatedScore = accumulatedScore + theNode.score var path = [NodeAnchor]() - if theAnchor.spanningLength > 1 { - path = reverseWalk( - at: location - theAnchor.spanningLength, score: theAnchor.accumulatedScore, joinedPhrase: "", - longPhrases: .init() - ) - } else { - path = reverseWalk( - at: location - theAnchor.spanningLength, score: theAnchor.accumulatedScore, - joinedPhrase: theNode.currentKeyValue.value, longPhrases: longPhrases - ) - } + path = reverseWalk( + at: location - theAnchor.spanningLength, score: theAnchor.accumulatedScore, + joinedPhrase: (theAnchor.spanningLength > 1) ? "" : theNode.currentKeyValue.value, + longPhrases: .init() + ) path.insert(theAnchor, at: 0) paths.append(path) } diff --git a/Source/Modules/LanguageParsers/Megrez/2_Grid.swift b/Source/Modules/LanguageParsers/Megrez/2_Grid.swift index 3eec69ba..8b1b82b8 100644 --- a/Source/Modules/LanguageParsers/Megrez/2_Grid.swift +++ b/Source/Modules/LanguageParsers/Megrez/2_Grid.swift @@ -38,6 +38,9 @@ extension Megrez { /// 軌格的寬度,也就是其內的幅位陣列當中的幅位數量。 var width: Int { mutSpans.count } + /// 軌格是否為空。 + var isEmpty: Bool { mutSpans.isEmpty } + public init(spanLength: Int = 10) { mutMaxBuildSpanLength = spanLength mutSpans = [Megrez.Span]() @@ -256,9 +259,9 @@ extension Megrez.Grid { strOutput += "\(np.currentKeyValue.value);\n" if (p + ni) < mutSpans.count { - let destinatedSpan = mutSpans[p + ni] - for q in 0...(destinatedSpan.maximumLength) { - if let dn = destinatedSpan.node(length: q) { + let destinationSpan = mutSpans[p + ni] + for q in 0...(destinationSpan.maximumLength) { + if let dn = destinationSpan.node(length: q) { strOutput += np.currentKeyValue.value + " -> " + dn.currentKeyValue.value + ";\n" } } diff --git a/Source/Modules/LanguageParsers/Megrez/3_Span.swift b/Source/Modules/LanguageParsers/Megrez/3_Span.swift index 6ea9d45a..9d000575 100644 --- a/Source/Modules/LanguageParsers/Megrez/3_Span.swift +++ b/Source/Modules/LanguageParsers/Megrez/3_Span.swift @@ -49,9 +49,7 @@ extension Megrez { mutating func insert(node: Node, length: Int) { let length = abs(length) // 防呆 mutLengthNodeMap[length] = node - if length > mutMaximumLength { - mutMaximumLength = length - } + mutMaximumLength = max(mutMaximumLength, length) } /// 移除任何比給定的長度更長的節點。 @@ -60,21 +58,19 @@ extension Megrez { mutating func removeNodeOfLengthGreaterThan(_ length: Int) { let length = abs(length) // 防呆 if length > mutMaximumLength { return } - var max = 0 + var lenMax = 0 var removalList: [Int: Megrez.Node] = [:] for key in mutLengthNodeMap.keys { if key > length { removalList[key] = mutLengthNodeMap[key] } else { - if key > max { - max = key - } + lenMax = max(lenMax, key) } } for key in removalList.keys { mutLengthNodeMap.removeValue(forKey: key) } - mutMaximumLength = max + mutMaximumLength = lenMax } /// 給定節點長度,獲取節點。 diff --git a/Source/Modules/LanguageParsers/Megrez/4_Node.swift b/Source/Modules/LanguageParsers/Megrez/4_Node.swift index 813cc30c..4f86ad46 100644 --- a/Source/Modules/LanguageParsers/Megrez/4_Node.swift +++ b/Source/Modules/LanguageParsers/Megrez/4_Node.swift @@ -25,7 +25,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. extension Megrez { /// 節點。 - public class Node: CustomStringConvertible { + public class Node { /// 當前節點對應的語言模型。 private let mutLM: LanguageModel = .init() /// 鍵。 @@ -109,23 +109,16 @@ extension Megrez { for neta in precedingKeyValues { let bigrams = mutPrecedingBigramMap[neta] ?? [] for bigram in bigrams { - if bigram.score > max { - if let valRetrieved = mutValueUnigramIndexMap[bigram.keyValue.value] { - newIndex = valRetrieved as Int - max = bigram.score - } + guard bigram.score > max else { continue } + if let valRetrieved = mutValueUnigramIndexMap[bigram.keyValue.value] { + newIndex = valRetrieved as Int + max = bigram.score } } } } - - if mutScore != max { - mutScore = max - } - - if mutSelectedUnigramIndex != newIndex { - mutSelectedUnigramIndex = newIndex - } + mutScore = max + mutSelectedUnigramIndex = newIndex } /// 選中位於給定索引位置的候選字詞。 @@ -170,13 +163,5 @@ extension Megrez { } return 0.0 } - - public static func == (lhs: Node, rhs: Node) -> Bool { - lhs.mutUnigrams == rhs.mutUnigrams && lhs.mutCandidates == rhs.mutCandidates - && lhs.mutValueUnigramIndexMap == rhs.mutValueUnigramIndexMap - && lhs.mutPrecedingBigramMap == rhs.mutPrecedingBigramMap - && lhs.mutCandidateFixed == rhs.mutCandidateFixed - && lhs.mutSelectedUnigramIndex == rhs.mutSelectedUnigramIndex - } } } diff --git a/Source/Modules/LanguageParsers/Megrez/5_LanguageModel.swift b/Source/Modules/LanguageParsers/Megrez/5_LanguageModel.swift index 776f6442..d585aba7 100644 --- a/Source/Modules/LanguageParsers/Megrez/5_LanguageModel.swift +++ b/Source/Modules/LanguageParsers/Megrez/5_LanguageModel.swift @@ -24,11 +24,11 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ extension Megrez { - /// 語言模型框架,回頭實際使用時需要派生一個型別、且重寫相關函數。 + /// 語言模型框架,回頭實際使用時需要派生一個型別、且重寫相關函式。 open class LanguageModel { public init() {} - // 這裡寫了一點假內容,不然有些 Swift 格式化工具會破壞掉函數的參數設計。 + // 這裡寫了一點假內容,不然有些 Swift 格式化工具會破壞掉函式的參數設計。 /// 給定鍵,讓語言模型找給一筆單元圖。 open func unigramsFor(key: String) -> [Megrez.Unigram] { diff --git a/Source/Modules/LanguageParsers/Megrez/6_Unigram.swift b/Source/Modules/LanguageParsers/Megrez/6_Unigram.swift index 62b0726c..bced45ad 100644 --- a/Source/Modules/LanguageParsers/Megrez/6_Unigram.swift +++ b/Source/Modules/LanguageParsers/Megrez/6_Unigram.swift @@ -49,7 +49,7 @@ extension Megrez { hasher.combine(score) } - // 這個函數不再需要了。 + // 這個函式不再需要了。 public static func compareScore(a: Unigram, b: Unigram) -> Bool { a.score > b.score } diff --git a/Source/UI/CandidateUI/ctlCandidateUniversal.swift b/Source/UI/CandidateUI/ctlCandidateUniversal.swift index 568f5f03..c421f8d3 100644 --- a/Source/UI/CandidateUI/ctlCandidateUniversal.swift +++ b/Source/UI/CandidateUI/ctlCandidateUniversal.swift @@ -178,7 +178,7 @@ private class vwrCandidateUniversal: NSView { if index == highlightedIndex { let colorBlendAmount: CGFloat = IME.isDarkMode() ? 0.25 : 0 // The background color of the highlightened candidate - switch ctlInputMethod.currentKeyHandler.inputMode { + switch IME.currentInputMode { case InputMode.imeModeCHS: NSColor.systemRed.blended( withFraction: colorBlendAmount, @@ -202,7 +202,7 @@ private class vwrCandidateUniversal: NSView { } else { NSColor.controlBackgroundColor.setFill() } - switch ctlInputMethod.currentKeyHandler.inputMode { + switch IME.currentInputMode { case InputMode.imeModeCHS: if #available(macOS 12.0, *) { activeCandidateAttr[.languageIdentifier] = "zh-Hans" as AnyObject @@ -248,7 +248,7 @@ private class vwrCandidateUniversal: NSView { if index == highlightedIndex { let colorBlendAmount: CGFloat = IME.isDarkMode() ? 0.25 : 0 // The background color of the highlightened candidate - switch ctlInputMethod.currentKeyHandler.inputMode { + switch IME.currentInputMode { case InputMode.imeModeCHS: NSColor.systemRed.blended( withFraction: colorBlendAmount, @@ -272,7 +272,7 @@ private class vwrCandidateUniversal: NSView { } else { NSColor.controlBackgroundColor.setFill() } - switch ctlInputMethod.currentKeyHandler.inputMode { + switch IME.currentInputMode { case InputMode.imeModeCHS: if #available(macOS 12.0, *) { activeCandidateAttr[.languageIdentifier] = "zh-Hans" as AnyObject diff --git a/Update-Info.plist b/Update-Info.plist index 71c02ceb..c81a51b3 100644 --- a/Update-Info.plist +++ b/Update-Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.7.0 + 1.7.1 CFBundleVersion - 1970 + 1971 UpdateInfoEndpoint https://gitee.com/vchewing/vChewing-macOS/raw/main/Update-Info.plist UpdateInfoSite diff --git a/vChewing.pkgproj b/vChewing.pkgproj index c781ad21..06472123 100644 --- a/vChewing.pkgproj +++ b/vChewing.pkgproj @@ -726,7 +726,7 @@ USE_HFS+_COMPRESSION VERSION - 1.7.0 + 1.7.1 TYPE 0 diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 07127204..b77c9f52 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -17,15 +17,15 @@ 5B38F59D281E2E49007D5F5D /* 4_Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1A15FC0EB100ABF4B3 /* 4_Node.swift */; }; 5B38F59E281E2E49007D5F5D /* 6_Bigram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1415FC0EB100ABF4B3 /* 6_Bigram.swift */; }; 5B38F59F281E2E49007D5F5D /* 3_NodeAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1B15FC0EB100ABF4B3 /* 3_NodeAnchor.swift */; }; - 5B38F5A1281E2E49007D5F5D /* 1_BlockReadingBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1515FC0EB100ABF4B3 /* 1_BlockReadingBuilder.swift */; }; + 5B38F5A1281E2E49007D5F5D /* 1_Compositor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1515FC0EB100ABF4B3 /* 1_Compositor.swift */; }; 5B38F5A2281E2E49007D5F5D /* 0_Megrez.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1615FC0EB100ABF4B3 /* 0_Megrez.swift */; }; 5B38F5A3281E2E49007D5F5D /* 3_Span.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1C15FC0EB100ABF4B3 /* 3_Span.swift */; }; 5B38F5A4281E2E49007D5F5D /* 5_LanguageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F1915FC0EB100ABF4B3 /* 5_LanguageModel.swift */; }; + 5B3A87BC28597CDB0090E163 /* SymbolNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B3A87BB28597CDB0090E163 /* SymbolNode.swift */; }; 5B40730C281672610023DFFF /* lmAssociates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B407309281672610023DFFF /* lmAssociates.swift */; }; 5B40730D281672610023DFFF /* lmReplacements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B40730A281672610023DFFF /* lmReplacements.swift */; }; 5B54E743283A7D89001ECBDC /* lmCoreNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B54E742283A7D89001ECBDC /* lmCoreNS.swift */; }; 5B5E535227EF261400C6AA1E /* IME.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5E535127EF261400C6AA1E /* IME.swift */; }; - 5B61B0CA280BEFD4002E3CFA /* KeyHandler_Misc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B61B0C9280BEFD4002E3CFA /* KeyHandler_Misc.swift */; }; 5B62A32927AE77D100A19448 /* FSEventStreamHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A32827AE77D100A19448 /* FSEventStreamHelper.swift */; }; 5B62A33227AE792F00A19448 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33127AE792F00A19448 /* InputSourceHelper.swift */; }; 5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33527AE795800A19448 /* mgrPrefs.swift */; }; @@ -111,7 +111,7 @@ D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; }; D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */; }; D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */; }; - D4A13D5A27A59F0B003BE359 /* ctlInputMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A13D5927A59D5C003BE359 /* ctlInputMethod.swift */; }; + D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */; }; D4E33D8A27A838CF006DB1CF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8827A838CF006DB1CF /* Localizable.strings */; }; D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8D27A838F0006DB1CF /* InfoPlist.strings */; }; D4E569DC27A34D0E00AC2CEF /* CTools.m in Sources */ = {isa = PBXBuildFile; fileRef = D4E569DB27A34CC100AC2CEF /* CTools.m */; }; @@ -187,11 +187,11 @@ 5B2DB17127AF8771006D874E /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; name = Makefile; path = Data/Makefile; sourceTree = ""; }; 5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = vChewingKeyLayout.bundle; sourceTree = ""; }; 5B3133BE280B229700A4A505 /* KeyHandler_States.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = KeyHandler_States.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + 5B3A87BB28597CDB0090E163 /* SymbolNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolNode.swift; sourceTree = ""; }; 5B407309281672610023DFFF /* lmAssociates.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; lineEnding = 0; path = lmAssociates.swift; sourceTree = ""; usesTabs = 0; }; 5B40730A281672610023DFFF /* lmReplacements.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; lineEnding = 0; path = lmReplacements.swift; sourceTree = ""; usesTabs = 0; }; 5B54E742283A7D89001ECBDC /* lmCoreNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lmCoreNS.swift; sourceTree = ""; }; 5B5E535127EF261400C6AA1E /* IME.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = IME.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; - 5B61B0C9280BEFD4002E3CFA /* KeyHandler_Misc.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = KeyHandler_Misc.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A32827AE77D100A19448 /* FSEventStreamHelper.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = FSEventStreamHelper.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A33127AE792F00A19448 /* InputSourceHelper.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputSourceHelper.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A33527AE795800A19448 /* mgrPrefs.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = mgrPrefs.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; @@ -284,7 +284,7 @@ 6A0D4EF515FC0DA600ABF4B3 /* IME-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "IME-Info.plist"; sourceTree = ""; }; 6A0D4EF615FC0DA600ABF4B3 /* vChewing-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "vChewing-Prefix.pch"; sourceTree = ""; tabWidth = 4; usesTabs = 0; }; 6A0D4F1415FC0EB100ABF4B3 /* 6_Bigram.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = 6_Bigram.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; - 6A0D4F1515FC0EB100ABF4B3 /* 1_BlockReadingBuilder.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = 1_BlockReadingBuilder.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + 6A0D4F1515FC0EB100ABF4B3 /* 1_Compositor.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = 1_Compositor.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 6A0D4F1615FC0EB100ABF4B3 /* 0_Megrez.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = 0_Megrez.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 6A0D4F1715FC0EB100ABF4B3 /* 2_Grid.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = 2_Grid.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 6A0D4F1815FC0EB100ABF4B3 /* 7_KeyValuePair.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = 7_KeyValuePair.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; @@ -310,7 +310,7 @@ D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = main.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlPrefWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlNonModalAlertWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; - D4A13D5927A59D5C003BE359 /* ctlInputMethod.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlInputMethod.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlInputMethod_Core.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; D4E33D8927A838CF006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; D4E33D8E27A838F0006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; D4E569DA27A34CC100AC2CEF /* CTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = CTools.h; sourceTree = ""; tabWidth = 4; usesTabs = 0; }; @@ -405,6 +405,8 @@ isa = PBXGroup; children = ( 5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */, + D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */, + 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */, D4E569DA27A34CC100AC2CEF /* CTools.h */, D4E569DB27A34CC100AC2CEF /* CTools.m */, D456576D279E4F7B00DF6BC9 /* InputSignal.swift */, @@ -412,7 +414,6 @@ 5BD0113C2818543900609769 /* KeyHandler_Core.swift */, 5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */, 5B7F225C2808501000DDD3CB /* KeyHandler_HandleInput.swift */, - 5B61B0C9280BEFD4002E3CFA /* KeyHandler_Misc.swift */, 5B3133BE280B229700A4A505 /* KeyHandler_States.swift */, 5B62A33727AE79CD00A19448 /* StringUtils.swift */, 5BAA8FBD282CAF380066C406 /* SyllableComposer.swift */, @@ -442,8 +443,6 @@ isa = PBXGroup; children = ( 5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */, - 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */, - D4A13D5927A59D5C003BE359 /* ctlInputMethod.swift */, 5B5E535127EF261400C6AA1E /* IME.swift */, 5B62A33127AE792F00A19448 /* InputSourceHelper.swift */, 5B62A33527AE795800A19448 /* mgrPrefs.swift */, @@ -466,6 +465,7 @@ 5B949BDA2816DDBC00D87B5D /* LMConsolidator.swift */, 5BD0113A28180D6100609769 /* LMInstantiator.swift */, 5BAEFACF28012565001F42C9 /* mgrLangModel.swift */, + 5B3A87BB28597CDB0090E163 /* SymbolNode.swift */, ); path = LangModelRelated; sourceTree = ""; @@ -761,7 +761,7 @@ isa = PBXGroup; children = ( 6A0D4F1615FC0EB100ABF4B3 /* 0_Megrez.swift */, - 6A0D4F1515FC0EB100ABF4B3 /* 1_BlockReadingBuilder.swift */, + 6A0D4F1515FC0EB100ABF4B3 /* 1_Compositor.swift */, 6A0D4F1715FC0EB100ABF4B3 /* 2_Grid.swift */, 6A0D4F1B15FC0EB100ABF4B3 /* 3_NodeAnchor.swift */, 6A0D4F1C15FC0EB100ABF4B3 /* 3_Span.swift */, @@ -1059,7 +1059,7 @@ D461B792279DAC010070E734 /* InputState.swift in Sources */, 5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */, D47B92C027972AD100458394 /* main.swift in Sources */, - D4A13D5A27A59F0B003BE359 /* ctlInputMethod.swift in Sources */, + D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */, 5BA9FD4827FEF3C9002DE248 /* PreferencesWindowController.swift in Sources */, 5BD0113B28180D6100609769 /* LMInstantiator.swift in Sources */, D4E569DC27A34D0E00AC2CEF /* CTools.m in Sources */, @@ -1071,6 +1071,7 @@ D456576E279E4F7B00DF6BC9 /* InputSignal.swift in Sources */, 5BA9FD1027FEDB6B002DE248 /* suiPrefPaneKeyboard.swift in Sources */, 5B3133BF280B229700A4A505 /* KeyHandler_States.swift in Sources */, + 5B3A87BC28597CDB0090E163 /* SymbolNode.swift in Sources */, 5BA9FD4327FEF3C8002DE248 /* Preferences.swift in Sources */, 5BA9FD4427FEF3C8002DE248 /* SegmentedControlStyleViewController.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */, @@ -1100,7 +1101,6 @@ 5B62A33227AE792F00A19448 /* InputSourceHelper.swift in Sources */, 5B5E535227EF261400C6AA1E /* IME.swift in Sources */, 5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */, - 5B61B0CA280BEFD4002E3CFA /* KeyHandler_Misc.swift in Sources */, 5B38F59A281E2E49007D5F5D /* 6_Unigram.swift in Sources */, 5BA9FD4027FEF3C8002DE248 /* Localization.swift in Sources */, 5BAA8FBE282CAF380066C406 /* SyllableComposer.swift in Sources */, @@ -1113,7 +1113,7 @@ 5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */, 5BA9FD3F27FEF3C8002DE248 /* Pane.swift in Sources */, 5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */, - 5B38F5A1281E2E49007D5F5D /* 1_BlockReadingBuilder.swift in Sources */, + 5B38F5A1281E2E49007D5F5D /* 1_Compositor.swift in Sources */, 5BDC1CFA27FDF1310052C2B9 /* apiUpdate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1304,7 +1304,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1970; + CURRENT_PROJECT_VERSION = 1971; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1328,7 +1328,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.11.5; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.7.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor; @@ -1361,7 +1361,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1970; + CURRENT_PROJECT_VERSION = 1971; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -1381,7 +1381,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.11.5; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.7.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor; @@ -1499,7 +1499,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1970; + CURRENT_PROJECT_VERSION = 1971; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -1535,7 +1535,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.11.5; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.7.1; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1567,7 +1567,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1970; + CURRENT_PROJECT_VERSION = 1971; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; @@ -1598,7 +1598,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.11.5; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.7.1; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1625,7 +1625,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1970; + CURRENT_PROJECT_VERSION = 1971; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -1651,7 +1651,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.11.5; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.7.1; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "org.atelierInmu.vChewing.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1678,7 +1678,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1970; + CURRENT_PROJECT_VERSION = 1971; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -1699,7 +1699,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.11.5; - MARKETING_VERSION = 1.7.0; + MARKETING_VERSION = 1.7.1; PRODUCT_BUNDLE_IDENTIFIER = "org.atelierInmu.vChewing.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";