diff --git a/AUTHORS b/AUTHORS index a08f15e5..8a513d36 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,12 +15,11 @@ $ 3rd-Party Modules Used: $ Contributors and volunteers of the upstream repo, having no responsibility in discussing anything in the current repo: - Zonble Yang: - - McBopomofo for macOS 2.x architect, especially state-based IME behavior management. + - McBopomofo for macOS 2.x architect. - Voltaire candidate window MK2 (massively modified as MK3 in vChewing by Shiki Suen). - Notifier window and Tooltip UI. - - NSStringUtils and FSEventStreamHelper. + - FSEventStreamHelper. - App-style installer (only preserved for developer purposes). - - InputSource Helper. - mgrPrefs (userdefaults manager). - apiUpdate. - Mengjuei Hsieh: diff --git a/Installer/AppDelegate.swift b/Installer/AppDelegate.swift index 5383f960..f9415522 100644 --- a/Installer/AppDelegate.swift +++ b/Installer/AppDelegate.swift @@ -9,6 +9,7 @@ // requirements defined in MIT License. import Cocoa +import InputMethodKit private let kTargetBin = "vChewing" private let kTargetType = "app" @@ -47,6 +48,17 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { private var translocationRemovalStartTime: Date? private var currentVersionNumber: Int = 0 + let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app") + + var allRegisteredInstancesOfThisInputMethod: [TISInputSource] { + guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any], + let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any] + else { + return [] + } + return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) } + } + func runAlertPanel(title: String, message: String, buttonTitle: String) { let alert = NSAlert() alert.alertStyle = .informational @@ -230,23 +242,20 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { endAppWithDelay() } - let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app") - _ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)") - guard let imeBundle = Bundle(url: imeURLInstalled), - let imeIdentifier = imeBundle.bundleIdentifier + guard let theBundle = Bundle(url: imeURLInstalled), + let imeIdentifier = theBundle.bundleIdentifier else { endAppWithDelay() return } - let imeBundleURL = imeBundle.bundleURL - var inputSource = InputSourceHelper.inputSource(for: imeIdentifier) + let imeBundleURL = theBundle.bundleURL - if inputSource == nil { + if allRegisteredInstancesOfThisInputMethod.isEmpty { NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).") - let status = InputSourceHelper.registerTnputSource(at: imeBundleURL) + let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr) if !status { let message = String( format: NSLocalizedString( @@ -262,8 +271,7 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { return } - inputSource = InputSourceHelper.inputSource(for: imeIdentifier) - if inputSource == nil { + if allRegisteredInstancesOfThisInputMethod.isEmpty { let message = String( format: NSLocalizedString( "Cannot find input source %@ after registration.", comment: "" @@ -285,19 +293,20 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { NSLog("Installer runs with the pre-macOS 12 flow.") } - // If the IME is not enabled, enable it. Also, unconditionally enable it on macOS 12.0+, + // Unconditionally enable the IME on macOS 12.0+, // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* // enabled in the user's current set of IMEs (which means the IME does not show up in // the user's input menu). - var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!) - if !mainInputSourceEnabled || isMacOS12OrAbove { - mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!) - if mainInputSourceEnabled { + var mainInputSourceEnabled = false + + allRegisteredInstancesOfThisInputMethod.forEach { + if $0.activate() { NSLog("Input method enabled: \(imeIdentifier)") } else { NSLog("Failed to enable input method: \(imeIdentifier)") } + mainInputSourceEnabled = $0.isActivated } // Alert Panel diff --git a/Source/Data b/Source/Data index 4d6ed238..0293f06c 160000 --- a/Source/Data +++ b/Source/Data @@ -1 +1 @@ -Subproject commit 4d6ed238c037c4d4f5464f6914198a503fd1f21a +Subproject commit 0293f06c92ba9b95dd42debf662b8f8bb5834bdd diff --git a/Source/Modules/AppDelegate.swift b/Source/Modules/AppDelegate.swift index 56993a25..05e1c6f1 100644 --- a/Source/Modules/AppDelegate.swift +++ b/Source/Modules/AppDelegate.swift @@ -12,7 +12,7 @@ import Cocoa import InputMethodKit @objc(AppDelegate) -class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate, +class AppDelegate: NSObject, NSApplicationDelegate, FSEventStreamHelperDelegate, NSUserNotificationCenterDelegate { func helper(_: FSEventStreamHelper, didReceive _: [FSEventStreamHelper.Event]) { @@ -30,7 +30,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega private var ctlPrefWindowInstance: ctlPrefWindow? private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window private var checkTask: URLSessionTask? - private var updateNextStepURL: URL? public var fsStreamHelper = FSEventStreamHelper( path: mgrLangModel.dataFolderPath(isDefaultFolder: false), queue: DispatchQueue(label: "vChewing User Phrases") @@ -42,7 +41,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega ctlPrefWindowInstance = nil ctlAboutWindowInstance = nil checkTask = nil - updateNextStepURL = nil fsStreamHelper.stop() fsStreamHelper.delegate = nil } @@ -137,7 +135,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega case .success(let apiResult): switch apiResult { case .shouldUpdate(let report): - updateNextStepURL = report.siteUrl let content = String( format: NSLocalizedString( "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@", @@ -150,21 +147,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega report.versionDescription ) IME.prtDebugIntel("vChewingDebug: \(content)") - currentAlertType = "Update" - ctlNonModalAlertWindow.shared.show( - title: NSLocalizedString( - "New Version Available", comment: "" - ), - content: content, - confirmButtonTitle: NSLocalizedString( - "Visit Website", comment: "" - ), - cancelButtonTitle: NSLocalizedString( - "Not Now", comment: "" - ), - cancelAsDefault: false, - delegate: self - ) + let alert = NSAlert() + alert.messageText = NSLocalizedString("New Version Available", comment: "") + alert.informativeText = content + alert.addButton(withTitle: NSLocalizedString("Visit Website", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Not Now", comment: "")) + let result = alert.runModal() + if result == NSApplication.ModalResponse.alertFirstButtonReturn { + if let siteURL = report.siteUrl { + NSWorkspace.shared.open(siteURL) + } + } NSApp.setActivationPolicy(.accessory) case .noNeedToUpdate, .ignored: break @@ -172,9 +165,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega case .failure(let error): switch error { case VersionUpdateApiError.connectionError(let message): - let title = NSLocalizedString( - "Update Check Failed", comment: "" - ) + let title = NSLocalizedString("Update Check Failed", comment: "") let content = String( format: NSLocalizedString( "There may be no internet connection or the server failed to respond.\n\nError message: %@", @@ -183,13 +174,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega ) let buttonTitle = NSLocalizedString("Dismiss", comment: "") IME.prtDebugIntel("vChewingDebug: \(content)") - currentAlertType = "Update" - ctlNonModalAlertWindow.shared.show( - title: title, content: content, - confirmButtonTitle: buttonTitle, - cancelButtonTitle: nil, - cancelAsDefault: false, delegate: nil - ) + + let alert = NSAlert() + alert.messageText = title + alert.informativeText = content + alert.addButton(withTitle: buttonTitle) + alert.runModal() NSApp.setActivationPolicy(.accessory) default: break @@ -205,41 +195,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega "This will remove vChewing Input Method from this user account, requiring your confirmation.", comment: "" )) - ctlNonModalAlertWindow.shared.show( - title: NSLocalizedString("Uninstallation", comment: ""), content: content, - confirmButtonTitle: NSLocalizedString("OK", comment: ""), - cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, - delegate: self - ) + let alert = NSAlert() + alert.messageText = NSLocalizedString("Uninstallation", comment: "") + alert.informativeText = content + alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Not Now", comment: "")) + let result = alert.runModal() + if result == NSApplication.ModalResponse.alertFirstButtonReturn { + NSWorkspace.shared.openFile( + mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder" + ) + IME.uninstall(isSudo: false, selfKill: true) + } NSApp.setActivationPolicy(.accessory) } - func ctlNonModalAlertWindowDidConfirm(_: ctlNonModalAlertWindow) { - switch currentAlertType { - case "Uninstall": - NSWorkspace.shared.openFile( - mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder" - ) - IME.uninstall(isSudo: false, selfKill: true) - case "Update": - if let updateNextStepURL = updateNextStepURL { - NSWorkspace.shared.open(updateNextStepURL) - } - updateNextStepURL = nil - default: - break - } - } - - func ctlNonModalAlertWindowDidCancel(_: ctlNonModalAlertWindow) { - switch currentAlertType { - case "Update": - updateNextStepURL = nil - default: - break - } - } - // New About Window @IBAction func about(_: Any) { (NSApp.delegate as? AppDelegate)?.showAbout() diff --git a/Source/Modules/ControllerModules/AppleKeyboardConverter.swift b/Source/Modules/ControllerModules/AppleKeyboardConverter.swift index c8731c68..45061982 100644 --- a/Source/Modules/ControllerModules/AppleKeyboardConverter.swift +++ b/Source/Modules/ControllerModules/AppleKeyboardConverter.swift @@ -7,23 +7,8 @@ // requirements defined in MIT License. enum AppleKeyboardConverter { - static let arrDynamicBasicKeyLayout: [String] = [ - "com.apple.keylayout.ZhuyinBopomofo", - "com.apple.keylayout.ZhuyinEten", - "org.atelierInmu.vChewing.keyLayouts.vchewingdachen", - "org.atelierInmu.vChewing.keyLayouts.vchewingmitac", - "org.atelierInmu.vChewing.keyLayouts.vchewingibm", - "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou", - "org.atelierInmu.vChewing.keyLayouts.vchewingeten", - "org.unknown.keylayout.vChewingDachen", - "org.unknown.keylayout.vChewingFakeSeigyou", - "org.unknown.keylayout.vChewingETen", - "org.unknown.keylayout.vChewingIBM", - "org.unknown.keylayout.vChewingMiTAC", - ] - static var isDynamicBasicKeyboardLayoutEnabled: Bool { - AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout) + IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout) } static func cnvStringApple2ABC(_ strProcessed: String) -> String { diff --git a/Source/Modules/ControllerModules/IMEState.swift b/Source/Modules/ControllerModules/IMEState.swift new file mode 100644 index 00000000..a8d732e1 --- /dev/null +++ b/Source/Modules/ControllerModules/IMEState.swift @@ -0,0 +1,206 @@ +// (c) 2021 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Foundation + +// 用以讓每個狀態自描述的 enum。 +public enum StateType: String { + case ofDeactivated = "Deactivated" + case ofEmpty = "Empty" + case ofAbortion = "Abortion" // 該狀態會自動轉為 Empty + case ofCommitting = "Committing" + case ofAssociates = "Associates" + case ofNotEmpty = "NotEmpty" + case ofInputting = "Inputting" + case ofMarking = "Marking" + case ofCandidates = "Candidates" + case ofSymbolTable = "SymbolTable" +} + +// 所有 IMEState 均遵守該協定: +public protocol IMEStateProtocol { + var type: StateType { get } + var data: StateData { get } + var candidates: [(String, String)] { get } + var hasComposition: Bool { get } + var isCandidateContainer: Bool { get } + var displayedText: String { get } + var textToCommit: String { get set } + var tooltip: String { get set } + var attributedString: NSAttributedString { get } + var convertedToInputting: IMEState { get } + var isFilterable: Bool { get } + var node: SymbolNode { get set } +} + +/// 用以呈現輸入法控制器(ctlInputMethod)的各種狀態。 +/// +/// 從實際角度來看,輸入法屬於有限態械(Finite State Machine)。其藉由滑鼠/鍵盤 +/// 等輸入裝置接收輸入訊號,據此切換至對應的狀態,再根據狀態更新使用者介面內容, +/// 最終生成文字輸出、遞交給接收文字輸入行為的客體應用。此乃單向資訊流序,且使用 +/// 者介面內容與文字輸出均無條件地遵循某一個指定的資料來源。 +/// +/// IMEState 型別用以呈現輸入法控制器正在做的事情,且分狀態儲存各種狀態限定的 +/// 常數與變數。對輸入法而言,使用狀態模式(而非策略模式)來做這種常數變數隔離, +/// 可能會讓新手覺得會有些牛鼎烹雞,卻實際上變相減少了在程式維護方面的管理難度、 +/// 不需要再在某個狀態下為了該狀態不需要的變數與常數的處置策略而煩惱。 +/// +/// 對 IMEState 型別下的諸多狀態的切換,應以生成新副本來取代舊有副本的形式來完 +/// 成。唯一例外是 IMEState.ofMarking、擁有可以將自身轉變為 IMEState.ofInputting +/// 的成員函式,但也只是生成副本、來交給輸入法控制器來處理而已。每個狀態都有 +/// 各自的構造器 (Constructor)。 +/// +/// 輸入法控制器持下述狀態: +/// +/// - .Deactivated: 使用者沒在使用輸入法。 +/// - .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。因為逐字選字模式不需要在 +/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 +/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給 +/// 客體應用、準備新的輸入行為。 +/// - .Abortion: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些 +/// 內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .Empty()。 +/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 +/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。 +/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 +/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、還是將這個範圍的 +/// 詞音組合放入語彙濾除清單。 +/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。 +/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 +public struct IMEState: IMEStateProtocol { + public var type: StateType = .ofEmpty + public var data: StateData = .init() + public var node: SymbolNode = .init("") + init(_ data: StateData = .init(), type: StateType = .ofEmpty) { + self.data = data + self.type = type + } + + init(_ data: StateData = .init(), type: StateType = .ofEmpty, node: SymbolNode) { + self.data = data + self.type = type + self.node = node + self.data.candidates = { node.children?.map(\.title) ?? [String]() }().map { ("", $0) } + } +} + +// MARK: - 針對不同的狀態,規定不同的構造器 + +extension IMEState { + public static func ofDeactivated() -> IMEState { .init(type: .ofDeactivated) } + public static func ofEmpty() -> IMEState { .init(type: .ofEmpty) } + public static func ofAbortion() -> IMEState { .init(type: .ofAbortion) } + public static func ofCommitting(textToCommit: String) -> IMEState { + var result = IMEState(type: .ofCommitting) + result.data.textToCommit = textToCommit + ChineseConverter.ensureCurrencyNumerals(target: &result.data.textToCommit) + return result + } + + public static func ofAssociates(candidates: [(String, String)]) -> IMEState { + var result = IMEState(type: .ofAssociates) + result.data.candidates = candidates + return result + } + + public static func ofNotEmpty(displayTextSegments: [String], cursor: Int) -> IMEState { + var result = IMEState(type: .ofNotEmpty) + // 注意資料的設定順序,一定得先設定 displayTextSegments。 + result.data.displayTextSegments = displayTextSegments + result.data.cursor = cursor + return result + } + + public static func ofInputting(displayTextSegments: [String], cursor: Int) -> IMEState { + var result = IMEState.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) + result.type = .ofInputting + return result + } + + public static func ofMarking( + displayTextSegments: [String], markedReadings: [String], cursor: Int, marker: Int + ) + -> IMEState + { + var result = IMEState.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) + result.type = .ofMarking + result.data.marker = marker + result.data.markedReadings = markedReadings + StateData.Marking.updateParameters(&result.data) + return result + } + + public static func ofCandidates(candidates: [(String, String)], displayTextSegments: [String], cursor: Int) + -> IMEState + { + var result = IMEState.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) + result.type = .ofCandidates + result.data.candidates = candidates + return result + } + + public static func ofSymbolTable(node: SymbolNode) -> IMEState { + var result = IMEState(type: .ofNotEmpty, node: node) + result.type = .ofSymbolTable + return result + } +} + +// MARK: - 規定一個狀態該怎樣返回自己的資料值 + +extension IMEState { + public var isFilterable: Bool { data.isFilterable } + public var candidates: [(String, String)] { data.candidates } + public var convertedToInputting: IMEState { + if type == .ofInputting { return self } + var result = IMEState.ofInputting(displayTextSegments: data.displayTextSegments, cursor: data.cursor) + result.tooltip = data.tooltipBackupForInputting + return result + } + + public var textToCommit: String { + get { + data.textToCommit + } + set { + data.textToCommit = newValue + } + } + + public var tooltip: String { + get { + data.tooltip + } + set { + data.tooltip = newValue + } + } + + public var attributedString: NSAttributedString { + switch type { + case .ofMarking: return data.attributedStringMarking + case .ofAssociates, .ofSymbolTable: return data.attributedStringPlaceholder + default: return data.attributedStringNormal + } + } + + public var hasComposition: Bool { + switch type { + case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true + default: return false + } + } + + public var isCandidateContainer: Bool { + switch type { + case .ofCandidates, .ofAssociates, .ofSymbolTable: return true + default: return false + } + } + + public var displayedText: String { data.displayedText } +} diff --git a/Source/Modules/ControllerModules/IMEStateData.swift b/Source/Modules/ControllerModules/IMEStateData.swift new file mode 100644 index 00000000..d2c0f0d8 --- /dev/null +++ b/Source/Modules/ControllerModules/IMEStateData.swift @@ -0,0 +1,240 @@ +// (c) 2021 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Foundation + +public struct StateData { + var displayedText: String = "" { + didSet { + let result = IME.kanjiConversionIfRequired(displayedText) + if result.utf16.count == displayedText.utf16.count, result.count == displayedText.count { + displayedText = result + } + } + } + + // MARK: Cursor & Marker & Range for UTF8 + + var cursor: Int = 0 { + didSet { + cursor = min(max(cursor, 0), displayedText.count) + } + } + + var marker: Int = 0 { + didSet { + marker = min(max(marker, 0), displayedText.count) + } + } + + var markedRange: Range { + min(cursor, marker).. { + min(u16Cursor, u16Marker).. String { + var arrOutput = [String]() + for neta in data.markedReadings { + var neta = neta + if neta.isEmpty { continue } + if neta.contains("_") { + arrOutput.append("??") + continue + } + if mgrPrefs.showHanyuPinyinInCompositionBuffer { // 恢復陰平標記->注音轉拼音->轉教科書式標調 + neta = Tekkon.restoreToneOneInZhuyinKey(target: neta) + neta = Tekkon.cnvPhonaToHanyuPinyin(target: neta) + neta = Tekkon.cnvHanyuPinyinToTextbookStyle(target: neta) + } else { + neta = Tekkon.cnvZhuyinChainToTextbookReading(target: neta) + } + arrOutput.append(neta) + } + return arrOutput.joined(separator: " ") + } + + /// 更新工具提示內容、以及對應配對是否在庫。 + /// - Parameter data: 要處理的狀態資料包。 + public static func updateParameters(_ data: inout StateData) { + var tooltipGenerated: String { + if mgrPrefs.phraseReplacementEnabled { + ctlInputMethod.tooltipController.setColor(state: .warning) + return NSLocalizedString( + "⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: "" + ) + } + if data.markedRange.isEmpty { + return "" + } + + let text = data.displayedText.charComponents[data.markedRange].joined() + if data.markedRange.count < mgrPrefs.allowedMarkRange.lowerBound { + ctlInputMethod.tooltipController.setColor(state: .denialInsufficiency) + return String( + format: NSLocalizedString( + "\"%@\" length must ≥ 2 for a user phrase.", comment: "" + ) + "\n// " + generateReadingThread(data), text + ) + } else if data.markedRange.count > mgrPrefs.allowedMarkRange.upperBound { + ctlInputMethod.tooltipController.setColor(state: .denialOverflow) + return String( + format: NSLocalizedString( + "\"%@\" length should ≤ %d for a user phrase.", comment: "" + ) + "\n// " + generateReadingThread(data), text, mgrPrefs.allowedMarkRange.upperBound + ) + } + + let joined = data.markedReadings.joined(separator: "-") + let exist = mgrLangModel.checkIfUserPhraseExist( + userPhrase: text, mode: IME.currentInputMode, key: joined + ) + if exist { + data.markedTargetExists = exist + ctlInputMethod.tooltipController.setColor(state: .prompt) + return String( + format: NSLocalizedString( + "\"%@\" already exists: ENTER to boost, SHIFT+COMMAND+ENTER to nerf, \n BackSpace or Delete key to exclude.", + comment: "" + ) + "\n// " + generateReadingThread(data), text + ) + } + ctlInputMethod.tooltipController.resetColor() + return String( + format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n// " + + generateReadingThread(data), + text + ) + } + data.tooltip = tooltipGenerated + } + } +} diff --git a/Source/Modules/ControllerModules/InputState.swift b/Source/Modules/ControllerModules/InputState.swift deleted file mode 100644 index c3e92e93..00000000 --- a/Source/Modules/ControllerModules/InputState.swift +++ /dev/null @@ -1,582 +0,0 @@ -// (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). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -import Foundation - -// 註:所有 InputState 型別均不適合使用 Struct,因為 Struct 無法相互繼承派生。 - -// 用以讓每個狀態自描述的 enum。 -public enum StateType { - case ofDeactivated - case ofAssociatedPhrases - case ofEmpty - case ofEmptyIgnoringPreviousState - case ofCommitting - case ofNotEmpty - case ofInputting - case ofMarking - case ofChoosingCandidate - case ofSymbolTable -} - -// 所有 InputState 均遵守該協定: -public protocol InputStateProtocol { - var type: StateType { get } -} - -/// 此型別用以呈現輸入法控制器(ctlInputMethod)的各種狀態。 -/// -/// 從實際角度來看,輸入法屬於有限態械(Finite State Machine)。其藉由滑鼠/鍵盤 -/// 等輸入裝置接收輸入訊號,據此切換至對應的狀態,再根據狀態更新使用者介面內容, -/// 最終生成文字輸出、遞交給接收文字輸入行為的客體應用。此乃單向資訊流序,且使用 -/// 者介面內容與文字輸出均無條件地遵循某一個指定的資料來源。 -/// -/// InputState 型別用以呈現輸入法控制器正在做的事情,且分狀態儲存各種狀態限定的 -/// 常數與變數。對輸入法而言,使用狀態模式(而非策略模式)來做這種常數變數隔離, -/// 可能會讓新手覺得會有些牛鼎烹雞,卻實際上變相減少了在程式維護方面的管理難度、 -/// 不需要再在某個狀態下為了該狀態不需要的變數與常數的處置策略而煩惱。 -/// -/// 對 InputState 型別下的諸多狀態的切換,應以生成新副本來取代舊有副本的形式來完 -/// 成。唯一例外是 InputState.Marking、擁有可以將自身轉變為 InputState.Inputting -/// 的成員函式,但也只是生成副本、來交給輸入法控制器來處理而已。 -/// -/// 輸入法控制器持下述狀態: -/// -/// - .Deactivated: 使用者沒在使用輸入法。 -/// - .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。因為逐字選字模式不需要在 -/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 -/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給 -/// 客體應用、準備新的輸入行為。 -/// - .EmptyIgnoringPreviousState: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些 -/// 內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .Empty()。 -/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 -/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。 -/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 -/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、還是將這個範圍的 -/// 詞音組合放入語彙濾除清單。 -/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。 -/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 -public enum InputState { - /// .Deactivated: 使用者沒在使用輸入法。 - class Deactivated: InputStateProtocol { - public var type: StateType { .ofDeactivated } - var description: String { - "" - } - } - - // MARK: - - - /// .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。 - /// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。 - class Empty: InputStateProtocol { - public var type: StateType { .ofEmpty } - - var composingBuffer: String { - "" - } - - var description: String { - "" - } - } - - // MARK: - - - /// .EmptyIgnoringPreviousState: 與 Empty 類似, - /// 但會扔掉上一個狀態的內容、不將這些內容遞交給客體應用。 - /// 該狀態在處理完畢之後會被立刻切換至 .Empty()。 - class EmptyIgnoringPreviousState: Empty { - override public var type: StateType { .ofEmptyIgnoringPreviousState } - override var description: String { - "" - } - } - - // MARK: - - - /// .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。 - class Committing: InputStateProtocol { - public var type: StateType { .ofCommitting } - private(set) var textToCommit: String = "" - - convenience init(textToCommit: String) { - self.init() - self.textToCommit = textToCommit - ChineseConverter.ensureCurrencyNumerals(target: &self.textToCommit) - } - - var description: String { - "" - } - } - - // MARK: - - - /// .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。 - /// 因為逐字選字模式不需要在組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 - class AssociatedPhrases: InputStateProtocol { - public var type: StateType { .ofAssociatedPhrases } - private(set) var candidates: [(String, String)] = [] - private(set) var isTypingVertical: Bool = false - init(candidates: [(String, String)], isTypingVertical: Bool) { - self.candidates = candidates - self.isTypingVertical = isTypingVertical - } - - var attributedString: NSMutableAttributedString { - let attributedString = NSMutableAttributedString( - string: " ", - attributes: [ - .underlineStyle: NSUnderlineStyle.single.rawValue, - .markedClauseSegment: 0, - ] - ) - return attributedString - } - - var description: String { - "" - } - } - - // MARK: - - - /// .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。 - /// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 - /// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、 - /// 還是將這個範圍的詞音組合放入語彙濾除清單。 - /// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。 - /// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 - class NotEmpty: InputStateProtocol { - public var type: StateType { .ofNotEmpty } - private(set) var composingBuffer: String - private(set) var cursorIndex: Int = 0 { didSet { cursorIndex = max(cursorIndex, 0) } } - private(set) var reading: String = "" - private(set) var nodeValuesArray = [String]() - public var composingBufferConverted: String { - let converted = IME.kanjiConversionIfRequired(composingBuffer) - if converted.utf16.count != composingBuffer.utf16.count - || converted.count != composingBuffer.count - { - return composingBuffer - } - return converted - } - - public var committingBufferConverted: String { composingBufferConverted } - - init(composingBuffer: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) { - self.composingBuffer = composingBuffer - self.reading = reading - // 為了簡化運算,將 reading 本身也變成一個字詞節點。 - if !reading.isEmpty { - var newNodeValuesArray = [String]() - var temporaryNode = "" - var charCounter = 0 - for node in nodeValuesArray { - for char in node { - if charCounter == cursorIndex - reading.utf16.count { - newNodeValuesArray.append(temporaryNode) - temporaryNode = "" - newNodeValuesArray.append(reading) - } - temporaryNode += String(char) - charCounter += 1 - } - newNodeValuesArray.append(temporaryNode) - temporaryNode = "" - } - self.nodeValuesArray = newNodeValuesArray - } else { - self.nodeValuesArray = nodeValuesArray - } - defer { self.cursorIndex = cursorIndex } - } - - var attributedString: NSMutableAttributedString { - /// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況, - /// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。 - let attributedString = NSMutableAttributedString(string: composingBufferConverted) - var newBegin = 0 - for (i, neta) in nodeValuesArray.enumerated() { - attributedString.setAttributes( - [ - /// 不能用 .thick,否則會看不到游標。 - .underlineStyle: NSUnderlineStyle.single.rawValue, - .markedClauseSegment: i, - ], range: NSRange(location: newBegin, length: neta.utf16.count) - ) - newBegin += neta.utf16.count - } - return attributedString - } - - var description: String { - "" - } - } - - // MARK: - - - /// .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。 - class Inputting: NotEmpty { - override public var type: StateType { .ofInputting } - var textToCommit: String = "" - var tooltip: String = "" - - override public var committingBufferConverted: String { - let committingBuffer = nodeValuesArray.joined() - let converted = IME.kanjiConversionIfRequired(committingBuffer) - if converted.utf16.count != composingBuffer.utf16.count - || converted.count != composingBuffer.count - { - return composingBuffer - } - return converted - } - - override init(composingBuffer: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) { - super.init( - composingBuffer: composingBuffer, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray - ) - } - - override var description: String { - ", textToCommit:\(textToCommit)>" - } - } - - // MARK: - - - /// .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、 - /// 還是將這個範圍的詞音組合放入語彙濾除清單。 - class Marking: NotEmpty { - override public var type: StateType { .ofMarking } - private var allowedMarkRange: ClosedRange = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength - private(set) var markerIndex: Int = 0 { didSet { markerIndex = max(markerIndex, 0) } } - private(set) var markedRange: Range - private var literalMarkedRange: Range { - let lowerBoundLiteral = composingBuffer.charIndexLiteral(from: markedRange.lowerBound) - let upperBoundLiteral = composingBuffer.charIndexLiteral(from: markedRange.upperBound) - return lowerBoundLiteral..注音轉拼音->轉教科書式標調 - neta = Tekkon.restoreToneOneInZhuyinKey(target: neta) - neta = Tekkon.cnvPhonaToHanyuPinyin(target: neta) - neta = Tekkon.cnvHanyuPinyinToTextbookStyle(target: neta) - } else { - neta = Tekkon.cnvZhuyinChainToTextbookReading(target: neta) - } - arrOutput.append(neta) - } - return arrOutput.joined(separator: " ") - } - - private var markedTargetExists = false - var tooltip: String { - if composingBuffer.count != readings.count { - ctlInputMethod.tooltipController.setColor(state: .redAlert) - return NSLocalizedString( - "⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: "" - ) - } - if mgrPrefs.phraseReplacementEnabled { - ctlInputMethod.tooltipController.setColor(state: .warning) - return NSLocalizedString( - "⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: "" - ) - } - if markedRange.isEmpty { - return "" - } - - let text = composingBuffer.utf16SubString(with: markedRange) - if literalMarkedRange.count < allowedMarkRange.lowerBound { - ctlInputMethod.tooltipController.setColor(state: .denialInsufficiency) - return String( - format: NSLocalizedString( - "\"%@\" length must ≥ 2 for a user phrase.", comment: "" - ) + "\n// " + literalReadingThread, text - ) - } else if literalMarkedRange.count > allowedMarkRange.upperBound { - ctlInputMethod.tooltipController.setColor(state: .denialOverflow) - return String( - format: NSLocalizedString( - "\"%@\" length should ≤ %d for a user phrase.", comment: "" - ) + "\n// " + literalReadingThread, text, allowedMarkRange.upperBound - ) - } - - let selectedReadings = readings[literalMarkedRange] - let joined = selectedReadings.joined(separator: "-") - let exist = mgrLangModel.checkIfUserPhraseExist( - userPhrase: text, mode: IME.currentInputMode, key: joined - ) - if exist { - markedTargetExists = exist - ctlInputMethod.tooltipController.setColor(state: .prompt) - return String( - format: NSLocalizedString( - "\"%@\" already exists: ENTER to boost, SHIFT+COMMAND+ENTER to nerf, \n BackSpace or Delete key to exclude.", - comment: "" - ) + "\n// " + literalReadingThread, text - ) - } - ctlInputMethod.tooltipController.resetColor() - return String( - format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n// " - + literalReadingThread, - text - ) - } - - var tooltipForInputting: String = "" - private(set) var readings: [String] - - init( - composingBuffer: String, cursorIndex: Int, markerIndex: Int, readings: [String], nodeValuesArray: [String] = [] - ) { - let begin = min(cursorIndex, markerIndex) - let end = max(cursorIndex, markerIndex) - markedRange = begin.." - } - - var convertedToInputting: Inputting { - let state = Inputting( - composingBuffer: composingBuffer, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray - ) - state.tooltip = tooltipForInputting - return state - } - - var validToFilter: Bool { markedTargetExists ? allowedMarkRange.contains(literalMarkedRange.count) : false } - - var bufferReadingCountMisMatch: Bool { composingBuffer.count != readings.count } - - var chkIfUserPhraseExists: Bool { - let text = composingBuffer.utf16SubString(with: markedRange) - let selectedReadings = readings[literalMarkedRange] - let joined = selectedReadings.joined(separator: "-") - return mgrLangModel.checkIfUserPhraseExist( - userPhrase: text, mode: IME.currentInputMode, key: joined - ) - } - - var userPhrase: String { - let text = composingBuffer.utf16SubString(with: markedRange) - let selectedReadings = readings[literalMarkedRange] - let joined = selectedReadings.joined(separator: "-") - let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : "" - return "\(text) \(joined)\(nerfedScore)" - } - - var userPhraseConverted: String { - let text = - ChineseConverter.crossConvert(composingBuffer.utf16SubString(with: markedRange)) ?? "" - let selectedReadings = readings[literalMarkedRange] - let joined = selectedReadings.joined(separator: "-") - let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : "" - let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙" - return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)" - } - } - - // MARK: - - - /// .ChoosingCandidate: 叫出選字窗、允許使用者選字。 - class ChoosingCandidate: NotEmpty { - override public var type: StateType { .ofChoosingCandidate } - private(set) var candidates: [(String, String)] - private(set) var isTypingVertical: Bool - // 該變數改為可以隨時更改的內容,不然的話 ctlInputMethod.candidateSelectionChanged() 會上演俄羅斯套娃(崩潰)。 - public var chosenCandidateString: String = "" { - didSet { - // 去掉讀音資訊,且最終留存「執行康熙 / JIS 轉換之前」的結果。 - if chosenCandidateString.contains("\u{17}") { - chosenCandidateString = String(chosenCandidateString.split(separator: "\u{17}")[0]) - } - if !chosenCandidateString.contains("\u{1A}") { return } - chosenCandidateString = String(chosenCandidateString.split(separator: "\u{1A}").reversed()[0]) - } - } - - init( - composingBuffer: String, cursorIndex: Int, candidates: [(String, String)], isTypingVertical: Bool, - nodeValuesArray: [String] = [] - ) { - self.candidates = candidates - self.isTypingVertical = isTypingVertical - super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray) - } - - // 這個函式尚未經過嚴格的單元測試。請在使用時確保 chosenCandidateString 為空。 - // 不為空的話,該參數的返回值就會有對應的影響、顯示成類似 macOS 內建注音輸入法那樣子。 - // 本來想給輸入法拓展這方面的功能的,奈何 ctlInputMethod.candidateSelectionChanged() 這函式太氣人。 - // 想要講的幹話已經在那邊講完了,感興趣的可以去看看。 - override var attributedString: NSMutableAttributedString { - guard !chosenCandidateString.isEmpty else { return super.attributedString } - let bufferTextRear = composingBuffer.utf16SubString(with: 0.. = { - switch mgrPrefs.useRearCursorMode { - case false: return (max(0, cursorIndex - chosenCandidateString.utf16.count)).. = { - switch mgrPrefs.useRearCursorMode { - case false: return (max(0, cursorIndexU8 - chosenCandidateString.count)).." - } - } - - // MARK: - - - /// .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。 - class SymbolTable: ChoosingCandidate { - override public var type: StateType { .ofSymbolTable } - var node: SymbolNode - - init(node: SymbolNode, previous: SymbolNode? = nil, isTypingVertical: Bool) { - self.node = node - if let previous = previous { - self.node.previous = previous - } - let candidates = node.children?.map(\.title) ?? [String]() - super.init( - composingBuffer: "", cursorIndex: 0, candidates: candidates.map { ("", $0) }, - isTypingVertical: isTypingVertical - ) - } - - // InputState.SymbolTable 這個狀態比較特殊,不能把真空組字區交出去。 - // 不然的話,在絕大多數終端機類應用當中、以及在 MS Word 等軟體當中 - // 會出現符號選字窗無法響應方向鍵的問題。 - // 如有誰要修奇摩注音的一點通選單的話,修復原理也是一樣的。 - // Crediting Qwertyyb: https://github.com/qwertyyb/Fire/issues/55#issuecomment-1133497700 - override var attributedString: NSMutableAttributedString { - let attributedString = NSMutableAttributedString( - string: " ", - attributes: [ - .underlineStyle: NSUnderlineStyle.single.rawValue, - .markedClauseSegment: 0, - ] - ) - return attributedString - } - - override var description: String { - "" - } - } -} diff --git a/Source/Modules/ControllerModules/KeyHandler_Core.swift b/Source/Modules/ControllerModules/KeyHandler_Core.swift index 40b33e51..d52cb05d 100644 --- a/Source/Modules/ControllerModules/KeyHandler_Core.swift +++ b/Source/Modules/ControllerModules/KeyHandler_Core.swift @@ -25,7 +25,7 @@ protocol KeyHandlerDelegate { _: KeyHandler, didSelectCandidateAt index: Int, ctlCandidate controller: ctlCandidateProtocol ) - func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool) + func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool) -> Bool } @@ -35,8 +35,6 @@ protocol KeyHandlerDelegate { public class KeyHandler { /// 半衰模組的衰減指數 let kEpsilon: Double = 0.000001 - /// 檢測是否出現游標切斷組字圈內字符的情況 - var isCursorCuttingChar = false /// 檢測是否內容為空(注拼槽與組字器都是空的) var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty } @@ -88,6 +86,21 @@ public class KeyHandler { // MARK: - Functions dealing with Megrez. + /// 獲取當前標記得範圍。這個函式只能是函式、而非只讀變數。 + /// - Returns: 當前標記範圍。 + func currentMarkedRange() -> Range { + min(compositor.cursor, compositor.marker).. Bool { + let index = isMarker ? compositor.marker : compositor.cursor + var isBound = (index == compositor.walkedNodes.contextRange(ofGivenCursor: index).lowerBound) + if index == compositor.width { isBound = true } + let rawResult = compositor.walkedNodes.findNode(at: index)?.isReadingMismatched ?? false + return !isBound && rawResult + } + /// 實際上要拿給 Megrez 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。 /// /// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。 @@ -107,11 +120,10 @@ public class KeyHandler { // 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。 if mgrPrefs.isDebugModeEnabled { let result = compositor.dumpDOT + let appSupportPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending( + "/vChewing-visualization.dot") do { - try result.write( - toFile: "/private/var/tmp/vChewing-visualization.dot", - atomically: true, encoding: .utf8 - ) + try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8) } catch { IME.prtDebugIntel("Failed from writing dumpDOT results.") } @@ -122,7 +134,7 @@ public class KeyHandler { /// - Parameter key: 給定的聯想詞的開頭字。 /// - Returns: 抓取到的聯想詞陣列。 /// 不會是 nil,但那些負責接收結果的函式會對空白陣列結果做出正確的處理。 - func buildAssociatePhraseArray(withPair pair: Megrez.Compositor.Candidate) -> [(String, String)] { + func buildAssociatePhraseArray(withPair pair: Megrez.Compositor.KeyValuePaired) -> [(String, String)] { var arrResult: [(String, String)] = [] if currentLM.hasAssociatedPhrasesFor(pair: pair) { arrResult = currentLM.associatedPhrasesFor(pair: pair).map { ("", $0) } @@ -144,7 +156,7 @@ public class KeyHandler { /// 威注音輸入法截至 v1.9.3 SP2 版為止都受到上游的這個 Bug 的影響,且在 v1.9.4 版利用該函式修正了這個缺陷。 /// 該修正必須搭配至少天權星組字引擎 v2.0.2 版方可生效。算法可能比較囉唆,但至少在常用情形下不會再發生該問題。 /// - Parameter theCandidate: 要拿來覆寫的詞音配對。 - func consolidateCursorContext(with theCandidate: Megrez.Compositor.Candidate) { + func consolidateCursorContext(with theCandidate: Megrez.Compositor.KeyValuePaired) { var grid = compositor var frontBoundaryEX = actualCandidateCursor + 1 var rearBoundaryEX = actualCandidateCursor @@ -195,7 +207,7 @@ public class KeyHandler { let values = currentNode.currentPair.value.charComponents for (subPosition, key) in currentNode.keyArray.enumerated() { guard values.count > subPosition else { break } // 防呆,應該沒有發生的可能性 - let thePair = Megrez.Compositor.Candidate( + let thePair = Megrez.Compositor.KeyValuePaired( key: key, value: values[subPosition] ) compositor.overrideCandidate(thePair, at: position) @@ -214,15 +226,12 @@ public class KeyHandler { /// - respectCursorPushing: 若該選項為 true,則會在選字之後始終將游標推送至選字後的節錨的前方。 /// - consolidate: 在固化節點之前,先鞏固上下文。該選項可能會破壞在內文組字區內就地輪替候選字詞時的體驗。 func fixNode(candidate: (String, String), respectCursorPushing: Bool = true, preConsolidate: Bool = false) { - let theCandidate: Megrez.Compositor.Candidate = .init(key: candidate.0, value: candidate.1) + let theCandidate: Megrez.Compositor.KeyValuePaired = .init(key: candidate.0, value: candidate.1) /// 必須先鞏固當前組字器游標上下文、以消滅意料之外的影響,但在內文組字區內就地輪替候選字詞時除外。 - if preConsolidate { - consolidateCursorContext(with: theCandidate) - } + if preConsolidate { consolidateCursorContext(with: theCandidate) } // 回到正常流程。 - if !compositor.overrideCandidate(theCandidate, at: actualCandidateCursor) { return } let previousWalk = compositor.walkedNodes // 開始爬軌。 @@ -261,7 +270,7 @@ public class KeyHandler { func getCandidatesArray(fixOrder: Bool = true) -> [(String, String)] { /// 警告:不要對游標前置風格使用 nodesCrossing,否則會導致游標行為與 macOS 內建注音輸入法不一致。 /// 微軟新注音輸入法的游標後置風格也是不允許 nodeCrossing 的。 - var arrCandidates: [Megrez.Compositor.Candidate] = { + var arrCandidates: [Megrez.Compositor.KeyValuePaired] = { switch mgrPrefs.useRearCursorMode { case false: return compositor.fetchCandidates(at: actualCandidateCursor, filter: .endAt) @@ -281,8 +290,8 @@ public class KeyHandler { } let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = fetchSuggestionsFromUOM(apply: false) - let arrSuggestedCandidates: [Megrez.Compositor.Candidate] = arrSuggestedUnigrams.map { - Megrez.Compositor.Candidate(key: $0.0, value: $0.1.value) + let arrSuggestedCandidates: [Megrez.Compositor.KeyValuePaired] = arrSuggestedUnigrams.map { + Megrez.Compositor.KeyValuePaired(key: $0.0, value: $0.1.value) } arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates arrCandidates = arrCandidates.deduplicate @@ -307,7 +316,7 @@ public class KeyHandler { if !suggestion.isEmpty, let newestSuggestedCandidate = suggestion.candidates.last { let overrideBehavior: Megrez.Compositor.Node.OverrideType = suggestion.forceHighScoreOverride ? .withHighScore : .withTopUnigramScore - let suggestedPair: Megrez.Compositor.Candidate = .init( + let suggestedPair: Megrez.Compositor.KeyValuePaired = .init( key: newestSuggestedCandidate.0, value: newestSuggestedCandidate.1.value ) IME.prtDebugIntel( diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift index ff6b89ed..caec6545 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift @@ -23,9 +23,9 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handleCandidate( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { guard var ctlCandidateCurrent = delegate?.ctlCandidate() else { @@ -41,7 +41,7 @@ extension KeyHandler { || ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold) if cancelCandidateKey { - if state is InputState.AssociatedPhrases + if state.type == .ofAssociates || mgrPrefs.useSCPCTypingMode || compositor.isEmpty { @@ -49,13 +49,12 @@ extension KeyHandler { // 就將當前的組字緩衝區析構處理、強制重設輸入狀態。 // 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。 // 所以這裡需要對 compositor.isEmpty 做判定。 - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) } else { stateCallback(buildInputtingState) } - if let state = state as? InputState.SymbolTable, let nodePrevious = state.node.previous { - stateCallback(InputState.SymbolTable(node: nodePrevious, isTypingVertical: state.isTypingVertical)) + if state.type == .ofSymbolTable, let nodePrevious = state.node.previous, let _ = nodePrevious.children { + stateCallback(IMEState.ofSymbolTable(node: nodePrevious)) } return true } @@ -63,9 +62,8 @@ extension KeyHandler { // MARK: Enter if input.isEnter { - if state is InputState.AssociatedPhrases, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter { - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + if state.type == .ofAssociates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter { + stateCallback(IMEState.ofAbortion()) return true } delegate?.keyHandler( @@ -243,23 +241,15 @@ extension KeyHandler { // MARK: End Key - var candidates: [(String, String)]! - - if let state = state as? InputState.ChoosingCandidate { - candidates = state.candidates - } else if let state = state as? InputState.AssociatedPhrases { - candidates = state.candidates - } - - if candidates.isEmpty { + if state.candidates.isEmpty { return false } else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 if input.isEnd || input.emacsKey == EmacsKey.end { - if ctlCandidateCurrent.selectedCandidateIndex == candidates.count - 1 { + if ctlCandidateCurrent.selectedCandidateIndex == state.candidates.count - 1 { IME.prtDebugIntel("9B69AAAD") errorCallback() } else { - ctlCandidateCurrent.selectedCandidateIndex = candidates.count - 1 + ctlCandidateCurrent.selectedCandidateIndex = state.candidates.count - 1 } return true } @@ -267,13 +257,13 @@ extension KeyHandler { // MARK: 聯想詞處理 (Associated Phrases) - if state is InputState.AssociatedPhrases { + if state.type == .ofAssociates { if !input.isShiftHold { return false } } var index: Int = NSNotFound let match: String = - (state is InputState.AssociatedPhrases) ? input.inputTextIgnoringModifiers ?? "" : input.text + (state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text for j in 0.. Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool? { // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) @@ -62,7 +62,7 @@ extension KeyHandler { composer.receiveKey(fromString: input.text) keyConsumedByReading = true - // 沒有調號的話,只需要 updateClientComposingBuffer() 且終止處理(return true)即可。 + // 沒有調號的話,只需要 updateClientdisplayedText() 且終止處理(return true)即可。 // 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。 if !composer.hasToneMarker() { stateCallback(buildInputtingState) @@ -100,8 +100,7 @@ extension KeyHandler { switch compositor.isEmpty { case false: stateCallback(buildInputtingState) case true: - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) } return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。 } @@ -118,37 +117,32 @@ extension KeyHandler { // 之後就是更新組字區了。先清空注拼槽的內容。 composer.clear() - // 再以回呼組字狀態的方式來執行 updateClientComposingBuffer()。 + // 再以回呼組字狀態的方式來執行 updateClientdisplayedText()。 let inputting = buildInputtingState stateCallback(inputting) /// 逐字選字模式的處理。 if mgrPrefs.useSCPCTypingMode { - let choosingCandidates: InputState.ChoosingCandidate = buildCandidate( + let candidateState: IMEState = buildCandidate( state: inputting, isTypingVertical: input.isTypingVertical ) - if choosingCandidates.candidates.count == 1, let firstCandidate = choosingCandidates.candidates.first { + if candidateState.candidates.count == 1, let firstCandidate = candidateState.candidates.first { let reading: String = firstCandidate.0 let text: String = firstCandidate.1 - stateCallback(InputState.Committing(textToCommit: text)) + stateCallback(IMEState.ofCommitting(textToCommit: text)) if !mgrPrefs.associatedPhrasesEnabled { - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) } else { - if let associatedPhrases = + let associatedPhrases = buildAssociatePhraseState( - withPair: .init(key: reading, value: text), - isTypingVertical: input.isTypingVertical - ), !associatedPhrases.candidates.isEmpty - { - stateCallback(associatedPhrases) - } else { - stateCallback(InputState.Empty()) - } + withPair: .init(key: reading, value: text) + ) + stateCallback(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases) } } else { - stateCallback(choosingCandidates) + stateCallback(candidateState) } } // 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。 @@ -157,7 +151,7 @@ extension KeyHandler { /// 是說此時注拼槽並非為空、卻還沒組音。這種情況下只可能是「注拼槽內只有聲調」。 if keyConsumedByReading { - // 以回呼組字狀態的方式來執行 updateClientComposingBuffer()。 + // 以回呼組字狀態的方式來執行 updateClientdisplayedText()。 stateCallback(buildInputtingState) return true } diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift index ebcb3ff4..d708993c 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift @@ -25,8 +25,8 @@ extension KeyHandler { /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handle( input: InputSignalProtocol, - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { // 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。 @@ -38,8 +38,8 @@ extension KeyHandler { // 提前過濾掉一些不合規的按鍵訊號輸入,免得相關按鍵訊號被送給 Megrez 引發輸入法崩潰。 if input.isInvalid { // 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。 - // 因為「.EmptyIgnoringPreviousState」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。 - if state is InputState.Empty || state is InputState.Deactivated { + // 因為「.Abortion」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。 + if state.type == .ofEmpty || state.type == .ofDeactivated { return false } IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.") @@ -51,7 +51,7 @@ extension KeyHandler { // 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。 let isFunctionKey: Bool = input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey) - if !(state is InputState.NotEmpty) && !(state is InputState.AssociatedPhrases) && isFunctionKey { + if state.type != .ofAssociates, !state.hasComposition, !state.isCandidateContainer, isFunctionKey { return false } @@ -68,7 +68,7 @@ extension KeyHandler { // 略過對 BackSpace 的處理。 } else if input.isCapsLockOn || input.isASCIIModeInput { // 但願能夠處理這種情況下所有可能的按鍵組合。 - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) // 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。 if input.isUpperCaseASCIILetterKey { @@ -82,8 +82,8 @@ extension KeyHandler { } // 將整個組字區的內容遞交給客體應用。 - stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased())) + stateCallback(IMEState.ofEmpty()) return true } @@ -94,19 +94,19 @@ extension KeyHandler { // 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。 // 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。 if input.isNumericPadKey { - if !(state is InputState.ChoosingCandidate || state is InputState.AssociatedPhrases - || state is InputState.SymbolTable) + if !(state.type == .ofCandidates || state.type == .ofAssociates + || state.type == .ofSymbolTable) { - stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) + stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased())) + stateCallback(IMEState.ofEmpty()) return true } } // MARK: 處理候選字詞 (Handle Candidates) - if state is InputState.ChoosingCandidate { + if [.ofCandidates, .ofSymbolTable].contains(state.type) { return handleCandidate( state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) @@ -114,26 +114,26 @@ extension KeyHandler { // MARK: 處理聯想詞 (Handle Associated Phrases) - if state is InputState.AssociatedPhrases { + if state.type == .ofAssociates { if handleCandidate( state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) { return true } else { - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) } } // MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking) - if let marking = state as? InputState.Marking { + if state.type == .ofMarking { if handleMarkingState( - marking, input: input, stateCallback: stateCallback, + state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) { return true } - state = marking.convertedToInputting + state = state.convertedToInputting stateCallback(state) } @@ -147,7 +147,7 @@ extension KeyHandler { // MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.) - if let currentState = state as? InputState.NotEmpty, composer.isEmpty, !input.isOptionHold, + if state.hasComposition, composer.isEmpty, !input.isOptionHold, input.isCursorClockLeft || input.isCursorClockRight || input.isSpace || input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior) { @@ -155,12 +155,12 @@ extension KeyHandler { /// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話……… if !mgrPrefs.chooseCandidateUsingSpace { if compositor.cursor >= compositor.length { - let composingBuffer = currentState.composingBuffer - if !composingBuffer.isEmpty { - stateCallback(InputState.Committing(textToCommit: composingBuffer)) + let displayedText = state.displayedText + if !displayedText.isEmpty { + stateCallback(IMEState.ofCommitting(textToCommit: displayedText)) } - stateCallback(InputState.Committing(textToCommit: " ")) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: " ")) + stateCallback(IMEState.ofEmpty()) } else if currentLM.hasUnigramsFor(key: " ") { compositor.insertKey(" ") walk() @@ -175,7 +175,7 @@ extension KeyHandler { ) } } - stateCallback(buildCandidate(state: currentState, isTypingVertical: input.isTypingVertical)) + stateCallback(buildCandidate(state: state)) return true } @@ -236,7 +236,7 @@ extension KeyHandler { // MARK: Clock-Left & Clock-Right if input.isCursorClockLeft || input.isCursorClockRight { - if input.isOptionHold, state is InputState.Inputting { + if input.isOptionHold, state.type == .ofInputting { if input.isCursorClockRight { return handleInlineCandidateRotation( state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback @@ -277,7 +277,7 @@ extension KeyHandler { // MARK: Punctuation list - if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold { + if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold, state.type != .ofDeactivated { if input.isOptionHold { if currentLM.hasUnigramsFor(key: "_punctuation_list") { if composer.isEmpty { @@ -285,7 +285,7 @@ extension KeyHandler { walk() let inputting = buildInputtingState stateCallback(inputting) - stateCallback(buildCandidate(state: inputting, isTypingVertical: input.isTypingVertical)) + stateCallback(buildCandidate(state: inputting)) } else { // 不要在注音沒敲完整的情況下叫出統合符號選單。 IME.prtDebugIntel("17446655") errorCallback() @@ -297,14 +297,14 @@ extension KeyHandler { // 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。 // 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。 _ = handleEnter(state: state, stateCallback: stateCallback) - stateCallback(InputState.SymbolTable(node: SymbolNode.root, isTypingVertical: input.isTypingVertical)) + stateCallback(IMEState.ofSymbolTable(node: SymbolNode.root)) return true } } // MARK: 全形/半形阿拉伯數字輸入 (FW / HW Arabic Numbers Input) - if state is InputState.Empty { + if state.type == .ofEmpty { if input.isMainAreaNumKey, input.isShiftHold, input.isOptionHold, !input.isControlHold, !input.isCommandHold { // NOTE: 將來棄用 macOS 10.11 El Capitan 支援的時候,把這裡由 CFStringTransform 改為 StringTransform: // https://developer.apple.com/documentation/foundation/stringtransform @@ -312,9 +312,9 @@ extension KeyHandler { let string = NSMutableString(string: stringRAW) CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true) stateCallback( - InputState.Committing(textToCommit: mgrPrefs.halfWidthPunctuationEnabled ? stringRAW : string as String) + IMEState.ofCommitting(textToCommit: mgrPrefs.halfWidthPunctuationEnabled ? stringRAW : string as String) ) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) return true } } @@ -357,10 +357,10 @@ extension KeyHandler { // MARK: 全形/半形空白 (Full-Width / Half-Width Space) /// 該功能僅可在當前組字區沒有任何內容的時候使用。 - if state is InputState.Empty { + if state.type == .ofEmpty { if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold { - stateCallback(InputState.Committing(textToCommit: input.isShiftHold ? " " : " ")) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: input.isShiftHold ? " " : " ")) + stateCallback(IMEState.ofEmpty()) return true } } @@ -371,14 +371,14 @@ extension KeyHandler { if input.isShiftHold { // 這裡先不要判斷 isOptionHold。 switch mgrPrefs.upperCaseLetterKeyBehavior { case 1: - stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) + stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased())) + stateCallback(IMEState.ofEmpty()) return true case 2: - stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(textToCommit: inputText.uppercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofEmpty()) + stateCallback(IMEState.ofCommitting(textToCommit: inputText.uppercased())) + stateCallback(IMEState.ofEmpty()) return true default: // 包括 case 0,直接塞給組字區。 let letter = "_letter_\(inputText)" @@ -401,7 +401,7 @@ extension KeyHandler { /// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 /// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 /// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 - if (state is InputState.NotEmpty) || !composer.isEmpty { + if state.hasComposition || !composer.isEmpty { IME.prtDebugIntel( "Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)") IME.prtDebugIntel("A9BFF20E") diff --git a/Source/Modules/ControllerModules/KeyHandler_States.swift b/Source/Modules/ControllerModules/KeyHandler_States.swift index a2763e77..8ee851db 100644 --- a/Source/Modules/ControllerModules/KeyHandler_States.swift +++ b/Source/Modules/ControllerModules/KeyHandler_States.swift @@ -17,101 +17,73 @@ import Foundation extension KeyHandler { // MARK: - 構築狀態(State Building) - /// 生成「正在輸入」狀態。 - var buildInputtingState: InputState.Inputting { + /// 生成「正在輸入」狀態。相關的內容會被拿給狀態機械用來處理在電腦螢幕上顯示的內容。 + var buildInputtingState: IMEState { /// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容 /// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。 - var tooltipParameterRef: [String] = ["", ""] - let nodeValuesArray: [String] = compositor.walkedNodes.values.map { + var displayTextSegments: [String] = compositor.walkedNodes.values.map { guard let delegate = delegate, delegate.isVerticalTyping else { return $0 } guard mgrPrefs.hardenVerticalPunctuations else { return $0 } var neta = $0 ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping) return neta } + var cursor = convertCursorForDisplay(compositor.cursor) + let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer) + if !reading.isEmpty { + var newDisplayTextSegments = [String]() + var temporaryNode = "" + var charCounter = 0 + for node in displayTextSegments { + for char in node { + if charCounter == cursor { + newDisplayTextSegments.append(temporaryNode) + temporaryNode = "" + newDisplayTextSegments.append(reading) + } + temporaryNode += String(char) + charCounter += 1 + } + newDisplayTextSegments.append(temporaryNode) + temporaryNode = "" + } + if newDisplayTextSegments == displayTextSegments { newDisplayTextSegments.append(reading) } + displayTextSegments = newDisplayTextSegments + cursor += reading.count + } + /// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。 + return IMEState.ofInputting(displayTextSegments: displayTextSegments, cursor: cursor) + } + + /// 生成「正在輸入」狀態。 + func convertCursorForDisplay(_ rawCursor: Int) -> Int { var composedStringCursorIndex = 0 var readingCursorIndex = 0 - /// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度, - /// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。 - /// 這樣就可以免除不必要的類型轉換。 for theNode in compositor.walkedNodes { let strNodeValue = theNode.value - let arrSplit: [String] = Array(strNodeValue).charComponents - let codepointCount = arrSplit.count /// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。 /// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來 /// 累加、以此為依據,來校正「可見游標位置」。 - let spanningLength: Int = theNode.spanLength - if readingCursorIndex + spanningLength <= compositor.cursor { - composedStringCursorIndex += strNodeValue.utf16.count + let spanningLength: Int = theNode.keyArray.count + if readingCursorIndex + spanningLength <= rawCursor { + composedStringCursorIndex += strNodeValue.count readingCursorIndex += spanningLength continue } - if codepointCount == spanningLength { - for i in 0.. InputState.ChoosingCandidate { - InputState.ChoosingCandidate( - composingBuffer: currentState.composingBuffer, - cursorIndex: currentState.cursorIndex, + state currentState: IMEStateProtocol, + isTypingVertical _: Bool = false + ) -> IMEState { + IMEState.ofCandidates( candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection), - isTypingVertical: isTypingVertical, - nodeValuesArray: compositor.walkedNodes.values + displayTextSegments: compositor.walkedNodes.values, + cursor: currentState.data.cursor ) } @@ -147,16 +117,13 @@ extension KeyHandler { /// 是否為空:如果陣列為空的話,直接回呼一個空狀態。 /// - Parameters: /// - key: 給定的索引鍵(也就是給定的聯想詞的開頭字)。 - /// - isTypingVertical: 是否縱排輸入? /// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。 func buildAssociatePhraseState( - withPair pair: Megrez.Compositor.Candidate, - isTypingVertical: Bool - ) -> InputState.AssociatedPhrases! { + withPair pair: Megrez.Compositor.KeyValuePaired + ) -> IMEState { // 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。 - InputState.AssociatedPhrases( - candidates: buildAssociatePhraseArray(withPair: pair), isTypingVertical: isTypingVertical - ) + IMEState.ofAssociates( + candidates: buildAssociatePhraseArray(withPair: pair)) } // MARK: - 用以處理就地新增自訂語彙時的行為 @@ -169,9 +136,9 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleMarkingState( - _ state: InputState.Marking, + _ state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { if input.isEsc { @@ -190,7 +157,7 @@ extension KeyHandler { if input.isEnter { if let keyHandlerDelegate = delegate { // 先判斷是否是在摁了降權組合鍵的時候目標不在庫。 - if input.isShiftHold, input.isCommandHold, !state.validToFilter { + if input.isShiftHold, input.isCommandHold, !state.isFilterable { IME.prtDebugIntel("2EAC1F7A") errorCallback() return true @@ -207,7 +174,7 @@ extension KeyHandler { // BackSpace & Delete if input.isBackSpace || input.isDelete { if let keyHandlerDelegate = delegate { - if !state.validToFilter { + if !state.isFilterable { IME.prtDebugIntel("1F88B191") errorCallback() return true @@ -224,18 +191,19 @@ extension KeyHandler { // Shift + Left if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold { - var index = state.markerIndex - if index > 0 { - index = state.composingBuffer.utf16PreviousPosition(for: index) - let marking = InputState.Marking( - composingBuffer: state.composingBuffer, - cursorIndex: state.cursorIndex, - markerIndex: index, - readings: state.readings, - nodeValuesArray: compositor.walkedNodes.values + if compositor.marker > 0 { + compositor.marker -= 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .rear, isMarker: true) + } + var marking = IMEState.ofMarking( + displayTextSegments: state.data.displayTextSegments, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipForInputting = state.tooltipForInputting - stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking) + marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting + stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking) } else { IME.prtDebugIntel("1149908D") errorCallback() @@ -246,18 +214,19 @@ extension KeyHandler { // Shift + Right if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold { - var index = state.markerIndex - if index < (state.composingBuffer.utf16.count) { - index = state.composingBuffer.utf16NextPosition(for: index) - let marking = InputState.Marking( - composingBuffer: state.composingBuffer, - cursorIndex: state.cursorIndex, - markerIndex: index, - readings: state.readings, - nodeValuesArray: compositor.walkedNodes.values + if compositor.marker < compositor.width { + compositor.marker += 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .front, isMarker: true) + } + var marking = IMEState.ofMarking( + displayTextSegments: state.data.displayTextSegments, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipForInputting = state.tooltipForInputting - stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking) + marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting + stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking) } else { IME.prtDebugIntel("9B51408D") errorCallback() @@ -280,9 +249,9 @@ extension KeyHandler { /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handlePunctuation( _ customPunctuation: String, - state: InputStateProtocol, + state: IMEStateProtocol, usingVerticalTyping isTypingVertical: Bool, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { if !currentLM.hasUnigramsFor(key: customPunctuation) { @@ -312,8 +281,8 @@ extension KeyHandler { if candidateState.candidates.count == 1 { clear() // 這句不要砍,因為下文可能會回呼 candidateState。 if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty { - stateCallback(InputState.Committing(textToCommit: candidateToCommit.1)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: candidateToCommit.1)) + stateCallback(IMEState.ofEmpty()) } else { stateCallback(candidateState) } @@ -331,13 +300,13 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnter( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard let currentState = state as? InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } - stateCallback(InputState.Committing(textToCommit: currentState.composingBuffer)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: state.displayedText)) + stateCallback(IMEState.ofEmpty()) return true } @@ -349,23 +318,23 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleCtrlCommandEnter( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } - var composingBuffer = compositor.keys.joined(separator: "-") + var displayedText = compositor.keys.joined(separator: "-") if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin { - composingBuffer = Tekkon.restoreToneOneInZhuyinKey(target: composingBuffer) // 恢復陰平標記 - composingBuffer = Tekkon.cnvPhonaToHanyuPinyin(target: composingBuffer) // 注音轉拼音 + displayedText = Tekkon.restoreToneOneInZhuyinKey(target: displayedText) // 恢復陰平標記 + displayedText = Tekkon.cnvPhonaToHanyuPinyin(target: displayedText) // 注音轉拼音 } if let delegate = delegate, !delegate.clientBundleIdentifier.contains("vChewingPhraseEditor") { - composingBuffer = composingBuffer.replacingOccurrences(of: "-", with: " ") + displayedText = displayedText.replacingOccurrences(of: "-", with: " ") } - stateCallback(InputState.Committing(textToCommit: composingBuffer)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: displayedText)) + stateCallback(IMEState.ofEmpty()) return true } @@ -377,10 +346,10 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleCtrlOptionCommandEnter( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } var composed = "" @@ -400,8 +369,8 @@ extension KeyHandler { composed += key.contains("_") ? value : "\(value)(\(key))" } - stateCallback(InputState.Committing(textToCommit: composed)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofCommitting(textToCommit: composed)) + stateCallback(IMEState.ofEmpty()) return true } @@ -415,12 +384,12 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackSpace( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } // 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。 switch mgrPrefs.specifyShiftBackSpaceKeyBehavior { @@ -434,15 +403,13 @@ extension KeyHandler { stateCallback(buildInputtingState) return true case 1: - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) return true default: break } if input.isShiftHold, input.isOptionHold { - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) return true } @@ -465,8 +432,7 @@ extension KeyHandler { switch composer.isEmpty && compositor.isEmpty { case false: stateCallback(buildInputtingState) case true: - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) } return true } @@ -481,16 +447,15 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleDelete( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if input.isShiftHold { - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) return true } @@ -510,11 +475,10 @@ extension KeyHandler { let inputting = buildInputtingState // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 - switch inputting.composingBuffer.isEmpty { + switch inputting.displayedText.isEmpty { case false: stateCallback(inputting) case true: - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) } return true } @@ -528,11 +492,11 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleClockKey( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("9B6F908D") errorCallback() @@ -550,11 +514,11 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleHome( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("ABC44080") @@ -584,11 +548,11 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnd( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("9B69908D") @@ -617,16 +581,15 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEsc( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if mgrPrefs.escToCleanInputBuffer { /// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。 /// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。 - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) } else { if composer.isEmpty { return true } /// 如果注拼槽不是空的話,則清空之。 @@ -634,8 +597,7 @@ extension KeyHandler { switch compositor.isEmpty { case false: stateCallback(buildInputtingState) case true: - stateCallback(InputState.EmptyIgnoringPreviousState()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.ofAbortion()) } } return true @@ -651,12 +613,12 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleForward( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard let currentState = state as? InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("B3BA5257") @@ -667,16 +629,18 @@ extension KeyHandler { if input.isShiftHold { // Shift + Right - if currentState.cursorIndex < currentState.composingBuffer.utf16.count { - let nextPosition = currentState.composingBuffer.utf16NextPosition( - for: currentState.cursorIndex) - let marking: InputState.Marking! = InputState.Marking( - composingBuffer: currentState.composingBuffer, - cursorIndex: currentState.cursorIndex, - markerIndex: nextPosition, - readings: compositor.keys + if compositor.cursor < compositor.width { + compositor.marker = compositor.cursor + 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .front, isMarker: true) + } + var marking = IMEState.ofMarking( + displayTextSegments: compositor.walkedNodes.values, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipForInputting = currentState.tooltip + marking.data.tooltipBackupForInputting = state.tooltip stateCallback(marking) } else { IME.prtDebugIntel("BB7F6DB9") @@ -684,7 +648,6 @@ extension KeyHandler { stateCallback(state) } } else if input.isOptionHold { - isCursorCuttingChar = false if input.isControlHold { return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } @@ -699,12 +662,10 @@ extension KeyHandler { } else { if compositor.cursor < compositor.length { compositor.cursor += 1 - var inputtingState = buildInputtingState - if isCursorCuttingChar == true { + if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .front) - inputtingState = buildInputtingState } - stateCallback(inputtingState) + stateCallback(buildInputtingState) } else { IME.prtDebugIntel("A96AAD58") errorCallback() @@ -725,12 +686,12 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackward( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard let currentState = state as? InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("6ED95318") @@ -741,16 +702,18 @@ extension KeyHandler { if input.isShiftHold { // Shift + left - if currentState.cursorIndex > 0 { - let previousPosition = currentState.composingBuffer.utf16PreviousPosition( - for: currentState.cursorIndex) - let marking: InputState.Marking! = InputState.Marking( - composingBuffer: currentState.composingBuffer, - cursorIndex: currentState.cursorIndex, - markerIndex: previousPosition, - readings: compositor.keys + if compositor.cursor > 0 { + compositor.marker = compositor.cursor - 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .rear, isMarker: true) + } + var marking = IMEState.ofMarking( + displayTextSegments: compositor.walkedNodes.values, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipForInputting = currentState.tooltip + marking.data.tooltipBackupForInputting = state.tooltip stateCallback(marking) } else { IME.prtDebugIntel("D326DEA3") @@ -758,7 +721,6 @@ extension KeyHandler { stateCallback(state) } } else if input.isOptionHold { - isCursorCuttingChar = false if input.isControlHold { return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } @@ -773,12 +735,10 @@ extension KeyHandler { } else { if compositor.cursor > 0 { compositor.cursor -= 1 - var inputtingState = buildInputtingState - if isCursorCuttingChar == true { + if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .rear) - inputtingState = buildInputtingState } - stateCallback(inputtingState) + stateCallback(buildInputtingState) } else { IME.prtDebugIntel("7045E6F3") errorCallback() @@ -799,14 +759,14 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleInlineCandidateRotation( - state: InputStateProtocol, + state: IMEStateProtocol, reverseModifier: Bool, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false } - guard state is InputState.Inputting else { - guard state is InputState.Empty else { + guard state.type == .ofInputting else { + guard state.type == .ofEmpty else { IME.prtDebugIntel("6044F081") errorCallback() return true diff --git a/Source/Modules/ControllerModules/NSEventExtension.swift b/Source/Modules/ControllerModules/NSEventExtension.swift index 9dde44ce..1bd242a9 100644 --- a/Source/Modules/ControllerModules/NSEventExtension.swift +++ b/Source/Modules/ControllerModules/NSEventExtension.swift @@ -8,7 +8,36 @@ import Cocoa -// MARK: - NSEvent Extension +// MARK: - NSEvent Extension - Reconstructors + +extension NSEvent { + public func reinitiate( + with type: NSEvent.EventType? = nil, + location: NSPoint? = nil, + modifierFlags: NSEvent.ModifierFlags? = nil, + timestamp: TimeInterval? = nil, + windowNumber: Int? = nil, + characters: String? = nil, + charactersIgnoringModifiers: String? = nil, + isARepeat: Bool? = nil, + keyCode: UInt16? = nil + ) -> NSEvent? { + NSEvent.keyEvent( + with: type ?? self.type, + location: location ?? locationInWindow, + modifierFlags: modifierFlags ?? self.modifierFlags, + timestamp: timestamp ?? self.timestamp, + windowNumber: windowNumber ?? self.windowNumber, + context: nil, + characters: characters ?? self.characters ?? "", + charactersIgnoringModifiers: charactersIgnoringModifiers ?? self.characters ?? "", + isARepeat: isARepeat ?? self.isARepeat, + keyCode: keyCode ?? self.keyCode + ) + } +} + +// MARK: - NSEvent Extension - InputSignalProtocol extension NSEvent: InputSignalProtocol { public var isASCIIModeInput: Bool { ctlInputMethod.isASCIIModeSituation } diff --git a/Source/Modules/ControllerModules/StringUtils.swift b/Source/Modules/ControllerModules/StringUtils.swift deleted file mode 100644 index 91d310cf..00000000 --- a/Source/Modules/ControllerModules/StringUtils.swift +++ /dev/null @@ -1,62 +0,0 @@ -// (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). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -import Foundation - -/// Shiki's Notes: The cursor index in the IMK inline composition buffer -/// still uses UTF16 index measurements. This means that any attempt of -/// using Swift native UTF8 handlings to replace Zonble's NSString (or -/// .utf16) handlings below will still result in unavoidable necessities -/// of solving the UTF16->UTF8 conversions in another approach. Therefore, -/// I strongly advise against any attempt of such until the day that IMK is -/// capable of handling the cursor index in its inline composition buffer using -/// UTF8 measurements. - -extension String { - /// Converts the index in an NSString or .utf16 to the index in a Swift string. - /// - /// An Emoji might be compose by more than one UTF-16 code points. However, - /// the length of an NSString is only the sum of the UTF-16 code points. It - /// causes that the NSString and Swift string representation of the same - /// string have different lengths once the string contains such Emoji. The - /// method helps to find the index in a Swift string by passing the index - /// in an NSString (or .utf16). - public func charIndexLiteral(from utf16Index: Int) -> Int { - var length = 0 - for (i, character) in enumerated() { - length += character.utf16.count - if length > utf16Index { - return (i) - } - } - return count - } - - public func utf16NextPosition(for index: Int) -> Int { - let fixedIndex = min(charIndexLiteral(from: index) + 1, count) - return self[.. Int { - let fixedIndex = max(charIndexLiteral(from: index) - 1, 0) - return self[..) -> String { - let arr = Array(utf16)[r].map { $0 } - return String(utf16CodeUnits: arr, count: arr.count) - } - - public var charComponents: [String] { map { String($0) } } -} - -extension Array where Element == String.Element { - public var charComponents: [String] { map { String($0) } } -} diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Common.swift b/Source/Modules/ControllerModules/ctlInputMethod_Common.swift index 76e6b900..2ec05c81 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Common.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Common.swift @@ -31,11 +31,9 @@ extension ctlInputMethod { if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) { NotifierController.notify( message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n" - + { - toggleASCIIMode() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (toggleASCIIMode() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } if shouldUseHandle { diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift index 4dd5a663..d8c90ca0 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift @@ -32,16 +32,20 @@ class ctlInputMethod: IMKInputController { // MARK: - - /// 按鍵調度模組的副本。 - var keyHandler: KeyHandler = .init() - /// 用以記錄當前輸入法狀態的變數。 - var state: InputStateProtocol = InputState.Empty() - /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。 - var isASCIIMode: Bool = false /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。 static var isASCIIModeSituation: Bool = false /// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。 static var isVerticalTypingSituation: Bool = false + /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。 + var isASCIIMode: Bool = false + /// 按鍵調度模組的副本。 + var keyHandler: KeyHandler = .init() + /// 用以記錄當前輸入法狀態的變數。 + var state: IMEStateProtocol = IMEState.ofEmpty() { + didSet { + IME.prtDebugIntel("Current State: \(state.type.rawValue)") + } + } /// 切換當前 ctlInputMethod 副本的英數輸入模式開關。 func toggleASCIIMode() -> Bool { @@ -65,15 +69,15 @@ class ctlInputMethod: IMKInputController { /// 重設按鍵調度模組,會將當前尚未遞交的內容遞交出去。 func resetKeyHandler() { // 過濾掉尚未完成拼寫的注音。 - if state is InputState.Inputting, mgrPrefs.trimUnfinishedReadingsOnCommit { + if state.type == .ofInputting, mgrPrefs.trimUnfinishedReadingsOnCommit { keyHandler.composer.clear() handle(state: keyHandler.buildInputtingState) } - if let state = state as? InputState.NotEmpty { + if state.hasComposition { /// 將傳回的新狀態交給調度函式。 - handle(state: InputState.Committing(textToCommit: state.composingBufferConverted)) + handle(state: IMEState.ofCommitting(textToCommit: state.displayedText)) } - handle(state: InputState.Empty()) + handle(state: IMEState.ofEmpty()) } // MARK: - IMKInputController 方法 @@ -115,11 +119,9 @@ class ctlInputMethod: IMKInputController { } else { NotifierController.notify( message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n" - + { - isASCIIMode - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (isASCIIMode + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } } @@ -129,7 +131,7 @@ class ctlInputMethod: IMKInputController { if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier { // 強制重設當前鍵盤佈局、使其與偏好設定同步。 setKeyLayout() - handle(state: InputState.Empty()) + handle(state: IMEState.ofEmpty()) } // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。 (NSApp.delegate as? AppDelegate)?.checkForUpdate() } @@ -139,7 +141,7 @@ class ctlInputMethod: IMKInputController { override func deactivateServer(_ sender: Any!) { _ = sender // 防止格式整理工具毀掉與此對應的參數。 resetKeyHandler() // 這條會自動搞定 Empty 狀態。 - handle(state: InputState.Deactivated()) + handle(state: IMEState.ofDeactivated()) } /// 切換至某一個輸入法的某個副本時(比如威注音的簡體輸入法副本與繁體輸入法副本),會觸發該函式。 @@ -170,7 +172,7 @@ class ctlInputMethod: IMKInputController { if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier { // 強制重設當前鍵盤佈局、使其與偏好設定同步。這裡的這一步也不能省略。 setKeyLayout() - handle(state: InputState.Empty()) + handle(state: IMEState.ofEmpty()) } // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。 } @@ -224,20 +226,7 @@ class ctlInputMethod: IMKInputController { // Shift+Enter 是個特殊情形,不提前攔截處理的話、會有垃圾參數傳給 delegate 的 keyHandler 從而崩潰。 // 所以這裡直接將 Shift Flags 清空。 if event.isShiftHold, event.isEnter { - guard - let newEvent = NSEvent.keyEvent( - with: event.type, - location: event.locationInWindow, - modifierFlags: [], - timestamp: event.timestamp, - windowNumber: event.windowNumber, - context: nil, - characters: event.characters ?? "", - charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? event.characters ?? "", - isARepeat: event.isARepeat, - keyCode: event.keyCode - ) - else { + guard let newEvent = event.reinitiate(modifierFlags: []) else { NSSound.beep() return true } @@ -247,20 +236,8 @@ class ctlInputMethod: IMKInputController { // 聯想詞選字。 if let newChar = ctlCandidateIMK.defaultIMKSelectionKey[event.keyCode], event.isShiftHold, - isAssociatedPhrasesState + isAssociatedPhrasesState, let newEvent = event.reinitiate(modifierFlags: [], characters: newChar) { - let newEvent = NSEvent.keyEvent( - with: event.type, - location: event.locationInWindow, - modifierFlags: [], - timestamp: event.timestamp, - windowNumber: event.windowNumber, - context: nil, - characters: newChar, - charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? event.characters ?? "", - isARepeat: event.isARepeat, - keyCode: event.keyCode - ) ctlCandidateCurrent.handleKeyboardEvent(newEvent) } @@ -288,8 +265,8 @@ class ctlInputMethod: IMKInputController { /// - Returns: 字串內容,或者 nil。 override func composedString(_ sender: Any!) -> Any! { _ = sender // 防止格式整理工具毀掉與此對應的參數。 - guard let state = state as? InputState.NotEmpty else { return "" } - return state.committingBufferConverted + guard state.hasComposition else { return "" } + return state.displayedText } /// 輸入法要被換掉或關掉的時候,要做的事情。 @@ -309,7 +286,7 @@ class ctlInputMethod: IMKInputController { _ = sender // 防止格式整理工具毀掉與此對應的參數。 var arrResult = [String]() - // 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。 + // 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。 func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") { for theCandidate in candidates { let theConverted = IME.kanjiConversionIfRequired(theCandidate.1) @@ -325,12 +302,12 @@ class ctlInputMethod: IMKInputController { } } - if let state = state as? InputState.AssociatedPhrases { + if state.type == .ofAssociates { handleCandidatesPrepared(state.candidates, prefix: "⇧") - } else if let state = state as? InputState.SymbolTable { + } else if state.type == .ofSymbolTable { // 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。 arrResult = state.candidates.map(\.1) - } else if let state = state as? InputState.ChoosingCandidate { + } else if state.type == .ofCandidates { guard !state.candidates.isEmpty else { return .init() } if state.candidates[0].0.contains("_punctuation") { arrResult = state.candidates.map(\.1) // 標點符號選單處理。 @@ -363,17 +340,16 @@ class ctlInputMethod: IMKInputController { /// - Parameter candidateString: 已經確認的候選字詞內容。 override open func candidateSelected(_ candidateString: NSAttributedString!) { let candidateString: NSAttributedString = candidateString ?? .init(string: "") - if state is InputState.AssociatedPhrases { + if state.type == .ofAssociates { if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter { - handle(state: InputState.EmptyIgnoringPreviousState()) - handle(state: InputState.Empty()) + handle(state: IMEState.ofAbortion()) return } } var indexDeducted = 0 - // 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。 + // 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。 func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") { for (i, neta) in candidates.enumerated() { let theConverted = IME.kanjiConversionIfRequired(neta.1) @@ -403,11 +379,11 @@ class ctlInputMethod: IMKInputController { } } - if let state = state as? InputState.AssociatedPhrases { + if state.type == .ofAssociates { handleCandidatesSelected(state.candidates, prefix: "⇧") - } else if let state = state as? InputState.SymbolTable { + } else if state.type == .ofSymbolTable { handleSymbolCandidatesSelected(state.candidates) - } else if let state = state as? InputState.ChoosingCandidate { + } else if state.type == .ofCandidates { guard !state.candidates.isEmpty else { return } if state.candidates[0].0.contains("_punctuation") { handleSymbolCandidatesSelected(state.candidates) // 標點符號選單處理。 diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift b/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift index c3226eef..1927a05d 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift @@ -37,21 +37,20 @@ extension ctlInputMethod: KeyHandlerDelegate { ctlCandidate(controller, didSelectCandidateAtIndex: index) } - func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool) + func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool) -> Bool { - guard let state = state as? InputState.Marking else { return false } - if state.bufferReadingCountMisMatch { return false } + guard state.type == .ofMarking else { return false } let refInputModeReversed: InputMode = (keyHandler.inputMode == InputMode.imeModeCHT) ? InputMode.imeModeCHS : InputMode.imeModeCHT if !mgrLangModel.writeUserPhrase( - state.userPhrase, inputMode: keyHandler.inputMode, - areWeDuplicating: state.chkIfUserPhraseExists, + state.data.userPhrase, inputMode: keyHandler.inputMode, + areWeDuplicating: state.data.chkIfUserPhraseExists, areWeDeleting: addToFilter ) || !mgrLangModel.writeUserPhrase( - state.userPhraseConverted, inputMode: refInputModeReversed, + state.data.userPhraseConverted, inputMode: refInputModeReversed, areWeDuplicating: false, areWeDeleting: addToFilter ) @@ -65,7 +64,7 @@ extension ctlInputMethod: KeyHandlerDelegate { // MARK: - Candidate Controller Delegate extension ctlInputMethod: ctlCandidateDelegate { - var isAssociatedPhrasesState: Bool { state is InputState.AssociatedPhrases } + var isAssociatedPhrasesState: Bool { state.type == .ofAssociates } /// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。 /// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。 @@ -78,9 +77,7 @@ extension ctlInputMethod: ctlCandidateDelegate { func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.ChoosingCandidate { - return state.candidates.count - } else if let state = state as? InputState.AssociatedPhrases { + if state.isCandidateContainer { return state.candidates.count } return 0 @@ -91,9 +88,7 @@ extension ctlInputMethod: ctlCandidateDelegate { /// - Returns: 候選字詞陣列(字音配對)。 func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.ChoosingCandidate { - return state.candidates - } else if let state = state as? InputState.AssociatedPhrases { + if state.isCandidateContainer { return state.candidates } return .init() @@ -103,9 +98,7 @@ extension ctlInputMethod: ctlCandidateDelegate { -> (String, String) { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.ChoosingCandidate { - return state.candidates[index] - } else if let state = state as? InputState.AssociatedPhrases { + if state.isCandidateContainer { return state.candidates[index] } return ("", "") @@ -114,22 +107,20 @@ extension ctlInputMethod: ctlCandidateDelegate { func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.SymbolTable, + if state.type == .ofSymbolTable, let node = state.node.children?[index] { if let children = node.children, !children.isEmpty { - handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現 - handle( - state: InputState.SymbolTable(node: node, previous: state.node, isTypingVertical: state.isTypingVertical) - ) + handle(state: IMEState.ofEmpty()) // 防止縱橫排選字窗同時出現 + handle(state: IMEState.ofSymbolTable(node: node)) } else { - handle(state: InputState.Committing(textToCommit: node.title)) - handle(state: InputState.Empty()) + handle(state: IMEState.ofCommitting(textToCommit: node.title)) + handle(state: IMEState.ofEmpty()) } return } - if let state = state as? InputState.ChoosingCandidate { + if [.ofCandidates, .ofSymbolTable].contains(state.type) { let selectedValue = state.candidates[index] keyHandler.fixNode( candidate: selectedValue, respectCursorPushing: true, @@ -139,17 +130,15 @@ extension ctlInputMethod: ctlCandidateDelegate { let inputting = keyHandler.buildInputtingState if mgrPrefs.useSCPCTypingMode { - handle(state: InputState.Committing(textToCommit: inputting.composingBufferConverted)) + handle(state: IMEState.ofCommitting(textToCommit: inputting.displayedText)) // 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。 - if mgrPrefs.associatedPhrasesEnabled, - let associatePhrases = keyHandler.buildAssociatePhraseState( - withPair: .init(key: selectedValue.0, value: selectedValue.1), - isTypingVertical: state.isTypingVertical - ), !associatePhrases.candidates.isEmpty - { - handle(state: associatePhrases) + if mgrPrefs.associatedPhrasesEnabled { + let associates = keyHandler.buildAssociatePhraseState( + withPair: .init(key: selectedValue.0, value: selectedValue.1) + ) + handle(state: associates.candidates.isEmpty ? IMEState.ofEmpty() : associates) } else { - handle(state: InputState.Empty()) + handle(state: IMEState.ofEmpty()) } } else { handle(state: inputting) @@ -157,25 +146,25 @@ extension ctlInputMethod: ctlCandidateDelegate { return } - if let state = state as? InputState.AssociatedPhrases { + if state.type == .ofAssociates { let selectedValue = state.candidates[index] - handle(state: InputState.Committing(textToCommit: selectedValue.1)) + handle(state: IMEState.ofCommitting(textToCommit: selectedValue.1)) // 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。 // 不然的話,一旦你選中了由多個字組成的聯想候選詞,則連續聯想會被打斷。 guard let valueKept = selectedValue.1.last else { - handle(state: InputState.Empty()) + handle(state: IMEState.ofEmpty()) return } - if mgrPrefs.associatedPhrasesEnabled, - let associatePhrases = keyHandler.buildAssociatePhraseState( - withPair: .init(key: selectedValue.0, value: String(valueKept)), - isTypingVertical: state.isTypingVertical - ), !associatePhrases.candidates.isEmpty - { - handle(state: associatePhrases) - return + if mgrPrefs.associatedPhrasesEnabled { + let associates = keyHandler.buildAssociatePhraseState( + withPair: .init(key: selectedValue.0, value: String(valueKept)) + ) + if !associates.candidates.isEmpty { + handle(state: associates) + return + } } - handle(state: InputState.Empty()) + handle(state: IMEState.ofEmpty()) } } } diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift index 6424e402..9dd74dac 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift @@ -13,11 +13,11 @@ import Cocoa // MARK: - Tooltip Display and Candidate Display Methods extension ctlInputMethod { - func show(tooltip: String, composingBuffer: String, cursorIndex: Int) { + func show(tooltip: String, displayedText: String, u16Cursor: Int) { guard let client = client() else { return } var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0) - var cursor = cursorIndex - if cursor == composingBuffer.count, cursor != 0 { + var cursor = u16Cursor + if cursor == displayedText.count, cursor != 0 { cursor -= 1 } while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 { @@ -38,24 +38,14 @@ extension ctlInputMethod { ctlInputMethod.tooltipController.show(tooltip: tooltip, at: finalOrigin) } - func show(candidateWindowWith state: InputStateProtocol) { + func show(candidateWindowWith state: IMEStateProtocol) { guard let client = client() else { return } - var isTypingVertical: Bool { - if let state = state as? InputState.ChoosingCandidate { - return state.isTypingVertical - } else if let state = state as? InputState.AssociatedPhrases { - return state.isTypingVertical - } - return false - } var isCandidateWindowVertical: Bool { var candidates: [(String, String)] = .init() - if let state = state as? InputState.ChoosingCandidate { - candidates = state.candidates - } else if let state = state as? InputState.AssociatedPhrases { + if state.isCandidateContainer { candidates = state.candidates } - if isTypingVertical { return true } + if isVerticalTyping { return true } // 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。 guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false } // 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。 @@ -106,7 +96,7 @@ extension ctlInputMethod { let candidateKeys = mgrPrefs.candidateKeys let keyLabels = candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys) - let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : "" + let keyLabelSuffix = state.type == .ofAssociates ? "^" : "" ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map { CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix) } @@ -126,9 +116,9 @@ extension ctlInputMethod { var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0) var cursor = 0 - if let state = state as? InputState.ChoosingCandidate { - cursor = state.cursorIndex - if cursor == state.composingBuffer.count, cursor != 0 { + if [.ofCandidates, .ofSymbolTable].contains(state.type) { + cursor = state.data.cursor + if cursor == state.displayedText.count, cursor != 0 { cursor -= 1 } } @@ -140,7 +130,7 @@ extension ctlInputMethod { cursor -= 1 } - if isTypingVertical { + if isVerticalTyping { ctlInputMethod.ctlCandidateCurrent.set( windowTopLeftPoint: NSPoint( x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0 diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift index d182a85d..be42a443 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift @@ -18,29 +18,77 @@ extension ctlInputMethod { /// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數, /// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。 /// - Parameter newState: 新狀態。 - func handle(state newState: InputStateProtocol) { - let prevState = state + func handle(state newState: IMEStateProtocol) { + let previous = state state = newState - - switch newState { - case let newState as InputState.Deactivated: - handle(state: newState, previous: prevState) - case let newState as InputState.Empty: - handle(state: newState, previous: prevState) - case let newState as InputState.EmptyIgnoringPreviousState: - handle(state: newState, previous: prevState) - case let newState as InputState.Committing: - handle(state: newState, previous: prevState) - case let newState as InputState.Inputting: - handle(state: newState, previous: prevState) - case let newState as InputState.Marking: - handle(state: newState, previous: prevState) - case let newState as InputState.ChoosingCandidate: - handle(state: newState, previous: prevState) - case let newState as InputState.AssociatedPhrases: - handle(state: newState, previous: prevState) - case let newState as InputState.SymbolTable: - handle(state: newState, previous: prevState) + switch state.type { + case .ofDeactivated: + ctlInputMethod.ctlCandidateCurrent.delegate = nil + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + if previous.hasComposition { + commit(text: previous.displayedText) + } + clearInlineDisplay() + // 最後一道保險 + keyHandler.clear() + case .ofEmpty, .ofAbortion: + var previous = previous + if state.type == .ofAbortion { + state = IMEState.ofEmpty() + previous = state + } + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + // 全專案用以判斷「.Abortion」的地方僅此一處。 + if previous.hasComposition, state.type != .ofAbortion { + commit(text: previous.displayedText) + } + // 在這裡手動再取消一次選字窗與工具提示的顯示,可謂雙重保險。 + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + clearInlineDisplay() + // 最後一道保險 + keyHandler.clear() + case .ofCommitting: + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + let textToCommit = state.textToCommit + if !textToCommit.isEmpty { commit(text: textToCommit) } + clearInlineDisplay() + // 最後一道保險 + keyHandler.clear() + case .ofInputting: + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + let textToCommit = state.textToCommit + if !textToCommit.isEmpty { commit(text: textToCommit) } + setInlineDisplayWithCursor() + if !state.tooltip.isEmpty { + show( + tooltip: state.tooltip, displayedText: state.displayedText, + u16Cursor: state.data.u16Cursor + ) + } + case .ofMarking: + ctlInputMethod.ctlCandidateCurrent.visible = false + setInlineDisplayWithCursor() + if state.tooltip.isEmpty { + ctlInputMethod.tooltipController.hide() + } else { + let cursorReference: Int = { + if state.data.marker >= state.data.cursor { return state.data.u16Cursor } + return state.data.u16Marker // 這樣可以讓工具提示視窗始終盡量往書寫方向的後方顯示。 + }() + show( + tooltip: state.tooltip, displayedText: state.displayedText, + u16Cursor: cursorReference + ) + } + case .ofCandidates, .ofAssociates, .ofSymbolTable: + ctlInputMethod.tooltipController.hide() + setInlineDisplayWithCursor() + show(candidateWindowWith: state) default: break } } @@ -48,7 +96,7 @@ extension ctlInputMethod { /// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。 func setInlineDisplayWithCursor() { guard let client = client() else { return } - if let state = state as? InputState.AssociatedPhrases { + if state.type == .ofAssociates { client.setMarkedText( state.attributedString, selectionRange: NSRange(location: 0, length: 0), replacementRange: NSRange(location: NSNotFound, length: NSNotFound) @@ -56,49 +104,19 @@ extension ctlInputMethod { return } - guard let state = state as? InputState.NotEmpty else { - clearInlineDisplay() + if state.hasComposition || state.isCandidateContainer { + /// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度 + /// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。 + /// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。 + client.setMarkedText( + state.attributedString, selectionRange: NSRange(location: state.data.u16Cursor, length: 0), + replacementRange: NSRange(location: NSNotFound, length: NSNotFound) + ) return } - var identifier: AnyObject { - switch IME.currentInputMode { - case InputMode.imeModeCHS: - if #available(macOS 12.0, *) { - return "zh-Hans" as AnyObject - } - case InputMode.imeModeCHT: - if #available(macOS 12.0, *) { - return (mgrPrefs.shiftJISShinjitaiOutputEnabled || mgrPrefs.chineseConversionEnabled) - ? "ja" as AnyObject : "zh-Hant" as AnyObject - } - default: - break - } - return "" as AnyObject - } - - // [Shiki's Note] This might needs to be bug-reported to Apple: - // The LanguageIdentifier attribute of an NSAttributeString designated to - // IMK Client().SetMarkedText won't let the actual font respect your languageIdentifier - // settings. Still, this might behaves as Apple's current expectation, I'm afraid. - if #available(macOS 12.0, *) { - state.attributedString.setAttributes( - [.languageIdentifier: identifier], - range: NSRange( - location: 0, - length: state.composingBuffer.utf16.count - ) - ) - } - - /// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度 - /// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。 - /// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。 - client.setMarkedText( - state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0), - replacementRange: NSRange(location: NSNotFound, length: NSNotFound) - ) + // 其它情形。 + clearInlineDisplay() } /// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。 @@ -123,109 +141,4 @@ extension ctlInputMethod { buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound) ) } - - private func handle(state: InputState.Deactivated, previous: InputStateProtocol) { - _ = state // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.ctlCandidateCurrent.delegate = nil - ctlInputMethod.ctlCandidateCurrent.visible = false - ctlInputMethod.tooltipController.hide() - if let previous = previous as? InputState.NotEmpty { - commit(text: previous.committingBufferConverted) - } - clearInlineDisplay() - // 最後一道保險 - keyHandler.clear() - } - - private func handle(state: InputState.Empty, previous: InputStateProtocol) { - _ = state // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.ctlCandidateCurrent.visible = false - ctlInputMethod.tooltipController.hide() - // 全專案用以判斷「.EmptyIgnoringPreviousState」的地方僅此一處。 - if let previous = previous as? InputState.NotEmpty, - !(state is InputState.EmptyIgnoringPreviousState) - { - commit(text: previous.committingBufferConverted) - } - // 在這裡手動再取消一次選字窗與工具提示的顯示,可謂雙重保險。 - ctlInputMethod.ctlCandidateCurrent.visible = false - ctlInputMethod.tooltipController.hide() - clearInlineDisplay() - // 最後一道保險 - keyHandler.clear() - } - - private func handle( - state: InputState.EmptyIgnoringPreviousState, previous: InputStateProtocol - ) { - _ = state // 防止格式整理工具毀掉與此對應的參數。 - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - // 這個函式就是去掉 previous state 使得沒有任何東西可以 commit。 - handle(state: InputState.Empty()) - } - - private func handle(state: InputState.Committing, previous: InputStateProtocol) { - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.ctlCandidateCurrent.visible = false - ctlInputMethod.tooltipController.hide() - let textToCommit = state.textToCommit - if !textToCommit.isEmpty { - commit(text: textToCommit) - } - clearInlineDisplay() - // 最後一道保險 - keyHandler.clear() - } - - private func handle(state: InputState.Inputting, previous: InputStateProtocol) { - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.ctlCandidateCurrent.visible = false - ctlInputMethod.tooltipController.hide() - let textToCommit = state.textToCommit - if !textToCommit.isEmpty { - commit(text: textToCommit) - } - setInlineDisplayWithCursor() - if !state.tooltip.isEmpty { - show( - tooltip: state.tooltip, composingBuffer: state.composingBuffer, - cursorIndex: state.cursorIndex - ) - } - } - - private func handle(state: InputState.Marking, previous: InputStateProtocol) { - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.ctlCandidateCurrent.visible = false - setInlineDisplayWithCursor() - if state.tooltip.isEmpty { - ctlInputMethod.tooltipController.hide() - } else { - show( - tooltip: state.tooltip, composingBuffer: state.composingBuffer, - cursorIndex: state.markerIndex - ) - } - } - - private func handle(state: InputState.ChoosingCandidate, previous: InputStateProtocol) { - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.tooltipController.hide() - setInlineDisplayWithCursor() - show(candidateWindowWith: state) - } - - private func handle(state: InputState.SymbolTable, previous: InputStateProtocol) { - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.tooltipController.hide() - setInlineDisplayWithCursor() - show(candidateWindowWith: state) - } - - private func handle(state: InputState.AssociatedPhrases, previous: InputStateProtocol) { - _ = previous // 防止格式整理工具毀掉與此對應的參數。 - ctlInputMethod.tooltipController.hide() - setInlineDisplayWithCursor() - show(candidateWindowWith: state) - } } diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift b/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift index c8235ca8..d1ac194b 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift @@ -212,11 +212,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n" - + { - mgrPrefs.toggleSCPCTypingModeEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleSCPCTypingModeEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -224,11 +222,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n" - + { - mgrPrefs.toggleChineseConversionEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleChineseConversionEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -236,11 +232,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n" - + { - mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -248,11 +242,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n" - + { - mgrPrefs.toggleCurrencyNumeralsEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleCurrencyNumeralsEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -260,11 +252,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n" - + { - mgrPrefs.toggleHalfWidthPunctuationEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleHalfWidthPunctuationEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -272,11 +262,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n" - + { - mgrPrefs.toggleCNS11643Enabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleCNS11643Enabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -284,11 +272,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n" - + { - mgrPrefs.toggleSymbolInputEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleSymbolInputEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -296,11 +282,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n" - + { - mgrPrefs.toggleAssociatedPhrasesEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.toggleAssociatedPhrasesEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -308,11 +292,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n" - + { - mgrPrefs.togglePhraseReplacementEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") - }() + + (mgrPrefs.togglePhraseReplacementEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } diff --git a/Source/Modules/IMEModules/IME.swift b/Source/Modules/IMEModules/IME.swift index 027074ce..9d800519 100644 --- a/Source/Modules/IMEModules/IME.swift +++ b/Source/Modules/IMEModules/IME.swift @@ -13,7 +13,7 @@ import InputMethodKit public enum vChewing {} // The type of input modes. -public enum InputMode: String { +public enum InputMode: String, CaseIterable { case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS" case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT" case imeModeNULL = "" @@ -131,11 +131,11 @@ public enum IME { ), mgrLangModel.dataFolderPath(isDefaultFolder: false) ) - ctlNonModalAlertWindow.shared.show( - title: NSLocalizedString("Unable to create the user phrase file.", comment: ""), - content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""), - cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil - ) + let alert = NSAlert() + alert.messageText = NSLocalizedString("Unable to create the user phrase file.", comment: "") + alert.informativeText = content + alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) + alert.runModal() NSApp.setActivationPolicy(.accessory) return false } @@ -223,152 +223,6 @@ public enum IME { } return 0 } - - // MARK: - Registering the input method. - - @discardableResult static func registerInputMethod() -> Int32 { - guard let bundleID = Bundle.main.bundleIdentifier else { - return -1 - } - let bundleUrl = Bundle.main.bundleURL - var maybeInputSource = InputSourceHelper.inputSource(for: bundleID) - - if maybeInputSource == nil { - NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)") - // then register - let status = InputSourceHelper.registerTnputSource(at: bundleUrl) - - if !status { - NSLog( - "Fatal error: Cannot register input source \(bundleID) at \(bundleUrl.absoluteString)." - ) - return -1 - } - - maybeInputSource = InputSourceHelper.inputSource(for: bundleID) - } - - guard let inputSource = maybeInputSource else { - NSLog("Fatal error: Cannot find input source \(bundleID) after registration.") - return -1 - } - - if !InputSourceHelper.inputSourceEnabled(for: inputSource) { - NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).") - let status = InputSourceHelper.enable(inputSource: inputSource) - if !status { - NSLog("Fatal error: Cannot enable input source \(bundleID).") - return -1 - } - if !InputSourceHelper.inputSourceEnabled(for: inputSource) { - NSLog("Fatal error: Cannot enable input source \(bundleID).") - return -1 - } - } - - if CommandLine.arguments.count > 2, CommandLine.arguments[2] == "--all" { - let enabled = InputSourceHelper.enableAllInputMode(for: bundleID) - NSLog( - enabled - ? "All input sources enabled for \(bundleID)" - : "Cannot enable all input sources for \(bundleID), but this is ignored") - } - return 0 - } - - // MARK: - 準備枚舉系統內所有的 ASCII 鍵盤佈局 - - struct CarbonKeyboardLayout { - var strName: String = "" - var strValue: String = "" - } - - static let arrWhitelistedKeyLayoutsASCII: [String] = [ - "com.apple.keylayout.ABC", - "com.apple.keylayout.ABC-AZERTY", - "com.apple.keylayout.ABC-QWERTZ", - "com.apple.keylayout.British", - "com.apple.keylayout.Colemak", - "com.apple.keylayout.Dvorak", - "com.apple.keylayout.Dvorak-Left", - "com.apple.keylayout.DVORAK-QWERTYCMD", - "com.apple.keylayout.Dvorak-Right", - ] - static var arrEnumerateSystemKeyboardLayouts: [IME.CarbonKeyboardLayout] { - // 提前塞入 macOS 內建的兩款動態鍵盤佈局 - var arrKeyLayouts: [IME.CarbonKeyboardLayout] = [] - arrKeyLayouts += [ - IME.CarbonKeyboardLayout( - strName: NSLocalizedString("Apple Chewing - Dachen", comment: ""), - strValue: "com.apple.keylayout.ZhuyinBopomofo" - ), - IME.CarbonKeyboardLayout( - strName: NSLocalizedString("Apple Chewing - Eten Traditional", comment: ""), - strValue: "com.apple.keylayout.ZhuyinEten" - ), - ] - - // 準備枚舉系統內所有的 ASCII 鍵盤佈局 - var arrKeyLayoutsMACV: [IME.CarbonKeyboardLayout] = [] - var arrKeyLayoutsASCII: [IME.CarbonKeyboardLayout] = [] - let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] - for source in list { - if let ptrCategory = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { - let category = Unmanaged.fromOpaque(ptrCategory).takeUnretainedValue() - if category != kTISCategoryKeyboardInputSource { - continue - } - } else { - continue - } - - if let ptrASCIICapable = TISGetInputSourceProperty( - source, kTISPropertyInputSourceIsASCIICapable - ) { - let asciiCapable = Unmanaged.fromOpaque(ptrASCIICapable) - .takeUnretainedValue() - if asciiCapable != kCFBooleanTrue { - continue - } - } else { - continue - } - - if let ptrSourceType = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { - let sourceType = Unmanaged.fromOpaque(ptrSourceType).takeUnretainedValue() - if sourceType != kTISTypeKeyboardLayout { - continue - } - } else { - continue - } - - guard let ptrSourceID = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), - let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) - else { - continue - } - - let sourceID = String(Unmanaged.fromOpaque(ptrSourceID).takeUnretainedValue()) - let localizedName = String( - Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue()) - - if sourceID.contains("vChewing") { - arrKeyLayoutsMACV += [ - IME.CarbonKeyboardLayout(strName: localizedName, strValue: sourceID) - ] - } - - if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) { - arrKeyLayoutsASCII += [ - IME.CarbonKeyboardLayout(strName: localizedName, strValue: sourceID) - ] - } - } - arrKeyLayouts += arrKeyLayoutsMACV - arrKeyLayouts += arrKeyLayoutsASCII - return arrKeyLayouts - } } // MARK: - Root Extensions @@ -382,6 +236,16 @@ extension RangeReplaceableCollection where Element: Hashable { } } +// MARK: - String charComponents Extension + +extension String { + public var charComponents: [String] { map { String($0) } } +} + +extension Array where Element == String.Element { + public var charComponents: [String] { map { String($0) } } +} + // MARK: - String Tildes Expansion Extension extension String { diff --git a/Source/Modules/IMEModules/IMKHelper.swift b/Source/Modules/IMEModules/IMKHelper.swift new file mode 100644 index 00000000..62873e88 --- /dev/null +++ b/Source/Modules/IMEModules/IMKHelper.swift @@ -0,0 +1,226 @@ +// (c) 2021 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Foundation +import InputMethodKit + +// MARK: - IMKHelper by The vChewing Project (MIT License). + +enum IMKHelper { + /// 威注音有專門統計過,實際上會有差異的英數鍵盤佈局只有這幾種。 + /// 精簡成這種清單的話,不但節省 SwiftUI 的繪製壓力,也方便使用者做選擇。 + static let arrWhitelistedKeyLayoutsASCII: [String] = [ + "com.apple.keylayout.ABC", + "com.apple.keylayout.ABC-AZERTY", + "com.apple.keylayout.ABC-QWERTZ", + "com.apple.keylayout.British", + "com.apple.keylayout.Colemak", + "com.apple.keylayout.Dvorak", + "com.apple.keylayout.Dvorak-Left", + "com.apple.keylayout.DVORAK-QWERTYCMD", + "com.apple.keylayout.Dvorak-Right", + ] + + static let arrDynamicBasicKeyLayouts: [String] = [ + "com.apple.keylayout.ZhuyinBopomofo", + "com.apple.keylayout.ZhuyinEten", + "org.atelierInmu.vChewing.keyLayouts.vchewingdachen", + "org.atelierInmu.vChewing.keyLayouts.vchewingmitac", + "org.atelierInmu.vChewing.keyLayouts.vchewingibm", + "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou", + "org.atelierInmu.vChewing.keyLayouts.vchewingeten", + "org.unknown.keylayout.vChewingDachen", + "org.unknown.keylayout.vChewingFakeSeigyou", + "org.unknown.keylayout.vChewingETen", + "org.unknown.keylayout.vChewingIBM", + "org.unknown.keylayout.vChewingMiTAC", + ] + + static var allowedBasicLayoutsAsTISInputSources: [TISInputSource?] { + // 為了保證清單順序,先弄兩個容器。 + var containerA: [TISInputSource?] = [] + var containerB: [TISInputSource?] = [] + var containerC: [TISInputSource?] = [] + + let rawDictionary = TISInputSource.rawTISInputSources(onlyASCII: false) + + IMKHelper.arrWhitelistedKeyLayoutsASCII.forEach { + if let neta = rawDictionary[$0], !arrDynamicBasicKeyLayouts.contains(neta.identifier) { + containerC.append(neta) + } + } + + IMKHelper.arrDynamicBasicKeyLayouts.forEach { + if let neta = rawDictionary[$0] { + if neta.identifier.contains("com.apple") { + containerA.append(neta) + } else { + containerB.append(neta) + } + } + } + + // 這裡的 nil 是用來讓選單插入分隔符用的。 + if !containerA.isEmpty { containerA.append(nil) } + if !containerB.isEmpty { containerB.append(nil) } + + return containerA + containerB + containerC + } + + struct CarbonKeyboardLayout { + var strName: String = "" + var strValue: String = "" + } +} + +// MARK: - 與輸入法的具體的安裝過程有關的命令 + +extension IMKHelper { + @discardableResult static func registerInputMethod() -> Int32 { + TISInputSource.registerInputMethod() ? 0 : -1 + } +} + +// MARK: - TISInputSource Extension by The vChewing Project (MIT License). + +extension TISInputSource { + public static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] { + TISInputSource.modes.compactMap { TISInputSource.generate(from: $0) } + } + + public static var modes: [String] { + guard let components = Bundle.main.infoDictionary?["ComponentInputModeDict"] as? [String: Any], + let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any] + else { + return [] + } + return tsInputModeListKey.keys.map { $0 } + } + + @discardableResult public static func registerInputMethod() -> Bool { + let instances = TISInputSource.allRegisteredInstancesOfThisInputMethod + if instances.isEmpty { + // 有實例尚未登記。執行登記手續。 + NSLog("Registering input source.") + if !TISInputSource.registerInputSource() { + NSLog("Input source registration failed.") + return false + } + } + var succeeded = true + instances.forEach { + NSLog("Enabling input source: \($0.identifier)") + if !$0.activate() { + NSLog("Failed from enabling input source: \($0.identifier)") + succeeded = false + } + } + return succeeded + } + + @discardableResult public static func registerInputSource() -> Bool { + TISRegisterInputSource(Bundle.main.bundleURL as CFURL) == noErr + } + + @discardableResult public func activate() -> Bool { + TISEnableInputSource(self) == noErr + } + + @discardableResult public func select() -> Bool { + if !isSelectable { + NSLog("Non-selectable: \(identifier)") + return false + } + if TISSelectInputSource(self) != noErr { + NSLog("Failed from switching to \(identifier)") + return false + } + return true + } + + @discardableResult public func deactivate() -> Bool { + TISDisableInputSource(self) == noErr + } + + public var isActivated: Bool { + unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputSourceIsEnabled), to: CFBoolean.self) + == kCFBooleanTrue + } + + public var isSelectable: Bool { + unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputSourceIsSelectCapable), to: CFBoolean.self) + == kCFBooleanTrue + } + + public static func generate(from identifier: String) -> TISInputSource? { + TISInputSource.rawTISInputSources(onlyASCII: false)[identifier] ?? nil + } + + public var inputModeID: String { + unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputModeID), to: NSString.self) as String + } + + public var vChewingLocalizedName: String { + switch identifier { + case "com.apple.keylayout.ZhuyinBopomofo": + return NSLocalizedString("Apple Zhuyin Bopomofo (Dachen)", comment: "") + case "com.apple.keylayout.ZhuyinEten": + return NSLocalizedString("Apple Zhuyin Eten (Traditional)", comment: "") + default: return localizedName + } + } +} + +// MARK: - TISInputSource Extension by Mizuno Hiroki (a.k.a. "Mzp") (MIT License) + +// Ref: Original source codes are written in Swift 4 from Mzp's InputMethodKit textbook. +// Note: Slightly modified by vChewing Project: Using Dictionaries when necessary. + +extension TISInputSource { + public var localizedName: String { + unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyLocalizedName), to: NSString.self) as String + } + + public var identifier: String { + unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputSourceID), to: NSString.self) as String + } + + public var scriptCode: Int { + let r = TISGetInputSourceProperty(self, "TSMInputSourcePropertyScriptCode" as CFString) + return unsafeBitCast(r, to: NSString.self).integerValue + } + + public static func rawTISInputSources(onlyASCII: Bool = false) -> [String: TISInputSource] { + // 為了指定檢索條件,先構築 CFDictionary 辭典。 + // 第二項代指辭典容量。 + let conditions = CFDictionaryCreateMutable(nil, 2, nil, nil) + if onlyASCII { + // 第一條件:僅接收靜態鍵盤佈局結果。 + CFDictionaryAddValue( + conditions, unsafeBitCast(kTISPropertyInputSourceType, to: UnsafeRawPointer.self), + unsafeBitCast(kTISTypeKeyboardLayout, to: UnsafeRawPointer.self) + ) + // 第二條件:只能輸入 ASCII 內容。 + CFDictionaryAddValue( + conditions, unsafeBitCast(kTISPropertyInputSourceIsASCIICapable, to: UnsafeRawPointer.self), + unsafeBitCast(kCFBooleanTrue, to: UnsafeRawPointer.self) + ) + } + // 返回鍵盤配列清單。 + var result = TISCreateInputSourceList(conditions, true).takeRetainedValue() as? [TISInputSource] ?? .init() + if onlyASCII { + result = result.filter { $0.scriptCode == 0 } + } + var resultDictionary: [String: TISInputSource] = [:] + result.forEach { + resultDictionary[$0.inputModeID] = $0 + resultDictionary[$0.identifier] = $0 + } + return resultDictionary + } +} diff --git a/Source/Modules/IMEModules/InputSourceHelper.swift b/Source/Modules/IMEModules/InputSourceHelper.swift deleted file mode 100644 index 7face9d8..00000000 --- a/Source/Modules/IMEModules/InputSourceHelper.swift +++ /dev/null @@ -1,112 +0,0 @@ -// (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). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -import Cocoa -import InputMethodKit - -public class InputSourceHelper: NSObject { - @available(*, unavailable) - override public init() { - super.init() - } - - public static func allInstalledInputSources() -> [TISInputSource] { - TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] - } - - public static func inputSource(for propertyKey: CFString, stringValue: String) - -> TISInputSource? - { - let stringID = CFStringGetTypeID() - for source in allInstalledInputSources() { - if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) { - let property = Unmanaged.fromOpaque(propertyPtr).takeUnretainedValue() - let typeID = CFGetTypeID(property) - if typeID != stringID { - continue - } - if stringValue == property as? String { - return source - } - } - } - return nil - } - - public static func inputSource(for sourceID: String) -> TISInputSource? { - inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) - } - - public static func inputSourceEnabled(for source: TISInputSource) -> Bool { - if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { - let value = Unmanaged.fromOpaque(valuePts).takeUnretainedValue() - return value == kCFBooleanTrue - } - return false - } - - public static func enable(inputSource: TISInputSource) -> Bool { - let status = TISEnableInputSource(inputSource) - return status == noErr - } - - public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { - var enabled = false - for source in allInstalledInputSources() { - guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), - let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID) - else { - continue - } - let bundleID = Unmanaged.fromOpaque(bundleIDPtr).takeUnretainedValue() - if String(bundleID) == inputSourceBundleD { - let modeEnabled = enable(inputSource: source) - if !modeEnabled { - return false - } - enabled = true - } - } - - return enabled - } - - public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { - for source in allInstalledInputSources() { - guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), - let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) - else { - continue - } - let inputsSourceBundleID = Unmanaged.fromOpaque(bundleIDPtr) - .takeUnretainedValue() - let inputsSourceModeID = Unmanaged.fromOpaque(modePtr).takeUnretainedValue() - if modeID == String(inputsSourceModeID), bundleID == String(inputsSourceBundleID) { - let enabled = enable(inputSource: source) - print( - "Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)" - ) - return enabled - } - } - print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)") - return false - } - - public static func disable(inputSource: TISInputSource) -> Bool { - let status = TISDisableInputSource(inputSource) - return status == noErr - } - - public static func registerTnputSource(at url: URL) -> Bool { - let status = TISRegisterInputSource(url as CFURL) - return status == noErr - } -} diff --git a/Source/Modules/IMEModules/mgrPrefs.swift b/Source/Modules/IMEModules/mgrPrefs.swift index 61cf321c..80401d7f 100644 --- a/Source/Modules/IMEModules/mgrPrefs.swift +++ b/Source/Modules/IMEModules/mgrPrefs.swift @@ -438,6 +438,7 @@ public enum mgrPrefs { @UserDefault(key: UserDef.kTrimUnfinishedReadingsOnCommit.rawValue, defaultValue: true) static var trimUnfinishedReadingsOnCommit: Bool + // MARK: - Settings (Tier 2) @UserDefault(key: UserDef.kUseIMKCandidateWindow.rawValue, defaultValue: false) @@ -458,6 +459,8 @@ public enum mgrPrefs { @UserDefault(key: UserDef.kMaxCandidateLength.rawValue, defaultValue: 10) static var maxCandidateLength: Int + static var allowedMarkRange: ClosedRange = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength + @UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep.rawValue, defaultValue: true) static var shouldNotFartInLieuOfBeep: Bool diff --git a/Source/Modules/LangModelRelated/LMInstantiator.swift b/Source/Modules/LangModelRelated/LMInstantiator.swift index 6d02c367..829ed8fa 100644 --- a/Source/Modules/LangModelRelated/LMInstantiator.swift +++ b/Source/Modules/LangModelRelated/LMInstantiator.swift @@ -245,11 +245,11 @@ extension vChewing { return !unigramsFor(key: key).isEmpty } - public func associatedPhrasesFor(pair: Megrez.Compositor.Candidate) -> [String] { + public func associatedPhrasesFor(pair: Megrez.Compositor.KeyValuePaired) -> [String] { lmAssociates.valuesFor(pair: pair) } - public func hasAssociatedPhrasesFor(pair: Megrez.Compositor.Candidate) -> Bool { + public func hasAssociatedPhrasesFor(pair: Megrez.Compositor.KeyValuePaired) -> Bool { lmAssociates.hasValuesFor(pair: pair) } diff --git a/Source/Modules/LangModelRelated/LMSymbolNode.swift b/Source/Modules/LangModelRelated/LMSymbolNode.swift index 53806ef0..4e298acf 100644 --- a/Source/Modules/LangModelRelated/LMSymbolNode.swift +++ b/Source/Modules/LangModelRelated/LMSymbolNode.swift @@ -10,24 +10,34 @@ import Foundation -class SymbolNode { +public class SymbolNode { var title: String var children: [SymbolNode]? var previous: SymbolNode? - init(_ title: String, _ children: [SymbolNode]? = nil) { + init(_ title: String, _ children: [SymbolNode]? = nil, previous: SymbolNode? = nil) { self.title = title self.children = children + self.children?.forEach { + $0.previous = self + } + self.previous = previous } init(_ title: String, symbols: String) { self.title = title children = Array(symbols).map { SymbolNode(String($0), nil) } + children?.forEach { + $0.previous = self + } } init(_ title: String, symbols: [String]) { self.title = title children = symbols.map { SymbolNode($0, nil) } + children?.forEach { + $0.previous = self + } } static func parseUserSymbolNodeData() { diff --git a/Source/Modules/LangModelRelated/SubLMs/lmAssociates.swift b/Source/Modules/LangModelRelated/SubLMs/lmAssociates.swift index 4bb7e076..ee6c89b8 100644 --- a/Source/Modules/LangModelRelated/SubLMs/lmAssociates.swift +++ b/Source/Modules/LangModelRelated/SubLMs/lmAssociates.swift @@ -78,7 +78,7 @@ extension vChewing { // This function will be implemented only if further hard-necessity comes. } - public func valuesFor(pair: Megrez.Compositor.Candidate) -> [String] { + public func valuesFor(pair: Megrez.Compositor.KeyValuePaired) -> [String] { var pairs: [String] = [] if let arrRangeRecords: [(Range, Int)] = rangeMap[pair.toNGramKey] { for (netaRange, index) in arrRangeRecords { @@ -98,7 +98,7 @@ extension vChewing { return pairs.filter { set.insert($0).inserted } } - public func hasValuesFor(pair: Megrez.Compositor.Candidate) -> Bool { + public func hasValuesFor(pair: Megrez.Compositor.KeyValuePaired) -> Bool { if rangeMap[pair.toNGramKey] != nil { return true } return rangeMap[pair.value] != nil } diff --git a/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift b/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift index fade1ead..e7a52382 100644 --- a/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift +++ b/Source/Modules/LangModelRelated/SubLMs/lmUserOverride.swift @@ -319,8 +319,8 @@ extension vChewing.LMUserOverride { // 前置單元只記錄讀音,在其後的單元則同時記錄讀音與字詞 let strCurrent = kvCurrent.key - var kvPrevious = Megrez.Compositor.Candidate() - var kvAnterior = Megrez.Compositor.Candidate() + var kvPrevious = Megrez.Compositor.KeyValuePaired() + var kvAnterior = Megrez.Compositor.KeyValuePaired() var readingStack = "" var trigramKey: String { "(\(kvAnterior.toNGramKey),\(kvPrevious.toNGramKey),\(strCurrent))" } var result: String { diff --git a/Source/Modules/LanguageParsers/Megrez/0_Megrez.swift b/Source/Modules/LanguageParsers/Megrez/0_Megrez.swift index 3bdc535c..f30622fa 100644 --- a/Source/Modules/LanguageParsers/Megrez/0_Megrez.swift +++ b/Source/Modules/LanguageParsers/Megrez/0_Megrez.swift @@ -4,9 +4,7 @@ // This code is released under the MIT license (SPDX-License-Identifier: MIT) /// The namespace for this package. -public enum Megrez { - public typealias KeyValuePaired = Compositor.Candidate // 相容性措施。 -} +public enum Megrez {} // 著作權聲明: // 除了 Megrez 專有的修改與實作以外,該套件所有程式邏輯來自於 Gramambular、算法歸 Lukhnos Liu 所有。 diff --git a/Source/Modules/LanguageParsers/Megrez/1_Compositor.swift b/Source/Modules/LanguageParsers/Megrez/1_Compositor.swift index 078fdf97..7c8f86c0 100644 --- a/Source/Modules/LanguageParsers/Megrez/1_Compositor.swift +++ b/Source/Modules/LanguageParsers/Megrez/1_Compositor.swift @@ -23,7 +23,15 @@ extension Megrez { /// 公開:多字讀音鍵當中用以分割漢字讀音的記號的預設值,是「-」。 public static let kDefaultSeparator: String = "-" /// 該組字器的游標位置。 - public var cursor: Int = 0 { didSet { cursor = max(0, min(cursor, length)) } } + public var cursor: Int = 0 { + didSet { + cursor = max(0, min(cursor, length)) + marker = cursor + } + } + + /// 該組字器的標記器位置。 + public var marker: Int = 0 { didSet { marker = max(0, min(marker, length)) } } /// 公開:多字讀音鍵當中用以分割漢字讀音的記號,預設為「-」。 public var separator = kDefaultSeparator /// 公開:組字器內已經插入的單筆索引鍵的數量。 @@ -88,37 +96,46 @@ extension Megrez { } /// 按幅位來前後移動游標。 - /// - Parameter direction: 移動方向。 + /// - Parameters: + /// - direction: 移動方向。 + /// - isMarker: 要移動的是否為選擇標記(而非游標)。 /// - Returns: 該操作是否順利完成。 - @discardableResult public mutating func jumpCursorBySpan(to direction: TypingDirection) -> Bool { + @discardableResult public mutating func jumpCursorBySpan(to direction: TypingDirection, isMarker: Bool = false) + -> Bool + { + var target = isMarker ? marker : cursor switch direction { case .front: - if cursor == width { return false } + if target == width { return false } case .rear: - if cursor == 0 { return false } + if target == 0 { return false } } - guard let currentRegion = cursorRegionMap[cursor] else { return false } + guard let currentRegion = cursorRegionMap[target] else { return false } let aRegionForward = max(currentRegion - 1, 0) let currentRegionBorderRear: Int = walkedNodes[0.. walkedNodes.count) ? keys.count : walkedNodes[0...currentRegion].map(\.spanLength).reduce(0, +) case .rear: - cursor = walkedNodes[0.. Bool { + public static func == (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool { lhs.key == rhs.key && lhs.value == rhs.value } - public static func < (lhs: Candidate, rhs: Candidate) -> Bool { + public static func < (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool { (lhs.key.count < rhs.key.count) || (lhs.key.count == rhs.key.count && lhs.value < rhs.value) } - public static func > (lhs: Candidate, rhs: Candidate) -> Bool { + public static func > (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool { (lhs.key.count > rhs.key.count) || (lhs.key.count == rhs.key.count && lhs.value > rhs.value) } - public static func <= (lhs: Candidate, rhs: Candidate) -> Bool { + public static func <= (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool { (lhs.key.count <= rhs.key.count) || (lhs.key.count == rhs.key.count && lhs.value <= rhs.value) } - public static func >= (lhs: Candidate, rhs: Candidate) -> Bool { + public static func >= (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool { (lhs.key.count >= rhs.key.count) || (lhs.key.count == rhs.key.count && lhs.value >= rhs.value) } } @@ -60,8 +60,8 @@ extension Megrez.Compositor { /// 話,那麼這裡會用到 location - 1、以免去在呼叫該函式後再處理的麻煩。 /// - Parameter location: 游標位置。 /// - Returns: 候選字音配對陣列。 - public func fetchCandidates(at location: Int, filter: CandidateFetchFilter = .all) -> [Candidate] { - var result = [Candidate]() + public func fetchCandidates(at location: Int, filter: CandidateFetchFilter = .all) -> [KeyValuePaired] { + var result = [KeyValuePaired]() guard !keys.isEmpty else { return result } let location = max(min(location, keys.count - 1), 0) // 防呆 let anchors: [NodeAnchor] = fetchOverlappingNodes(at: location).stableSorted { @@ -96,7 +96,7 @@ extension Megrez.Compositor { /// - overrideType: 指定覆寫行為。 /// - Returns: 該操作是否成功執行。 @discardableResult public func overrideCandidate( - _ candidate: Candidate, at location: Int, overrideType: Node.OverrideType = .withHighScore + _ candidate: KeyValuePaired, at location: Int, overrideType: Node.OverrideType = .withHighScore ) -> Bool { @@ -154,7 +154,7 @@ extension Megrez.Compositor { anchor.node.reset() continue } - anchor.node.overridingScore /= 2 + anchor.node.overridingScore /= 4 } } return true diff --git a/Source/Modules/LanguageParsers/Megrez/6_Node.swift b/Source/Modules/LanguageParsers/Megrez/6_Node.swift index ebbc9df9..1ae23d43 100644 --- a/Source/Modules/LanguageParsers/Megrez/6_Node.swift +++ b/Source/Modules/LanguageParsers/Megrez/6_Node.swift @@ -42,7 +42,7 @@ extension Megrez.Compositor { didSet { currentUnigramIndex = min(max(0, currentUnigramIndex), unigrams.count - 1) } } - public var currentPair: Megrez.Compositor.Candidate { .init(key: key, value: value) } + public var currentPair: Megrez.Compositor.KeyValuePaired { .init(key: key, value: value) } public func hash(into hasher: inout Hasher) { hasher.combine(key) @@ -70,8 +70,12 @@ extension Megrez.Compositor { overrideType = .withNoOverrides } + /// 檢查當前節點是否「讀音字長與候選字字長不一致」。 + public var isReadingMismatched: Bool { + keyArray.count != value.count + } + /// 給出目前的最高權重單元圖。該結果可能會受節點覆寫狀態所影響。 - /// - Returns: 目前的最高權重單元圖。該結果可能會受節點覆寫狀態所影響。 public var currentUnigram: Megrez.Unigram { unigrams.isEmpty ? .init() : unigrams[currentUnigramIndex] } diff --git a/Source/Modules/UIModules/CandidateUI/ctlCandidateIMK.swift b/Source/Modules/UIModules/CandidateUI/ctlCandidateIMK.swift index 7ce9eb91..bae2b48d 100644 --- a/Source/Modules/UIModules/CandidateUI/ctlCandidateIMK.swift +++ b/Source/Modules/UIModules/CandidateUI/ctlCandidateIMK.swift @@ -229,18 +229,7 @@ public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol { if let newChar = ctlCandidateIMK.defaultIMKSelectionKey[event.keyCode] { /// 根據 KeyCode 重新換算一下選字鍵的 NSEvent,糾正其 Character 數值。 /// 反正 IMK 選字窗目前也沒辦法修改選字鍵。 - let newEvent = NSEvent.keyEvent( - with: event.type, - location: event.locationInWindow, - modifierFlags: event.modifierFlags, - timestamp: event.timestamp, - windowNumber: event.windowNumber, - context: nil, - characters: newChar, - charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? event.characters ?? "", - isARepeat: event.isARepeat, - keyCode: event.keyCode - ) + let newEvent = event.reinitiate(characters: newChar) if let newEvent = newEvent { if mgrPrefs.useSCPCTypingMode, delegate.isAssociatedPhrasesState { // 註:input.isShiftHold 已經在 ctlInputMethod.handle() 內處理,因為在那邊處理才有效。 @@ -300,17 +289,6 @@ extension ctlCandidateIMK { let mapNumPadKeyCodeTranslation: [UInt16: UInt16] = [ 83: 18, 84: 19, 85: 20, 86: 21, 87: 23, 88: 22, 89: 26, 91: 28, 92: 25, ] - return NSEvent.keyEvent( - with: event.type, - location: event.locationInWindow, - modifierFlags: event.modifierFlags, - timestamp: event.timestamp, - windowNumber: event.windowNumber, - context: nil, - characters: event.characters ?? "", - charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? event.characters ?? "", - isARepeat: event.isARepeat, - keyCode: mapNumPadKeyCodeTranslation[event.keyCode] ?? event.keyCode - ) + return event.reinitiate(keyCode: mapNumPadKeyCodeTranslation[event.keyCode] ?? event.keyCode) } } diff --git a/Source/Modules/UIModules/PrefUI/suiPrefPaneKeyboard.swift b/Source/Modules/UIModules/PrefUI/suiPrefPaneKeyboard.swift index 362ba82f..d75bea8b 100644 --- a/Source/Modules/UIModules/PrefUI/suiPrefPaneKeyboard.swift +++ b/Source/Modules/UIModules/PrefUI/suiPrefPaneKeyboard.swift @@ -93,12 +93,12 @@ struct suiPrefPaneKeyboard: View { mgrPrefs.mandarinParser = value switch value { case 0: - if !AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout) { + if !IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout) { mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ZhuyinBopomofo" selBasicKeyboardLayout = mgrPrefs.basicKeyboardLayout } default: - if AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout) { + if IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout) { mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ABC" selBasicKeyboardLayout = mgrPrefs.basicKeyboardLayout } @@ -170,15 +170,19 @@ struct suiPrefPaneKeyboard: View { selection: $selBasicKeyboardLayout.onChange { let value = selBasicKeyboardLayout mgrPrefs.basicKeyboardLayout = value - if AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(value) { + if IMKHelper.arrDynamicBasicKeyLayouts.contains(value) { mgrPrefs.mandarinParser = 0 selMandarinParser = mgrPrefs.mandarinParser } } ) { - ForEach(0...(IME.arrEnumerateSystemKeyboardLayouts.count - 1), id: \.self) { id in - Text(IME.arrEnumerateSystemKeyboardLayouts[id].strName).tag( - IME.arrEnumerateSystemKeyboardLayouts[id].strValue) + ForEach(0...(IMKHelper.allowedBasicLayoutsAsTISInputSources.count - 1), id: \.self) { id in + let theEntry = IMKHelper.allowedBasicLayoutsAsTISInputSources[id] + if let theEntry = theEntry { + Text(theEntry.vChewingLocalizedName).tag(theEntry.identifier) + } else { + Divider() + } }.id(UUID()) } .labelsHidden() diff --git a/Source/Modules/main.swift b/Source/Modules/main.swift index aa44f436..3fb131c1 100644 --- a/Source/Modules/main.swift +++ b/Source/Modules/main.swift @@ -11,7 +11,11 @@ import Cocoa import InputMethodKit -let kConnectionName = "org.atelierInmu.inputmethod.vChewing_Connection" +guard let kConnectionName = Bundle.main.infoDictionary?["InputMethodConnectionName"] as? String +else { + NSLog("Fatal error: Failed from retrieving connection name from info.plist file.") + exit(-1) +} switch max(CommandLine.arguments.count - 1, 0) { case 0: break @@ -19,7 +23,7 @@ switch max(CommandLine.arguments.count - 1, 0) { switch CommandLine.arguments[1] { case "install": if CommandLine.arguments[1] == "install" { - let exitCode = IME.registerInputMethod() + let exitCode = IMKHelper.registerInputMethod() exit(exitCode) } case "uninstall": diff --git a/Source/Resources/IME-Info.plist b/Source/Resources/IME-Info.plist index c3fa892b..54a5892d 100644 --- a/Source/Resources/IME-Info.plist +++ b/Source/Resources/IME-Info.plist @@ -52,7 +52,7 @@ tsInputModePrimaryInScriptKey tsInputModeScriptKey - smTradChinese + smUnicode org.atelierInmu.inputmethod.vChewing.IMECHT @@ -80,7 +80,7 @@ tsInputModePrimaryInScriptKey tsInputModeScriptKey - smTradChinese + smUnicode tsVisibleInputModeOrderedArrayKey diff --git a/Source/WindowControllers/ctlNonModalAlertWindow.swift b/Source/WindowControllers/ctlNonModalAlertWindow.swift deleted file mode 100644 index e8c6c37c..00000000 --- a/Source/WindowControllers/ctlNonModalAlertWindow.swift +++ /dev/null @@ -1,114 +0,0 @@ -// (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). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -import Cocoa - -protocol ctlNonModalAlertWindowDelegate: AnyObject { - func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) - func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow) -} - -class ctlNonModalAlertWindow: NSWindowController { - static let shared = ctlNonModalAlertWindow(windowNibName: "frmNonModalAlertWindow") - - @IBOutlet var titleTextField: NSTextField! - @IBOutlet var contentTextField: NSTextField! - @IBOutlet var confirmButton: NSButton! - @IBOutlet var cancelButton: NSButton! - weak var delegate: ctlNonModalAlertWindowDelegate? - - func show( - title: String, content: String, confirmButtonTitle: String, cancelButtonTitle: String?, - cancelAsDefault: Bool, delegate: ctlNonModalAlertWindowDelegate? - ) { - guard let window = window else { return } - if window.isVisible == true { - self.delegate?.ctlNonModalAlertWindowDidCancel(self) - } - - self.delegate = delegate - - var oldFrame = confirmButton.frame - confirmButton.title = confirmButtonTitle - confirmButton.sizeToFit() - - var newFrame = confirmButton.frame - newFrame.size.width = max(90, newFrame.size.width + 10) - newFrame.origin.x += oldFrame.size.width - newFrame.size.width - confirmButton.frame = newFrame - - if let cancelButtonTitle = cancelButtonTitle { - cancelButton.title = cancelButtonTitle - cancelButton.sizeToFit() - var adjustFrame = cancelButton.frame - adjustFrame.size.width = max(90, adjustFrame.size.width + 10) - adjustFrame.origin.x = newFrame.origin.x - adjustFrame.size.width - confirmButton.frame = adjustFrame - cancelButton.isHidden = false - } else { - cancelButton.isHidden = true - } - - cancelButton.nextKeyView = confirmButton - confirmButton.nextKeyView = cancelButton - - if cancelButtonTitle != nil { - if cancelAsDefault { - window.defaultButtonCell = cancelButton.cell as? NSButtonCell - } else { - cancelButton.keyEquivalent = " " - window.defaultButtonCell = confirmButton.cell as? NSButtonCell - } - } else { - window.defaultButtonCell = confirmButton.cell as? NSButtonCell - } - - titleTextField.stringValue = title - - oldFrame = contentTextField.frame - contentTextField.stringValue = content - - var infiniteHeightFrame = oldFrame - infiniteHeightFrame.size.width -= 4.0 - infiniteHeightFrame.size.height = 10240 - newFrame = (content as NSString).boundingRect( - with: infiniteHeightFrame.size, options: [.usesLineFragmentOrigin], - attributes: [.font: contentTextField.font!] - ) - newFrame.size.width = max(newFrame.size.width, oldFrame.size.width) - newFrame.size.height += 4.0 - newFrame.origin = oldFrame.origin - newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height) - contentTextField.frame = newFrame - - var windowFrame = window.frame - windowFrame.size.height += (newFrame.size.height - oldFrame.size.height) - window.level = NSWindow.Level(Int(CGShieldingWindowLevel()) + 1) - window.setFrame(windowFrame, display: true) - window.center() - window.makeKeyAndOrderFront(self) - NSApp.activate(ignoringOtherApps: true) - } - - @IBAction func confirmButtonAction(_: Any) { - delegate?.ctlNonModalAlertWindowDidConfirm(self) - window?.orderOut(self) - } - - @IBAction func cancelButtonAction(_ sender: Any) { - cancel(sender) - } - - func cancel(_: Any) { - delegate?.ctlNonModalAlertWindowDidCancel(self) - delegate = nil - window?.orderOut(self) - } -} diff --git a/Source/WindowControllers/ctlPrefWindow.swift b/Source/WindowControllers/ctlPrefWindow.swift index f777c66e..5b5e4ac7 100644 --- a/Source/WindowControllers/ctlPrefWindow.swift +++ b/Source/WindowControllers/ctlPrefWindow.swift @@ -93,88 +93,24 @@ class ctlPrefWindow: NSWindowController { currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem uiLanguageButton.select(currentLanguageSelectItem) - let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] var usKeyboardLayoutItem: NSMenuItem? var chosenBaseKeyboardLayoutItem: NSMenuItem? basicKeyboardLayoutButton.menu?.removeAllItems() - let itmAppleZhuyinBopomofo = NSMenuItem() - itmAppleZhuyinBopomofo.title = NSLocalizedString("Apple Zhuyin Bopomofo (Dachen)", comment: "") - itmAppleZhuyinBopomofo.representedObject = String( - "com.apple.keylayout.ZhuyinBopomofo") - basicKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinBopomofo) - - let itmAppleZhuyinEten = NSMenuItem() - itmAppleZhuyinEten.title = NSLocalizedString("Apple Zhuyin Eten (Traditional)", comment: "") - itmAppleZhuyinEten.representedObject = String("com.apple.keylayout.ZhuyinEten") - basicKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinEten) - let basicKeyboardLayoutID = mgrPrefs.basicKeyboardLayout - for source in list { - if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { - let category = Unmanaged.fromOpaque(categoryPtr).takeUnretainedValue() - if category != kTISCategoryKeyboardInputSource { - continue - } - } else { + for source in IMKHelper.allowedBasicLayoutsAsTISInputSources { + guard let source = source else { + basicKeyboardLayoutButton.menu?.addItem(NSMenuItem.separator()) continue } - - if let asciiCapablePtr = TISGetInputSourceProperty( - source, kTISPropertyInputSourceIsASCIICapable - ) { - let asciiCapable = Unmanaged.fromOpaque(asciiCapablePtr) - .takeUnretainedValue() - if asciiCapable != kCFBooleanTrue { - continue - } - } else { - continue - } - - if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { - let sourceType = Unmanaged.fromOpaque(sourceTypePtr).takeUnretainedValue() - if sourceType != kTISTypeKeyboardLayout { - continue - } - } else { - continue - } - - guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), - let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) - else { - continue - } - - let sourceID = String(Unmanaged.fromOpaque(sourceIDPtr).takeUnretainedValue()) - let localizedName = String( - Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue()) - let menuItem = NSMenuItem() - menuItem.title = localizedName - menuItem.representedObject = sourceID - - if sourceID == "com.apple.keylayout.US" { - usKeyboardLayoutItem = menuItem - } - if basicKeyboardLayoutID == sourceID { - chosenBaseKeyboardLayoutItem = menuItem - } - if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) || sourceID.contains("vChewing") { - basicKeyboardLayoutButton.menu?.addItem(menuItem) - } - } - - switch basicKeyboardLayoutID { - case "com.apple.keylayout.ZhuyinBopomofo": - chosenBaseKeyboardLayoutItem = itmAppleZhuyinBopomofo - case "com.apple.keylayout.ZhuyinEten": - chosenBaseKeyboardLayoutItem = itmAppleZhuyinEten - default: - break // nothing to do + menuItem.title = source.vChewingLocalizedName + menuItem.representedObject = source.identifier + if source.identifier == "com.apple.keylayout.US" { usKeyboardLayoutItem = menuItem } + if basicKeyboardLayoutID == source.identifier { chosenBaseKeyboardLayoutItem = menuItem } + basicKeyboardLayoutButton.menu?.addItem(menuItem) } basicKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem) diff --git a/Source/WindowNIBs/Base.lproj/frmNonModalAlertWindow.xib b/Source/WindowNIBs/Base.lproj/frmNonModalAlertWindow.xib deleted file mode 100644 index 20368443..00000000 --- a/Source/WindowNIBs/Base.lproj/frmNonModalAlertWindow.xib +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Update-Info.plist b/Update-Info.plist index b4a8c1ea..ed43c90e 100644 --- a/Update-Info.plist +++ b/Update-Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 2.3.1 + 2.4.0 CFBundleVersion - 2310 + 2400 UpdateInfoEndpoint https://gitee.com/vchewing/vChewing-macOS/raw/main/Update-Info.plist UpdateInfoSite diff --git a/vChewing.pkgproj b/vChewing.pkgproj index 542e0251..9c0234c8 100644 --- a/vChewing.pkgproj +++ b/vChewing.pkgproj @@ -726,7 +726,7 @@ USE_HFS+_COMPRESSION VERSION - 2.3.1 + 2.4.0 TYPE 0 diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 133975ea..baf22521 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 5B09307628B6FC3B0021F8C5 /* shortcuts.html in Resources */ = {isa = PBXBuildFile; fileRef = 5B09307828B6FC3B0021F8C5 /* shortcuts.html */; }; 5B0AF8B527B2C8290096FE54 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0AF8B427B2C8290096FE54 /* StringExtension.swift */; }; 5B11328927B94CFB00E58451 /* AppleKeyboardConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */; }; + 5B175FFB28C5CDDC0078D1B4 /* IMKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */; }; 5B20430728BEE30900BFC6FD /* BookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B20430628BEE30900BFC6FD /* BookmarkManager.swift */; }; 5B21176C287539BB000443A9 /* ctlInputMethod_HandleStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */; }; 5B21176E28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */; }; @@ -17,7 +18,7 @@ 5B2170E0289FACAD00BE7304 /* 7_LangModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170D7289FACAC00BE7304 /* 7_LangModel.swift */; }; 5B2170E1289FACAD00BE7304 /* 0_Megrez.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */; }; 5B2170E2289FACAD00BE7304 /* 8_Unigram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170D9289FACAC00BE7304 /* 8_Unigram.swift */; }; - 5B2170E3289FACAD00BE7304 /* 3_Candidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170DA289FACAC00BE7304 /* 3_Candidate.swift */; }; + 5B2170E3289FACAD00BE7304 /* 3_KeyValuePaired.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170DA289FACAC00BE7304 /* 3_KeyValuePaired.swift */; }; 5B2170E4289FACAD00BE7304 /* 2_Walker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170DB289FACAC00BE7304 /* 2_Walker.swift */; }; 5B2170E5289FACAD00BE7304 /* 6_Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170DC289FACAC00BE7304 /* 6_Node.swift */; }; 5B2170E6289FACAD00BE7304 /* 4_Span.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2170DD289FACAC00BE7304 /* 4_Span.swift */; }; @@ -32,14 +33,11 @@ 5B5948CE289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5948CD289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift */; }; 5B5E535227EF261400C6AA1E /* IME.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5E535127EF261400C6AA1E /* IME.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 */; }; - 5B62A33827AE79CD00A19448 /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33727AE79CD00A19448 /* StringUtils.swift */; }; 5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */; }; 5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */; }; 5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34327AE7CD900A19448 /* TooltipController.swift */; }; 5B62A34A27AE7CD900A19448 /* NotifierController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34527AE7CD900A19448 /* NotifierController.swift */; }; - 5B62A35327AE89C400A19448 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33127AE792F00A19448 /* InputSourceHelper.swift */; }; 5B6C141228A9D4B30098ADF8 /* ctlInputMethod_Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6C141128A9D4B30098ADF8 /* ctlInputMethod_Common.swift */; }; 5B73FB5E27B2BE1300E9BF49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B73FB6027B2BE1300E9BF49 /* InfoPlist.strings */; }; 5B782EC4280C243C007276DE /* KeyHandler_HandleCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */; }; @@ -80,7 +78,6 @@ 5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */; }; 5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; }; 5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75E27AED54C0023B93A /* Fart.m4a */; }; - 5BBBB76B27AED5DB0023B93A /* frmNonModalAlertWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB76527AED5DB0023B93A /* frmNonModalAlertWindow.xib */; }; 5BBBB76D27AED5DB0023B93A /* frmAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */; }; 5BBBB77327AED70B0023B93A /* MenuIcon-TCVIM@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB76F27AED70B0023B93A /* MenuIcon-TCVIM@2x.png */; }; 5BBBB77427AED70B0023B93A /* MenuIcon-SCVIM@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB77027AED70B0023B93A /* MenuIcon-SCVIM@2x.png */; }; @@ -117,6 +114,9 @@ 5BEDB724283B4C250078EB25 /* data-symbols.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71E283B4AEA0078EB25 /* data-symbols.plist */; }; 5BEDB725283B4C250078EB25 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; }; 5BF0B84C28C070B000795FC6 /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */; }; + 5BF13B9428C627BB00E99EC1 /* IMKHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */; }; + 5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF56F9728C39A2700DD6839 /* IMEState.swift */; }; + 5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF56F9928C39D1800DD6839 /* IMEStateData.swift */; }; 5BF9DA2728840E6200DBD48E /* template-usersymbolphrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2228840E6200DBD48E /* template-usersymbolphrases.txt */; }; 5BF9DA2828840E6200DBD48E /* template-exclusions.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2328840E6200DBD48E /* template-exclusions.txt */; }; 5BF9DA2928840E6200DBD48E /* template-associatedPhrases-chs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BF9DA2428840E6200DBD48E /* template-associatedPhrases-chs.txt */; }; @@ -134,10 +134,8 @@ 6ACA41FD15FC1D9000935EF6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41F015FC1D9000935EF6 /* MainMenu.xib */; }; 6ACA420215FC1E5200935EF6 /* vChewing.app in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */; }; D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F76B278CA1BA004A2160 /* AppDelegate.swift */; }; - D461B792279DAC010070E734 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; }; 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_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 */; }; @@ -213,6 +211,7 @@ 5B0AF8B427B2C8290096FE54 /* StringExtension.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = StringExtension.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B0C5EDF27C7D9870078037C /* dataCompiler.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = dataCompiler.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppleKeyboardConverter.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + 5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMKHelper.swift; sourceTree = ""; }; 5B18BA6F27C7BD8B0056EB19 /* LICENSE-CHS.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "LICENSE-CHS.txt"; sourceTree = ""; }; 5B18BA7027C7BD8B0056EB19 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; 5B18BA7127C7BD8B0056EB19 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -229,7 +228,7 @@ 5B2170D7289FACAC00BE7304 /* 7_LangModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7_LangModel.swift; sourceTree = ""; }; 5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 0_Megrez.swift; sourceTree = ""; }; 5B2170D9289FACAC00BE7304 /* 8_Unigram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 8_Unigram.swift; sourceTree = ""; }; - 5B2170DA289FACAC00BE7304 /* 3_Candidate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 3_Candidate.swift; sourceTree = ""; }; + 5B2170DA289FACAC00BE7304 /* 3_KeyValuePaired.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 3_KeyValuePaired.swift; sourceTree = ""; }; 5B2170DB289FACAC00BE7304 /* 2_Walker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 2_Walker.swift; sourceTree = ""; }; 5B2170DC289FACAC00BE7304 /* 6_Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6_Node.swift; sourceTree = ""; }; 5B2170DD289FACAC00BE7304 /* 4_Span.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 4_Span.swift; sourceTree = ""; }; @@ -248,9 +247,7 @@ 5B5948CD289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMInstantiator_DateTimeExtension.swift; sourceTree = ""; }; 5B5E535127EF261400C6AA1E /* IME.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = IME.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; }; - 5B62A33727AE79CD00A19448 /* StringUtils.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = StringUtils.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlAboutWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlCandidate.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A34327AE7CD900A19448 /* TooltipController.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = TooltipController.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; @@ -297,7 +294,6 @@ 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlInputMethod_Menu.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = ""; }; 5BBBB75E27AED54C0023B93A /* Fart.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.m4a; sourceTree = ""; }; - 5BBBB76627AED5DB0023B93A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmNonModalAlertWindow.xib; sourceTree = ""; }; 5BBBB76A27AED5DB0023B93A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmAboutWindow.xib; sourceTree = ""; }; 5BBBB76F27AED70B0023B93A /* MenuIcon-TCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM@2x.png"; sourceTree = ""; }; 5BBBB77027AED70B0023B93A /* MenuIcon-SCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM@2x.png"; sourceTree = ""; }; @@ -346,6 +342,8 @@ 5BEDB720283B4AEA0078EB25 /* data-cht.plist */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "data-cht.plist"; path = "Data/data-cht.plist"; sourceTree = ""; }; 5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 5BF255CD28B2694E003ECB60 /* vChewing-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "vChewing-Bridging-Header.h"; sourceTree = ""; }; + 5BF56F9728C39A2700DD6839 /* IMEState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMEState.swift; sourceTree = ""; }; + 5BF56F9928C39D1800DD6839 /* IMEStateData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMEStateData.swift; sourceTree = ""; }; 5BF9DA2228840E6200DBD48E /* template-usersymbolphrases.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-usersymbolphrases.txt"; sourceTree = ""; usesTabs = 0; }; 5BF9DA2328840E6200DBD48E /* template-exclusions.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-exclusions.txt"; sourceTree = ""; usesTabs = 0; }; 5BF9DA2428840E6200DBD48E /* template-associatedPhrases-chs.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; name = "template-associatedPhrases-chs.txt"; path = "../Data/components/chs/template-associatedPhrases-chs.txt"; sourceTree = ""; usesTabs = 0; }; @@ -366,10 +364,8 @@ 6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Installer-Info.plist"; path = "Installer/Installer-Info.plist"; sourceTree = SOURCE_ROOT; }; D427F76B278CA1BA004A2160 /* AppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; - D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputState.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 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_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 = ""; }; @@ -498,14 +494,14 @@ 5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */, 5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */, 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */, - D461B791279DAC010070E734 /* InputState.swift */, + 5BF56F9728C39A2700DD6839 /* IMEState.swift */, + 5BF56F9928C39D1800DD6839 /* IMEStateData.swift */, 5BD0113C2818543900609769 /* KeyHandler_Core.swift */, 5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */, 5BE3779F288FED8D0037365B /* KeyHandler_HandleComposition.swift */, 5B7F225C2808501000DDD3CB /* KeyHandler_HandleInput.swift */, 5B3133BE280B229700A4A505 /* KeyHandler_States.swift */, 5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */, - 5B62A33727AE79CD00A19448 /* StringUtils.swift */, 5BAA8FBD282CAF380066C406 /* SyllableComposer.swift */, ); path = ControllerModules; @@ -524,7 +520,7 @@ children = ( 5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */, 5B5E535127EF261400C6AA1E /* IME.swift */, - 5B62A33127AE792F00A19448 /* InputSourceHelper.swift */, + 5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */, 5B62A33527AE795800A19448 /* mgrPrefs.swift */, ); path = IMEModules; @@ -586,7 +582,6 @@ isa = PBXGroup; children = ( 5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */, - D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */, D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */, ); path = WindowControllers; @@ -596,7 +591,6 @@ isa = PBXGroup; children = ( 5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */, - 5BBBB76527AED5DB0023B93A /* frmNonModalAlertWindow.xib */, 5B7BC4AE27AFFBE800F66C24 /* frmPrefWindow.xib */, 6A187E2816004C5900466B2E /* MainMenu.xib */, ); @@ -867,7 +861,7 @@ 5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */, 5B2170DE289FACAC00BE7304 /* 1_Compositor.swift */, 5B2170DB289FACAC00BE7304 /* 2_Walker.swift */, - 5B2170DA289FACAC00BE7304 /* 3_Candidate.swift */, + 5B2170DA289FACAC00BE7304 /* 3_KeyValuePaired.swift */, 5B2170DD289FACAC00BE7304 /* 4_Span.swift */, 5B2170DF289FACAC00BE7304 /* 5_Vertex.swift */, 5B2170DC289FACAC00BE7304 /* 6_Node.swift */, @@ -1070,7 +1064,6 @@ 5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */, 6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */, D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */, - 5BBBB76B27AED5DB0023B93A /* frmNonModalAlertWindow.xib in Resources */, 5BEDB723283B4C250078EB25 /* data-cht.plist in Resources */, 5BEDB721283B4C250078EB25 /* data-cns.plist in Resources */, 5BF9DA2D288427E000DBD48E /* template-associatedPhrases-cht.txt in Resources */, @@ -1205,7 +1198,7 @@ 5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */, 5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */, 5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */, - D461B792279DAC010070E734 /* InputState.swift in Sources */, + 5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */, 5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */, D47B92C027972AD100458394 /* main.swift in Sources */, D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */, @@ -1217,7 +1210,6 @@ 5B84579F2871AD2200C93B01 /* HotenkaChineseConverter.swift in Sources */, 5B887F302826AEA400B6651E /* lmCoreEX.swift in Sources */, 5BA9FD4627FEF3C9002DE248 /* Container.swift in Sources */, - D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */, 5B2170E5289FACAD00BE7304 /* 6_Node.swift in Sources */, 5B949BD92816DC5400D87B5D /* LineReader.swift in Sources */, 5BA9FD1027FEDB6B002DE248 /* suiPrefPaneKeyboard.swift in Sources */, @@ -1232,6 +1224,7 @@ D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */, 5BD0113D2818543900609769 /* KeyHandler_Core.swift in Sources */, 5B2170E4289FACAD00BE7304 /* 2_Walker.swift in Sources */, + 5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */, 5BA9FD4227FEF3C8002DE248 /* PreferencePane.swift in Sources */, 5BA0DF312817857D009E73BB /* lmUserOverride.swift in Sources */, 5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */, @@ -1246,17 +1239,16 @@ 5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */, 5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */, 5B782EC4280C243C007276DE /* KeyHandler_HandleCandidate.swift in Sources */, - 5B62A33827AE79CD00A19448 /* StringUtils.swift in Sources */, - 5B2170E3289FACAD00BE7304 /* 3_Candidate.swift in Sources */, + 5B2170E3289FACAD00BE7304 /* 3_KeyValuePaired.swift in Sources */, 5BA9FD0F27FEDB6B002DE248 /* suiPrefPaneGeneral.swift in Sources */, 5B2170E6289FACAD00BE7304 /* 4_Span.swift in Sources */, + 5B175FFB28C5CDDC0078D1B4 /* IMKHelper.swift in Sources */, 5BA9FD4927FEF3C9002DE248 /* Section.swift in Sources */, 5BA9FD3E27FEF3C8002DE248 /* Utilities.swift in Sources */, 5B242403284B0D6500520FE4 /* ctlCandidateUniversal.swift in Sources */, 5BA9FD1127FEDB6B002DE248 /* ctlPrefUI.swift in Sources */, 5B8457A12871ADBE00C93B01 /* HotenkaCCBridge.swift in Sources */, 5B40730D281672610023DFFF /* lmReplacements.swift in Sources */, - 5B62A33227AE792F00A19448 /* InputSourceHelper.swift in Sources */, 5B5E535227EF261400C6AA1E /* IME.swift in Sources */, 5B2170E0289FACAD00BE7304 /* 7_LangModel.swift in Sources */, 5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */, @@ -1285,7 +1277,7 @@ files = ( D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */, D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */, - 5B62A35327AE89C400A19448 /* InputSourceHelper.swift in Sources */, + 5BF13B9428C627BB00E99EC1 /* IMKHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1344,14 +1336,6 @@ name = frmPrefWindow.xib; sourceTree = ""; }; - 5BBBB76527AED5DB0023B93A /* frmNonModalAlertWindow.xib */ = { - isa = PBXVariantGroup; - children = ( - 5BBBB76627AED5DB0023B93A /* Base */, - ); - name = frmNonModalAlertWindow.xib; - sourceTree = ""; - }; 5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */ = { isa = PBXVariantGroup; children = ( @@ -1471,7 +1455,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -1481,7 +1465,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewingTests; @@ -1510,13 +1494,13 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; ENABLE_NS_ASSERTIONS = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewingTests; @@ -1548,7 +1532,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; DEAD_CODE_STRIPPING = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1570,7 +1554,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor; @@ -1600,7 +1584,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; DEAD_CODE_STRIPPING = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; @@ -1618,7 +1602,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor; @@ -1734,7 +1718,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -1763,7 +1747,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1793,7 +1777,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -1816,7 +1800,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1840,7 +1824,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -1861,7 +1845,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingInstaller; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1884,7 +1868,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2310; + CURRENT_PROJECT_VERSION = 2400; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -1899,7 +1883,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingInstaller; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";