2.4.0 // Dezonblization - phase 1. Merge PR #120 from upd/2.4.0
This commit is contained in:
commit
bd30564bdb
5
AUTHORS
5
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:
|
$ Contributors and volunteers of the upstream repo, having no responsibility in discussing anything in the current repo:
|
||||||
|
|
||||||
- Zonble Yang:
|
- 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).
|
- Voltaire candidate window MK2 (massively modified as MK3 in vChewing by Shiki Suen).
|
||||||
- Notifier window and Tooltip UI.
|
- Notifier window and Tooltip UI.
|
||||||
- NSStringUtils and FSEventStreamHelper.
|
- FSEventStreamHelper.
|
||||||
- App-style installer (only preserved for developer purposes).
|
- App-style installer (only preserved for developer purposes).
|
||||||
- InputSource Helper.
|
|
||||||
- mgrPrefs (userdefaults manager).
|
- mgrPrefs (userdefaults manager).
|
||||||
- apiUpdate.
|
- apiUpdate.
|
||||||
- Mengjuei Hsieh:
|
- Mengjuei Hsieh:
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import InputMethodKit
|
||||||
|
|
||||||
private let kTargetBin = "vChewing"
|
private let kTargetBin = "vChewing"
|
||||||
private let kTargetType = "app"
|
private let kTargetType = "app"
|
||||||
|
@ -47,6 +48,17 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
private var translocationRemovalStartTime: Date?
|
private var translocationRemovalStartTime: Date?
|
||||||
private var currentVersionNumber: Int = 0
|
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) {
|
func runAlertPanel(title: String, message: String, buttonTitle: String) {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.alertStyle = .informational
|
alert.alertStyle = .informational
|
||||||
|
@ -230,23 +242,20 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
endAppWithDelay()
|
endAppWithDelay()
|
||||||
}
|
}
|
||||||
|
|
||||||
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
|
|
||||||
|
|
||||||
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
|
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
|
||||||
|
|
||||||
guard let imeBundle = Bundle(url: imeURLInstalled),
|
guard let theBundle = Bundle(url: imeURLInstalled),
|
||||||
let imeIdentifier = imeBundle.bundleIdentifier
|
let imeIdentifier = theBundle.bundleIdentifier
|
||||||
else {
|
else {
|
||||||
endAppWithDelay()
|
endAppWithDelay()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let imeBundleURL = imeBundle.bundleURL
|
let imeBundleURL = theBundle.bundleURL
|
||||||
var inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
|
|
||||||
|
|
||||||
if inputSource == nil {
|
if allRegisteredInstancesOfThisInputMethod.isEmpty {
|
||||||
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
|
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
|
||||||
let status = InputSourceHelper.registerTnputSource(at: imeBundleURL)
|
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
|
||||||
if !status {
|
if !status {
|
||||||
let message = String(
|
let message = String(
|
||||||
format: NSLocalizedString(
|
format: NSLocalizedString(
|
||||||
|
@ -262,8 +271,7 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
|
if allRegisteredInstancesOfThisInputMethod.isEmpty {
|
||||||
if inputSource == nil {
|
|
||||||
let message = String(
|
let message = String(
|
||||||
format: NSLocalizedString(
|
format: NSLocalizedString(
|
||||||
"Cannot find input source %@ after registration.", comment: ""
|
"Cannot find input source %@ after registration.", comment: ""
|
||||||
|
@ -285,19 +293,20 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
|
||||||
NSLog("Installer runs with the pre-macOS 12 flow.")
|
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*
|
// 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
|
// enabled in the user's current set of IMEs (which means the IME does not show up in
|
||||||
// the user's input menu).
|
// the user's input menu).
|
||||||
|
|
||||||
var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!)
|
var mainInputSourceEnabled = false
|
||||||
if !mainInputSourceEnabled || isMacOS12OrAbove {
|
|
||||||
mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!)
|
allRegisteredInstancesOfThisInputMethod.forEach {
|
||||||
if mainInputSourceEnabled {
|
if $0.activate() {
|
||||||
NSLog("Input method enabled: \(imeIdentifier)")
|
NSLog("Input method enabled: \(imeIdentifier)")
|
||||||
} else {
|
} else {
|
||||||
NSLog("Failed to enable input method: \(imeIdentifier)")
|
NSLog("Failed to enable input method: \(imeIdentifier)")
|
||||||
}
|
}
|
||||||
|
mainInputSourceEnabled = $0.isActivated
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alert Panel
|
// Alert Panel
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4d6ed238c037c4d4f5464f6914198a503fd1f21a
|
Subproject commit 0293f06c92ba9b95dd42debf662b8f8bb5834bdd
|
|
@ -12,7 +12,7 @@ import Cocoa
|
||||||
import InputMethodKit
|
import InputMethodKit
|
||||||
|
|
||||||
@objc(AppDelegate)
|
@objc(AppDelegate)
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate,
|
class AppDelegate: NSObject, NSApplicationDelegate,
|
||||||
FSEventStreamHelperDelegate, NSUserNotificationCenterDelegate
|
FSEventStreamHelperDelegate, NSUserNotificationCenterDelegate
|
||||||
{
|
{
|
||||||
func helper(_: FSEventStreamHelper, didReceive _: [FSEventStreamHelper.Event]) {
|
func helper(_: FSEventStreamHelper, didReceive _: [FSEventStreamHelper.Event]) {
|
||||||
|
@ -30,7 +30,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
||||||
private var ctlPrefWindowInstance: ctlPrefWindow?
|
private var ctlPrefWindowInstance: ctlPrefWindow?
|
||||||
private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window
|
private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window
|
||||||
private var checkTask: URLSessionTask?
|
private var checkTask: URLSessionTask?
|
||||||
private var updateNextStepURL: URL?
|
|
||||||
public var fsStreamHelper = FSEventStreamHelper(
|
public var fsStreamHelper = FSEventStreamHelper(
|
||||||
path: mgrLangModel.dataFolderPath(isDefaultFolder: false),
|
path: mgrLangModel.dataFolderPath(isDefaultFolder: false),
|
||||||
queue: DispatchQueue(label: "vChewing User Phrases")
|
queue: DispatchQueue(label: "vChewing User Phrases")
|
||||||
|
@ -42,7 +41,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
||||||
ctlPrefWindowInstance = nil
|
ctlPrefWindowInstance = nil
|
||||||
ctlAboutWindowInstance = nil
|
ctlAboutWindowInstance = nil
|
||||||
checkTask = nil
|
checkTask = nil
|
||||||
updateNextStepURL = nil
|
|
||||||
fsStreamHelper.stop()
|
fsStreamHelper.stop()
|
||||||
fsStreamHelper.delegate = nil
|
fsStreamHelper.delegate = nil
|
||||||
}
|
}
|
||||||
|
@ -137,7 +135,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
||||||
case .success(let apiResult):
|
case .success(let apiResult):
|
||||||
switch apiResult {
|
switch apiResult {
|
||||||
case .shouldUpdate(let report):
|
case .shouldUpdate(let report):
|
||||||
updateNextStepURL = report.siteUrl
|
|
||||||
let content = String(
|
let content = String(
|
||||||
format: NSLocalizedString(
|
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?%@",
|
"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
|
report.versionDescription
|
||||||
)
|
)
|
||||||
IME.prtDebugIntel("vChewingDebug: \(content)")
|
IME.prtDebugIntel("vChewingDebug: \(content)")
|
||||||
currentAlertType = "Update"
|
let alert = NSAlert()
|
||||||
ctlNonModalAlertWindow.shared.show(
|
alert.messageText = NSLocalizedString("New Version Available", comment: "")
|
||||||
title: NSLocalizedString(
|
alert.informativeText = content
|
||||||
"New Version Available", comment: ""
|
alert.addButton(withTitle: NSLocalizedString("Visit Website", comment: ""))
|
||||||
),
|
alert.addButton(withTitle: NSLocalizedString("Not Now", comment: ""))
|
||||||
content: content,
|
let result = alert.runModal()
|
||||||
confirmButtonTitle: NSLocalizedString(
|
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
||||||
"Visit Website", comment: ""
|
if let siteURL = report.siteUrl {
|
||||||
),
|
NSWorkspace.shared.open(siteURL)
|
||||||
cancelButtonTitle: NSLocalizedString(
|
}
|
||||||
"Not Now", comment: ""
|
}
|
||||||
),
|
|
||||||
cancelAsDefault: false,
|
|
||||||
delegate: self
|
|
||||||
)
|
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
case .noNeedToUpdate, .ignored:
|
case .noNeedToUpdate, .ignored:
|
||||||
break
|
break
|
||||||
|
@ -172,9 +165,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
switch error {
|
switch error {
|
||||||
case VersionUpdateApiError.connectionError(let message):
|
case VersionUpdateApiError.connectionError(let message):
|
||||||
let title = NSLocalizedString(
|
let title = NSLocalizedString("Update Check Failed", comment: "")
|
||||||
"Update Check Failed", comment: ""
|
|
||||||
)
|
|
||||||
let content = String(
|
let content = String(
|
||||||
format: NSLocalizedString(
|
format: NSLocalizedString(
|
||||||
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
|
"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: "")
|
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
|
||||||
IME.prtDebugIntel("vChewingDebug: \(content)")
|
IME.prtDebugIntel("vChewingDebug: \(content)")
|
||||||
currentAlertType = "Update"
|
|
||||||
ctlNonModalAlertWindow.shared.show(
|
let alert = NSAlert()
|
||||||
title: title, content: content,
|
alert.messageText = title
|
||||||
confirmButtonTitle: buttonTitle,
|
alert.informativeText = content
|
||||||
cancelButtonTitle: nil,
|
alert.addButton(withTitle: buttonTitle)
|
||||||
cancelAsDefault: false, delegate: nil
|
alert.runModal()
|
||||||
)
|
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -205,39 +195,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
|
||||||
"This will remove vChewing Input Method from this user account, requiring your confirmation.",
|
"This will remove vChewing Input Method from this user account, requiring your confirmation.",
|
||||||
comment: ""
|
comment: ""
|
||||||
))
|
))
|
||||||
ctlNonModalAlertWindow.shared.show(
|
let alert = NSAlert()
|
||||||
title: NSLocalizedString("Uninstallation", comment: ""), content: content,
|
alert.messageText = NSLocalizedString("Uninstallation", comment: "")
|
||||||
confirmButtonTitle: NSLocalizedString("OK", comment: ""),
|
alert.informativeText = content
|
||||||
cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false,
|
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||||
delegate: self
|
alert.addButton(withTitle: NSLocalizedString("Not Now", comment: ""))
|
||||||
)
|
let result = alert.runModal()
|
||||||
NSApp.setActivationPolicy(.accessory)
|
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
||||||
}
|
|
||||||
|
|
||||||
func ctlNonModalAlertWindowDidConfirm(_: ctlNonModalAlertWindow) {
|
|
||||||
switch currentAlertType {
|
|
||||||
case "Uninstall":
|
|
||||||
NSWorkspace.shared.openFile(
|
NSWorkspace.shared.openFile(
|
||||||
mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder"
|
mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder"
|
||||||
)
|
)
|
||||||
IME.uninstall(isSudo: false, selfKill: true)
|
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
|
|
||||||
}
|
}
|
||||||
|
NSApp.setActivationPolicy(.accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New About Window
|
// New About Window
|
||||||
|
|
|
@ -7,23 +7,8 @@
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
enum AppleKeyboardConverter {
|
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 {
|
static var isDynamicBasicKeyboardLayoutEnabled: Bool {
|
||||||
AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout)
|
IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func cnvStringApple2ABC(_ strProcessed: String) -> String {
|
static func cnvStringApple2ABC(_ strProcessed: String) -> String {
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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<Int> {
|
||||||
|
min(cursor, marker)..<max(cursor, marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Cursor & Marker & Range for UTF16 (Read-Only)
|
||||||
|
|
||||||
|
/// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度,
|
||||||
|
/// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。
|
||||||
|
/// 這樣就可以免除不必要的類型轉換。
|
||||||
|
var u16Cursor: Int {
|
||||||
|
displayedText.charComponents[0..<cursor].joined().utf16.count
|
||||||
|
}
|
||||||
|
|
||||||
|
var u16Marker: Int {
|
||||||
|
displayedText.charComponents[0..<marker].joined().utf16.count
|
||||||
|
}
|
||||||
|
|
||||||
|
var u16MarkedRange: Range<Int> {
|
||||||
|
min(u16Cursor, u16Marker)..<max(u16Cursor, u16Marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Other data for non-empty states.
|
||||||
|
|
||||||
|
var markedTargetExists: Bool = false
|
||||||
|
var displayTextSegments = [String]() {
|
||||||
|
didSet {
|
||||||
|
displayedText = displayTextSegments.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reading: String = ""
|
||||||
|
var markedReadings = [String]()
|
||||||
|
var candidates = [(String, String)]()
|
||||||
|
var textToCommit: String = ""
|
||||||
|
var tooltip: String = ""
|
||||||
|
var tooltipBackupForInputting: String = ""
|
||||||
|
var attributedStringPlaceholder: NSAttributedString = .init(
|
||||||
|
string: " ",
|
||||||
|
attributes: [
|
||||||
|
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||||
|
.markedClauseSegment: 0,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
var isFilterable: Bool {
|
||||||
|
markedTargetExists ? mgrPrefs.allowedMarkRange.contains(markedRange.count) : false
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributedStringNormal: NSAttributedString {
|
||||||
|
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||||
|
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||||
|
let attributedString = NSMutableAttributedString(string: displayedText)
|
||||||
|
var newBegin = 0
|
||||||
|
for (i, neta) in displayTextSegments.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 attributedStringMarking: NSAttributedString {
|
||||||
|
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||||
|
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||||
|
let attributedString = NSMutableAttributedString(string: displayedText)
|
||||||
|
let end = u16MarkedRange.upperBound
|
||||||
|
|
||||||
|
attributedString.setAttributes(
|
||||||
|
[
|
||||||
|
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||||
|
.markedClauseSegment: 0,
|
||||||
|
], range: NSRange(location: 0, length: u16MarkedRange.lowerBound)
|
||||||
|
)
|
||||||
|
attributedString.setAttributes(
|
||||||
|
[
|
||||||
|
.underlineStyle: NSUnderlineStyle.thick.rawValue,
|
||||||
|
.markedClauseSegment: 1,
|
||||||
|
],
|
||||||
|
range: NSRange(
|
||||||
|
location: u16MarkedRange.lowerBound,
|
||||||
|
length: u16MarkedRange.upperBound - u16MarkedRange.lowerBound
|
||||||
|
)
|
||||||
|
)
|
||||||
|
attributedString.setAttributes(
|
||||||
|
[
|
||||||
|
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||||
|
.markedClauseSegment: 2,
|
||||||
|
],
|
||||||
|
range: NSRange(
|
||||||
|
location: end,
|
||||||
|
length: displayedText.utf16.count - end
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return attributedString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - IMEState 工具函式
|
||||||
|
|
||||||
|
extension StateData {
|
||||||
|
var chkIfUserPhraseExists: Bool {
|
||||||
|
let text = displayedText.charComponents[markedRange].joined()
|
||||||
|
let joined = markedReadings.joined(separator: "-")
|
||||||
|
return mgrLangModel.checkIfUserPhraseExist(
|
||||||
|
userPhrase: text, mode: IME.currentInputMode, key: joined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userPhrase: String {
|
||||||
|
let text = displayedText.charComponents[markedRange].joined()
|
||||||
|
let joined = markedReadings.joined(separator: "-")
|
||||||
|
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||||
|
return "\(text) \(joined)\(nerfedScore)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var userPhraseConverted: String {
|
||||||
|
let text =
|
||||||
|
ChineseConverter.crossConvert(displayedText.charComponents[markedRange].joined()) ?? ""
|
||||||
|
let joined = markedReadings.joined(separator: "-")
|
||||||
|
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||||
|
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
||||||
|
return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Marking {
|
||||||
|
private static func generateReadingThread(_ data: StateData) -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
|
||||||
"<InputState.Deactivated>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。
|
|
||||||
/// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。
|
|
||||||
class Empty: InputStateProtocol {
|
|
||||||
public var type: StateType { .ofEmpty }
|
|
||||||
|
|
||||||
var composingBuffer: String {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
"<InputState.Empty>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .EmptyIgnoringPreviousState: 與 Empty 類似,
|
|
||||||
/// 但會扔掉上一個狀態的內容、不將這些內容遞交給客體應用。
|
|
||||||
/// 該狀態在處理完畢之後會被立刻切換至 .Empty()。
|
|
||||||
class EmptyIgnoringPreviousState: Empty {
|
|
||||||
override public var type: StateType { .ofEmptyIgnoringPreviousState }
|
|
||||||
override var description: String {
|
|
||||||
"<InputState.EmptyIgnoringPreviousState>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
"<InputState.Committing textToCommit:\(textToCommit)>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
"<InputState.AssociatedPhrases, candidates:\(candidates), isTypingVertical:\(isTypingVertical)>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
"<InputState.NotEmpty, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
"<InputState.Inputting, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>, textToCommit:\(textToCommit)>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、
|
|
||||||
/// 還是將這個範圍的詞音組合放入語彙濾除清單。
|
|
||||||
class Marking: NotEmpty {
|
|
||||||
override public var type: StateType { .ofMarking }
|
|
||||||
private var allowedMarkRange: ClosedRange<Int> = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength
|
|
||||||
private(set) var markerIndex: Int = 0 { didSet { markerIndex = max(markerIndex, 0) } }
|
|
||||||
private(set) var markedRange: Range<Int>
|
|
||||||
private var literalMarkedRange: Range<Int> {
|
|
||||||
let lowerBoundLiteral = composingBuffer.charIndexLiteral(from: markedRange.lowerBound)
|
|
||||||
let upperBoundLiteral = composingBuffer.charIndexLiteral(from: markedRange.upperBound)
|
|
||||||
return lowerBoundLiteral..<upperBoundLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
var literalReadingThread: String {
|
|
||||||
var arrOutput = [String]()
|
|
||||||
for neta in readings[literalMarkedRange] {
|
|
||||||
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: " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
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..<end
|
|
||||||
self.readings = readings
|
|
||||||
super.init(
|
|
||||||
composingBuffer: composingBuffer, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray
|
|
||||||
)
|
|
||||||
defer { self.markerIndex = markerIndex }
|
|
||||||
}
|
|
||||||
|
|
||||||
override var attributedString: NSMutableAttributedString {
|
|
||||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
|
||||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
|
||||||
let attributedString = NSMutableAttributedString(string: composingBufferConverted)
|
|
||||||
let end = markedRange.upperBound
|
|
||||||
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 0,
|
|
||||||
], range: NSRange(location: 0, length: markedRange.lowerBound)
|
|
||||||
)
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.thick.rawValue,
|
|
||||||
.markedClauseSegment: 1,
|
|
||||||
],
|
|
||||||
range: NSRange(
|
|
||||||
location: markedRange.lowerBound,
|
|
||||||
length: markedRange.upperBound - markedRange.lowerBound
|
|
||||||
)
|
|
||||||
)
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 2,
|
|
||||||
],
|
|
||||||
range: NSRange(
|
|
||||||
location: end,
|
|
||||||
length: composingBuffer.utf16.count - end
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return attributedString
|
|
||||||
}
|
|
||||||
|
|
||||||
override var description: String {
|
|
||||||
"<InputState.Marking, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex), markedRange:\(markedRange)>"
|
|
||||||
}
|
|
||||||
|
|
||||||
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..<cursorIndex)
|
|
||||||
let bufferTextFront = composingBuffer.utf16SubString(with: cursorIndex..<(composingBuffer.utf16.count))
|
|
||||||
let cursorIndexU8 = bufferTextRear.count - 1
|
|
||||||
// 排除一些不應該出現的情形。
|
|
||||||
if (mgrPrefs.useRearCursorMode && bufferTextFront.count < chosenCandidateString.count)
|
|
||||||
|| (!mgrPrefs.useRearCursorMode && bufferTextRear.count < chosenCandidateString.count)
|
|
||||||
{
|
|
||||||
return super.attributedString
|
|
||||||
}
|
|
||||||
// u16Range 是用來畫線的,因為 NSAttributedString 只認 NSRange。
|
|
||||||
let u16Range: Range<Int> = {
|
|
||||||
switch mgrPrefs.useRearCursorMode {
|
|
||||||
case false: return (max(0, cursorIndex - chosenCandidateString.utf16.count))..<cursorIndex
|
|
||||||
case true:
|
|
||||||
return
|
|
||||||
cursorIndex..<min(cursorIndex + chosenCandidateString.utf16.count, composingBuffer.utf16.count - 1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// u8Range 是用來計算字串的。
|
|
||||||
let u8Range: Range<Int> = {
|
|
||||||
switch mgrPrefs.useRearCursorMode {
|
|
||||||
case false: return (max(0, cursorIndexU8 - chosenCandidateString.count))..<cursorIndexU8
|
|
||||||
case true:
|
|
||||||
return cursorIndexU8..<min(cursorIndexU8 + chosenCandidateString.count, composingBuffer.count - 1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
let strSegmentedRear = composingBuffer.charComponents[0..<u8Range.lowerBound].joined()
|
|
||||||
let strSegmentedFront = composingBuffer.charComponents[u8Range.upperBound...].joined()
|
|
||||||
let newBufferConverted: String = NotEmpty(
|
|
||||||
composingBuffer: strSegmentedRear + chosenCandidateString + strSegmentedFront, cursorIndex: 0
|
|
||||||
).composingBufferConverted
|
|
||||||
guard newBufferConverted.count == composingBuffer.count else { return super.attributedString }
|
|
||||||
|
|
||||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
|
||||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
|
||||||
let attributedStringResult = NSMutableAttributedString(string: newBufferConverted)
|
|
||||||
attributedStringResult.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 0,
|
|
||||||
], range: NSRange(location: 0, length: u16Range.lowerBound)
|
|
||||||
)
|
|
||||||
attributedStringResult.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.thick.rawValue,
|
|
||||||
.markedClauseSegment: 1,
|
|
||||||
], range: NSRange(location: u16Range.lowerBound, length: u16Range.count)
|
|
||||||
)
|
|
||||||
attributedStringResult.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 2,
|
|
||||||
], range: NSRange(location: u16Range.upperBound, length: newBufferConverted.utf16.count)
|
|
||||||
)
|
|
||||||
|
|
||||||
return attributedStringResult
|
|
||||||
}
|
|
||||||
|
|
||||||
override var description: String {
|
|
||||||
"<InputState.ChoosingCandidate, candidates:\(candidates), isTypingVertical:\(isTypingVertical), composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
"<InputState.SymbolTable, candidates:\(candidates), isTypingVertical:\(isTypingVertical)>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ protocol KeyHandlerDelegate {
|
||||||
_: KeyHandler, didSelectCandidateAt index: Int,
|
_: KeyHandler, didSelectCandidateAt index: Int,
|
||||||
ctlCandidate controller: ctlCandidateProtocol
|
ctlCandidate controller: ctlCandidateProtocol
|
||||||
)
|
)
|
||||||
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool)
|
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
|
||||||
-> Bool
|
-> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,6 @@ protocol KeyHandlerDelegate {
|
||||||
public class KeyHandler {
|
public class KeyHandler {
|
||||||
/// 半衰模組的衰減指數
|
/// 半衰模組的衰減指數
|
||||||
let kEpsilon: Double = 0.000001
|
let kEpsilon: Double = 0.000001
|
||||||
/// 檢測是否出現游標切斷組字圈內字符的情況
|
|
||||||
var isCursorCuttingChar = false
|
|
||||||
/// 檢測是否內容為空(注拼槽與組字器都是空的)
|
/// 檢測是否內容為空(注拼槽與組字器都是空的)
|
||||||
var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty }
|
var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty }
|
||||||
|
|
||||||
|
@ -88,6 +86,21 @@ public class KeyHandler {
|
||||||
|
|
||||||
// MARK: - Functions dealing with Megrez.
|
// MARK: - Functions dealing with Megrez.
|
||||||
|
|
||||||
|
/// 獲取當前標記得範圍。這個函式只能是函式、而非只讀變數。
|
||||||
|
/// - Returns: 當前標記範圍。
|
||||||
|
func currentMarkedRange() -> Range<Int> {
|
||||||
|
min(compositor.cursor, compositor.marker)..<max(compositor.cursor, compositor.marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 檢測是否出現游標切斷組字圈內字符的情況
|
||||||
|
func isCursorCuttingChar(isMarker: Bool = false) -> 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 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。
|
/// 實際上要拿給 Megrez 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。
|
||||||
///
|
///
|
||||||
/// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。
|
/// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。
|
||||||
|
@ -107,11 +120,10 @@ public class KeyHandler {
|
||||||
// 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。
|
// 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。
|
||||||
if mgrPrefs.isDebugModeEnabled {
|
if mgrPrefs.isDebugModeEnabled {
|
||||||
let result = compositor.dumpDOT
|
let result = compositor.dumpDOT
|
||||||
|
let appSupportPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending(
|
||||||
|
"/vChewing-visualization.dot")
|
||||||
do {
|
do {
|
||||||
try result.write(
|
try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8)
|
||||||
toFile: "/private/var/tmp/vChewing-visualization.dot",
|
|
||||||
atomically: true, encoding: .utf8
|
|
||||||
)
|
|
||||||
} catch {
|
} catch {
|
||||||
IME.prtDebugIntel("Failed from writing dumpDOT results.")
|
IME.prtDebugIntel("Failed from writing dumpDOT results.")
|
||||||
}
|
}
|
||||||
|
@ -122,7 +134,7 @@ public class KeyHandler {
|
||||||
/// - Parameter key: 給定的聯想詞的開頭字。
|
/// - Parameter key: 給定的聯想詞的開頭字。
|
||||||
/// - Returns: 抓取到的聯想詞陣列。
|
/// - Returns: 抓取到的聯想詞陣列。
|
||||||
/// 不會是 nil,但那些負責接收結果的函式會對空白陣列結果做出正確的處理。
|
/// 不會是 nil,但那些負責接收結果的函式會對空白陣列結果做出正確的處理。
|
||||||
func buildAssociatePhraseArray(withPair pair: Megrez.Compositor.Candidate) -> [(String, String)] {
|
func buildAssociatePhraseArray(withPair pair: Megrez.Compositor.KeyValuePaired) -> [(String, String)] {
|
||||||
var arrResult: [(String, String)] = []
|
var arrResult: [(String, String)] = []
|
||||||
if currentLM.hasAssociatedPhrasesFor(pair: pair) {
|
if currentLM.hasAssociatedPhrasesFor(pair: pair) {
|
||||||
arrResult = currentLM.associatedPhrasesFor(pair: pair).map { ("", $0) }
|
arrResult = currentLM.associatedPhrasesFor(pair: pair).map { ("", $0) }
|
||||||
|
@ -144,7 +156,7 @@ public class KeyHandler {
|
||||||
/// 威注音輸入法截至 v1.9.3 SP2 版為止都受到上游的這個 Bug 的影響,且在 v1.9.4 版利用該函式修正了這個缺陷。
|
/// 威注音輸入法截至 v1.9.3 SP2 版為止都受到上游的這個 Bug 的影響,且在 v1.9.4 版利用該函式修正了這個缺陷。
|
||||||
/// 該修正必須搭配至少天權星組字引擎 v2.0.2 版方可生效。算法可能比較囉唆,但至少在常用情形下不會再發生該問題。
|
/// 該修正必須搭配至少天權星組字引擎 v2.0.2 版方可生效。算法可能比較囉唆,但至少在常用情形下不會再發生該問題。
|
||||||
/// - Parameter theCandidate: 要拿來覆寫的詞音配對。
|
/// - Parameter theCandidate: 要拿來覆寫的詞音配對。
|
||||||
func consolidateCursorContext(with theCandidate: Megrez.Compositor.Candidate) {
|
func consolidateCursorContext(with theCandidate: Megrez.Compositor.KeyValuePaired) {
|
||||||
var grid = compositor
|
var grid = compositor
|
||||||
var frontBoundaryEX = actualCandidateCursor + 1
|
var frontBoundaryEX = actualCandidateCursor + 1
|
||||||
var rearBoundaryEX = actualCandidateCursor
|
var rearBoundaryEX = actualCandidateCursor
|
||||||
|
@ -195,7 +207,7 @@ public class KeyHandler {
|
||||||
let values = currentNode.currentPair.value.charComponents
|
let values = currentNode.currentPair.value.charComponents
|
||||||
for (subPosition, key) in currentNode.keyArray.enumerated() {
|
for (subPosition, key) in currentNode.keyArray.enumerated() {
|
||||||
guard values.count > subPosition else { break } // 防呆,應該沒有發生的可能性
|
guard values.count > subPosition else { break } // 防呆,應該沒有發生的可能性
|
||||||
let thePair = Megrez.Compositor.Candidate(
|
let thePair = Megrez.Compositor.KeyValuePaired(
|
||||||
key: key, value: values[subPosition]
|
key: key, value: values[subPosition]
|
||||||
)
|
)
|
||||||
compositor.overrideCandidate(thePair, at: position)
|
compositor.overrideCandidate(thePair, at: position)
|
||||||
|
@ -214,15 +226,12 @@ public class KeyHandler {
|
||||||
/// - respectCursorPushing: 若該選項為 true,則會在選字之後始終將游標推送至選字後的節錨的前方。
|
/// - respectCursorPushing: 若該選項為 true,則會在選字之後始終將游標推送至選字後的節錨的前方。
|
||||||
/// - consolidate: 在固化節點之前,先鞏固上下文。該選項可能會破壞在內文組字區內就地輪替候選字詞時的體驗。
|
/// - consolidate: 在固化節點之前,先鞏固上下文。該選項可能會破壞在內文組字區內就地輪替候選字詞時的體驗。
|
||||||
func fixNode(candidate: (String, String), respectCursorPushing: Bool = true, preConsolidate: Bool = false) {
|
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 {
|
if preConsolidate { consolidateCursorContext(with: theCandidate) }
|
||||||
consolidateCursorContext(with: theCandidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回到正常流程。
|
// 回到正常流程。
|
||||||
|
|
||||||
if !compositor.overrideCandidate(theCandidate, at: actualCandidateCursor) { return }
|
if !compositor.overrideCandidate(theCandidate, at: actualCandidateCursor) { return }
|
||||||
let previousWalk = compositor.walkedNodes
|
let previousWalk = compositor.walkedNodes
|
||||||
// 開始爬軌。
|
// 開始爬軌。
|
||||||
|
@ -261,7 +270,7 @@ public class KeyHandler {
|
||||||
func getCandidatesArray(fixOrder: Bool = true) -> [(String, String)] {
|
func getCandidatesArray(fixOrder: Bool = true) -> [(String, String)] {
|
||||||
/// 警告:不要對游標前置風格使用 nodesCrossing,否則會導致游標行為與 macOS 內建注音輸入法不一致。
|
/// 警告:不要對游標前置風格使用 nodesCrossing,否則會導致游標行為與 macOS 內建注音輸入法不一致。
|
||||||
/// 微軟新注音輸入法的游標後置風格也是不允許 nodeCrossing 的。
|
/// 微軟新注音輸入法的游標後置風格也是不允許 nodeCrossing 的。
|
||||||
var arrCandidates: [Megrez.Compositor.Candidate] = {
|
var arrCandidates: [Megrez.Compositor.KeyValuePaired] = {
|
||||||
switch mgrPrefs.useRearCursorMode {
|
switch mgrPrefs.useRearCursorMode {
|
||||||
case false:
|
case false:
|
||||||
return compositor.fetchCandidates(at: actualCandidateCursor, filter: .endAt)
|
return compositor.fetchCandidates(at: actualCandidateCursor, filter: .endAt)
|
||||||
|
@ -281,8 +290,8 @@ public class KeyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = fetchSuggestionsFromUOM(apply: false)
|
let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = fetchSuggestionsFromUOM(apply: false)
|
||||||
let arrSuggestedCandidates: [Megrez.Compositor.Candidate] = arrSuggestedUnigrams.map {
|
let arrSuggestedCandidates: [Megrez.Compositor.KeyValuePaired] = arrSuggestedUnigrams.map {
|
||||||
Megrez.Compositor.Candidate(key: $0.0, value: $0.1.value)
|
Megrez.Compositor.KeyValuePaired(key: $0.0, value: $0.1.value)
|
||||||
}
|
}
|
||||||
arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates
|
arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates
|
||||||
arrCandidates = arrCandidates.deduplicate
|
arrCandidates = arrCandidates.deduplicate
|
||||||
|
@ -307,7 +316,7 @@ public class KeyHandler {
|
||||||
if !suggestion.isEmpty, let newestSuggestedCandidate = suggestion.candidates.last {
|
if !suggestion.isEmpty, let newestSuggestedCandidate = suggestion.candidates.last {
|
||||||
let overrideBehavior: Megrez.Compositor.Node.OverrideType =
|
let overrideBehavior: Megrez.Compositor.Node.OverrideType =
|
||||||
suggestion.forceHighScoreOverride ? .withHighScore : .withTopUnigramScore
|
suggestion.forceHighScoreOverride ? .withHighScore : .withTopUnigramScore
|
||||||
let suggestedPair: Megrez.Compositor.Candidate = .init(
|
let suggestedPair: Megrez.Compositor.KeyValuePaired = .init(
|
||||||
key: newestSuggestedCandidate.0, value: newestSuggestedCandidate.1.value
|
key: newestSuggestedCandidate.0, value: newestSuggestedCandidate.1.value
|
||||||
)
|
)
|
||||||
IME.prtDebugIntel(
|
IME.prtDebugIntel(
|
||||||
|
|
|
@ -23,9 +23,9 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleCandidate(
|
func handleCandidate(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard var ctlCandidateCurrent = delegate?.ctlCandidate() else {
|
guard var ctlCandidateCurrent = delegate?.ctlCandidate() else {
|
||||||
|
@ -41,7 +41,7 @@ extension KeyHandler {
|
||||||
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
|
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
|
||||||
|
|
||||||
if cancelCandidateKey {
|
if cancelCandidateKey {
|
||||||
if state is InputState.AssociatedPhrases
|
if state.type == .ofAssociates
|
||||||
|| mgrPrefs.useSCPCTypingMode
|
|| mgrPrefs.useSCPCTypingMode
|
||||||
|| compositor.isEmpty
|
|| compositor.isEmpty
|
||||||
{
|
{
|
||||||
|
@ -49,13 +49,12 @@ extension KeyHandler {
|
||||||
// 就將當前的組字緩衝區析構處理、強制重設輸入狀態。
|
// 就將當前的組字緩衝區析構處理、強制重設輸入狀態。
|
||||||
// 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。
|
// 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。
|
||||||
// 所以這裡需要對 compositor.isEmpty 做判定。
|
// 所以這裡需要對 compositor.isEmpty 做判定。
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
} else {
|
} else {
|
||||||
stateCallback(buildInputtingState)
|
stateCallback(buildInputtingState)
|
||||||
}
|
}
|
||||||
if let state = state as? InputState.SymbolTable, let nodePrevious = state.node.previous {
|
if state.type == .ofSymbolTable, let nodePrevious = state.node.previous, let _ = nodePrevious.children {
|
||||||
stateCallback(InputState.SymbolTable(node: nodePrevious, isTypingVertical: state.isTypingVertical))
|
stateCallback(IMEState.ofSymbolTable(node: nodePrevious))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -63,9 +62,8 @@ extension KeyHandler {
|
||||||
// MARK: Enter
|
// MARK: Enter
|
||||||
|
|
||||||
if input.isEnter {
|
if input.isEnter {
|
||||||
if state is InputState.AssociatedPhrases, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
if state.type == .ofAssociates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
delegate?.keyHandler(
|
delegate?.keyHandler(
|
||||||
|
@ -243,23 +241,15 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: End Key
|
// MARK: End Key
|
||||||
|
|
||||||
var candidates: [(String, String)]!
|
if state.candidates.isEmpty {
|
||||||
|
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
|
||||||
candidates = state.candidates
|
|
||||||
} else if let state = state as? InputState.AssociatedPhrases {
|
|
||||||
candidates = state.candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
if candidates.isEmpty {
|
|
||||||
return false
|
return false
|
||||||
} else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
} else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
||||||
if input.isEnd || input.emacsKey == EmacsKey.end {
|
if input.isEnd || input.emacsKey == EmacsKey.end {
|
||||||
if ctlCandidateCurrent.selectedCandidateIndex == candidates.count - 1 {
|
if ctlCandidateCurrent.selectedCandidateIndex == state.candidates.count - 1 {
|
||||||
IME.prtDebugIntel("9B69AAAD")
|
IME.prtDebugIntel("9B69AAAD")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
} else {
|
} else {
|
||||||
ctlCandidateCurrent.selectedCandidateIndex = candidates.count - 1
|
ctlCandidateCurrent.selectedCandidateIndex = state.candidates.count - 1
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -267,13 +257,13 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: 聯想詞處理 (Associated Phrases)
|
// MARK: 聯想詞處理 (Associated Phrases)
|
||||||
|
|
||||||
if state is InputState.AssociatedPhrases {
|
if state.type == .ofAssociates {
|
||||||
if !input.isShiftHold { return false }
|
if !input.isShiftHold { return false }
|
||||||
}
|
}
|
||||||
|
|
||||||
var index: Int = NSNotFound
|
var index: Int = NSNotFound
|
||||||
let match: String =
|
let match: String =
|
||||||
(state is InputState.AssociatedPhrases) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
(state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
||||||
|
|
||||||
for j in 0..<ctlCandidateCurrent.keyLabels.count {
|
for j in 0..<ctlCandidateCurrent.keyLabels.count {
|
||||||
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
|
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
|
||||||
|
@ -293,7 +283,7 @@ extension KeyHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state is InputState.AssociatedPhrases { return false }
|
if state.type == .ofAssociates { return false }
|
||||||
|
|
||||||
// MARK: 逐字選字模式的處理 (SCPC Mode Processing)
|
// MARK: 逐字選字模式的處理 (SCPC Mode Processing)
|
||||||
|
|
||||||
|
@ -333,10 +323,9 @@ extension KeyHandler {
|
||||||
didSelectCandidateAt: candidateIndex,
|
didSelectCandidateAt: candidateIndex,
|
||||||
ctlCandidate: ctlCandidateCurrent
|
ctlCandidate: ctlCandidateCurrent
|
||||||
)
|
)
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return handle(
|
return handle(
|
||||||
input: input, state: InputState.Empty(), stateCallback: stateCallback, errorCallback: errorCallback
|
input: input, state: IMEState.ofEmpty(), stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -19,7 +19,7 @@ extension KeyHandler {
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleComposition(
|
func handleComposition(
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool? {
|
) -> Bool? {
|
||||||
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
||||||
|
@ -62,7 +62,7 @@ extension KeyHandler {
|
||||||
composer.receiveKey(fromString: input.text)
|
composer.receiveKey(fromString: input.text)
|
||||||
keyConsumedByReading = true
|
keyConsumedByReading = true
|
||||||
|
|
||||||
// 沒有調號的話,只需要 updateClientComposingBuffer() 且終止處理(return true)即可。
|
// 沒有調號的話,只需要 updateClientdisplayedText() 且終止處理(return true)即可。
|
||||||
// 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。
|
// 有調號的話,則不需要這樣,而是轉而繼續在此之後的處理。
|
||||||
if !composer.hasToneMarker() {
|
if !composer.hasToneMarker() {
|
||||||
stateCallback(buildInputtingState)
|
stateCallback(buildInputtingState)
|
||||||
|
@ -100,8 +100,7 @@ extension KeyHandler {
|
||||||
switch compositor.isEmpty {
|
switch compositor.isEmpty {
|
||||||
case false: stateCallback(buildInputtingState)
|
case false: stateCallback(buildInputtingState)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。
|
return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。
|
||||||
}
|
}
|
||||||
|
@ -118,37 +117,32 @@ extension KeyHandler {
|
||||||
// 之後就是更新組字區了。先清空注拼槽的內容。
|
// 之後就是更新組字區了。先清空注拼槽的內容。
|
||||||
composer.clear()
|
composer.clear()
|
||||||
|
|
||||||
// 再以回呼組字狀態的方式來執行 updateClientComposingBuffer()。
|
// 再以回呼組字狀態的方式來執行 updateClientdisplayedText()。
|
||||||
let inputting = buildInputtingState
|
let inputting = buildInputtingState
|
||||||
stateCallback(inputting)
|
stateCallback(inputting)
|
||||||
|
|
||||||
/// 逐字選字模式的處理。
|
/// 逐字選字模式的處理。
|
||||||
if mgrPrefs.useSCPCTypingMode {
|
if mgrPrefs.useSCPCTypingMode {
|
||||||
let choosingCandidates: InputState.ChoosingCandidate = buildCandidate(
|
let candidateState: IMEState = buildCandidate(
|
||||||
state: inputting,
|
state: inputting,
|
||||||
isTypingVertical: input.isTypingVertical
|
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 reading: String = firstCandidate.0
|
||||||
let text: String = firstCandidate.1
|
let text: String = firstCandidate.1
|
||||||
stateCallback(InputState.Committing(textToCommit: text))
|
stateCallback(IMEState.ofCommitting(textToCommit: text))
|
||||||
|
|
||||||
if !mgrPrefs.associatedPhrasesEnabled {
|
if !mgrPrefs.associatedPhrasesEnabled {
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
} else {
|
} else {
|
||||||
if let associatedPhrases =
|
let associatedPhrases =
|
||||||
buildAssociatePhraseState(
|
buildAssociatePhraseState(
|
||||||
withPair: .init(key: reading, value: text),
|
withPair: .init(key: reading, value: text)
|
||||||
isTypingVertical: input.isTypingVertical
|
)
|
||||||
), !associatedPhrases.candidates.isEmpty
|
stateCallback(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases)
|
||||||
{
|
|
||||||
stateCallback(associatedPhrases)
|
|
||||||
} else {
|
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stateCallback(choosingCandidates)
|
stateCallback(candidateState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。
|
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。
|
||||||
|
@ -157,7 +151,7 @@ extension KeyHandler {
|
||||||
|
|
||||||
/// 是說此時注拼槽並非為空、卻還沒組音。這種情況下只可能是「注拼槽內只有聲調」。
|
/// 是說此時注拼槽並非為空、卻還沒組音。這種情況下只可能是「注拼槽內只有聲調」。
|
||||||
if keyConsumedByReading {
|
if keyConsumedByReading {
|
||||||
// 以回呼組字狀態的方式來執行 updateClientComposingBuffer()。
|
// 以回呼組字狀態的方式來執行 updateClientdisplayedText()。
|
||||||
stateCallback(buildInputtingState)
|
stateCallback(buildInputtingState)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ extension KeyHandler {
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handle(
|
func handle(
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
// 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。
|
// 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。
|
||||||
|
@ -38,8 +38,8 @@ extension KeyHandler {
|
||||||
// 提前過濾掉一些不合規的按鍵訊號輸入,免得相關按鍵訊號被送給 Megrez 引發輸入法崩潰。
|
// 提前過濾掉一些不合規的按鍵訊號輸入,免得相關按鍵訊號被送給 Megrez 引發輸入法崩潰。
|
||||||
if input.isInvalid {
|
if input.isInvalid {
|
||||||
// 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。
|
// 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。
|
||||||
// 因為「.EmptyIgnoringPreviousState」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。
|
// 因為「.Abortion」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。
|
||||||
if state is InputState.Empty || state is InputState.Deactivated {
|
if state.type == .ofEmpty || state.type == .ofDeactivated {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.")
|
IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.")
|
||||||
|
@ -51,7 +51,7 @@ extension KeyHandler {
|
||||||
// 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。
|
// 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。
|
||||||
let isFunctionKey: Bool =
|
let isFunctionKey: Bool =
|
||||||
input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ extension KeyHandler {
|
||||||
// 略過對 BackSpace 的處理。
|
// 略過對 BackSpace 的處理。
|
||||||
} else if input.isCapsLockOn || input.isASCIIModeInput {
|
} else if input.isCapsLockOn || input.isASCIIModeInput {
|
||||||
// 但願能夠處理這種情況下所有可能的按鍵組合。
|
// 但願能夠處理這種情況下所有可能的按鍵組合。
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
|
|
||||||
// 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。
|
// 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。
|
||||||
if input.isUpperCaseASCIILetterKey {
|
if input.isUpperCaseASCIILetterKey {
|
||||||
|
@ -82,8 +82,8 @@ extension KeyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 將整個組字區的內容遞交給客體應用。
|
// 將整個組字區的內容遞交給客體應用。
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.lowercased()))
|
stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -94,19 +94,19 @@ extension KeyHandler {
|
||||||
// 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。
|
// 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。
|
||||||
// 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。
|
// 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。
|
||||||
if input.isNumericPadKey {
|
if input.isNumericPadKey {
|
||||||
if !(state is InputState.ChoosingCandidate || state is InputState.AssociatedPhrases
|
if !(state.type == .ofCandidates || state.type == .ofAssociates
|
||||||
|| state is InputState.SymbolTable)
|
|| state.type == .ofSymbolTable)
|
||||||
{
|
{
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.lowercased()))
|
stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: 處理候選字詞 (Handle Candidates)
|
// MARK: 處理候選字詞 (Handle Candidates)
|
||||||
|
|
||||||
if state is InputState.ChoosingCandidate {
|
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
|
||||||
return handleCandidate(
|
return handleCandidate(
|
||||||
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
)
|
)
|
||||||
|
@ -114,26 +114,26 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: 處理聯想詞 (Handle Associated Phrases)
|
// MARK: 處理聯想詞 (Handle Associated Phrases)
|
||||||
|
|
||||||
if state is InputState.AssociatedPhrases {
|
if state.type == .ofAssociates {
|
||||||
if handleCandidate(
|
if handleCandidate(
|
||||||
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking)
|
// MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking)
|
||||||
|
|
||||||
if let marking = state as? InputState.Marking {
|
if state.type == .ofMarking {
|
||||||
if handleMarkingState(
|
if handleMarkingState(
|
||||||
marking, input: input, stateCallback: stateCallback,
|
state, input: input, stateCallback: stateCallback,
|
||||||
errorCallback: errorCallback
|
errorCallback: errorCallback
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
state = marking.convertedToInputting
|
state = state.convertedToInputting
|
||||||
stateCallback(state)
|
stateCallback(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.)
|
// 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.isCursorClockLeft || input.isCursorClockRight || input.isSpace
|
||||||
|| input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior)
|
|| input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior)
|
||||||
{
|
{
|
||||||
|
@ -155,12 +155,12 @@ extension KeyHandler {
|
||||||
/// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話………
|
/// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話………
|
||||||
if !mgrPrefs.chooseCandidateUsingSpace {
|
if !mgrPrefs.chooseCandidateUsingSpace {
|
||||||
if compositor.cursor >= compositor.length {
|
if compositor.cursor >= compositor.length {
|
||||||
let composingBuffer = currentState.composingBuffer
|
let displayedText = state.displayedText
|
||||||
if !composingBuffer.isEmpty {
|
if !displayedText.isEmpty {
|
||||||
stateCallback(InputState.Committing(textToCommit: composingBuffer))
|
stateCallback(IMEState.ofCommitting(textToCommit: displayedText))
|
||||||
}
|
}
|
||||||
stateCallback(InputState.Committing(textToCommit: " "))
|
stateCallback(IMEState.ofCommitting(textToCommit: " "))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
} else if currentLM.hasUnigramsFor(key: " ") {
|
} else if currentLM.hasUnigramsFor(key: " ") {
|
||||||
compositor.insertKey(" ")
|
compositor.insertKey(" ")
|
||||||
walk()
|
walk()
|
||||||
|
@ -175,7 +175,7 @@ extension KeyHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stateCallback(buildCandidate(state: currentState, isTypingVertical: input.isTypingVertical))
|
stateCallback(buildCandidate(state: state))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ extension KeyHandler {
|
||||||
// MARK: Clock-Left & Clock-Right
|
// MARK: Clock-Left & Clock-Right
|
||||||
|
|
||||||
if input.isCursorClockLeft || input.isCursorClockRight {
|
if input.isCursorClockLeft || input.isCursorClockRight {
|
||||||
if input.isOptionHold, state is InputState.Inputting {
|
if input.isOptionHold, state.type == .ofInputting {
|
||||||
if input.isCursorClockRight {
|
if input.isCursorClockRight {
|
||||||
return handleInlineCandidateRotation(
|
return handleInlineCandidateRotation(
|
||||||
state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback
|
state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
|
@ -277,7 +277,7 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: Punctuation list
|
// MARK: Punctuation list
|
||||||
|
|
||||||
if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold {
|
if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold, state.type != .ofDeactivated {
|
||||||
if input.isOptionHold {
|
if input.isOptionHold {
|
||||||
if currentLM.hasUnigramsFor(key: "_punctuation_list") {
|
if currentLM.hasUnigramsFor(key: "_punctuation_list") {
|
||||||
if composer.isEmpty {
|
if composer.isEmpty {
|
||||||
|
@ -285,7 +285,7 @@ extension KeyHandler {
|
||||||
walk()
|
walk()
|
||||||
let inputting = buildInputtingState
|
let inputting = buildInputtingState
|
||||||
stateCallback(inputting)
|
stateCallback(inputting)
|
||||||
stateCallback(buildCandidate(state: inputting, isTypingVertical: input.isTypingVertical))
|
stateCallback(buildCandidate(state: inputting))
|
||||||
} else { // 不要在注音沒敲完整的情況下叫出統合符號選單。
|
} else { // 不要在注音沒敲完整的情況下叫出統合符號選單。
|
||||||
IME.prtDebugIntel("17446655")
|
IME.prtDebugIntel("17446655")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -297,14 +297,14 @@ extension KeyHandler {
|
||||||
// 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。
|
// 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。
|
||||||
// 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。
|
// 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。
|
||||||
_ = handleEnter(state: state, stateCallback: stateCallback)
|
_ = handleEnter(state: state, stateCallback: stateCallback)
|
||||||
stateCallback(InputState.SymbolTable(node: SymbolNode.root, isTypingVertical: input.isTypingVertical))
|
stateCallback(IMEState.ofSymbolTable(node: SymbolNode.root))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: 全形/半形阿拉伯數字輸入 (FW / HW Arabic Numbers Input)
|
// 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 {
|
if input.isMainAreaNumKey, input.isShiftHold, input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
||||||
// NOTE: 將來棄用 macOS 10.11 El Capitan 支援的時候,把這裡由 CFStringTransform 改為 StringTransform:
|
// NOTE: 將來棄用 macOS 10.11 El Capitan 支援的時候,把這裡由 CFStringTransform 改為 StringTransform:
|
||||||
// https://developer.apple.com/documentation/foundation/stringtransform
|
// https://developer.apple.com/documentation/foundation/stringtransform
|
||||||
|
@ -312,9 +312,9 @@ extension KeyHandler {
|
||||||
let string = NSMutableString(string: stringRAW)
|
let string = NSMutableString(string: stringRAW)
|
||||||
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true)
|
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true)
|
||||||
stateCallback(
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,10 +357,10 @@ extension KeyHandler {
|
||||||
// MARK: 全形/半形空白 (Full-Width / Half-Width Space)
|
// MARK: 全形/半形空白 (Full-Width / Half-Width Space)
|
||||||
|
|
||||||
/// 該功能僅可在當前組字區沒有任何內容的時候使用。
|
/// 該功能僅可在當前組字區沒有任何內容的時候使用。
|
||||||
if state is InputState.Empty {
|
if state.type == .ofEmpty {
|
||||||
if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
||||||
stateCallback(InputState.Committing(textToCommit: input.isShiftHold ? " " : " "))
|
stateCallback(IMEState.ofCommitting(textToCommit: input.isShiftHold ? " " : " "))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,14 +371,14 @@ extension KeyHandler {
|
||||||
if input.isShiftHold { // 這裡先不要判斷 isOptionHold。
|
if input.isShiftHold { // 這裡先不要判斷 isOptionHold。
|
||||||
switch mgrPrefs.upperCaseLetterKeyBehavior {
|
switch mgrPrefs.upperCaseLetterKeyBehavior {
|
||||||
case 1:
|
case 1:
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.lowercased()))
|
stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
case 2:
|
case 2:
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.uppercased()))
|
stateCallback(IMEState.ofCommitting(textToCommit: inputText.uppercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
default: // 包括 case 0,直接塞給組字區。
|
default: // 包括 case 0,直接塞給組字區。
|
||||||
let letter = "_letter_\(inputText)"
|
let letter = "_letter_\(inputText)"
|
||||||
|
@ -401,7 +401,7 @@ extension KeyHandler {
|
||||||
/// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。
|
/// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。
|
||||||
/// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。
|
/// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。
|
||||||
/// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。
|
/// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。
|
||||||
if (state is InputState.NotEmpty) || !composer.isEmpty {
|
if state.hasComposition || !composer.isEmpty {
|
||||||
IME.prtDebugIntel(
|
IME.prtDebugIntel(
|
||||||
"Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
|
"Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
|
||||||
IME.prtDebugIntel("A9BFF20E")
|
IME.prtDebugIntel("A9BFF20E")
|
||||||
|
|
|
@ -17,101 +17,73 @@ import Foundation
|
||||||
extension KeyHandler {
|
extension KeyHandler {
|
||||||
// MARK: - 構築狀態(State Building)
|
// MARK: - 構築狀態(State Building)
|
||||||
|
|
||||||
/// 生成「正在輸入」狀態。
|
/// 生成「正在輸入」狀態。相關的內容會被拿給狀態機械用來處理在電腦螢幕上顯示的內容。
|
||||||
var buildInputtingState: InputState.Inputting {
|
var buildInputtingState: IMEState {
|
||||||
/// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容
|
/// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容
|
||||||
/// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。
|
/// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。
|
||||||
var tooltipParameterRef: [String] = ["", ""]
|
var displayTextSegments: [String] = compositor.walkedNodes.values.map {
|
||||||
let nodeValuesArray: [String] = compositor.walkedNodes.values.map {
|
|
||||||
guard let delegate = delegate, delegate.isVerticalTyping else { return $0 }
|
guard let delegate = delegate, delegate.isVerticalTyping else { return $0 }
|
||||||
guard mgrPrefs.hardenVerticalPunctuations else { return $0 }
|
guard mgrPrefs.hardenVerticalPunctuations else { return $0 }
|
||||||
var neta = $0
|
var neta = $0
|
||||||
ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping)
|
ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping)
|
||||||
return neta
|
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 composedStringCursorIndex = 0
|
||||||
var readingCursorIndex = 0
|
var readingCursorIndex = 0
|
||||||
/// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度,
|
|
||||||
/// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。
|
|
||||||
/// 這樣就可以免除不必要的類型轉換。
|
|
||||||
for theNode in compositor.walkedNodes {
|
for theNode in compositor.walkedNodes {
|
||||||
let strNodeValue = theNode.value
|
let strNodeValue = theNode.value
|
||||||
let arrSplit: [String] = Array(strNodeValue).charComponents
|
|
||||||
let codepointCount = arrSplit.count
|
|
||||||
/// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。
|
/// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。
|
||||||
/// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來
|
/// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來
|
||||||
/// 累加、以此為依據,來校正「可見游標位置」。
|
/// 累加、以此為依據,來校正「可見游標位置」。
|
||||||
let spanningLength: Int = theNode.spanLength
|
let spanningLength: Int = theNode.keyArray.count
|
||||||
if readingCursorIndex + spanningLength <= compositor.cursor {
|
if readingCursorIndex + spanningLength <= rawCursor {
|
||||||
composedStringCursorIndex += strNodeValue.utf16.count
|
composedStringCursorIndex += strNodeValue.count
|
||||||
readingCursorIndex += spanningLength
|
readingCursorIndex += spanningLength
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if codepointCount == spanningLength {
|
if !theNode.isReadingMismatched {
|
||||||
for i in 0..<codepointCount {
|
for _ in 0..<strNodeValue.count {
|
||||||
guard readingCursorIndex < compositor.cursor else { continue }
|
guard readingCursorIndex < rawCursor else { continue }
|
||||||
composedStringCursorIndex += arrSplit[i].utf16.count
|
composedStringCursorIndex += 1
|
||||||
readingCursorIndex += 1
|
readingCursorIndex += 1
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard readingCursorIndex < compositor.cursor else { continue }
|
guard readingCursorIndex < rawCursor else { continue }
|
||||||
composedStringCursorIndex += strNodeValue.utf16.count
|
composedStringCursorIndex += strNodeValue.count
|
||||||
readingCursorIndex += spanningLength
|
readingCursorIndex += spanningLength
|
||||||
readingCursorIndex = min(readingCursorIndex, compositor.cursor)
|
readingCursorIndex = min(readingCursorIndex, rawCursor)
|
||||||
/// 接下來再處理這麼一種情況:
|
|
||||||
/// 某些錨點內的當前候選字詞長度與讀音長度不相等。
|
|
||||||
/// 但此時游標還是按照每個讀音單位來移動的,
|
|
||||||
/// 所以需要上下文工具提示來顯示游標的相對位置。
|
|
||||||
/// 這裡先計算一下要用在工具提示當中的顯示參數的內容。
|
|
||||||
switch compositor.cursor {
|
|
||||||
case compositor.keys.count...:
|
|
||||||
// 這裡的 compositor.cursor 數值不可能大於 readings.count,因為會被 Megrez 自動糾正。
|
|
||||||
tooltipParameterRef[0] = compositor.keys[compositor.cursor - 1]
|
|
||||||
case 0:
|
|
||||||
tooltipParameterRef[1] = compositor.keys[compositor.cursor]
|
|
||||||
default:
|
|
||||||
tooltipParameterRef[0] = compositor.keys[compositor.cursor - 1]
|
|
||||||
tooltipParameterRef[1] = compositor.keys[compositor.cursor]
|
|
||||||
}
|
}
|
||||||
}
|
return composedStringCursorIndex
|
||||||
|
|
||||||
isCursorCuttingChar = !tooltipParameterRef[0].isEmpty || !tooltipParameterRef[1].isEmpty
|
|
||||||
|
|
||||||
/// 再接下來,藉由已經計算成功的「可見游標位置」,咱們計算一下在這個游標之前與之後的
|
|
||||||
/// 組字區內容,以便之後在這之間插入正在輸入的漢字讀音(藉由鐵恨 composer 注拼槽取得)。
|
|
||||||
var arrHead = [String.UTF16View.Element]()
|
|
||||||
var arrTail = [String.UTF16View.Element]()
|
|
||||||
|
|
||||||
for (i, n) in nodeValuesArray.joined().utf16.enumerated() {
|
|
||||||
if i < composedStringCursorIndex {
|
|
||||||
arrHead.append(n)
|
|
||||||
} else {
|
|
||||||
arrTail.append(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 現在呢,咱們拿到了游標前後的 stringview 資料,準備著手生成要在組字區內顯示用的內容。
|
|
||||||
/// 在這對前後資料當中插入目前正在輸入的讀音資料即可。
|
|
||||||
let head = String(utf16CodeUnits: arrHead, count: arrHead.count)
|
|
||||||
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer)
|
|
||||||
let tail = String(utf16CodeUnits: arrTail, count: arrTail.count)
|
|
||||||
let composedText = head + reading + tail
|
|
||||||
let cursorIndex = composedStringCursorIndex + reading.utf16.count
|
|
||||||
|
|
||||||
// 防止組字區內出現不可列印的字元。
|
|
||||||
var cleanedComposition = ""
|
|
||||||
for theChar in composedText {
|
|
||||||
guard let charCode = theChar.utf16.first else { continue }
|
|
||||||
if !(theChar.isASCII && !(charCode.isPrintable)) {
|
|
||||||
cleanedComposition += String(theChar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。
|
|
||||||
return InputState.Inputting(
|
|
||||||
composingBuffer: cleanedComposition, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 用以生成候選詞陣列及狀態
|
// MARK: - 用以生成候選詞陣列及狀態
|
||||||
|
@ -122,15 +94,13 @@ extension KeyHandler {
|
||||||
/// - isTypingVertical: 是否縱排輸入?
|
/// - isTypingVertical: 是否縱排輸入?
|
||||||
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
||||||
func buildCandidate(
|
func buildCandidate(
|
||||||
state currentState: InputState.NotEmpty,
|
state currentState: IMEStateProtocol,
|
||||||
isTypingVertical: Bool = false
|
isTypingVertical _: Bool = false
|
||||||
) -> InputState.ChoosingCandidate {
|
) -> IMEState {
|
||||||
InputState.ChoosingCandidate(
|
IMEState.ofCandidates(
|
||||||
composingBuffer: currentState.composingBuffer,
|
|
||||||
cursorIndex: currentState.cursorIndex,
|
|
||||||
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
|
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
|
||||||
isTypingVertical: isTypingVertical,
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
nodeValuesArray: compositor.walkedNodes.values
|
cursor: currentState.data.cursor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,16 +117,13 @@ extension KeyHandler {
|
||||||
/// 是否為空:如果陣列為空的話,直接回呼一個空狀態。
|
/// 是否為空:如果陣列為空的話,直接回呼一個空狀態。
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - key: 給定的索引鍵(也就是給定的聯想詞的開頭字)。
|
/// - key: 給定的索引鍵(也就是給定的聯想詞的開頭字)。
|
||||||
/// - isTypingVertical: 是否縱排輸入?
|
|
||||||
/// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。
|
/// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。
|
||||||
func buildAssociatePhraseState(
|
func buildAssociatePhraseState(
|
||||||
withPair pair: Megrez.Compositor.Candidate,
|
withPair pair: Megrez.Compositor.KeyValuePaired
|
||||||
isTypingVertical: Bool
|
) -> IMEState {
|
||||||
) -> InputState.AssociatedPhrases! {
|
|
||||||
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
||||||
InputState.AssociatedPhrases(
|
IMEState.ofAssociates(
|
||||||
candidates: buildAssociatePhraseArray(withPair: pair), isTypingVertical: isTypingVertical
|
candidates: buildAssociatePhraseArray(withPair: pair))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 用以處理就地新增自訂語彙時的行為
|
// MARK: - 用以處理就地新增自訂語彙時的行為
|
||||||
|
@ -169,9 +136,9 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleMarkingState(
|
func handleMarkingState(
|
||||||
_ state: InputState.Marking,
|
_ state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if input.isEsc {
|
if input.isEsc {
|
||||||
|
@ -190,7 +157,7 @@ extension KeyHandler {
|
||||||
if input.isEnter {
|
if input.isEnter {
|
||||||
if let keyHandlerDelegate = delegate {
|
if let keyHandlerDelegate = delegate {
|
||||||
// 先判斷是否是在摁了降權組合鍵的時候目標不在庫。
|
// 先判斷是否是在摁了降權組合鍵的時候目標不在庫。
|
||||||
if input.isShiftHold, input.isCommandHold, !state.validToFilter {
|
if input.isShiftHold, input.isCommandHold, !state.isFilterable {
|
||||||
IME.prtDebugIntel("2EAC1F7A")
|
IME.prtDebugIntel("2EAC1F7A")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
return true
|
return true
|
||||||
|
@ -207,7 +174,7 @@ extension KeyHandler {
|
||||||
// BackSpace & Delete
|
// BackSpace & Delete
|
||||||
if input.isBackSpace || input.isDelete {
|
if input.isBackSpace || input.isDelete {
|
||||||
if let keyHandlerDelegate = delegate {
|
if let keyHandlerDelegate = delegate {
|
||||||
if !state.validToFilter {
|
if !state.isFilterable {
|
||||||
IME.prtDebugIntel("1F88B191")
|
IME.prtDebugIntel("1F88B191")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
return true
|
return true
|
||||||
|
@ -224,18 +191,19 @@ extension KeyHandler {
|
||||||
|
|
||||||
// Shift + Left
|
// Shift + Left
|
||||||
if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold {
|
if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold {
|
||||||
var index = state.markerIndex
|
if compositor.marker > 0 {
|
||||||
if index > 0 {
|
compositor.marker -= 1
|
||||||
index = state.composingBuffer.utf16PreviousPosition(for: index)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
|
||||||
composingBuffer: state.composingBuffer,
|
}
|
||||||
cursorIndex: state.cursorIndex,
|
var marking = IMEState.ofMarking(
|
||||||
markerIndex: index,
|
displayTextSegments: state.data.displayTextSegments,
|
||||||
readings: state.readings,
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
nodeValuesArray: compositor.walkedNodes.values
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipForInputting = state.tooltipForInputting
|
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
|
||||||
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("1149908D")
|
IME.prtDebugIntel("1149908D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -246,18 +214,19 @@ extension KeyHandler {
|
||||||
|
|
||||||
// Shift + Right
|
// Shift + Right
|
||||||
if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold {
|
if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold {
|
||||||
var index = state.markerIndex
|
if compositor.marker < compositor.width {
|
||||||
if index < (state.composingBuffer.utf16.count) {
|
compositor.marker += 1
|
||||||
index = state.composingBuffer.utf16NextPosition(for: index)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .front, isMarker: true)
|
||||||
composingBuffer: state.composingBuffer,
|
}
|
||||||
cursorIndex: state.cursorIndex,
|
var marking = IMEState.ofMarking(
|
||||||
markerIndex: index,
|
displayTextSegments: state.data.displayTextSegments,
|
||||||
readings: state.readings,
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
nodeValuesArray: compositor.walkedNodes.values
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipForInputting = state.tooltipForInputting
|
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
|
||||||
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("9B51408D")
|
IME.prtDebugIntel("9B51408D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -280,9 +249,9 @@ extension KeyHandler {
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handlePunctuation(
|
func handlePunctuation(
|
||||||
_ customPunctuation: String,
|
_ customPunctuation: String,
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
usingVerticalTyping isTypingVertical: Bool,
|
usingVerticalTyping isTypingVertical: Bool,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if !currentLM.hasUnigramsFor(key: customPunctuation) {
|
if !currentLM.hasUnigramsFor(key: customPunctuation) {
|
||||||
|
@ -312,8 +281,8 @@ extension KeyHandler {
|
||||||
if candidateState.candidates.count == 1 {
|
if candidateState.candidates.count == 1 {
|
||||||
clear() // 這句不要砍,因為下文可能會回呼 candidateState。
|
clear() // 這句不要砍,因為下文可能會回呼 candidateState。
|
||||||
if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty {
|
if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty {
|
||||||
stateCallback(InputState.Committing(textToCommit: candidateToCommit.1))
|
stateCallback(IMEState.ofCommitting(textToCommit: candidateToCommit.1))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
} else {
|
} else {
|
||||||
stateCallback(candidateState)
|
stateCallback(candidateState)
|
||||||
}
|
}
|
||||||
|
@ -331,13 +300,13 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleEnter(
|
func handleEnter(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> 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(IMEState.ofCommitting(textToCommit: state.displayedText))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,23 +318,23 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleCtrlCommandEnter(
|
func handleCtrlCommandEnter(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> 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 {
|
if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||||
composingBuffer = Tekkon.restoreToneOneInZhuyinKey(target: composingBuffer) // 恢復陰平標記
|
displayedText = Tekkon.restoreToneOneInZhuyinKey(target: displayedText) // 恢復陰平標記
|
||||||
composingBuffer = Tekkon.cnvPhonaToHanyuPinyin(target: composingBuffer) // 注音轉拼音
|
displayedText = Tekkon.cnvPhonaToHanyuPinyin(target: displayedText) // 注音轉拼音
|
||||||
}
|
}
|
||||||
|
|
||||||
if let delegate = delegate, !delegate.clientBundleIdentifier.contains("vChewingPhraseEditor") {
|
if let delegate = delegate, !delegate.clientBundleIdentifier.contains("vChewingPhraseEditor") {
|
||||||
composingBuffer = composingBuffer.replacingOccurrences(of: "-", with: " ")
|
displayedText = displayedText.replacingOccurrences(of: "-", with: " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCallback(InputState.Committing(textToCommit: composingBuffer))
|
stateCallback(IMEState.ofCommitting(textToCommit: displayedText))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,10 +346,10 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleCtrlOptionCommandEnter(
|
func handleCtrlOptionCommandEnter(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
var composed = ""
|
var composed = ""
|
||||||
|
|
||||||
|
@ -400,8 +369,8 @@ extension KeyHandler {
|
||||||
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCallback(InputState.Committing(textToCommit: composed))
|
stateCallback(IMEState.ofCommitting(textToCommit: composed))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.ofEmpty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,12 +384,12 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleBackSpace(
|
func handleBackSpace(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
||||||
switch mgrPrefs.specifyShiftBackSpaceKeyBehavior {
|
switch mgrPrefs.specifyShiftBackSpaceKeyBehavior {
|
||||||
|
@ -434,15 +403,13 @@ extension KeyHandler {
|
||||||
stateCallback(buildInputtingState)
|
stateCallback(buildInputtingState)
|
||||||
return true
|
return true
|
||||||
case 1:
|
case 1:
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.isShiftHold, input.isOptionHold {
|
if input.isShiftHold, input.isOptionHold {
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,8 +432,7 @@ extension KeyHandler {
|
||||||
switch composer.isEmpty && compositor.isEmpty {
|
switch composer.isEmpty && compositor.isEmpty {
|
||||||
case false: stateCallback(buildInputtingState)
|
case false: stateCallback(buildInputtingState)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -481,16 +447,15 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleDelete(
|
func handleDelete(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if input.isShiftHold {
|
if input.isShiftHold {
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,11 +475,10 @@ extension KeyHandler {
|
||||||
|
|
||||||
let inputting = buildInputtingState
|
let inputting = buildInputtingState
|
||||||
// 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
// 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
||||||
switch inputting.composingBuffer.isEmpty {
|
switch inputting.displayedText.isEmpty {
|
||||||
case false: stateCallback(inputting)
|
case false: stateCallback(inputting)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -528,11 +492,11 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleClockKey(
|
func handleClockKey(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("9B6F908D")
|
IME.prtDebugIntel("9B6F908D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -550,11 +514,11 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleHome(
|
func handleHome(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("ABC44080")
|
IME.prtDebugIntel("ABC44080")
|
||||||
|
@ -584,11 +548,11 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleEnd(
|
func handleEnd(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("9B69908D")
|
IME.prtDebugIntel("9B69908D")
|
||||||
|
@ -617,16 +581,15 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleEsc(
|
func handleEsc(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if mgrPrefs.escToCleanInputBuffer {
|
if mgrPrefs.escToCleanInputBuffer {
|
||||||
/// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。
|
/// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。
|
||||||
/// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。
|
/// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
} else {
|
} else {
|
||||||
if composer.isEmpty { return true }
|
if composer.isEmpty { return true }
|
||||||
/// 如果注拼槽不是空的話,則清空之。
|
/// 如果注拼槽不是空的話,則清空之。
|
||||||
|
@ -634,8 +597,7 @@ extension KeyHandler {
|
||||||
switch compositor.isEmpty {
|
switch compositor.isEmpty {
|
||||||
case false: stateCallback(buildInputtingState)
|
case false: stateCallback(buildInputtingState)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.EmptyIgnoringPreviousState())
|
stateCallback(IMEState.ofAbortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -651,12 +613,12 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleForward(
|
func handleForward(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard let currentState = state as? InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("B3BA5257")
|
IME.prtDebugIntel("B3BA5257")
|
||||||
|
@ -667,16 +629,18 @@ extension KeyHandler {
|
||||||
|
|
||||||
if input.isShiftHold {
|
if input.isShiftHold {
|
||||||
// Shift + Right
|
// Shift + Right
|
||||||
if currentState.cursorIndex < currentState.composingBuffer.utf16.count {
|
if compositor.cursor < compositor.width {
|
||||||
let nextPosition = currentState.composingBuffer.utf16NextPosition(
|
compositor.marker = compositor.cursor + 1
|
||||||
for: currentState.cursorIndex)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking: InputState.Marking! = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .front, isMarker: true)
|
||||||
composingBuffer: currentState.composingBuffer,
|
}
|
||||||
cursorIndex: currentState.cursorIndex,
|
var marking = IMEState.ofMarking(
|
||||||
markerIndex: nextPosition,
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
readings: compositor.keys
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipForInputting = currentState.tooltip
|
marking.data.tooltipBackupForInputting = state.tooltip
|
||||||
stateCallback(marking)
|
stateCallback(marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("BB7F6DB9")
|
IME.prtDebugIntel("BB7F6DB9")
|
||||||
|
@ -684,7 +648,6 @@ extension KeyHandler {
|
||||||
stateCallback(state)
|
stateCallback(state)
|
||||||
}
|
}
|
||||||
} else if input.isOptionHold {
|
} else if input.isOptionHold {
|
||||||
isCursorCuttingChar = false
|
|
||||||
if input.isControlHold {
|
if input.isControlHold {
|
||||||
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
||||||
}
|
}
|
||||||
|
@ -699,12 +662,10 @@ extension KeyHandler {
|
||||||
} else {
|
} else {
|
||||||
if compositor.cursor < compositor.length {
|
if compositor.cursor < compositor.length {
|
||||||
compositor.cursor += 1
|
compositor.cursor += 1
|
||||||
var inputtingState = buildInputtingState
|
if isCursorCuttingChar() {
|
||||||
if isCursorCuttingChar == true {
|
|
||||||
compositor.jumpCursorBySpan(to: .front)
|
compositor.jumpCursorBySpan(to: .front)
|
||||||
inputtingState = buildInputtingState
|
|
||||||
}
|
}
|
||||||
stateCallback(inputtingState)
|
stateCallback(buildInputtingState)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("A96AAD58")
|
IME.prtDebugIntel("A96AAD58")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -725,12 +686,12 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleBackward(
|
func handleBackward(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard let currentState = state as? InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("6ED95318")
|
IME.prtDebugIntel("6ED95318")
|
||||||
|
@ -741,16 +702,18 @@ extension KeyHandler {
|
||||||
|
|
||||||
if input.isShiftHold {
|
if input.isShiftHold {
|
||||||
// Shift + left
|
// Shift + left
|
||||||
if currentState.cursorIndex > 0 {
|
if compositor.cursor > 0 {
|
||||||
let previousPosition = currentState.composingBuffer.utf16PreviousPosition(
|
compositor.marker = compositor.cursor - 1
|
||||||
for: currentState.cursorIndex)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking: InputState.Marking! = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
|
||||||
composingBuffer: currentState.composingBuffer,
|
}
|
||||||
cursorIndex: currentState.cursorIndex,
|
var marking = IMEState.ofMarking(
|
||||||
markerIndex: previousPosition,
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
readings: compositor.keys
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipForInputting = currentState.tooltip
|
marking.data.tooltipBackupForInputting = state.tooltip
|
||||||
stateCallback(marking)
|
stateCallback(marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("D326DEA3")
|
IME.prtDebugIntel("D326DEA3")
|
||||||
|
@ -758,7 +721,6 @@ extension KeyHandler {
|
||||||
stateCallback(state)
|
stateCallback(state)
|
||||||
}
|
}
|
||||||
} else if input.isOptionHold {
|
} else if input.isOptionHold {
|
||||||
isCursorCuttingChar = false
|
|
||||||
if input.isControlHold {
|
if input.isControlHold {
|
||||||
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
||||||
}
|
}
|
||||||
|
@ -773,12 +735,10 @@ extension KeyHandler {
|
||||||
} else {
|
} else {
|
||||||
if compositor.cursor > 0 {
|
if compositor.cursor > 0 {
|
||||||
compositor.cursor -= 1
|
compositor.cursor -= 1
|
||||||
var inputtingState = buildInputtingState
|
if isCursorCuttingChar() {
|
||||||
if isCursorCuttingChar == true {
|
|
||||||
compositor.jumpCursorBySpan(to: .rear)
|
compositor.jumpCursorBySpan(to: .rear)
|
||||||
inputtingState = buildInputtingState
|
|
||||||
}
|
}
|
||||||
stateCallback(inputtingState)
|
stateCallback(buildInputtingState)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("7045E6F3")
|
IME.prtDebugIntel("7045E6F3")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -799,14 +759,14 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleInlineCandidateRotation(
|
func handleInlineCandidateRotation(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
reverseModifier: Bool,
|
reverseModifier: Bool,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
|
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
|
||||||
guard state is InputState.Inputting else {
|
guard state.type == .ofInputting else {
|
||||||
guard state is InputState.Empty else {
|
guard state.type == .ofEmpty else {
|
||||||
IME.prtDebugIntel("6044F081")
|
IME.prtDebugIntel("6044F081")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -8,7 +8,36 @@
|
||||||
|
|
||||||
import Cocoa
|
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 {
|
extension NSEvent: InputSignalProtocol {
|
||||||
public var isASCIIModeInput: Bool { ctlInputMethod.isASCIIModeSituation }
|
public var isASCIIModeInput: Bool { ctlInputMethod.isASCIIModeSituation }
|
||||||
|
|
|
@ -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[..<self.index(startIndex, offsetBy: fixedIndex)].utf16.count
|
|
||||||
}
|
|
||||||
|
|
||||||
public func utf16PreviousPosition(for index: Int) -> Int {
|
|
||||||
let fixedIndex = max(charIndexLiteral(from: index) - 1, 0)
|
|
||||||
return self[..<self.index(startIndex, offsetBy: fixedIndex)].utf16.count
|
|
||||||
}
|
|
||||||
|
|
||||||
internal func utf16SubString(with r: Range<Int>) -> 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) } }
|
|
||||||
}
|
|
|
@ -31,11 +31,9 @@ extension ctlInputMethod {
|
||||||
if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) {
|
if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) {
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
||||||
+ {
|
+ (toggleASCIIMode()
|
||||||
toggleASCIIMode()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if shouldUseHandle {
|
if shouldUseHandle {
|
||||||
|
|
|
@ -32,16 +32,20 @@ class ctlInputMethod: IMKInputController {
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
/// 按鍵調度模組的副本。
|
|
||||||
var keyHandler: KeyHandler = .init()
|
|
||||||
/// 用以記錄當前輸入法狀態的變數。
|
|
||||||
var state: InputStateProtocol = InputState.Empty()
|
|
||||||
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。
|
|
||||||
var isASCIIMode: Bool = false
|
|
||||||
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。
|
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。
|
||||||
static var isASCIIModeSituation: Bool = false
|
static var isASCIIModeSituation: Bool = false
|
||||||
/// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。
|
/// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。
|
||||||
static var isVerticalTypingSituation: Bool = false
|
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 副本的英數輸入模式開關。
|
/// 切換當前 ctlInputMethod 副本的英數輸入模式開關。
|
||||||
func toggleASCIIMode() -> Bool {
|
func toggleASCIIMode() -> Bool {
|
||||||
|
@ -65,15 +69,15 @@ class ctlInputMethod: IMKInputController {
|
||||||
/// 重設按鍵調度模組,會將當前尚未遞交的內容遞交出去。
|
/// 重設按鍵調度模組,會將當前尚未遞交的內容遞交出去。
|
||||||
func resetKeyHandler() {
|
func resetKeyHandler() {
|
||||||
// 過濾掉尚未完成拼寫的注音。
|
// 過濾掉尚未完成拼寫的注音。
|
||||||
if state is InputState.Inputting, mgrPrefs.trimUnfinishedReadingsOnCommit {
|
if state.type == .ofInputting, mgrPrefs.trimUnfinishedReadingsOnCommit {
|
||||||
keyHandler.composer.clear()
|
keyHandler.composer.clear()
|
||||||
handle(state: keyHandler.buildInputtingState)
|
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 方法
|
// MARK: - IMKInputController 方法
|
||||||
|
@ -115,11 +119,9 @@ class ctlInputMethod: IMKInputController {
|
||||||
} else {
|
} else {
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
||||||
+ {
|
+ (isASCIIMode
|
||||||
isASCIIMode
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,7 +131,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
||||||
// 強制重設當前鍵盤佈局、使其與偏好設定同步。
|
// 強制重設當前鍵盤佈局、使其與偏好設定同步。
|
||||||
setKeyLayout()
|
setKeyLayout()
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.ofEmpty())
|
||||||
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
||||||
(NSApp.delegate as? AppDelegate)?.checkForUpdate()
|
(NSApp.delegate as? AppDelegate)?.checkForUpdate()
|
||||||
}
|
}
|
||||||
|
@ -139,7 +141,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
override func deactivateServer(_ sender: Any!) {
|
override func deactivateServer(_ sender: Any!) {
|
||||||
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
||||||
resetKeyHandler() // 這條會自動搞定 Empty 狀態。
|
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 {
|
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
||||||
// 強制重設當前鍵盤佈局、使其與偏好設定同步。這裡的這一步也不能省略。
|
// 強制重設當前鍵盤佈局、使其與偏好設定同步。這裡的這一步也不能省略。
|
||||||
setKeyLayout()
|
setKeyLayout()
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.ofEmpty())
|
||||||
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,20 +226,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
// Shift+Enter 是個特殊情形,不提前攔截處理的話、會有垃圾參數傳給 delegate 的 keyHandler 從而崩潰。
|
// Shift+Enter 是個特殊情形,不提前攔截處理的話、會有垃圾參數傳給 delegate 的 keyHandler 從而崩潰。
|
||||||
// 所以這裡直接將 Shift Flags 清空。
|
// 所以這裡直接將 Shift Flags 清空。
|
||||||
if event.isShiftHold, event.isEnter {
|
if event.isShiftHold, event.isEnter {
|
||||||
guard
|
guard let newEvent = event.reinitiate(modifierFlags: []) else {
|
||||||
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 {
|
|
||||||
NSSound.beep()
|
NSSound.beep()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -247,20 +236,8 @@ class ctlInputMethod: IMKInputController {
|
||||||
|
|
||||||
// 聯想詞選字。
|
// 聯想詞選字。
|
||||||
if let newChar = ctlCandidateIMK.defaultIMKSelectionKey[event.keyCode], event.isShiftHold,
|
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)
|
ctlCandidateCurrent.handleKeyboardEvent(newEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,8 +265,8 @@ class ctlInputMethod: IMKInputController {
|
||||||
/// - Returns: 字串內容,或者 nil。
|
/// - Returns: 字串內容,或者 nil。
|
||||||
override func composedString(_ sender: Any!) -> Any! {
|
override func composedString(_ sender: Any!) -> Any! {
|
||||||
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
||||||
guard let state = state as? InputState.NotEmpty else { return "" }
|
guard state.hasComposition else { return "" }
|
||||||
return state.committingBufferConverted
|
return state.displayedText
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 輸入法要被換掉或關掉的時候,要做的事情。
|
/// 輸入法要被換掉或關掉的時候,要做的事情。
|
||||||
|
@ -309,7 +286,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
||||||
var arrResult = [String]()
|
var arrResult = [String]()
|
||||||
|
|
||||||
// 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。
|
// 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。
|
||||||
func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") {
|
func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") {
|
||||||
for theCandidate in candidates {
|
for theCandidate in candidates {
|
||||||
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
|
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: "⇧")
|
handleCandidatesPrepared(state.candidates, prefix: "⇧")
|
||||||
} else if let state = state as? InputState.SymbolTable {
|
} else if state.type == .ofSymbolTable {
|
||||||
// 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。
|
// 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。
|
||||||
arrResult = state.candidates.map(\.1)
|
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() }
|
guard !state.candidates.isEmpty else { return .init() }
|
||||||
if state.candidates[0].0.contains("_punctuation") {
|
if state.candidates[0].0.contains("_punctuation") {
|
||||||
arrResult = state.candidates.map(\.1) // 標點符號選單處理。
|
arrResult = state.candidates.map(\.1) // 標點符號選單處理。
|
||||||
|
@ -363,17 +340,16 @@ class ctlInputMethod: IMKInputController {
|
||||||
/// - Parameter candidateString: 已經確認的候選字詞內容。
|
/// - Parameter candidateString: 已經確認的候選字詞內容。
|
||||||
override open func candidateSelected(_ candidateString: NSAttributedString!) {
|
override open func candidateSelected(_ candidateString: NSAttributedString!) {
|
||||||
let candidateString: NSAttributedString = candidateString ?? .init(string: "")
|
let candidateString: NSAttributedString = candidateString ?? .init(string: "")
|
||||||
if state is InputState.AssociatedPhrases {
|
if state.type == .ofAssociates {
|
||||||
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||||
handle(state: InputState.EmptyIgnoringPreviousState())
|
handle(state: IMEState.ofAbortion())
|
||||||
handle(state: InputState.Empty())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexDeducted = 0
|
var indexDeducted = 0
|
||||||
|
|
||||||
// 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。
|
// 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。
|
||||||
func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") {
|
func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") {
|
||||||
for (i, neta) in candidates.enumerated() {
|
for (i, neta) in candidates.enumerated() {
|
||||||
let theConverted = IME.kanjiConversionIfRequired(neta.1)
|
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: "⇧")
|
handleCandidatesSelected(state.candidates, prefix: "⇧")
|
||||||
} else if let state = state as? InputState.SymbolTable {
|
} else if state.type == .ofSymbolTable {
|
||||||
handleSymbolCandidatesSelected(state.candidates)
|
handleSymbolCandidatesSelected(state.candidates)
|
||||||
} else if let state = state as? InputState.ChoosingCandidate {
|
} else if state.type == .ofCandidates {
|
||||||
guard !state.candidates.isEmpty else { return }
|
guard !state.candidates.isEmpty else { return }
|
||||||
if state.candidates[0].0.contains("_punctuation") {
|
if state.candidates[0].0.contains("_punctuation") {
|
||||||
handleSymbolCandidatesSelected(state.candidates) // 標點符號選單處理。
|
handleSymbolCandidatesSelected(state.candidates) // 標點符號選單處理。
|
||||||
|
|
|
@ -37,21 +37,20 @@ extension ctlInputMethod: KeyHandlerDelegate {
|
||||||
ctlCandidate(controller, didSelectCandidateAtIndex: index)
|
ctlCandidate(controller, didSelectCandidateAtIndex: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool)
|
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
|
||||||
-> Bool
|
-> Bool
|
||||||
{
|
{
|
||||||
guard let state = state as? InputState.Marking else { return false }
|
guard state.type == .ofMarking else { return false }
|
||||||
if state.bufferReadingCountMisMatch { return false }
|
|
||||||
let refInputModeReversed: InputMode =
|
let refInputModeReversed: InputMode =
|
||||||
(keyHandler.inputMode == InputMode.imeModeCHT)
|
(keyHandler.inputMode == InputMode.imeModeCHT)
|
||||||
? InputMode.imeModeCHS : InputMode.imeModeCHT
|
? InputMode.imeModeCHS : InputMode.imeModeCHT
|
||||||
if !mgrLangModel.writeUserPhrase(
|
if !mgrLangModel.writeUserPhrase(
|
||||||
state.userPhrase, inputMode: keyHandler.inputMode,
|
state.data.userPhrase, inputMode: keyHandler.inputMode,
|
||||||
areWeDuplicating: state.chkIfUserPhraseExists,
|
areWeDuplicating: state.data.chkIfUserPhraseExists,
|
||||||
areWeDeleting: addToFilter
|
areWeDeleting: addToFilter
|
||||||
)
|
)
|
||||||
|| !mgrLangModel.writeUserPhrase(
|
|| !mgrLangModel.writeUserPhrase(
|
||||||
state.userPhraseConverted, inputMode: refInputModeReversed,
|
state.data.userPhraseConverted, inputMode: refInputModeReversed,
|
||||||
areWeDuplicating: false,
|
areWeDuplicating: false,
|
||||||
areWeDeleting: addToFilter
|
areWeDeleting: addToFilter
|
||||||
)
|
)
|
||||||
|
@ -65,7 +64,7 @@ extension ctlInputMethod: KeyHandlerDelegate {
|
||||||
// MARK: - Candidate Controller Delegate
|
// MARK: - Candidate Controller Delegate
|
||||||
|
|
||||||
extension ctlInputMethod: ctlCandidateDelegate {
|
extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
var isAssociatedPhrasesState: Bool { state is InputState.AssociatedPhrases }
|
var isAssociatedPhrasesState: Bool { state.type == .ofAssociates }
|
||||||
|
|
||||||
/// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。
|
/// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。
|
||||||
/// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。
|
/// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。
|
||||||
|
@ -78,9 +77,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
|
|
||||||
func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int {
|
func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int {
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
return state.candidates.count
|
|
||||||
} else if let state = state as? InputState.AssociatedPhrases {
|
|
||||||
return state.candidates.count
|
return state.candidates.count
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
@ -91,9 +88,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
/// - Returns: 候選字詞陣列(字音配對)。
|
/// - Returns: 候選字詞陣列(字音配對)。
|
||||||
func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] {
|
func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] {
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
return state.candidates
|
|
||||||
} else if let state = state as? InputState.AssociatedPhrases {
|
|
||||||
return state.candidates
|
return state.candidates
|
||||||
}
|
}
|
||||||
return .init()
|
return .init()
|
||||||
|
@ -103,9 +98,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
-> (String, String)
|
-> (String, String)
|
||||||
{
|
{
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
return state.candidates[index]
|
|
||||||
} else if let state = state as? InputState.AssociatedPhrases {
|
|
||||||
return state.candidates[index]
|
return state.candidates[index]
|
||||||
}
|
}
|
||||||
return ("", "")
|
return ("", "")
|
||||||
|
@ -114,22 +107,20 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) {
|
func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) {
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
|
|
||||||
if let state = state as? InputState.SymbolTable,
|
if state.type == .ofSymbolTable,
|
||||||
let node = state.node.children?[index]
|
let node = state.node.children?[index]
|
||||||
{
|
{
|
||||||
if let children = node.children, !children.isEmpty {
|
if let children = node.children, !children.isEmpty {
|
||||||
handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現
|
handle(state: IMEState.ofEmpty()) // 防止縱橫排選字窗同時出現
|
||||||
handle(
|
handle(state: IMEState.ofSymbolTable(node: node))
|
||||||
state: InputState.SymbolTable(node: node, previous: state.node, isTypingVertical: state.isTypingVertical)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
handle(state: InputState.Committing(textToCommit: node.title))
|
handle(state: IMEState.ofCommitting(textToCommit: node.title))
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.ofEmpty())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
|
||||||
let selectedValue = state.candidates[index]
|
let selectedValue = state.candidates[index]
|
||||||
keyHandler.fixNode(
|
keyHandler.fixNode(
|
||||||
candidate: selectedValue, respectCursorPushing: true,
|
candidate: selectedValue, respectCursorPushing: true,
|
||||||
|
@ -139,17 +130,15 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
let inputting = keyHandler.buildInputtingState
|
let inputting = keyHandler.buildInputtingState
|
||||||
|
|
||||||
if mgrPrefs.useSCPCTypingMode {
|
if mgrPrefs.useSCPCTypingMode {
|
||||||
handle(state: InputState.Committing(textToCommit: inputting.composingBufferConverted))
|
handle(state: IMEState.ofCommitting(textToCommit: inputting.displayedText))
|
||||||
// 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。
|
// 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。
|
||||||
if mgrPrefs.associatedPhrasesEnabled,
|
if mgrPrefs.associatedPhrasesEnabled {
|
||||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
let associates = keyHandler.buildAssociatePhraseState(
|
||||||
withPair: .init(key: selectedValue.0, value: selectedValue.1),
|
withPair: .init(key: selectedValue.0, value: selectedValue.1)
|
||||||
isTypingVertical: state.isTypingVertical
|
)
|
||||||
), !associatePhrases.candidates.isEmpty
|
handle(state: associates.candidates.isEmpty ? IMEState.ofEmpty() : associates)
|
||||||
{
|
|
||||||
handle(state: associatePhrases)
|
|
||||||
} else {
|
} else {
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.ofEmpty())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handle(state: inputting)
|
handle(state: inputting)
|
||||||
|
@ -157,25 +146,25 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let state = state as? InputState.AssociatedPhrases {
|
if state.type == .ofAssociates {
|
||||||
let selectedValue = state.candidates[index]
|
let selectedValue = state.candidates[index]
|
||||||
handle(state: InputState.Committing(textToCommit: selectedValue.1))
|
handle(state: IMEState.ofCommitting(textToCommit: selectedValue.1))
|
||||||
// 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。
|
// 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。
|
||||||
// 不然的話,一旦你選中了由多個字組成的聯想候選詞,則連續聯想會被打斷。
|
// 不然的話,一旦你選中了由多個字組成的聯想候選詞,則連續聯想會被打斷。
|
||||||
guard let valueKept = selectedValue.1.last else {
|
guard let valueKept = selectedValue.1.last else {
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.ofEmpty())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if mgrPrefs.associatedPhrasesEnabled,
|
if mgrPrefs.associatedPhrasesEnabled {
|
||||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
let associates = keyHandler.buildAssociatePhraseState(
|
||||||
withPair: .init(key: selectedValue.0, value: String(valueKept)),
|
withPair: .init(key: selectedValue.0, value: String(valueKept))
|
||||||
isTypingVertical: state.isTypingVertical
|
)
|
||||||
), !associatePhrases.candidates.isEmpty
|
if !associates.candidates.isEmpty {
|
||||||
{
|
handle(state: associates)
|
||||||
handle(state: associatePhrases)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handle(state: InputState.Empty())
|
}
|
||||||
|
handle(state: IMEState.ofEmpty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,11 @@ import Cocoa
|
||||||
// MARK: - Tooltip Display and Candidate Display Methods
|
// MARK: - Tooltip Display and Candidate Display Methods
|
||||||
|
|
||||||
extension ctlInputMethod {
|
extension ctlInputMethod {
|
||||||
func show(tooltip: String, composingBuffer: String, cursorIndex: Int) {
|
func show(tooltip: String, displayedText: String, u16Cursor: Int) {
|
||||||
guard let client = client() else { return }
|
guard let client = client() else { return }
|
||||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||||
var cursor = cursorIndex
|
var cursor = u16Cursor
|
||||||
if cursor == composingBuffer.count, cursor != 0 {
|
if cursor == displayedText.count, cursor != 0 {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, cursor >= 0 {
|
||||||
|
@ -38,24 +38,14 @@ extension ctlInputMethod {
|
||||||
ctlInputMethod.tooltipController.show(tooltip: tooltip, at: finalOrigin)
|
ctlInputMethod.tooltipController.show(tooltip: tooltip, at: finalOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func show(candidateWindowWith state: InputStateProtocol) {
|
func show(candidateWindowWith state: IMEStateProtocol) {
|
||||||
guard let client = client() else { return }
|
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 isCandidateWindowVertical: Bool {
|
||||||
var candidates: [(String, String)] = .init()
|
var candidates: [(String, String)] = .init()
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
candidates = state.candidates
|
|
||||||
} else if let state = state as? InputState.AssociatedPhrases {
|
|
||||||
candidates = state.candidates
|
candidates = state.candidates
|
||||||
}
|
}
|
||||||
if isTypingVertical { return true }
|
if isVerticalTyping { return true }
|
||||||
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
||||||
guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false }
|
guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false }
|
||||||
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
||||||
|
@ -106,7 +96,7 @@ extension ctlInputMethod {
|
||||||
let candidateKeys = mgrPrefs.candidateKeys
|
let candidateKeys = mgrPrefs.candidateKeys
|
||||||
let keyLabels =
|
let keyLabels =
|
||||||
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
||||||
let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : ""
|
let keyLabelSuffix = state.type == .ofAssociates ? "^" : ""
|
||||||
ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
||||||
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
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 lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||||
var cursor = 0
|
var cursor = 0
|
||||||
|
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
|
||||||
cursor = state.cursorIndex
|
cursor = state.data.cursor
|
||||||
if cursor == state.composingBuffer.count, cursor != 0 {
|
if cursor == state.displayedText.count, cursor != 0 {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,7 +130,7 @@ extension ctlInputMethod {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if isTypingVertical {
|
if isVerticalTyping {
|
||||||
ctlInputMethod.ctlCandidateCurrent.set(
|
ctlInputMethod.ctlCandidateCurrent.set(
|
||||||
windowTopLeftPoint: NSPoint(
|
windowTopLeftPoint: NSPoint(
|
||||||
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0
|
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0
|
||||||
|
|
|
@ -18,29 +18,77 @@ extension ctlInputMethod {
|
||||||
/// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數,
|
/// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數,
|
||||||
/// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。
|
/// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。
|
||||||
/// - Parameter newState: 新狀態。
|
/// - Parameter newState: 新狀態。
|
||||||
func handle(state newState: InputStateProtocol) {
|
func handle(state newState: IMEStateProtocol) {
|
||||||
let prevState = state
|
let previous = state
|
||||||
state = newState
|
state = newState
|
||||||
|
switch state.type {
|
||||||
switch newState {
|
case .ofDeactivated:
|
||||||
case let newState as InputState.Deactivated:
|
ctlInputMethod.ctlCandidateCurrent.delegate = nil
|
||||||
handle(state: newState, previous: prevState)
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
case let newState as InputState.Empty:
|
ctlInputMethod.tooltipController.hide()
|
||||||
handle(state: newState, previous: prevState)
|
if previous.hasComposition {
|
||||||
case let newState as InputState.EmptyIgnoringPreviousState:
|
commit(text: previous.displayedText)
|
||||||
handle(state: newState, previous: prevState)
|
}
|
||||||
case let newState as InputState.Committing:
|
clearInlineDisplay()
|
||||||
handle(state: newState, previous: prevState)
|
// 最後一道保險
|
||||||
case let newState as InputState.Inputting:
|
keyHandler.clear()
|
||||||
handle(state: newState, previous: prevState)
|
case .ofEmpty, .ofAbortion:
|
||||||
case let newState as InputState.Marking:
|
var previous = previous
|
||||||
handle(state: newState, previous: prevState)
|
if state.type == .ofAbortion {
|
||||||
case let newState as InputState.ChoosingCandidate:
|
state = IMEState.ofEmpty()
|
||||||
handle(state: newState, previous: prevState)
|
previous = state
|
||||||
case let newState as InputState.AssociatedPhrases:
|
}
|
||||||
handle(state: newState, previous: prevState)
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
case let newState as InputState.SymbolTable:
|
ctlInputMethod.tooltipController.hide()
|
||||||
handle(state: newState, previous: prevState)
|
// 全專案用以判斷「.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
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +96,7 @@ extension ctlInputMethod {
|
||||||
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
||||||
func setInlineDisplayWithCursor() {
|
func setInlineDisplayWithCursor() {
|
||||||
guard let client = client() else { return }
|
guard let client = client() else { return }
|
||||||
if let state = state as? InputState.AssociatedPhrases {
|
if state.type == .ofAssociates {
|
||||||
client.setMarkedText(
|
client.setMarkedText(
|
||||||
state.attributedString, selectionRange: NSRange(location: 0, length: 0),
|
state.attributedString, selectionRange: NSRange(location: 0, length: 0),
|
||||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||||
|
@ -56,49 +104,19 @@ extension ctlInputMethod {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let state = state as? InputState.NotEmpty else {
|
if state.hasComposition || state.isCandidateContainer {
|
||||||
clearInlineDisplay()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var identifier: AnyObject {
|
|
||||||
switch IME.currentInputMode {
|
|
||||||
case InputMode.imeModeCHS:
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
return "zh-Hans" as AnyObject
|
|
||||||
}
|
|
||||||
case InputMode.imeModeCHT:
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
return (mgrPrefs.shiftJISShinjitaiOutputEnabled || mgrPrefs.chineseConversionEnabled)
|
|
||||||
? "ja" as AnyObject : "zh-Hant" as AnyObject
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return "" as AnyObject
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Shiki's Note] This might needs to be bug-reported to Apple:
|
|
||||||
// The LanguageIdentifier attribute of an NSAttributeString designated to
|
|
||||||
// IMK Client().SetMarkedText won't let the actual font respect your languageIdentifier
|
|
||||||
// settings. Still, this might behaves as Apple's current expectation, I'm afraid.
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
state.attributedString.setAttributes(
|
|
||||||
[.languageIdentifier: identifier],
|
|
||||||
range: NSRange(
|
|
||||||
location: 0,
|
|
||||||
length: state.composingBuffer.utf16.count
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度
|
/// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度
|
||||||
/// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
/// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
||||||
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
||||||
client.setMarkedText(
|
client.setMarkedText(
|
||||||
state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0),
|
state.attributedString, selectionRange: NSRange(location: state.data.u16Cursor, length: 0),
|
||||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其它情形。
|
||||||
|
clearInlineDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。
|
/// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。
|
||||||
|
@ -123,109 +141,4 @@ extension ctlInputMethod {
|
||||||
buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,11 +212,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n"
|
message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleSCPCTypingModeEnabled()
|
||||||
mgrPrefs.toggleSCPCTypingModeEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,11 +222,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n"
|
message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleChineseConversionEnabled()
|
||||||
mgrPrefs.toggleChineseConversionEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,11 +232,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n"
|
message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
|
||||||
mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,11 +242,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n"
|
message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleCurrencyNumeralsEnabled()
|
||||||
mgrPrefs.toggleCurrencyNumeralsEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,11 +252,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n"
|
message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleHalfWidthPunctuationEnabled()
|
||||||
mgrPrefs.toggleHalfWidthPunctuationEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,11 +262,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n"
|
message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleCNS11643Enabled()
|
||||||
mgrPrefs.toggleCNS11643Enabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,11 +272,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n"
|
message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleSymbolInputEnabled()
|
||||||
mgrPrefs.toggleSymbolInputEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,11 +282,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n"
|
message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.toggleAssociatedPhrasesEnabled()
|
||||||
mgrPrefs.toggleAssociatedPhrasesEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,11 +292,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n"
|
message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n"
|
||||||
+ {
|
+ (mgrPrefs.togglePhraseReplacementEnabled()
|
||||||
mgrPrefs.togglePhraseReplacementEnabled()
|
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import InputMethodKit
|
||||||
public enum vChewing {}
|
public enum vChewing {}
|
||||||
|
|
||||||
// The type of input modes.
|
// The type of input modes.
|
||||||
public enum InputMode: String {
|
public enum InputMode: String, CaseIterable {
|
||||||
case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS"
|
case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS"
|
||||||
case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT"
|
case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT"
|
||||||
case imeModeNULL = ""
|
case imeModeNULL = ""
|
||||||
|
@ -131,11 +131,11 @@ public enum IME {
|
||||||
),
|
),
|
||||||
mgrLangModel.dataFolderPath(isDefaultFolder: false)
|
mgrLangModel.dataFolderPath(isDefaultFolder: false)
|
||||||
)
|
)
|
||||||
ctlNonModalAlertWindow.shared.show(
|
let alert = NSAlert()
|
||||||
title: NSLocalizedString("Unable to create the user phrase file.", comment: ""),
|
alert.messageText = NSLocalizedString("Unable to create the user phrase file.", comment: "")
|
||||||
content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""),
|
alert.informativeText = content
|
||||||
cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil
|
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||||
)
|
alert.runModal()
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -223,152 +223,6 @@ public enum IME {
|
||||||
}
|
}
|
||||||
return 0
|
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<CFString>.fromOpaque(ptrCategory).takeUnretainedValue()
|
|
||||||
if category != kTISCategoryKeyboardInputSource {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ptrASCIICapable = TISGetInputSourceProperty(
|
|
||||||
source, kTISPropertyInputSourceIsASCIICapable
|
|
||||||
) {
|
|
||||||
let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(ptrASCIICapable)
|
|
||||||
.takeUnretainedValue()
|
|
||||||
if asciiCapable != kCFBooleanTrue {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ptrSourceType = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) {
|
|
||||||
let sourceType = Unmanaged<CFString>.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<CFString>.fromOpaque(ptrSourceID).takeUnretainedValue())
|
|
||||||
let localizedName = String(
|
|
||||||
Unmanaged<CFString>.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
|
// 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
|
// MARK: - String Tildes Expansion Extension
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<CFTypeRef>.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<CFBoolean>.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<CFString>.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<CFString>.fromOpaque(bundleIDPtr)
|
|
||||||
.takeUnretainedValue()
|
|
||||||
let inputsSourceModeID = Unmanaged<CFString>.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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -438,6 +438,7 @@ public enum mgrPrefs {
|
||||||
|
|
||||||
@UserDefault(key: UserDef.kTrimUnfinishedReadingsOnCommit.rawValue, defaultValue: true)
|
@UserDefault(key: UserDef.kTrimUnfinishedReadingsOnCommit.rawValue, defaultValue: true)
|
||||||
static var trimUnfinishedReadingsOnCommit: Bool
|
static var trimUnfinishedReadingsOnCommit: Bool
|
||||||
|
|
||||||
// MARK: - Settings (Tier 2)
|
// MARK: - Settings (Tier 2)
|
||||||
|
|
||||||
@UserDefault(key: UserDef.kUseIMKCandidateWindow.rawValue, defaultValue: false)
|
@UserDefault(key: UserDef.kUseIMKCandidateWindow.rawValue, defaultValue: false)
|
||||||
|
@ -458,6 +459,8 @@ public enum mgrPrefs {
|
||||||
@UserDefault(key: UserDef.kMaxCandidateLength.rawValue, defaultValue: 10)
|
@UserDefault(key: UserDef.kMaxCandidateLength.rawValue, defaultValue: 10)
|
||||||
static var maxCandidateLength: Int
|
static var maxCandidateLength: Int
|
||||||
|
|
||||||
|
static var allowedMarkRange: ClosedRange<Int> = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength
|
||||||
|
|
||||||
@UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep.rawValue, defaultValue: true)
|
@UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep.rawValue, defaultValue: true)
|
||||||
static var shouldNotFartInLieuOfBeep: Bool
|
static var shouldNotFartInLieuOfBeep: Bool
|
||||||
|
|
||||||
|
|
|
@ -245,11 +245,11 @@ extension vChewing {
|
||||||
return !unigramsFor(key: key).isEmpty
|
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)
|
lmAssociates.valuesFor(pair: pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hasAssociatedPhrasesFor(pair: Megrez.Compositor.Candidate) -> Bool {
|
public func hasAssociatedPhrasesFor(pair: Megrez.Compositor.KeyValuePaired) -> Bool {
|
||||||
lmAssociates.hasValuesFor(pair: pair)
|
lmAssociates.hasValuesFor(pair: pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,24 +10,34 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class SymbolNode {
|
public class SymbolNode {
|
||||||
var title: String
|
var title: String
|
||||||
var children: [SymbolNode]?
|
var children: [SymbolNode]?
|
||||||
var previous: SymbolNode?
|
var previous: SymbolNode?
|
||||||
|
|
||||||
init(_ title: String, _ children: [SymbolNode]? = nil) {
|
init(_ title: String, _ children: [SymbolNode]? = nil, previous: SymbolNode? = nil) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.children = children
|
self.children = children
|
||||||
|
self.children?.forEach {
|
||||||
|
$0.previous = self
|
||||||
|
}
|
||||||
|
self.previous = previous
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ title: String, symbols: String) {
|
init(_ title: String, symbols: String) {
|
||||||
self.title = title
|
self.title = title
|
||||||
children = Array(symbols).map { SymbolNode(String($0), nil) }
|
children = Array(symbols).map { SymbolNode(String($0), nil) }
|
||||||
|
children?.forEach {
|
||||||
|
$0.previous = self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ title: String, symbols: [String]) {
|
init(_ title: String, symbols: [String]) {
|
||||||
self.title = title
|
self.title = title
|
||||||
children = symbols.map { SymbolNode($0, nil) }
|
children = symbols.map { SymbolNode($0, nil) }
|
||||||
|
children?.forEach {
|
||||||
|
$0.previous = self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func parseUserSymbolNodeData() {
|
static func parseUserSymbolNodeData() {
|
||||||
|
|
|
@ -78,7 +78,7 @@ extension vChewing {
|
||||||
// This function will be implemented only if further hard-necessity comes.
|
// 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] = []
|
var pairs: [String] = []
|
||||||
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
|
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
|
||||||
for (netaRange, index) in arrRangeRecords {
|
for (netaRange, index) in arrRangeRecords {
|
||||||
|
@ -98,7 +98,7 @@ extension vChewing {
|
||||||
return pairs.filter { set.insert($0).inserted }
|
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 }
|
if rangeMap[pair.toNGramKey] != nil { return true }
|
||||||
return rangeMap[pair.value] != nil
|
return rangeMap[pair.value] != nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -319,8 +319,8 @@ extension vChewing.LMUserOverride {
|
||||||
|
|
||||||
// 前置單元只記錄讀音,在其後的單元則同時記錄讀音與字詞
|
// 前置單元只記錄讀音,在其後的單元則同時記錄讀音與字詞
|
||||||
let strCurrent = kvCurrent.key
|
let strCurrent = kvCurrent.key
|
||||||
var kvPrevious = Megrez.Compositor.Candidate()
|
var kvPrevious = Megrez.Compositor.KeyValuePaired()
|
||||||
var kvAnterior = Megrez.Compositor.Candidate()
|
var kvAnterior = Megrez.Compositor.KeyValuePaired()
|
||||||
var readingStack = ""
|
var readingStack = ""
|
||||||
var trigramKey: String { "(\(kvAnterior.toNGramKey),\(kvPrevious.toNGramKey),\(strCurrent))" }
|
var trigramKey: String { "(\(kvAnterior.toNGramKey),\(kvPrevious.toNGramKey),\(strCurrent))" }
|
||||||
var result: String {
|
var result: String {
|
||||||
|
|
|
@ -4,9 +4,7 @@
|
||||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||||
|
|
||||||
/// The namespace for this package.
|
/// The namespace for this package.
|
||||||
public enum Megrez {
|
public enum Megrez {}
|
||||||
public typealias KeyValuePaired = Compositor.Candidate // 相容性措施。
|
|
||||||
}
|
|
||||||
|
|
||||||
// 著作權聲明:
|
// 著作權聲明:
|
||||||
// 除了 Megrez 專有的修改與實作以外,該套件所有程式邏輯來自於 Gramambular、算法歸 Lukhnos Liu 所有。
|
// 除了 Megrez 專有的修改與實作以外,該套件所有程式邏輯來自於 Gramambular、算法歸 Lukhnos Liu 所有。
|
||||||
|
|
|
@ -23,7 +23,15 @@ extension Megrez {
|
||||||
/// 公開:多字讀音鍵當中用以分割漢字讀音的記號的預設值,是「-」。
|
/// 公開:多字讀音鍵當中用以分割漢字讀音的記號的預設值,是「-」。
|
||||||
public static let kDefaultSeparator: String = "-"
|
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
|
public var separator = kDefaultSeparator
|
||||||
/// 公開:組字器內已經插入的單筆索引鍵的數量。
|
/// 公開:組字器內已經插入的單筆索引鍵的數量。
|
||||||
|
@ -88,37 +96,46 @@ extension Megrez {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 按幅位來前後移動游標。
|
/// 按幅位來前後移動游標。
|
||||||
/// - Parameter direction: 移動方向。
|
/// - Parameters:
|
||||||
|
/// - direction: 移動方向。
|
||||||
|
/// - isMarker: 要移動的是否為選擇標記(而非游標)。
|
||||||
/// - Returns: 該操作是否順利完成。
|
/// - 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 {
|
switch direction {
|
||||||
case .front:
|
case .front:
|
||||||
if cursor == width { return false }
|
if target == width { return false }
|
||||||
case .rear:
|
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 aRegionForward = max(currentRegion - 1, 0)
|
||||||
let currentRegionBorderRear: Int = walkedNodes[0..<currentRegion].map(\.spanLength).reduce(0, +)
|
let currentRegionBorderRear: Int = walkedNodes[0..<currentRegion].map(\.spanLength).reduce(0, +)
|
||||||
switch cursor {
|
switch target {
|
||||||
case currentRegionBorderRear:
|
case currentRegionBorderRear:
|
||||||
switch direction {
|
switch direction {
|
||||||
case .front:
|
case .front:
|
||||||
cursor =
|
target =
|
||||||
(currentRegion > walkedNodes.count)
|
(currentRegion > walkedNodes.count)
|
||||||
? keys.count : walkedNodes[0...currentRegion].map(\.spanLength).reduce(0, +)
|
? keys.count : walkedNodes[0...currentRegion].map(\.spanLength).reduce(0, +)
|
||||||
case .rear:
|
case .rear:
|
||||||
cursor = walkedNodes[0..<aRegionForward].map(\.spanLength).reduce(0, +)
|
target = walkedNodes[0..<aRegionForward].map(\.spanLength).reduce(0, +)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
switch direction {
|
switch direction {
|
||||||
case .front:
|
case .front:
|
||||||
cursor = currentRegionBorderRear + walkedNodes[currentRegion].spanLength
|
target = currentRegionBorderRear + walkedNodes[currentRegion].spanLength
|
||||||
case .rear:
|
case .rear:
|
||||||
cursor = currentRegionBorderRear
|
target = currentRegionBorderRear
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switch isMarker {
|
||||||
|
case false: cursor = target
|
||||||
|
case true: marker = target
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Megrez.Compositor {
|
extension Megrez.Compositor {
|
||||||
public struct Candidate: Equatable, Hashable, Comparable, CustomStringConvertible {
|
public struct KeyValuePaired: Equatable, Hashable, Comparable, CustomStringConvertible {
|
||||||
/// 鍵。一般情況下用來放置讀音等可以用來作為索引的內容。
|
/// 鍵。一般情況下用來放置讀音等可以用來作為索引的內容。
|
||||||
public var key: String
|
public var key: String
|
||||||
/// 資料值。
|
/// 資料值。
|
||||||
|
@ -32,23 +32,23 @@ extension Megrez.Compositor {
|
||||||
hasher.combine(value)
|
hasher.combine(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: Candidate, rhs: Candidate) -> Bool {
|
public static func == (lhs: KeyValuePaired, rhs: KeyValuePaired) -> Bool {
|
||||||
lhs.key == rhs.key && lhs.value == rhs.value
|
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)
|
(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)
|
(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)
|
(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)
|
(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、以免去在呼叫該函式後再處理的麻煩。
|
/// 話,那麼這裡會用到 location - 1、以免去在呼叫該函式後再處理的麻煩。
|
||||||
/// - Parameter location: 游標位置。
|
/// - Parameter location: 游標位置。
|
||||||
/// - Returns: 候選字音配對陣列。
|
/// - Returns: 候選字音配對陣列。
|
||||||
public func fetchCandidates(at location: Int, filter: CandidateFetchFilter = .all) -> [Candidate] {
|
public func fetchCandidates(at location: Int, filter: CandidateFetchFilter = .all) -> [KeyValuePaired] {
|
||||||
var result = [Candidate]()
|
var result = [KeyValuePaired]()
|
||||||
guard !keys.isEmpty else { return result }
|
guard !keys.isEmpty else { return result }
|
||||||
let location = max(min(location, keys.count - 1), 0) // 防呆
|
let location = max(min(location, keys.count - 1), 0) // 防呆
|
||||||
let anchors: [NodeAnchor] = fetchOverlappingNodes(at: location).stableSorted {
|
let anchors: [NodeAnchor] = fetchOverlappingNodes(at: location).stableSorted {
|
||||||
|
@ -96,7 +96,7 @@ extension Megrez.Compositor {
|
||||||
/// - overrideType: 指定覆寫行為。
|
/// - overrideType: 指定覆寫行為。
|
||||||
/// - Returns: 該操作是否成功執行。
|
/// - Returns: 該操作是否成功執行。
|
||||||
@discardableResult public func overrideCandidate(
|
@discardableResult public func overrideCandidate(
|
||||||
_ candidate: Candidate, at location: Int, overrideType: Node.OverrideType = .withHighScore
|
_ candidate: KeyValuePaired, at location: Int, overrideType: Node.OverrideType = .withHighScore
|
||||||
)
|
)
|
||||||
-> Bool
|
-> Bool
|
||||||
{
|
{
|
||||||
|
@ -154,7 +154,7 @@ extension Megrez.Compositor {
|
||||||
anchor.node.reset()
|
anchor.node.reset()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
anchor.node.overridingScore /= 2
|
anchor.node.overridingScore /= 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
|
@ -42,7 +42,7 @@ extension Megrez.Compositor {
|
||||||
didSet { currentUnigramIndex = min(max(0, currentUnigramIndex), unigrams.count - 1) }
|
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) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(key)
|
hasher.combine(key)
|
||||||
|
@ -70,8 +70,12 @@ extension Megrez.Compositor {
|
||||||
overrideType = .withNoOverrides
|
overrideType = .withNoOverrides
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 檢查當前節點是否「讀音字長與候選字字長不一致」。
|
||||||
|
public var isReadingMismatched: Bool {
|
||||||
|
keyArray.count != value.count
|
||||||
|
}
|
||||||
|
|
||||||
/// 給出目前的最高權重單元圖。該結果可能會受節點覆寫狀態所影響。
|
/// 給出目前的最高權重單元圖。該結果可能會受節點覆寫狀態所影響。
|
||||||
/// - Returns: 目前的最高權重單元圖。該結果可能會受節點覆寫狀態所影響。
|
|
||||||
public var currentUnigram: Megrez.Unigram {
|
public var currentUnigram: Megrez.Unigram {
|
||||||
unigrams.isEmpty ? .init() : unigrams[currentUnigramIndex]
|
unigrams.isEmpty ? .init() : unigrams[currentUnigramIndex]
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,18 +229,7 @@ public class ctlCandidateIMK: IMKCandidates, ctlCandidateProtocol {
|
||||||
if let newChar = ctlCandidateIMK.defaultIMKSelectionKey[event.keyCode] {
|
if let newChar = ctlCandidateIMK.defaultIMKSelectionKey[event.keyCode] {
|
||||||
/// 根據 KeyCode 重新換算一下選字鍵的 NSEvent,糾正其 Character 數值。
|
/// 根據 KeyCode 重新換算一下選字鍵的 NSEvent,糾正其 Character 數值。
|
||||||
/// 反正 IMK 選字窗目前也沒辦法修改選字鍵。
|
/// 反正 IMK 選字窗目前也沒辦法修改選字鍵。
|
||||||
let newEvent = NSEvent.keyEvent(
|
let newEvent = event.reinitiate(characters: newChar)
|
||||||
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
|
|
||||||
)
|
|
||||||
if let newEvent = newEvent {
|
if let newEvent = newEvent {
|
||||||
if mgrPrefs.useSCPCTypingMode, delegate.isAssociatedPhrasesState {
|
if mgrPrefs.useSCPCTypingMode, delegate.isAssociatedPhrasesState {
|
||||||
// 註:input.isShiftHold 已經在 ctlInputMethod.handle() 內處理,因為在那邊處理才有效。
|
// 註:input.isShiftHold 已經在 ctlInputMethod.handle() 內處理,因為在那邊處理才有效。
|
||||||
|
@ -300,17 +289,6 @@ extension ctlCandidateIMK {
|
||||||
let mapNumPadKeyCodeTranslation: [UInt16: UInt16] = [
|
let mapNumPadKeyCodeTranslation: [UInt16: UInt16] = [
|
||||||
83: 18, 84: 19, 85: 20, 86: 21, 87: 23, 88: 22, 89: 26, 91: 28, 92: 25,
|
83: 18, 84: 19, 85: 20, 86: 21, 87: 23, 88: 22, 89: 26, 91: 28, 92: 25,
|
||||||
]
|
]
|
||||||
return NSEvent.keyEvent(
|
return event.reinitiate(keyCode: mapNumPadKeyCodeTranslation[event.keyCode] ?? event.keyCode)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,12 +93,12 @@ struct suiPrefPaneKeyboard: View {
|
||||||
mgrPrefs.mandarinParser = value
|
mgrPrefs.mandarinParser = value
|
||||||
switch value {
|
switch value {
|
||||||
case 0:
|
case 0:
|
||||||
if !AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout) {
|
if !IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout) {
|
||||||
mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ZhuyinBopomofo"
|
mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ZhuyinBopomofo"
|
||||||
selBasicKeyboardLayout = mgrPrefs.basicKeyboardLayout
|
selBasicKeyboardLayout = mgrPrefs.basicKeyboardLayout
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout) {
|
if IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout) {
|
||||||
mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ABC"
|
mgrPrefs.basicKeyboardLayout = "com.apple.keylayout.ABC"
|
||||||
selBasicKeyboardLayout = mgrPrefs.basicKeyboardLayout
|
selBasicKeyboardLayout = mgrPrefs.basicKeyboardLayout
|
||||||
}
|
}
|
||||||
|
@ -170,15 +170,19 @@ struct suiPrefPaneKeyboard: View {
|
||||||
selection: $selBasicKeyboardLayout.onChange {
|
selection: $selBasicKeyboardLayout.onChange {
|
||||||
let value = selBasicKeyboardLayout
|
let value = selBasicKeyboardLayout
|
||||||
mgrPrefs.basicKeyboardLayout = value
|
mgrPrefs.basicKeyboardLayout = value
|
||||||
if AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(value) {
|
if IMKHelper.arrDynamicBasicKeyLayouts.contains(value) {
|
||||||
mgrPrefs.mandarinParser = 0
|
mgrPrefs.mandarinParser = 0
|
||||||
selMandarinParser = mgrPrefs.mandarinParser
|
selMandarinParser = mgrPrefs.mandarinParser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
ForEach(0...(IME.arrEnumerateSystemKeyboardLayouts.count - 1), id: \.self) { id in
|
ForEach(0...(IMKHelper.allowedBasicLayoutsAsTISInputSources.count - 1), id: \.self) { id in
|
||||||
Text(IME.arrEnumerateSystemKeyboardLayouts[id].strName).tag(
|
let theEntry = IMKHelper.allowedBasicLayoutsAsTISInputSources[id]
|
||||||
IME.arrEnumerateSystemKeyboardLayouts[id].strValue)
|
if let theEntry = theEntry {
|
||||||
|
Text(theEntry.vChewingLocalizedName).tag(theEntry.identifier)
|
||||||
|
} else {
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
}.id(UUID())
|
}.id(UUID())
|
||||||
}
|
}
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
|
|
@ -11,7 +11,11 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import InputMethodKit
|
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) {
|
switch max(CommandLine.arguments.count - 1, 0) {
|
||||||
case 0: break
|
case 0: break
|
||||||
|
@ -19,7 +23,7 @@ switch max(CommandLine.arguments.count - 1, 0) {
|
||||||
switch CommandLine.arguments[1] {
|
switch CommandLine.arguments[1] {
|
||||||
case "install":
|
case "install":
|
||||||
if CommandLine.arguments[1] == "install" {
|
if CommandLine.arguments[1] == "install" {
|
||||||
let exitCode = IME.registerInputMethod()
|
let exitCode = IMKHelper.registerInputMethod()
|
||||||
exit(exitCode)
|
exit(exitCode)
|
||||||
}
|
}
|
||||||
case "uninstall":
|
case "uninstall":
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
<key>tsInputModePrimaryInScriptKey</key>
|
<key>tsInputModePrimaryInScriptKey</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>tsInputModeScriptKey</key>
|
<key>tsInputModeScriptKey</key>
|
||||||
<string>smTradChinese</string>
|
<string>smUnicode</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>org.atelierInmu.inputmethod.vChewing.IMECHT</key>
|
<key>org.atelierInmu.inputmethod.vChewing.IMECHT</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
<key>tsInputModePrimaryInScriptKey</key>
|
<key>tsInputModePrimaryInScriptKey</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>tsInputModeScriptKey</key>
|
<key>tsInputModeScriptKey</key>
|
||||||
<string>smTradChinese</string>
|
<string>smUnicode</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>tsVisibleInputModeOrderedArrayKey</key>
|
<key>tsVisibleInputModeOrderedArrayKey</key>
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -93,89 +93,25 @@ class ctlPrefWindow: NSWindowController {
|
||||||
currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem
|
currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem
|
||||||
uiLanguageButton.select(currentLanguageSelectItem)
|
uiLanguageButton.select(currentLanguageSelectItem)
|
||||||
|
|
||||||
let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
|
|
||||||
var usKeyboardLayoutItem: NSMenuItem?
|
var usKeyboardLayoutItem: NSMenuItem?
|
||||||
var chosenBaseKeyboardLayoutItem: NSMenuItem?
|
var chosenBaseKeyboardLayoutItem: NSMenuItem?
|
||||||
|
|
||||||
basicKeyboardLayoutButton.menu?.removeAllItems()
|
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
|
let basicKeyboardLayoutID = mgrPrefs.basicKeyboardLayout
|
||||||
|
|
||||||
for source in list {
|
for source in IMKHelper.allowedBasicLayoutsAsTISInputSources {
|
||||||
if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) {
|
guard let source = source else {
|
||||||
let category = Unmanaged<CFString>.fromOpaque(categoryPtr).takeUnretainedValue()
|
basicKeyboardLayoutButton.menu?.addItem(NSMenuItem.separator())
|
||||||
if category != kTISCategoryKeyboardInputSource {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if let asciiCapablePtr = TISGetInputSourceProperty(
|
|
||||||
source, kTISPropertyInputSourceIsASCIICapable
|
|
||||||
) {
|
|
||||||
let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(asciiCapablePtr)
|
|
||||||
.takeUnretainedValue()
|
|
||||||
if asciiCapable != kCFBooleanTrue {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) {
|
|
||||||
let sourceType = Unmanaged<CFString>.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<CFString>.fromOpaque(sourceIDPtr).takeUnretainedValue())
|
|
||||||
let localizedName = String(
|
|
||||||
Unmanaged<CFString>.fromOpaque(localizedNamePtr).takeUnretainedValue())
|
|
||||||
|
|
||||||
let menuItem = NSMenuItem()
|
let menuItem = NSMenuItem()
|
||||||
menuItem.title = localizedName
|
menuItem.title = source.vChewingLocalizedName
|
||||||
menuItem.representedObject = sourceID
|
menuItem.representedObject = source.identifier
|
||||||
|
if source.identifier == "com.apple.keylayout.US" { usKeyboardLayoutItem = menuItem }
|
||||||
if sourceID == "com.apple.keylayout.US" {
|
if basicKeyboardLayoutID == source.identifier { chosenBaseKeyboardLayoutItem = menuItem }
|
||||||
usKeyboardLayoutItem = menuItem
|
|
||||||
}
|
|
||||||
if basicKeyboardLayoutID == sourceID {
|
|
||||||
chosenBaseKeyboardLayoutItem = menuItem
|
|
||||||
}
|
|
||||||
if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) || sourceID.contains("vChewing") {
|
|
||||||
basicKeyboardLayoutButton.menu?.addItem(menuItem)
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
basicKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem)
|
basicKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem)
|
||||||
|
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="macosx"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<objects>
|
|
||||||
<customObject id="-2" userLabel="File's Owner" customClass="ctlNonModalAlertWindow" customModule="vChewing" customModuleProvider="target">
|
|
||||||
<connections>
|
|
||||||
<outlet property="cancelButton" destination="71" id="83"/>
|
|
||||||
<outlet property="confirmButton" destination="5" id="82"/>
|
|
||||||
<outlet property="contentTextField" destination="59" id="79"/>
|
|
||||||
<outlet property="titleTextField" destination="39" id="78"/>
|
|
||||||
<outlet property="window" destination="1" id="3"/>
|
|
||||||
</connections>
|
|
||||||
</customObject>
|
|
||||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
|
||||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
|
||||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" visibleAtLaunch="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="1">
|
|
||||||
<windowStyleMask key="styleMask" titled="YES" fullSizeContentView="YES"/>
|
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
|
||||||
<rect key="contentRect" x="196" y="240" width="420" height="130"/>
|
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
|
|
||||||
<view key="contentView" id="2" customClass="NSVisualEffectView">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="420" height="130"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5">
|
|
||||||
<rect key="frame" x="314" y="13" width="92" height="32"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="78" id="OIg-RG-etq"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6">
|
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
</buttonCell>
|
|
||||||
<connections>
|
|
||||||
<action selector="confirmButtonAction:" target="-2" id="85"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="71">
|
|
||||||
<rect key="frame" x="222" y="13" width="92" height="32"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="78" id="Ky0-Zb-Gla"/>
|
|
||||||
</constraints>
|
|
||||||
<buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="73">
|
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
</buttonCell>
|
|
||||||
<connections>
|
|
||||||
<action selector="cancelButtonAction:" target="-2" id="86"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="12">
|
|
||||||
<rect key="frame" x="24" y="50" width="64" height="64"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" constant="64" id="A3W-eQ-z1A"/>
|
|
||||||
</constraints>
|
|
||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="AlertIcon" id="13"/>
|
|
||||||
</imageView>
|
|
||||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="39">
|
|
||||||
<rect key="frame" x="103" y="92" width="300" height="17"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="17" id="P7H-ef-iqw"/>
|
|
||||||
</constraints>
|
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Lorem ipsum" id="40">
|
|
||||||
<font key="font" metaFont="systemBold"/>
|
|
||||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
</textFieldCell>
|
|
||||||
</textField>
|
|
||||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" preferredMaxLayoutWidth="296" translatesAutoresizingMaskIntoConstraints="NO" id="59">
|
|
||||||
<rect key="frame" x="103" y="70" width="300" height="14"/>
|
|
||||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Lorem ipsum" id="60">
|
|
||||||
<font key="font" metaFont="smallSystem"/>
|
|
||||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
|
||||||
</textFieldCell>
|
|
||||||
</textField>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="59" firstAttribute="top" secondItem="39" secondAttribute="bottom" constant="8" symbolic="YES" id="1wt-ne-dF2"/>
|
|
||||||
<constraint firstItem="71" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="2" secondAttribute="leading" constant="20" symbolic="YES" id="7bF-DG-vdn"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="12" secondAttribute="bottom" constant="50" id="8xn-Ib-OoE"/>
|
|
||||||
<constraint firstItem="39" firstAttribute="leading" secondItem="12" secondAttribute="trailing" constant="17" id="Fby-g8-C6h"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="39" secondAttribute="trailing" constant="19" id="MvK-Vm-t8m"/>
|
|
||||||
<constraint firstItem="39" firstAttribute="top" secondItem="2" secondAttribute="top" constant="21" id="QZ0-fh-VbK"/>
|
|
||||||
<constraint firstItem="71" firstAttribute="baseline" secondItem="5" secondAttribute="baseline" id="WzK-fU-3PM"/>
|
|
||||||
<constraint firstItem="39" firstAttribute="leading" secondItem="59" secondAttribute="leading" id="amF-fF-0aV"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="5" secondAttribute="trailing" constant="21" id="b3T-ES-VrG"/>
|
|
||||||
<constraint firstItem="5" firstAttribute="leading" secondItem="71" secondAttribute="trailing" constant="14" id="kas-eq-M8s"/>
|
|
||||||
<constraint firstItem="39" firstAttribute="trailing" secondItem="59" secondAttribute="trailing" id="l6t-qJ-mLX"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="71" secondAttribute="bottom" constant="20" symbolic="YES" id="mHa-mT-h87"/>
|
|
||||||
<constraint firstItem="12" firstAttribute="leading" secondItem="2" secondAttribute="leading" constant="24" id="oRs-Yb-Ysu"/>
|
|
||||||
<constraint firstItem="12" firstAttribute="top" secondItem="2" secondAttribute="top" constant="16" id="reW-NP-Ec0"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<connections>
|
|
||||||
<outlet property="delegate" destination="-2" id="4"/>
|
|
||||||
</connections>
|
|
||||||
<point key="canvasLocation" x="140" y="146"/>
|
|
||||||
</window>
|
|
||||||
</objects>
|
|
||||||
<resources>
|
|
||||||
<image name="AlertIcon" width="64" height="64"/>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
|
@ -3,9 +3,9 @@
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.3.1</string>
|
<string>2.4.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2310</string>
|
<string>2400</string>
|
||||||
<key>UpdateInfoEndpoint</key>
|
<key>UpdateInfoEndpoint</key>
|
||||||
<string>https://gitee.com/vchewing/vChewing-macOS/raw/main/Update-Info.plist</string>
|
<string>https://gitee.com/vchewing/vChewing-macOS/raw/main/Update-Info.plist</string>
|
||||||
<key>UpdateInfoSite</key>
|
<key>UpdateInfoSite</key>
|
||||||
|
|
|
@ -726,7 +726,7 @@
|
||||||
<key>USE_HFS+_COMPRESSION</key>
|
<key>USE_HFS+_COMPRESSION</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>VERSION</key>
|
<key>VERSION</key>
|
||||||
<string>2.3.1</string>
|
<string>2.4.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>TYPE</key>
|
<key>TYPE</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
5B09307628B6FC3B0021F8C5 /* shortcuts.html in Resources */ = {isa = PBXBuildFile; fileRef = 5B09307828B6FC3B0021F8C5 /* shortcuts.html */; };
|
5B09307628B6FC3B0021F8C5 /* shortcuts.html in Resources */ = {isa = PBXBuildFile; fileRef = 5B09307828B6FC3B0021F8C5 /* shortcuts.html */; };
|
||||||
5B0AF8B527B2C8290096FE54 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0AF8B427B2C8290096FE54 /* StringExtension.swift */; };
|
5B0AF8B527B2C8290096FE54 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0AF8B427B2C8290096FE54 /* StringExtension.swift */; };
|
||||||
5B11328927B94CFB00E58451 /* AppleKeyboardConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B11328827B94CFB00E58451 /* AppleKeyboardConverter.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 */; };
|
5B20430728BEE30900BFC6FD /* BookmarkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B20430628BEE30900BFC6FD /* BookmarkManager.swift */; };
|
||||||
5B21176C287539BB000443A9 /* ctlInputMethod_HandleStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
5B5948CE289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5948CD289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift */; };
|
||||||
5B5E535227EF261400C6AA1E /* IME.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5E535127EF261400C6AA1E /* IME.swift */; };
|
5B5E535227EF261400C6AA1E /* IME.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5E535127EF261400C6AA1E /* IME.swift */; };
|
||||||
5B62A32927AE77D100A19448 /* FSEventStreamHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A32827AE77D100A19448 /* FSEventStreamHelper.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 */; };
|
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 */; };
|
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */; };
|
||||||
5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */; };
|
5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */; };
|
||||||
5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34327AE7CD900A19448 /* TooltipController.swift */; };
|
5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34327AE7CD900A19448 /* TooltipController.swift */; };
|
||||||
5B62A34A27AE7CD900A19448 /* NotifierController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34527AE7CD900A19448 /* NotifierController.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 */; };
|
5B6C141228A9D4B30098ADF8 /* ctlInputMethod_Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6C141128A9D4B30098ADF8 /* ctlInputMethod_Common.swift */; };
|
||||||
5B73FB5E27B2BE1300E9BF49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B73FB6027B2BE1300E9BF49 /* InfoPlist.strings */; };
|
5B73FB5E27B2BE1300E9BF49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B73FB6027B2BE1300E9BF49 /* InfoPlist.strings */; };
|
||||||
5B782EC4280C243C007276DE /* KeyHandler_HandleCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */; };
|
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 */; };
|
5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */; };
|
||||||
5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; };
|
5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; };
|
||||||
5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75E27AED54C0023B93A /* Fart.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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
5BEDB725283B4C250078EB25 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; };
|
||||||
5BF0B84C28C070B000795FC6 /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
6ACA41FD15FC1D9000935EF6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41F015FC1D9000935EF6 /* MainMenu.xib */; };
|
||||||
6ACA420215FC1E5200935EF6 /* vChewing.app in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */; };
|
6ACA420215FC1E5200935EF6 /* vChewing.app in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */; };
|
||||||
D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F76B278CA1BA004A2160 /* AppDelegate.swift */; };
|
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 */; };
|
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
||||||
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.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 */; };
|
D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */; };
|
||||||
D4E33D8A27A838CF006DB1CF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8827A838CF006DB1CF /* Localizable.strings */; };
|
D4E33D8A27A838CF006DB1CF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8827A838CF006DB1CF /* Localizable.strings */; };
|
||||||
D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D4E33D8D27A838F0006DB1CF /* InfoPlist.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 = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B0AF8B427B2C8290096FE54 /* StringExtension.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = StringExtension.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B0C5EDF27C7D9870078037C /* dataCompiler.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = dataCompiler.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B0C5EDF27C7D9870078037C /* dataCompiler.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = dataCompiler.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppleKeyboardConverter.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B11328827B94CFB00E58451 /* AppleKeyboardConverter.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppleKeyboardConverter.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
|
5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMKHelper.swift; sourceTree = "<group>"; };
|
||||||
5B18BA6F27C7BD8B0056EB19 /* LICENSE-CHS.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "LICENSE-CHS.txt"; sourceTree = "<group>"; };
|
5B18BA6F27C7BD8B0056EB19 /* LICENSE-CHS.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "LICENSE-CHS.txt"; sourceTree = "<group>"; };
|
||||||
5B18BA7027C7BD8B0056EB19 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = "<group>"; };
|
5B18BA7027C7BD8B0056EB19 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = "<group>"; };
|
||||||
5B18BA7127C7BD8B0056EB19 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
5B18BA7127C7BD8B0056EB19 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||||
|
@ -229,7 +228,7 @@
|
||||||
5B2170D7289FACAC00BE7304 /* 7_LangModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7_LangModel.swift; sourceTree = "<group>"; };
|
5B2170D7289FACAC00BE7304 /* 7_LangModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 7_LangModel.swift; sourceTree = "<group>"; };
|
||||||
5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 0_Megrez.swift; sourceTree = "<group>"; };
|
5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 0_Megrez.swift; sourceTree = "<group>"; };
|
||||||
5B2170D9289FACAC00BE7304 /* 8_Unigram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 8_Unigram.swift; sourceTree = "<group>"; };
|
5B2170D9289FACAC00BE7304 /* 8_Unigram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 8_Unigram.swift; sourceTree = "<group>"; };
|
||||||
5B2170DA289FACAC00BE7304 /* 3_Candidate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 3_Candidate.swift; sourceTree = "<group>"; };
|
5B2170DA289FACAC00BE7304 /* 3_KeyValuePaired.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 3_KeyValuePaired.swift; sourceTree = "<group>"; };
|
||||||
5B2170DB289FACAC00BE7304 /* 2_Walker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 2_Walker.swift; sourceTree = "<group>"; };
|
5B2170DB289FACAC00BE7304 /* 2_Walker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 2_Walker.swift; sourceTree = "<group>"; };
|
||||||
5B2170DC289FACAC00BE7304 /* 6_Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6_Node.swift; sourceTree = "<group>"; };
|
5B2170DC289FACAC00BE7304 /* 6_Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6_Node.swift; sourceTree = "<group>"; };
|
||||||
5B2170DD289FACAC00BE7304 /* 4_Span.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 4_Span.swift; sourceTree = "<group>"; };
|
5B2170DD289FACAC00BE7304 /* 4_Span.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 4_Span.swift; sourceTree = "<group>"; };
|
||||||
|
@ -248,9 +247,7 @@
|
||||||
5B5948CD289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMInstantiator_DateTimeExtension.swift; sourceTree = "<group>"; };
|
5B5948CD289CC04500C85824 /* LMInstantiator_DateTimeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LMInstantiator_DateTimeExtension.swift; sourceTree = "<group>"; };
|
||||||
5B5E535127EF261400C6AA1E /* IME.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = IME.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B5E535127EF261400C6AA1E /* IME.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = IME.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B62A32827AE77D100A19448 /* FSEventStreamHelper.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = FSEventStreamHelper.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B62A32827AE77D100A19448 /* FSEventStreamHelper.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = FSEventStreamHelper.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B62A33127AE792F00A19448 /* InputSourceHelper.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputSourceHelper.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
|
||||||
5B62A33527AE795800A19448 /* mgrPrefs.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = mgrPrefs.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B62A33527AE795800A19448 /* mgrPrefs.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = mgrPrefs.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B62A33727AE79CD00A19448 /* StringUtils.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = StringUtils.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
|
||||||
5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlAboutWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlAboutWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B62A34027AE7CD900A19448 /* ctlCandidate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlCandidate.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B62A34027AE7CD900A19448 /* ctlCandidate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlCandidate.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5B62A34327AE7CD900A19448 /* TooltipController.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = TooltipController.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5B62A34327AE7CD900A19448 /* TooltipController.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = TooltipController.swift; sourceTree = "<group>"; 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 = "<group>"; tabWidth = 2; usesTabs = 0; };
|
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlInputMethod_Menu.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = "<group>"; };
|
5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = "<group>"; };
|
||||||
5BBBB75E27AED54C0023B93A /* Fart.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.m4a; sourceTree = "<group>"; };
|
5BBBB75E27AED54C0023B93A /* Fart.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.m4a; sourceTree = "<group>"; };
|
||||||
5BBBB76627AED5DB0023B93A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmNonModalAlertWindow.xib; sourceTree = "<group>"; };
|
|
||||||
5BBBB76A27AED5DB0023B93A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmAboutWindow.xib; sourceTree = "<group>"; };
|
5BBBB76A27AED5DB0023B93A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmAboutWindow.xib; sourceTree = "<group>"; };
|
||||||
5BBBB76F27AED70B0023B93A /* MenuIcon-TCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM@2x.png"; sourceTree = "<group>"; };
|
5BBBB76F27AED70B0023B93A /* MenuIcon-TCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM@2x.png"; sourceTree = "<group>"; };
|
||||||
5BBBB77027AED70B0023B93A /* MenuIcon-SCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM@2x.png"; sourceTree = "<group>"; };
|
5BBBB77027AED70B0023B93A /* MenuIcon-SCVIM@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM@2x.png"; sourceTree = "<group>"; };
|
||||||
|
@ -346,6 +342,8 @@
|
||||||
5BEDB720283B4AEA0078EB25 /* data-cht.plist */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "data-cht.plist"; path = "Data/data-cht.plist"; sourceTree = "<group>"; };
|
5BEDB720283B4AEA0078EB25 /* data-cht.plist */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "data-cht.plist"; path = "Data/data-cht.plist"; sourceTree = "<group>"; };
|
||||||
5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = "<group>"; };
|
5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = "<group>"; };
|
||||||
5BF255CD28B2694E003ECB60 /* vChewing-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "vChewing-Bridging-Header.h"; sourceTree = "<group>"; };
|
5BF255CD28B2694E003ECB60 /* vChewing-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "vChewing-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
5BF56F9728C39A2700DD6839 /* IMEState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMEState.swift; sourceTree = "<group>"; };
|
||||||
|
5BF56F9928C39D1800DD6839 /* IMEStateData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IMEStateData.swift; sourceTree = "<group>"; };
|
||||||
5BF9DA2228840E6200DBD48E /* template-usersymbolphrases.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-usersymbolphrases.txt"; sourceTree = "<group>"; usesTabs = 0; };
|
5BF9DA2228840E6200DBD48E /* template-usersymbolphrases.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-usersymbolphrases.txt"; sourceTree = "<group>"; usesTabs = 0; };
|
||||||
5BF9DA2328840E6200DBD48E /* template-exclusions.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-exclusions.txt"; sourceTree = "<group>"; usesTabs = 0; };
|
5BF9DA2328840E6200DBD48E /* template-exclusions.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "template-exclusions.txt"; sourceTree = "<group>"; 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 = "<group>"; 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 = "<group>"; usesTabs = 0; };
|
||||||
|
@ -366,10 +364,8 @@
|
||||||
6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Installer-Info.plist"; path = "Installer/Installer-Info.plist"; sourceTree = SOURCE_ROOT; };
|
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 = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D427F76B278CA1BA004A2160 /* AppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppDelegate.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputState.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
|
||||||
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = main.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = main.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlPrefWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlPrefWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlNonModalAlertWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
|
||||||
D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlInputMethod_Core.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D4A13D5927A59D5C003BE359 /* ctlInputMethod_Core.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlInputMethod_Core.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D4E33D8927A838CF006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; };
|
D4E33D8927A838CF006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
D4E33D8E27A838F0006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
D4E33D8E27A838F0006DB1CF /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
|
@ -498,14 +494,14 @@
|
||||||
5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */,
|
5B21176D28753B35000443A9 /* ctlInputMethod_HandleDisplay.swift */,
|
||||||
5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */,
|
5B21176B287539BB000443A9 /* ctlInputMethod_HandleStates.swift */,
|
||||||
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
||||||
D461B791279DAC010070E734 /* InputState.swift */,
|
5BF56F9728C39A2700DD6839 /* IMEState.swift */,
|
||||||
|
5BF56F9928C39D1800DD6839 /* IMEStateData.swift */,
|
||||||
5BD0113C2818543900609769 /* KeyHandler_Core.swift */,
|
5BD0113C2818543900609769 /* KeyHandler_Core.swift */,
|
||||||
5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */,
|
5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */,
|
||||||
5BE3779F288FED8D0037365B /* KeyHandler_HandleComposition.swift */,
|
5BE3779F288FED8D0037365B /* KeyHandler_HandleComposition.swift */,
|
||||||
5B7F225C2808501000DDD3CB /* KeyHandler_HandleInput.swift */,
|
5B7F225C2808501000DDD3CB /* KeyHandler_HandleInput.swift */,
|
||||||
5B3133BE280B229700A4A505 /* KeyHandler_States.swift */,
|
5B3133BE280B229700A4A505 /* KeyHandler_States.swift */,
|
||||||
5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */,
|
5BF0B84B28C070B000795FC6 /* NSEventExtension.swift */,
|
||||||
5B62A33727AE79CD00A19448 /* StringUtils.swift */,
|
|
||||||
5BAA8FBD282CAF380066C406 /* SyllableComposer.swift */,
|
5BAA8FBD282CAF380066C406 /* SyllableComposer.swift */,
|
||||||
);
|
);
|
||||||
path = ControllerModules;
|
path = ControllerModules;
|
||||||
|
@ -524,7 +520,7 @@
|
||||||
children = (
|
children = (
|
||||||
5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */,
|
5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */,
|
||||||
5B5E535127EF261400C6AA1E /* IME.swift */,
|
5B5E535127EF261400C6AA1E /* IME.swift */,
|
||||||
5B62A33127AE792F00A19448 /* InputSourceHelper.swift */,
|
5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */,
|
||||||
5B62A33527AE795800A19448 /* mgrPrefs.swift */,
|
5B62A33527AE795800A19448 /* mgrPrefs.swift */,
|
||||||
);
|
);
|
||||||
path = IMEModules;
|
path = IMEModules;
|
||||||
|
@ -586,7 +582,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */,
|
5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */,
|
||||||
D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */,
|
|
||||||
D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */,
|
D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */,
|
||||||
);
|
);
|
||||||
path = WindowControllers;
|
path = WindowControllers;
|
||||||
|
@ -596,7 +591,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */,
|
5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */,
|
||||||
5BBBB76527AED5DB0023B93A /* frmNonModalAlertWindow.xib */,
|
|
||||||
5B7BC4AE27AFFBE800F66C24 /* frmPrefWindow.xib */,
|
5B7BC4AE27AFFBE800F66C24 /* frmPrefWindow.xib */,
|
||||||
6A187E2816004C5900466B2E /* MainMenu.xib */,
|
6A187E2816004C5900466B2E /* MainMenu.xib */,
|
||||||
);
|
);
|
||||||
|
@ -867,7 +861,7 @@
|
||||||
5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */,
|
5B2170D8289FACAC00BE7304 /* 0_Megrez.swift */,
|
||||||
5B2170DE289FACAC00BE7304 /* 1_Compositor.swift */,
|
5B2170DE289FACAC00BE7304 /* 1_Compositor.swift */,
|
||||||
5B2170DB289FACAC00BE7304 /* 2_Walker.swift */,
|
5B2170DB289FACAC00BE7304 /* 2_Walker.swift */,
|
||||||
5B2170DA289FACAC00BE7304 /* 3_Candidate.swift */,
|
5B2170DA289FACAC00BE7304 /* 3_KeyValuePaired.swift */,
|
||||||
5B2170DD289FACAC00BE7304 /* 4_Span.swift */,
|
5B2170DD289FACAC00BE7304 /* 4_Span.swift */,
|
||||||
5B2170DF289FACAC00BE7304 /* 5_Vertex.swift */,
|
5B2170DF289FACAC00BE7304 /* 5_Vertex.swift */,
|
||||||
5B2170DC289FACAC00BE7304 /* 6_Node.swift */,
|
5B2170DC289FACAC00BE7304 /* 6_Node.swift */,
|
||||||
|
@ -1070,7 +1064,6 @@
|
||||||
5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */,
|
5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */,
|
||||||
6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */,
|
6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */,
|
||||||
D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */,
|
D4E33D8F27A838F0006DB1CF /* InfoPlist.strings in Resources */,
|
||||||
5BBBB76B27AED5DB0023B93A /* frmNonModalAlertWindow.xib in Resources */,
|
|
||||||
5BEDB723283B4C250078EB25 /* data-cht.plist in Resources */,
|
5BEDB723283B4C250078EB25 /* data-cht.plist in Resources */,
|
||||||
5BEDB721283B4C250078EB25 /* data-cns.plist in Resources */,
|
5BEDB721283B4C250078EB25 /* data-cns.plist in Resources */,
|
||||||
5BF9DA2D288427E000DBD48E /* template-associatedPhrases-cht.txt in Resources */,
|
5BF9DA2D288427E000DBD48E /* template-associatedPhrases-cht.txt in Resources */,
|
||||||
|
@ -1205,7 +1198,7 @@
|
||||||
5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */,
|
5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */,
|
||||||
5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */,
|
5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */,
|
||||||
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,
|
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,
|
||||||
D461B792279DAC010070E734 /* InputState.swift in Sources */,
|
5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */,
|
||||||
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */,
|
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */,
|
||||||
D47B92C027972AD100458394 /* main.swift in Sources */,
|
D47B92C027972AD100458394 /* main.swift in Sources */,
|
||||||
D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */,
|
D4A13D5A27A59F0B003BE359 /* ctlInputMethod_Core.swift in Sources */,
|
||||||
|
@ -1217,7 +1210,6 @@
|
||||||
5B84579F2871AD2200C93B01 /* HotenkaChineseConverter.swift in Sources */,
|
5B84579F2871AD2200C93B01 /* HotenkaChineseConverter.swift in Sources */,
|
||||||
5B887F302826AEA400B6651E /* lmCoreEX.swift in Sources */,
|
5B887F302826AEA400B6651E /* lmCoreEX.swift in Sources */,
|
||||||
5BA9FD4627FEF3C9002DE248 /* Container.swift in Sources */,
|
5BA9FD4627FEF3C9002DE248 /* Container.swift in Sources */,
|
||||||
D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */,
|
|
||||||
5B2170E5289FACAD00BE7304 /* 6_Node.swift in Sources */,
|
5B2170E5289FACAD00BE7304 /* 6_Node.swift in Sources */,
|
||||||
5B949BD92816DC5400D87B5D /* LineReader.swift in Sources */,
|
5B949BD92816DC5400D87B5D /* LineReader.swift in Sources */,
|
||||||
5BA9FD1027FEDB6B002DE248 /* suiPrefPaneKeyboard.swift in Sources */,
|
5BA9FD1027FEDB6B002DE248 /* suiPrefPaneKeyboard.swift in Sources */,
|
||||||
|
@ -1232,6 +1224,7 @@
|
||||||
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */,
|
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */,
|
||||||
5BD0113D2818543900609769 /* KeyHandler_Core.swift in Sources */,
|
5BD0113D2818543900609769 /* KeyHandler_Core.swift in Sources */,
|
||||||
5B2170E4289FACAD00BE7304 /* 2_Walker.swift in Sources */,
|
5B2170E4289FACAD00BE7304 /* 2_Walker.swift in Sources */,
|
||||||
|
5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */,
|
||||||
5BA9FD4227FEF3C8002DE248 /* PreferencePane.swift in Sources */,
|
5BA9FD4227FEF3C8002DE248 /* PreferencePane.swift in Sources */,
|
||||||
5BA0DF312817857D009E73BB /* lmUserOverride.swift in Sources */,
|
5BA0DF312817857D009E73BB /* lmUserOverride.swift in Sources */,
|
||||||
5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */,
|
5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */,
|
||||||
|
@ -1246,17 +1239,16 @@
|
||||||
5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */,
|
5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */,
|
||||||
5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */,
|
5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */,
|
||||||
5B782EC4280C243C007276DE /* KeyHandler_HandleCandidate.swift in Sources */,
|
5B782EC4280C243C007276DE /* KeyHandler_HandleCandidate.swift in Sources */,
|
||||||
5B62A33827AE79CD00A19448 /* StringUtils.swift in Sources */,
|
5B2170E3289FACAD00BE7304 /* 3_KeyValuePaired.swift in Sources */,
|
||||||
5B2170E3289FACAD00BE7304 /* 3_Candidate.swift in Sources */,
|
|
||||||
5BA9FD0F27FEDB6B002DE248 /* suiPrefPaneGeneral.swift in Sources */,
|
5BA9FD0F27FEDB6B002DE248 /* suiPrefPaneGeneral.swift in Sources */,
|
||||||
5B2170E6289FACAD00BE7304 /* 4_Span.swift in Sources */,
|
5B2170E6289FACAD00BE7304 /* 4_Span.swift in Sources */,
|
||||||
|
5B175FFB28C5CDDC0078D1B4 /* IMKHelper.swift in Sources */,
|
||||||
5BA9FD4927FEF3C9002DE248 /* Section.swift in Sources */,
|
5BA9FD4927FEF3C9002DE248 /* Section.swift in Sources */,
|
||||||
5BA9FD3E27FEF3C8002DE248 /* Utilities.swift in Sources */,
|
5BA9FD3E27FEF3C8002DE248 /* Utilities.swift in Sources */,
|
||||||
5B242403284B0D6500520FE4 /* ctlCandidateUniversal.swift in Sources */,
|
5B242403284B0D6500520FE4 /* ctlCandidateUniversal.swift in Sources */,
|
||||||
5BA9FD1127FEDB6B002DE248 /* ctlPrefUI.swift in Sources */,
|
5BA9FD1127FEDB6B002DE248 /* ctlPrefUI.swift in Sources */,
|
||||||
5B8457A12871ADBE00C93B01 /* HotenkaCCBridge.swift in Sources */,
|
5B8457A12871ADBE00C93B01 /* HotenkaCCBridge.swift in Sources */,
|
||||||
5B40730D281672610023DFFF /* lmReplacements.swift in Sources */,
|
5B40730D281672610023DFFF /* lmReplacements.swift in Sources */,
|
||||||
5B62A33227AE792F00A19448 /* InputSourceHelper.swift in Sources */,
|
|
||||||
5B5E535227EF261400C6AA1E /* IME.swift in Sources */,
|
5B5E535227EF261400C6AA1E /* IME.swift in Sources */,
|
||||||
5B2170E0289FACAD00BE7304 /* 7_LangModel.swift in Sources */,
|
5B2170E0289FACAD00BE7304 /* 7_LangModel.swift in Sources */,
|
||||||
5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */,
|
5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */,
|
||||||
|
@ -1285,7 +1277,7 @@
|
||||||
files = (
|
files = (
|
||||||
D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */,
|
D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */,
|
||||||
D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */,
|
D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */,
|
||||||
5B62A35327AE89C400A19448 /* InputSourceHelper.swift in Sources */,
|
5BF13B9428C627BB00E99EC1 /* IMKHelper.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1344,14 +1336,6 @@
|
||||||
name = frmPrefWindow.xib;
|
name = frmPrefWindow.xib;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
5BBBB76527AED5DB0023B93A /* frmNonModalAlertWindow.xib */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
5BBBB76627AED5DB0023B93A /* Base */,
|
|
||||||
);
|
|
||||||
name = frmNonModalAlertWindow.xib;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */ = {
|
5BBBB76927AED5DB0023B93A /* frmAboutWindow.xib */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1471,7 +1455,7 @@
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
@ -1481,7 +1465,7 @@
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewingTests;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewingTests;
|
||||||
|
@ -1510,13 +1494,13 @@
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewingTests;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewingTests;
|
||||||
|
@ -1548,7 +1532,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
@ -1570,7 +1554,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor;
|
||||||
|
@ -1600,7 +1584,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
@ -1618,7 +1602,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingPhraseEditor;
|
||||||
|
@ -1734,7 +1718,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1763,7 +1747,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -1793,7 +1777,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
|
@ -1816,7 +1800,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.inputmethod.vChewing;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -1840,7 +1824,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
@ -1861,7 +1845,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingInstaller;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingInstaller;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -1884,7 +1868,7 @@
|
||||||
CODE_SIGN_IDENTITY = "-";
|
CODE_SIGN_IDENTITY = "-";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 2310;
|
CURRENT_PROJECT_VERSION = 2400;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
@ -1899,7 +1883,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.3.1;
|
MARKETING_VERSION = 2.4.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingInstaller;
|
PRODUCT_BUNDLE_IDENTIFIER = org.atelierInmu.vChewing.vChewingInstaller;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
Loading…
Reference in New Issue