Pre Merge pull request !26 from ShikiSuen/upd/1.5.4

This commit is contained in:
ShikiSuen 2022-05-05 05:30:53 +00:00 committed by Gitee
commit df42ddade4
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
148 changed files with 12953 additions and 14696 deletions

View File

@ -3,7 +3,7 @@
"accessLevel" : "private" "accessLevel" : "private"
}, },
"indentation" : { "indentation" : {
"tabs" : 1 "spaces" : 2
}, },
"indentConditionalCompilationBlocks" : true, "indentConditionalCompilationBlocks" : true,
"indentSwitchCaseLabels" : true, "indentSwitchCaseLabels" : true,
@ -51,6 +51,6 @@
"UseWhereClausesInForLoops" : false, "UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false "ValidateDocumentationComments" : false
}, },
"tabWidth" : 4, "tabWidth" : 8,
"version" : 1 "version" : 1
} }

View File

@ -27,18 +27,18 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension String { extension String {
fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") { fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914 // Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do { do {
let regex = try NSRegularExpression( let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines] pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
) )
let range = NSRange(startIndex..., in: self) let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches( self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith in: self, options: [], range: range, withTemplate: replaceWith
) )
} catch { return } } catch { return }
} }
} }
var verMarket: String = "1.0.0" var verMarket: String = "1.0.0"
@ -50,39 +50,39 @@ var dirUpdateInfoPlist = "./Update-Info.plist"
var theDictionary: NSDictionary? var theDictionary: NSDictionary?
if CommandLine.arguments.count == 3 { if CommandLine.arguments.count == 3 {
verMarket = CommandLine.arguments[1] verMarket = CommandLine.arguments[1]
verBuild = CommandLine.arguments[2] verBuild = CommandLine.arguments[2]
// Xcode project file version update. // Xcode project file version update.
do { do {
strXcodeProjContent += try String(contentsOfFile: dirXcodeProjectFile, encoding: .utf8) strXcodeProjContent += try String(contentsOfFile: dirXcodeProjectFile, encoding: .utf8)
} catch { } catch {
NSLog(" - Exception happened when reading raw phrases data.") NSLog(" - Exception happened when reading raw phrases data.")
} }
strXcodeProjContent.regReplace( strXcodeProjContent.regReplace(
pattern: #"CURRENT_PROJECT_VERSION = .*$"#, replaceWith: "CURRENT_PROJECT_VERSION = " + verBuild + ";" pattern: #"CURRENT_PROJECT_VERSION = .*$"#, replaceWith: "CURRENT_PROJECT_VERSION = " + verBuild + ";"
) )
strXcodeProjContent.regReplace( strXcodeProjContent.regReplace(
pattern: #"MARKETING_VERSION = .*$"#, replaceWith: "MARKETING_VERSION = " + verMarket + ";" pattern: #"MARKETING_VERSION = .*$"#, replaceWith: "MARKETING_VERSION = " + verMarket + ";"
) )
do { do {
try strXcodeProjContent.write(to: URL(fileURLWithPath: dirXcodeProjectFile), atomically: false, encoding: .utf8) try strXcodeProjContent.write(to: URL(fileURLWithPath: dirXcodeProjectFile), atomically: false, encoding: .utf8)
} catch { } catch {
NSLog(" -: Error on writing strings to file: \(error)") NSLog(" -: Error on writing strings to file: \(error)")
} }
NSLog(" - Xcode 專案版本資訊更新完成:\(verMarket) \(verBuild)") NSLog(" - Xcode 專案版本資訊更新完成:\(verMarket) \(verBuild)")
// Packages project file version update. // Packages project file version update.
theDictionary = NSDictionary(contentsOfFile: dirPackageProjectFile) theDictionary = NSDictionary(contentsOfFile: dirPackageProjectFile)
theDictionary?.setValue(verMarket, forKeyPath: "PACKAGES.PACKAGE_SETTINGS.VERSION") theDictionary?.setValue(verMarket, forKeyPath: "PACKAGES.PACKAGE_SETTINGS.VERSION")
theDictionary?.write(toFile: dirPackageProjectFile, atomically: true) theDictionary?.write(toFile: dirPackageProjectFile, atomically: true)
NSLog(" - Packages 專案版本資訊更新完成:\(verMarket) \(verBuild)") NSLog(" - Packages 專案版本資訊更新完成:\(verMarket) \(verBuild)")
// Update notification project file version update. // Update notification project file version update.
theDictionary = NSDictionary(contentsOfFile: dirUpdateInfoPlist) theDictionary = NSDictionary(contentsOfFile: dirUpdateInfoPlist)
theDictionary?.setValue(verBuild, forKeyPath: "CFBundleVersion") theDictionary?.setValue(verBuild, forKeyPath: "CFBundleVersion")
theDictionary?.setValue(verMarket, forKeyPath: "CFBundleShortVersionString") theDictionary?.setValue(verMarket, forKeyPath: "CFBundleShortVersionString")
theDictionary?.write(toFile: dirUpdateInfoPlist, atomically: true) theDictionary?.write(toFile: dirUpdateInfoPlist, atomically: true)
NSLog(" - 更新用通知 plist 版本資訊更新完成:\(verMarket) \(verBuild)") NSLog(" - 更新用通知 plist 版本資訊更新完成:\(verMarket) \(verBuild)")
} }

View File

@ -1,14 +1,14 @@
# 威注音輸入法研發參與相關說明 # 威注音輸入法研發參與相關說明
威注音輸入法歡迎有人參與。但為了不讓參與者們浪費各自的熱情,特設此文以說明該專案目前最需要協助的地方 威注音輸入法歡迎有熱心的志願者們參與
1. 有人能用 Swift 將該專案內這兩個源自 LibFormosa 的組件套件重寫: 威注音目前的 codebase 更能代表一個先進的 macOS 輸入法雛形專案的形態。目前的 dev 分支除了 Mandarin 模組(以及其與 KeyHandler 的對接的部分)以外被威注音使用的部分全都是清一色的 Swift codebase一目了然方便他人參與比某些其它開源品牌旗下的專案更具程式方面的生命力。為什麼這樣講呢那些傳統開源品牌的專案主要使用 C++ 這門不太友好的語言Mandarin 模組現在對我而言仍舊是天書,一大堆針對記憶體指針的操作完全看不懂。搞不清楚在這一層之上的功能邏輯的話,就無法制定 Swift 版的 coding 策略),這也是我這次用 Swift 重寫了語言模型引擎的原因(也是為後來者行方便)。
為了不讓參與者們浪費各自的熱情,特設此文以說明該專案目前最需要協助的地方。
1. 有人能用 Swift 將該專案內的這個源自 LibFormosa 的組件套件重寫:
- Mandarin 組件,用以分析普通話音韻數據、創建且控制 Syllable Composer 注音拼識組件。 - Mandarin 組件,用以分析普通話音韻數據、創建且控制 Syllable Composer 注音拼識組件。
- Gramambular 套裝,這包括了 Source 資料夾下的其餘全部的 (Obj)C(++) 檔案LMConsolidator 除外)。 - 一堆記憶體指針操作,實在看不懂這個組件的處理邏輯是什麼,無能為力。
- LMConsolidator 有 Swift 版本,已經用於威注音語彙編輯器內。給主程式用 C++ 版本僅為了與 Gramambular 協作方便。
- 這也包括了所有與 Language Model 有關的實現,因為都是 Gramambular 內的某個語言模組 Protocol 衍生出來的東西。
- LMInstantiator 是用來將語言模組副本化的組件,原本不屬於 Gramambular但與其衍生的各類語言模組高度耦合。
- KeyValueBlobReader 不屬於 Gramambular但與其衍生的各類語言模組高度耦合、也與 KeyHandler 高度耦合。
2. 讓 Alt+波浪鍵選單能夠在諸如 MS Word 以及終端機內正常工作(可以用方向鍵控制高亮候選內容,等)。 2. 讓 Alt+波浪鍵選單能夠在諸如 MS Word 以及終端機內正常工作(可以用方向鍵控制高亮候選內容,等)。
- 原理上而言恐怕得欺騙當前正在接受輸入的應用、使其誤以為當前有組字區。這只是推測。 - 原理上而言恐怕得欺騙當前正在接受輸入的應用、使其誤以為當前有組字區。這只是推測。
3. SQLite 實現。 3. SQLite 實現。
@ -25,11 +25,9 @@
該專案對源碼格式有規範,且 Swift 與其他 (Obj)C(++) 系語言持不同規範: 該專案對源碼格式有規範,且 Swift 與其他 (Obj)C(++) 系語言持不同規範:
- Swift: 採 [Apple 官方 Swift-Format](https://github.com/apple/swift-format),且施加如下例外修改項目: - Swift: 採 [Apple 官方 Swift-Format](https://github.com/apple/swift-format),且施加如下例外修改項目:
- Indentation 僅使用 `"indentation" : { "tabs" : 1 },`,不以空格來縮進。
- `"indentSwitchCaseLabels" : true,` - `"indentSwitchCaseLabels" : true,`
- `"lineLength" : 120,` - `"lineLength" : 120,`
- `"NoBlockComments" : false,` - `"NoBlockComments" : false,`
- `"tabWidth" : 4,`
- `"OnlyOneTrailingClosureArgument" : false,` // SwiftUI 相容 - `"OnlyOneTrailingClosureArgument" : false,` // SwiftUI 相容
- `"UseTripleSlashForDocumentationComments" : false,` - `"UseTripleSlashForDocumentationComments" : false,`
- `"DontRepeatTypeInStaticProperties" : false,` - `"DontRepeatTypeInStaticProperties" : false,`
@ -37,6 +35,6 @@
- 該規範以四個西文半形空格為行縮進單位。 - 該規範以四個西文半形空格為行縮進單位。
- 由於今後不會再用這類語言給該倉庫新增內容,所以相關規範就不改動了。 - 由於今後不會再用這類語言給該倉庫新增內容,所以相關規範就不改動了。
至於對 Swift 檔案改採 1-Tab 縮進,則是為了在尊重所有用戶的需求的同時、最大程度上節約檔案體積。使用者可自行修改 Xcode 的預設 Tab 縮進尺寸 之前,為了節省檔案體積,曾經對 Swift 檔案改採 1-Tab 縮進。然而,這會導致 Gitee 等線上 git 專案管理網站內的顯示變成 8-Space 縮進。於是,該專案對 Swift 檔案又改回了 2-Spaces 縮進
$ EOF. $ EOF.

View File

@ -29,55 +29,55 @@ import Foundation
// MARK: - // MARK: -
extension String { extension String {
fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") { fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914 // Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do { do {
let regex = try NSRegularExpression( let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines] pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
) )
let range = NSRange(startIndex..., in: self) let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches( self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith in: self, options: [], range: range, withTemplate: replaceWith
) )
} catch { return } } catch { return }
} }
} }
// MARK: - // MARK: -
// Ref: https://stackoverflow.com/a/32581409/4162914 // Ref: https://stackoverflow.com/a/32581409/4162914
extension Float { extension Float {
fileprivate func rounded(toPlaces places: Int) -> Float { fileprivate func rounded(toPlaces places: Int) -> Float {
let divisor = pow(10.0, Float(places)) let divisor = pow(10.0, Float(places))
return (self * divisor).rounded() / divisor return (self * divisor).rounded() / divisor
} }
} }
// MARK: - // MARK: -
// Ref: https://stackoverflow.com/a/41581695/4162914 // Ref: https://stackoverflow.com/a/41581695/4162914
precedencegroup ExponentiationPrecedence { precedencegroup ExponentiationPrecedence {
associativity: right associativity: right
higherThan: MultiplicationPrecedence higherThan: MultiplicationPrecedence
} }
infix operator **: ExponentiationPrecedence infix operator **: ExponentiationPrecedence
func ** (_ base: Double, _ exp: Double) -> Double { func ** (_ base: Double, _ exp: Double) -> Double {
pow(base, exp) pow(base, exp)
} }
func ** (_ base: Float, _ exp: Float) -> Float { func ** (_ base: Float, _ exp: Float) -> Float {
pow(base, exp) pow(base, exp)
} }
// MARK: - // MARK: -
struct Entry { struct Entry {
var valPhone: String = "" var valPhone: String = ""
var valPhrase: String = "" var valPhrase: String = ""
var valWeight: Float = -1.0 var valWeight: Float = -1.0
var valCount: Int = 0 var valCount: Int = 0
} }
// MARK: - // MARK: -
@ -105,322 +105,322 @@ private let urlOutputCHT: String = "./data-cht.txt"
// MARK: - // MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Entry] { func rawDictForPhrases(isCHS: Bool) -> [Entry] {
var arrEntryRAW: [Entry] = [] var arrEntryRAW: [Entry] = []
var strRAW = "" var strRAW = ""
let urlCustom: String = isCHS ? urlCHSforCustom : urlCHTforCustom let urlCustom: String = isCHS ? urlCHSforCustom : urlCHTforCustom
let urlMCBP: String = isCHS ? urlCHSforMCBP : urlCHTforMCBP let urlMCBP: String = isCHS ? urlCHSforMCBP : urlCHTforMCBP
let urlMOE: String = isCHS ? urlCHSforMOE : urlCHTforMOE let urlMOE: String = isCHS ? urlCHSforMOE : urlCHTforMOE
let urlVCHEW: String = isCHS ? urlCHSforVCHEW : urlCHTforVCHEW let urlVCHEW: String = isCHS ? urlCHSforVCHEW : urlCHTforVCHEW
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
// //
do { do {
strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8) strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlMCBP, encoding: .utf8) strRAW += try String(contentsOfFile: urlMCBP, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8) strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8) strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8)
} catch { } catch {
NSLog(" - Exception happened when reading raw phrases data.") NSLog(" - Exception happened when reading raw phrases data.")
return [] return []
} }
// //
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
// CJKWhiteSpace (\x{3000}) to ASCII Space // CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space // NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space // Tab to ASCII Space
// ASCII // ASCII
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32 strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
// //
let arrData = Array( let arrData = Array(
NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
for lineData in arrData { for lineData in arrData {
// //
let arrLineData = lineData.components(separatedBy: " ") let arrLineData = lineData.components(separatedBy: " ")
var varLineDataProcessed = "" var varLineDataProcessed = ""
var count = 0 var count = 0
for currentCell in arrLineData { for currentCell in arrLineData {
count += 1 count += 1
if count < 3 { if count < 3 {
varLineDataProcessed += currentCell + "\t" varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count { } else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-" varLineDataProcessed += currentCell + "-"
} else { } else {
varLineDataProcessed += currentCell varLineDataProcessed += currentCell
} }
} }
// Entry // Entry
let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t") let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 // count = 0 //
var phone = "" var phone = ""
var phrase = "" var phrase = ""
var occurrence = 0 var occurrence = 0
for cell in arrCells { for cell in arrCells {
count += 1 count += 1
switch count { switch count {
case 1: phrase = cell case 1: phrase = cell
case 3: phone = cell case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0 case 2: occurrence = Int(cell) ?? 0
default: break default: break
} }
} }
if phrase != "" { // if phrase != "" { //
arrEntryRAW += [ arrEntryRAW += [
Entry( Entry(
valPhone: phone, valPhrase: phrase, valWeight: 0.0, valPhone: phone, valPhrase: phrase, valWeight: 0.0,
valCount: occurrence valCount: occurrence
) )
] ]
} }
} }
NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。") NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。")
return arrEntryRAW return arrEntryRAW
} }
// MARK: - // MARK: -
func rawDictForKanjis(isCHS: Bool) -> [Entry] { func rawDictForKanjis(isCHS: Bool) -> [Entry] {
var arrEntryRAW: [Entry] = [] var arrEntryRAW: [Entry] = []
var strRAW = "" var strRAW = ""
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
// //
do { do {
strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8) strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8)
} catch { } catch {
NSLog(" - Exception happened when reading raw core kanji data.") NSLog(" - Exception happened when reading raw core kanji data.")
return [] return []
} }
// //
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
// CJKWhiteSpace (\x{3000}) to ASCII Space // CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space // NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space // Tab to ASCII Space
// ASCII // ASCII
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32 strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
// //
let arrData = Array( let arrData = Array(
NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
var varLineData = "" var varLineData = ""
for lineData in arrData { for lineData in arrData {
// 1,2,4 1,3,4 // 1,2,4 1,3,4
let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1) let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1)
.joined( .joined(
separator: "\t") separator: "\t")
let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2) let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2)
.joined( .joined(
separator: "\t") separator: "\t")
varLineData = varLineDataPre + "\t" + varLineDataPost varLineData = varLineDataPre + "\t" + varLineDataPost
let arrLineData = varLineData.components(separatedBy: " ") let arrLineData = varLineData.components(separatedBy: " ")
var varLineDataProcessed = "" var varLineDataProcessed = ""
var count = 0 var count = 0
for currentCell in arrLineData { for currentCell in arrLineData {
count += 1 count += 1
if count < 3 { if count < 3 {
varLineDataProcessed += currentCell + "\t" varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count { } else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-" varLineDataProcessed += currentCell + "-"
} else { } else {
varLineDataProcessed += currentCell varLineDataProcessed += currentCell
} }
} }
// Entry // Entry
let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t") let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 // count = 0 //
var phone = "" var phone = ""
var phrase = "" var phrase = ""
var occurrence = 0 var occurrence = 0
for cell in arrCells { for cell in arrCells {
count += 1 count += 1
switch count { switch count {
case 1: phrase = cell case 1: phrase = cell
case 3: phone = cell case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0 case 2: occurrence = Int(cell) ?? 0
default: break default: break
} }
} }
if phrase != "" { // if phrase != "" { //
arrEntryRAW += [ arrEntryRAW += [
Entry( Entry(
valPhone: phone, valPhrase: phrase, valWeight: 0.0, valPhone: phone, valPhrase: phrase, valWeight: 0.0,
valCount: occurrence valCount: occurrence
) )
] ]
} }
} }
NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。") NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。")
return arrEntryRAW return arrEntryRAW
} }
// MARK: - // MARK: -
func rawDictForNonKanjis(isCHS: Bool) -> [Entry] { func rawDictForNonKanjis(isCHS: Bool) -> [Entry] {
var arrEntryRAW: [Entry] = [] var arrEntryRAW: [Entry] = []
var strRAW = "" var strRAW = ""
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
// //
do { do {
strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8) strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8) strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8)
} catch { } catch {
NSLog(" - Exception happened when reading raw core kanji data.") NSLog(" - Exception happened when reading raw core kanji data.")
return [] return []
} }
// //
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
// CJKWhiteSpace (\x{3000}) to ASCII Space // CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space // NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space // Tab to ASCII Space
// ASCII // ASCII
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32 strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
// //
let arrData = Array( let arrData = Array(
NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
var varLineData = "" var varLineData = ""
for lineData in arrData { for lineData in arrData {
varLineData = lineData varLineData = lineData
// //
varLineData = varLineData.components(separatedBy: " ").prefix(3).joined( varLineData = varLineData.components(separatedBy: " ").prefix(3).joined(
separator: "\t") // separator: "\t") //
let arrLineData = varLineData.components(separatedBy: " ") let arrLineData = varLineData.components(separatedBy: " ")
var varLineDataProcessed = "" var varLineDataProcessed = ""
var count = 0 var count = 0
for currentCell in arrLineData { for currentCell in arrLineData {
count += 1 count += 1
if count < 3 { if count < 3 {
varLineDataProcessed += currentCell + "\t" varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count { } else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-" varLineDataProcessed += currentCell + "-"
} else { } else {
varLineDataProcessed += currentCell varLineDataProcessed += currentCell
} }
} }
// Entry // Entry
let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t") let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 // count = 0 //
var phone = "" var phone = ""
var phrase = "" var phrase = ""
var occurrence = 0 var occurrence = 0
for cell in arrCells { for cell in arrCells {
count += 1 count += 1
switch count { switch count {
case 1: phrase = cell case 1: phrase = cell
case 3: phone = cell case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0 case 2: occurrence = Int(cell) ?? 0
default: break default: break
} }
} }
if phrase != "" { // if phrase != "" { //
arrEntryRAW += [ arrEntryRAW += [
Entry( Entry(
valPhone: phone, valPhrase: phrase, valWeight: 0.0, valPhone: phone, valPhrase: phrase, valWeight: 0.0,
valCount: occurrence valCount: occurrence
) )
] ]
} }
} }
NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。") NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。")
return arrEntryRAW return arrEntryRAW
} }
func weightAndSort(_ arrStructUncalculated: [Entry], isCHS: Bool) -> [Entry] { func weightAndSort(_ arrStructUncalculated: [Entry], isCHS: Bool) -> [Entry] {
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
var arrStructCalculated: [Entry] = [] var arrStructCalculated: [Entry] = []
let fscale: Float = 2.7 let fscale: Float = 2.7
var norm: Float = 0.0 var norm: Float = 0.0
for entry in arrStructUncalculated { for entry in arrStructUncalculated {
if entry.valCount >= 0 { if entry.valCount >= 0 {
norm += fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) norm += fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0)
* Float(entry.valCount) * Float(entry.valCount)
} }
} }
// norm norm // norm norm
// //
// 1 0 0.5 // 1 0 0.5
for entry in arrStructUncalculated { for entry in arrStructUncalculated {
var weight: Float = 0 var weight: Float = 0
switch entry.valCount { switch entry.valCount {
case -2: // case -2: //
weight = -13 weight = -13
case -1: // case -1: //
weight = -13 weight = -13
case 0: // case 0: //
weight = log10( weight = log10(
fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) * 0.5 / norm) fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) * 0.5 / norm)
default: default:
weight = log10( weight = log10(
fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0)
* Float(entry.valCount) / norm) // Credit: MJHsieh. * Float(entry.valCount) / norm) // Credit: MJHsieh.
} }
let weightRounded: Float = weight.rounded(toPlaces: 3) // let weightRounded: Float = weight.rounded(toPlaces: 3) //
arrStructCalculated += [ arrStructCalculated += [
Entry( Entry(
valPhone: entry.valPhone, valPhrase: entry.valPhrase, valWeight: weightRounded, valPhone: entry.valPhone, valPhrase: entry.valPhrase, valWeight: weightRounded,
valCount: entry.valCount valCount: entry.valCount
) )
] ]
} }
NSLog(" - \(i18n): 成功計算權重。") NSLog(" - \(i18n): 成功計算權重。")
// ========================================== // ==========================================
// //
let arrStructSorted: [Entry] = arrStructCalculated.sorted(by: { lhs, rhs -> Bool in let arrStructSorted: [Entry] = arrStructCalculated.sorted(by: { lhs, rhs -> Bool in
(lhs.valPhone, rhs.valCount) < (rhs.valPhone, lhs.valCount) (lhs.valPhone, rhs.valCount) < (rhs.valPhone, lhs.valCount)
}) })
NSLog(" - \(i18n): 排序整理完畢,準備編譯要寫入的檔案內容。") NSLog(" - \(i18n): 排序整理完畢,準備編譯要寫入的檔案內容。")
return arrStructSorted return arrStructSorted
} }
func fileOutput(isCHS: Bool) { func fileOutput(isCHS: Bool) {
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
let pathOutput = urlCurrentFolder.appendingPathComponent( let pathOutput = urlCurrentFolder.appendingPathComponent(
isCHS ? urlOutputCHS : urlOutputCHT) isCHS ? urlOutputCHS : urlOutputCHT)
var strPrintLine = "" var strPrintLine = ""
// //
do { do {
strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8) strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8)
} catch { } catch {
NSLog(" - \(i18n): Exception happened when reading raw punctuation data.") NSLog(" - \(i18n): Exception happened when reading raw punctuation data.")
} }
NSLog(" - \(i18n): 成功插入標點符號與西文字母數據。") NSLog(" - \(i18n): 成功插入標點符號與西文字母數據。")
// //
var arrStructUnified: [Entry] = [] var arrStructUnified: [Entry] = []
arrStructUnified += rawDictForKanjis(isCHS: isCHS) arrStructUnified += rawDictForKanjis(isCHS: isCHS)
arrStructUnified += rawDictForNonKanjis(isCHS: isCHS) arrStructUnified += rawDictForNonKanjis(isCHS: isCHS)
arrStructUnified += rawDictForPhrases(isCHS: isCHS) arrStructUnified += rawDictForPhrases(isCHS: isCHS)
// //
arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS) arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS)
for entry in arrStructUnified { for entry in arrStructUnified {
strPrintLine += strPrintLine +=
entry.valPhone + " " + entry.valPhrase + " " + String(entry.valWeight) entry.valPhone + " " + entry.valPhrase + " " + String(entry.valWeight)
+ "\n" + "\n"
} }
NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。") NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。")
do { do {
try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8) try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8)
} catch { } catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)") NSLog(" - \(i18n): Error on writing strings to file: \(error)")
} }
NSLog(" - \(i18n): 寫入完成。") NSLog(" - \(i18n): 寫入完成。")
} }
// MARK: - // MARK: -
func main() { func main() {
NSLog("// 準備編譯繁體中文核心語料檔案。") NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false) fileOutput(isCHS: false)
NSLog("// 準備編譯簡體中文核心語料檔案。") NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true) fileOutput(isCHS: true)
} }
main() main()

View File

@ -31,11 +31,11 @@ private let kTargetType = "app"
private let kTargetBundle = "vChewing.app" private let kTargetBundle = "vChewing.app"
private let urlDestinationPartial = FileManager.default.urls( private let urlDestinationPartial = FileManager.default.urls(
for: .inputMethodsDirectory, in: .userDomainMask for: .inputMethodsDirectory, in: .userDomainMask
)[0] )[0]
private let urlTargetPartial = urlDestinationPartial.appendingPathComponent(kTargetBundle) private let urlTargetPartial = urlDestinationPartial.appendingPathComponent(kTargetBundle)
private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS/") private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS/")
.appendingPathComponent(kTargetBin) .appendingPathComponent(kTargetBin)
private let kDestinationPartial = urlDestinationPartial.path private let kDestinationPartial = urlDestinationPartial.path
private let kTargetPartialPath = urlTargetPartial.path private let kTargetPartialPath = urlTargetPartial.path
@ -47,315 +47,315 @@ private let kTranslocationRemovalDeadline: TimeInterval = 60.0
@NSApplicationMain @NSApplicationMain
@objc(AppDelegate) @objc(AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate { class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet private var installButton: NSButton! @IBOutlet private var installButton: NSButton!
@IBOutlet private var cancelButton: NSButton! @IBOutlet private var cancelButton: NSButton!
@IBOutlet private var progressSheet: NSWindow! @IBOutlet private var progressSheet: NSWindow!
@IBOutlet private var progressIndicator: NSProgressIndicator! @IBOutlet private var progressIndicator: NSProgressIndicator!
@IBOutlet private var appVersionLabel: NSTextField! @IBOutlet private var appVersionLabel: NSTextField!
@IBOutlet private var appCopyrightLabel: NSTextField! @IBOutlet private var appCopyrightLabel: NSTextField!
@IBOutlet private var appEULAContent: NSTextView! @IBOutlet private var appEULAContent: NSTextView!
private var archiveUtil: ArchiveUtil? private var archiveUtil: ArchiveUtil?
private var installingVersion = "" private var installingVersion = ""
private var upgrading = false private var upgrading = false
private var translocationRemovalStartTime: Date? private var translocationRemovalStartTime: Date?
private var currentVersionNumber: Int = 0 private var currentVersionNumber: Int = 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
alert.messageText = title alert.messageText = title
alert.informativeText = message alert.informativeText = message
alert.addButton(withTitle: buttonTitle) alert.addButton(withTitle: buttonTitle)
alert.runModal() alert.runModal()
} }
func applicationDidFinishLaunching(_: Notification) { func applicationDidFinishLaunching(_: Notification) {
guard guard
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
as? String, as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
else { else {
return return
} }
self.installingVersion = installingVersion self.installingVersion = installingVersion
archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle) archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle)
_ = archiveUtil?.validateIfNotarizedArchiveExists() _ = archiveUtil?.validateIfNotarizedArchiveExists()
cancelButton.nextKeyView = installButton cancelButton.nextKeyView = installButton
installButton.nextKeyView = cancelButton installButton.nextKeyView = cancelButton
if let cell = installButton.cell as? NSButtonCell { if let cell = installButton.cell as? NSButtonCell {
window?.defaultButtonCell = cell window?.defaultButtonCell = cell
} }
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"]
as? String as? String
{ {
appCopyrightLabel.stringValue = copyrightLabel appCopyrightLabel.stringValue = copyrightLabel
} }
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
appEULAContent.string = eulaContent appEULAContent.string = eulaContent
} }
appVersionLabel.stringValue = String( appVersionLabel.stringValue = String(
format: "%@ Build %@", versionString, installingVersion format: "%@ Build %@", versionString, installingVersion
) )
window?.title = String( window?.title = String(
format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "", format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "",
versionString, installingVersion versionString, installingVersion
) )
window?.standardWindowButton(.closeButton)?.isHidden = true window?.standardWindowButton(.closeButton)?.isHidden = true
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
window?.standardWindowButton(.zoomButton)?.isHidden = true window?.standardWindowButton(.zoomButton)?.isHidden = true
if FileManager.default.fileExists( if FileManager.default.fileExists(
atPath: (kTargetPartialPath as NSString).expandingTildeInPath) atPath: (kTargetPartialPath as NSString).expandingTildeInPath)
{ {
let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath) let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath)
let shortVersion = let shortVersion =
currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = let currentVersion =
currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0 currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0
if shortVersion != nil, let currentVersion = currentVersion, if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{ {
upgrading = true upgrading = true
} }
} }
if upgrading { if upgrading {
installButton.title = NSLocalizedString("Upgrade", comment: "") installButton.title = NSLocalizedString("Upgrade", comment: "")
} }
window?.center() window?.center()
window?.orderFront(self) window?.orderFront(self)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@IBAction func agreeAndInstallAction(_: AnyObject) { @IBAction func agreeAndInstallAction(_: AnyObject) {
cancelButton.isEnabled = false cancelButton.isEnabled = false
installButton.isEnabled = false installButton.isEnabled = false
removeThenInstallInputMethod() removeThenInstallInputMethod()
} }
@objc func timerTick(_ timer: Timer) { @objc func timerTick(_ timer: Timer) {
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date()) let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date())
if elapsed >= kTranslocationRemovalDeadline { if elapsed >= kTranslocationRemovalDeadline {
timer.invalidate() timer.invalidate()
window?.endSheet(progressSheet, returnCode: .cancel) window?.endSheet(progressSheet, returnCode: .cancel)
} else if appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) == false { } else if appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0 progressIndicator.doubleValue = 1.0
timer.invalidate() timer.invalidate()
window?.endSheet(progressSheet, returnCode: .continue) window?.endSheet(progressSheet, returnCode: .continue)
} }
} }
func removeThenInstallInputMethod() { func removeThenInstallInputMethod() {
if FileManager.default.fileExists( if FileManager.default.fileExists(
atPath: (kTargetPartialPath as NSString).expandingTildeInPath) atPath: (kTargetPartialPath as NSString).expandingTildeInPath)
== false == false
{ {
installInputMethod( installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false previousExists: false, previousVersionNotFullyDeactivatedWarning: false
) )
return return
} }
let shouldWaitForTranslocationRemoval = let shouldWaitForTranslocationRemoval =
appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) appBundleChronoshiftedToARandomizedPath(kTargetPartialPath)
&& (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false) && (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false)
// //
do { do {
let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath
let fileManager = FileManager.default let fileManager = FileManager.default
let fileURLString = String(format: "%@/%@", sourceDir, kTargetBundle) let fileURLString = String(format: "%@/%@", sourceDir, kTargetBundle)
let fileURL = URL(fileURLWithPath: fileURLString) let fileURL = URL(fileURLWithPath: fileURLString)
// //
if fileManager.fileExists(atPath: fileURLString) { if fileManager.fileExists(atPath: fileURLString) {
// //
try fileManager.trashItem(at: fileURL, resultingItemURL: nil) try fileManager.trashItem(at: fileURL, resultingItemURL: nil)
} else { } else {
NSLog("File does not exist") NSLog("File does not exist")
} }
} catch let error as NSError { } catch let error as NSError {
NSLog("An error took place: \(error)") NSLog("An error took place: \(error)")
} }
let killTask = Process() let killTask = Process()
killTask.launchPath = "/usr/bin/killall" killTask.launchPath = "/usr/bin/killall"
killTask.arguments = ["-9", kTargetBin] killTask.arguments = ["-9", kTargetBin]
killTask.launch() killTask.launch()
killTask.waitUntilExit() killTask.waitUntilExit()
if shouldWaitForTranslocationRemoval { if shouldWaitForTranslocationRemoval {
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
window?.beginSheet(progressSheet) { returnCode in window?.beginSheet(progressSheet) { returnCode in
DispatchQueue.main.async { DispatchQueue.main.async {
if returnCode == .continue { if returnCode == .continue {
self.installInputMethod( self.installInputMethod(
previousExists: true, previousExists: true,
previousVersionNotFullyDeactivatedWarning: false previousVersionNotFullyDeactivatedWarning: false
) )
} else { } else {
self.installInputMethod( self.installInputMethod(
previousExists: true, previousExists: true,
previousVersionNotFullyDeactivatedWarning: true previousVersionNotFullyDeactivatedWarning: true
) )
} }
} }
} }
translocationRemovalStartTime = Date() translocationRemovalStartTime = Date()
Timer.scheduledTimer( Timer.scheduledTimer(
timeInterval: kTranslocationRemovalTickInterval, target: self, timeInterval: kTranslocationRemovalTickInterval, target: self,
selector: #selector(timerTick(_:)), userInfo: nil, repeats: true selector: #selector(timerTick(_:)), userInfo: nil, repeats: true
) )
} else { } else {
installInputMethod( installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false previousExists: false, previousVersionNotFullyDeactivatedWarning: false
) )
} }
} }
func installInputMethod( func installInputMethod(
previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
) { ) {
guard guard
let targetBundle = archiveUtil?.unzipNotarizedArchive() let targetBundle = archiveUtil?.unzipNotarizedArchive()
?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
else { else {
return return
} }
let cpTask = Process() let cpTask = Process()
cpTask.launchPath = "/bin/cp" cpTask.launchPath = "/bin/cp"
cpTask.arguments = [ cpTask.arguments = [
"-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath, "-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath,
] ]
cpTask.launch() cpTask.launch()
cpTask.waitUntilExit() cpTask.waitUntilExit()
if cpTask.terminationStatus != 0 { if cpTask.terminationStatus != 0 {
runAlertPanel( runAlertPanel(
title: NSLocalizedString("Install Failed", comment: ""), title: NSLocalizedString("Install Failed", comment: ""),
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
buttonTitle: NSLocalizedString("Cancel", comment: "") buttonTitle: NSLocalizedString("Cancel", comment: "")
) )
endAppWithDelay() endAppWithDelay()
} }
guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath), guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath),
let imeIdentifier = imeBundle.bundleIdentifier let imeIdentifier = imeBundle.bundleIdentifier
else { else {
endAppWithDelay() endAppWithDelay()
return return
} }
let imeBundleURL = imeBundle.bundleURL let imeBundleURL = imeBundle.bundleURL
var inputSource = InputSourceHelper.inputSource(for: imeIdentifier) var inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
if inputSource == nil { if inputSource == nil {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).") NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
let status = InputSourceHelper.registerTnputSource(at: imeBundleURL) let status = InputSourceHelper.registerTnputSource(at: imeBundleURL)
if !status { if !status {
let message = String( let message = String(
format: NSLocalizedString( format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: "" "Cannot find input source %@ after registration.", comment: ""
), ),
imeIdentifier imeIdentifier
) )
runAlertPanel( runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message, title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "") buttonTitle: NSLocalizedString("Abort", comment: "")
) )
endAppWithDelay() endAppWithDelay()
return return
} }
inputSource = InputSourceHelper.inputSource(for: imeIdentifier) inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
if inputSource == nil { 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: ""
), ),
imeIdentifier imeIdentifier
) )
runAlertPanel( runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message, title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "") buttonTitle: NSLocalizedString("Abort", comment: "")
) )
} }
} }
var isMacOS12OrAbove = false var isMacOS12OrAbove = false
if #available(macOS 12.0, *) { if #available(macOS 12.0, *) {
NSLog("macOS 12 or later detected.") NSLog("macOS 12 or later detected.")
isMacOS12OrAbove = true isMacOS12OrAbove = true
} else { } else {
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+, // If the IME is not enabled, enable it. Also, unconditionally enable it 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 = InputSourceHelper.inputSourceEnabled(for: inputSource!)
if !mainInputSourceEnabled || isMacOS12OrAbove { if !mainInputSourceEnabled || isMacOS12OrAbove {
mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!) mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!)
if mainInputSourceEnabled { if mainInputSourceEnabled {
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)")
} }
} }
// Alert Panel // Alert Panel
let ntfPostInstall = NSAlert() let ntfPostInstall = NSAlert()
if warning { if warning {
ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "") ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "")
ntfPostInstall.informativeText = NSLocalizedString( ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.", "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.",
comment: "" comment: ""
) )
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
} else { } else {
if !mainInputSourceEnabled, !isMacOS12OrAbove { if !mainInputSourceEnabled, !isMacOS12OrAbove {
ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "") ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "")
ntfPostInstall.informativeText = NSLocalizedString( ntfPostInstall.informativeText = NSLocalizedString(
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.",
comment: "" comment: ""
) )
ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: "")) ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
} else { } else {
ntfPostInstall.messageText = NSLocalizedString( ntfPostInstall.messageText = NSLocalizedString(
"Installation Successful", comment: "" "Installation Successful", comment: ""
) )
ntfPostInstall.informativeText = NSLocalizedString( ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is ready to use.", comment: "" "vChewing is ready to use.", comment: ""
) )
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
} }
} }
ntfPostInstall.beginSheetModal(for: window!) { _ in ntfPostInstall.beginSheetModal(for: window!) { _ in
self.endAppWithDelay() self.endAppWithDelay()
} }
} }
func endAppWithDelay() { func endAppWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
NSApp.terminate(self) NSApp.terminate(self)
} }
} }
@IBAction func cancelAction(_: AnyObject) { @IBAction func cancelAction(_: AnyObject) {
NSApp.terminate(self) NSApp.terminate(self)
} }
func windowWillClose(_: Notification) { func windowWillClose(_: Notification) {
NSApp.terminate(self) NSApp.terminate(self)
} }
} }

View File

@ -27,109 +27,109 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
struct ArchiveUtil { struct ArchiveUtil {
var appName: String var appName: String
var targetAppBundleName: String var targetAppBundleName: String
init(appName: String, targetAppBundleName: String) { init(appName: String, targetAppBundleName: String) {
self.appName = appName self.appName = appName
self.targetAppBundleName = targetAppBundleName self.targetAppBundleName = targetAppBundleName
} }
// Returns YES if (1) a zip file under // Returns YES if (1) a zip file under
// Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if // Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if
// Resources/$_invalidAppBundleName does not exist. // Resources/$_invalidAppBundleName does not exist.
func validateIfNotarizedArchiveExists() -> Bool { func validateIfNotarizedArchiveExists() -> Bool {
guard let resourePath = Bundle.main.resourcePath, guard let resourePath = Bundle.main.resourcePath,
let notarizedArchivesPath = notarizedArchivesPath, let notarizedArchivesPath = notarizedArchivesPath,
let notarizedArchive = notarizedArchive, let notarizedArchive = notarizedArchive,
let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory( let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(
atPath: notarizedArchivesPath) atPath: notarizedArchivesPath)
else { else {
return false return false
} }
let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName) let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName)
let count = notarizedArchivesContent.count let count = notarizedArchivesContent.count
let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive) let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive)
let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath) let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath)
if !notarizedArchivesContent.isEmpty { if !notarizedArchivesContent.isEmpty {
// count > 0!isEmpty滿 // count > 0!isEmpty滿
if count != 1 || !notarizedArchiveExists || devModeAppBundleExists { if count != 1 || !notarizedArchiveExists || devModeAppBundleExists {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .informational alert.alertStyle = .informational
alert.messageText = "Internal Error" alert.messageText = "Internal Error"
alert.informativeText = alert.informativeText =
"devMode installer, expected archive name: \(notarizedArchive), " "devMode installer, expected archive name: \(notarizedArchive), "
+ "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)" + "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)"
alert.addButton(withTitle: "Terminate") alert.addButton(withTitle: "Terminate")
alert.runModal() alert.runModal()
NSApp.terminate(nil) NSApp.terminate(nil)
} else { } else {
return true return true
} }
} }
if !devModeAppBundleExists { if !devModeAppBundleExists {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .informational alert.alertStyle = .informational
alert.messageText = "Internal Error" alert.messageText = "Internal Error"
alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)" alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)"
alert.addButton(withTitle: "Terminate") alert.addButton(withTitle: "Terminate")
alert.runModal() alert.runModal()
NSApp.terminate(nil) NSApp.terminate(nil)
} }
return false return false
} }
func unzipNotarizedArchive() -> String? { func unzipNotarizedArchive() -> String? {
if !validateIfNotarizedArchiveExists() { if !validateIfNotarizedArchiveExists() {
return nil return nil
} }
guard let notarizedArchive = notarizedArchive, guard let notarizedArchive = notarizedArchive,
let resourcePath = Bundle.main.resourcePath let resourcePath = Bundle.main.resourcePath
else { else {
return nil return nil
} }
let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent( let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(
UUID().uuidString) UUID().uuidString)
let arguments: [String] = [notarizedArchive, "-d", tempFilePath] let arguments: [String] = [notarizedArchive, "-d", tempFilePath]
let unzipTask = Process() let unzipTask = Process()
unzipTask.launchPath = "/usr/bin/unzip" unzipTask.launchPath = "/usr/bin/unzip"
unzipTask.currentDirectoryPath = resourcePath unzipTask.currentDirectoryPath = resourcePath
unzipTask.arguments = arguments unzipTask.arguments = arguments
unzipTask.launch() unzipTask.launch()
unzipTask.waitUntilExit() unzipTask.waitUntilExit()
assert(unzipTask.terminationStatus == 0, "Must successfully unzipped") assert(unzipTask.terminationStatus == 0, "Must successfully unzipped")
let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName) let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName)
assert( assert(
FileManager.default.fileExists(atPath: result), FileManager.default.fileExists(atPath: result),
"App bundle must be unzipped at \(result)." "App bundle must be unzipped at \(result)."
) )
return result return result
} }
private var notarizedArchivesPath: String? { private var notarizedArchivesPath: String? {
guard let resourePath = Bundle.main.resourcePath else { guard let resourePath = Bundle.main.resourcePath else {
return nil return nil
} }
let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent( let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent(
"NotarizedArchives") "NotarizedArchives")
return notarizedArchivesPath return notarizedArchivesPath
} }
private var notarizedArchive: String? { private var notarizedArchive: String? {
guard let notarizedArchivesPath = notarizedArchivesPath, guard let notarizedArchivesPath = notarizedArchivesPath,
let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
as? String as? String
else { else {
return nil return nil
} }
let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip" let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip"
let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent( let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent(
notarizedArchiveBasename) notarizedArchiveBasename)
return notarizedArchive return notarizedArchive
} }
} }

View File

@ -56,8 +56,8 @@
/* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */ /* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "Derived from OpenVanilla McBopopmofo Project."; "QYf-Nf-hoi.title" = "Derived from OpenVanilla McBopopmofo Project.";
/* Class = "NSTextFieldCell"; title = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */ /* Class = "NSTextFieldCell"; title = "Mandarin Syllable Composer Engine by Lukhnos Liu.\nInput State Management Architecture by Zonble Yang.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nMegrez is a rewritten unigram engine by Shiki Suen using Swift, replacing Lukhnos' C++ Gramambular engine."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; "VW8-s5-Wpn.title" = "Mandarin Syllable Composer Engine by Lukhnos Liu.\nInput State Management Architecture by Zonble Yang.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nMegrez is a rewritten unigram engine by Shiki Suen using Swift, replacing Lukhnos' C++ Gramambular engine.";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */ /* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information."; // "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";

View File

@ -56,8 +56,8 @@
/* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */ /* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "OpenVanilla 小麦注音プロジェクトから派生。"; "QYf-Nf-hoi.title" = "OpenVanilla 小麦注音プロジェクトから派生。";
/* Class = "NSTextFieldCell"; title = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */ /* Class = "NSTextFieldCell"; title = "Mandarin Syllable Composer Engine by Lukhnos Liu.\nInput State Management Architecture by Zonble Yang.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.\nMegrez is a rewritten unigram engine by Shiki Suen using Swift, replacing Lukhnos' C++ Gramambular engine."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "小麦注音入力エンジン開発Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, など。\nmacOS 版威注音の開発Shiki Suen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。"; "VW8-s5-Wpn.title" = "ボポモフォエンジン開発Lukhnos Liu。\n入力状態管理システム開発Zonble Yang。\nmacOS 版威注音の開発Shiki Suen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。\nMegrez 辞書処理エンジンShiki SuenLukhnos の Gramambular C++ エンジンを Swift で再開発したものである)。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */ /* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
"eo3-TK-0rB.title" = "Placeholder for showing copyright information."; "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";

View File

@ -58,7 +58,7 @@
/* Class = "NSTextFieldCell"; title = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc. /* Class = "NSTextFieldCell"; title = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.
vChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */ vChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "小麦注音引擎研发Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, 等。\n威注音 macOS 程式研发Shiki Suen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。"; "VW8-s5-Wpn.title" = "注音拼音输入处理引擎研发Lukhnos Liu。\n输入法状态管理引擎研发Zonble Yang。\n威注音 macOS 程式研发Shiki Suen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。\n天权星语汇引擎Shiki Suen用 Swift 将 Lukhnos 的 C++ Gramambular 重写而得。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */ /* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information."; // "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";

View File

@ -58,7 +58,7 @@
/* Class = "NSTextFieldCell"; title = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc. /* Class = "NSTextFieldCell"; title = "McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al.\nvChewing macOS Development: Shiki Suen, Hiraku Wang, etc.
vChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */ vChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "小麥注音引擎研發Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, 等。\n威注音 macOS 程式研發Shiki Suen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。"; "VW8-s5-Wpn.title" = "注音拼音輸入處理引擎研發Lukhnos Liu。\n輸入法狀態管理引擎研發Zonble Yang。\n威注音 macOS 程式研發Shiki Suen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。\n天權星語彙引擎Shiki Suen用 Swift 將 Lukhnos 的 C++ Gramambular 重寫而得。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */ /* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information."; // "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";

View File

@ -3,9 +3,11 @@
vChewing macOS: MIT-NTL License 麻理(去商标)授权合约 vChewing macOS: MIT-NTL License 麻理(去商标)授权合约
© 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project. © 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project.
小麦注音引擎研发Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, 等。 注音拼音输入处理引擎研发Lukhnos Liu。
输入法状态管理引擎研发Zonble Yang。
威注音 macOS 程式研发Shiki Suen, Hiraku Wang, 等。 威注音 macOS 程式研发Shiki Suen, Hiraku Wang, 等。
威注音词库维护Shiki Suen。 威注音词库维护Shiki Suen。
天权星语汇引擎Shiki Suen用 Swift 将 Lukhnos 的 C++ Gramambular 重写而得。
软件之著作权利人依此麻理授权条款,将其对于软件之著作权利授权释出,只须使用者践履以下二项麻理授权条款叙明之义务性规定,其即享有对此软件程式及其相关说明文档自由不受限制地进行利用之权利,范围包括「使用、重制、修改、合并、出版、散布、再授权、及贩售程式重制作品」等诸多方面之应用,而散布程式之人、更可将上述权利传递予其后收受程式之后手,倘若其后收受程式之人亦服膺以下二项麻理授权条款之义务性规定,则其对程式亦享有与前手运用范围相同之同一权利。 软件之著作权利人依此麻理授权条款,将其对于软件之著作权利授权释出,只须使用者践履以下二项麻理授权条款叙明之义务性规定,其即享有对此软件程式及其相关说明文档自由不受限制地进行利用之权利,范围包括「使用、重制、修改、合并、出版、散布、再授权、及贩售程式重制作品」等诸多方面之应用,而散布程式之人、更可将上述权利传递予其后收受程式之后手,倘若其后收受程式之人亦服膺以下二项麻理授权条款之义务性规定,则其对程式亦享有与前手运用范围相同之同一权利。

View File

@ -3,9 +3,11 @@
vChewing macOS: MIT-NTL License 麻理(去商標)授權合約 vChewing macOS: MIT-NTL License 麻理(去商標)授權合約
© 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project. © 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project.
小麥注音引擎研發Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, 等。 注音拼音輸入處理引擎研發Lukhnos Liu。
輸入法狀態管理引擎研發Zonble Yang。
威注音 macOS 程式研發Shiki Suen, Hiraku Wang, 等。 威注音 macOS 程式研發Shiki Suen, Hiraku Wang, 等。
威注音詞庫維護Shiki Suen。 威注音詞庫維護Shiki Suen。
天權星語彙引擎Shiki Suen用 Swift 將 Lukhnos 的 C++ Gramambular 重寫而得。
軟體之著作權利人依此麻理授權條款,將其對於軟體之著作權利授權釋出,只須使用者踐履以下二項麻理授權條款敘明之義務性規定,其即享有對此軟體程式及其相關說明文檔自由不受限制地進行利用之權利,範圍包括「使用、重製、修改、合併、出版、散布、再授權、及販售程式重製作品」等諸多方面之應用,而散布程式之人、更可將上述權利傳遞予其後收受程式之後手,倘若其後收受程式之人亦服膺以下二項麻理授權條款之義務性規定,則其對程式亦享有與前手運用範圍相同之同一權利。 軟體之著作權利人依此麻理授權條款,將其對於軟體之著作權利授權釋出,只須使用者踐履以下二項麻理授權條款敘明之義務性規定,其即享有對此軟體程式及其相關說明文檔自由不受限制地進行利用之權利,範圍包括「使用、重製、修改、合併、出版、散布、再授權、及販售程式重製作品」等諸多方面之應用,而散布程式之人、更可將上述權利傳遞予其後收受程式之後手,倘若其後收受程式之人亦服膺以下二項麻理授權條款之義務性規定,則其對程式亦享有與前手運用範圍相同之同一權利。

View File

@ -2,10 +2,11 @@
vChewing macOS: MIT商標不許可ライセンス (MIT-NTL License) vChewing macOS: MIT商標不許可ライセンス (MIT-NTL License)
© 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project. ボポモフォエンジン開発Lukhnos Liu。
小麦注音入力エンジン開発Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, など 入力状態管理システム開発Zonble Yang
macOS 版威注音の開発Hiraku Wang, Shiki Suen, など。 macOS 版威注音の開発:Shiki Suen, Hiraku Wang, など。
威注音語彙データの維持Shiki Suen。 威注音語彙データの維持Shiki Suen。
Megrez 辞書処理エンジンShiki SuenLukhnos の Gramambular C++ エンジンを Swift で再開発したものである)。
以下に定める条件に従い、本ソフトウェアおよび関連文書のファイル(以下「ソフトウェア」)の複製を取得するすべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複写、変更、結合、掲載、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供する相手に同じことを許可する権利も無制限に含まれます。 以下に定める条件に従い、本ソフトウェアおよび関連文書のファイル(以下「ソフトウェア」)の複製を取得するすべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複写、変更、結合、掲載、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供する相手に同じことを許可する権利も無制限に含まれます。

View File

@ -3,9 +3,11 @@ DISCLAIMER: The vChewing project, having no relationship of cooperation or affil
vChewing macOS: MIT-NTL License vChewing macOS: MIT-NTL License
© 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project. © 2011-2022 OpenVanilla Project & © 2021-2022 vChewing Project.
McBopomofo Engine by Mengjuei Hsieh, Lukhnos Liu, Zonble Yang, et al. Mandarin Syllable Composer Engine by Lukhnos Liu.
Input State Management Architecture by Zonble Yang.
vChewing macOS Development: Shiki Suen, Hiraku Wang, etc. vChewing macOS Development: Shiki Suen, Hiraku Wang, etc.
vChewing Phrase Database Maintained by Shiki Suen. vChewing Phrase Database Maintained by Shiki Suen.
Megrez is a rewritten unigram engine by Shiki Suen using Swift, replacing Lukhnos' C++ Gramambular engine.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -28,6 +28,7 @@ clang-format: clang-format-swift clang-format-cpp
clang-format-swift: clang-format-swift:
@git ls-files --exclude-standard | grep -E '\.swift$$' | xargs swift-format format --in-place --configuration ./.clang-format-swift.json --parallel @git ls-files --exclude-standard | grep -E '\.swift$$' | xargs swift-format format --in-place --configuration ./.clang-format-swift.json --parallel
@git ls-files --exclude-standard | grep -E '\.swift$$' | xargs swift-format lint --configuration ./.clang-format-swift.json --parallel
clang-format-cpp: clang-format-cpp:
@git ls-files --exclude-standard | grep -E '\.(cpp|hpp|c|cc|cxx|hxx|ixx|h|m|mm|hh)$$' | xargs clang-format -i @git ls-files --exclude-standard | grep -E '\.(cpp|hpp|c|cc|cxx|hxx|ixx|h|m|mm|hh)$$' | xargs clang-format -i

View File

@ -3,87 +3,83 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "SwiftyOpenCC", name: "SwiftyOpenCC",
products: [ products: [
.library( .library(
name: "OpenCC", name: "OpenCC",
targets: ["OpenCC"] targets: ["OpenCC"]
) )
], ],
targets: [ targets: [
.target( .target(
name: "OpenCC", name: "OpenCC",
dependencies: ["copencc"], dependencies: ["copencc"],
resources: [ resources: [
.copy("Dictionary") .copy("Dictionary")
] ]
), ),
.testTarget( .testTarget(
name: "OpenCCTests", name: "OpenCCTests",
dependencies: ["OpenCC"], dependencies: ["OpenCC"],
resources: [ resources: [
.copy("benchmark"), .copy("benchmark"),
.copy("testcases"), .copy("testcases"),
] ]
), ),
.target( .target(
name: "copencc", name: "copencc",
exclude: [ exclude: [
"src/benchmark", "src/benchmark",
"src/tools", "src/tools",
"src/BinaryDictTest.cpp", "src/BinaryDictTest.cpp",
"src/Config.cpp", "src/Config.cpp",
"src/ConfigTest.cpp", "src/ConfigTest.cpp",
"src/ConversionChainTest.cpp", "src/ConversionChainTest.cpp",
"src/ConversionTest.cpp", "src/ConversionTest.cpp",
"src/DartsDictTest.cpp", "src/DartsDictTest.cpp",
"src/DictGroupTest.cpp", "src/DictGroupTest.cpp",
"src/MarisaDictTest.cpp", "src/MarisaDictTest.cpp",
"src/MaxMatchSegmentationTest.cpp", "src/MaxMatchSegmentationTest.cpp",
"src/PhraseExtractTest.cpp", "src/PhraseExtractTest.cpp",
"src/SerializedValuesTest.cpp", "src/SerializedValuesTest.cpp",
"src/SimpleConverter.cpp", "src/SimpleConverter.cpp",
"src/SimpleConverterTest.cpp", "src/SimpleConverterTest.cpp",
"src/TextDictTest.cpp", "src/TextDictTest.cpp",
"src/UTF8StringSliceTest.cpp", "src/UTF8StringSliceTest.cpp",
"src/UTF8UtilTest.cpp", "src/UTF8UtilTest.cpp",
"deps/google-benchmark", "deps/google-benchmark",
"deps/gtest-1.11.0",
"deps/pybind11-2.5.0",
"deps/rapidjson-1.1.0",
"deps/tclap-1.2.2",
"src/CmdLineOutput.hpp", "src/CmdLineOutput.hpp",
"src/Config.hpp", "src/Config.hpp",
"src/ConfigTestBase.hpp", "src/ConfigTestBase.hpp",
"src/DictGroupTestBase.hpp", "src/DictGroupTestBase.hpp",
"src/SimpleConverter.hpp", "src/SimpleConverter.hpp",
"src/TestUtils.hpp", "src/TestUtils.hpp",
"src/TestUtilsUTF8.hpp", "src/TestUtilsUTF8.hpp",
"src/TextDictTestBase.hpp", "src/TextDictTestBase.hpp",
"src/py_opencc.cpp", "src/py_opencc.cpp",
// ??? // ???
"src/README.md", "src/README.md",
"src/CMakeLists.txt", "src/CMakeLists.txt",
"deps/marisa-0.2.6/AUTHORS", "deps/marisa-0.2.6/AUTHORS",
"deps/marisa-0.2.6/CMakeLists.txt", "deps/marisa-0.2.6/CMakeLists.txt",
"deps/marisa-0.2.6/COPYING.md", "deps/marisa-0.2.6/COPYING.md",
"deps/marisa-0.2.6/README.md", "deps/marisa-0.2.6/README.md",
], ],
sources: [ sources: [
"source.cpp", "source.cpp",
"src", "src",
"deps/marisa-0.2.6", "deps/marisa-0.2.6",
], ],
cxxSettings: [ cxxSettings: [
.headerSearchPath("src"), .headerSearchPath("src"),
.headerSearchPath("deps/darts-clone"), .headerSearchPath("deps/darts-clone"),
.headerSearchPath("deps/marisa-0.2.6/include"), .headerSearchPath("deps/marisa-0.2.6/include"),
.headerSearchPath("deps/marisa-0.2.6/lib"), .headerSearchPath("deps/marisa-0.2.6/lib"),
.define("ENABLE_DARTS"), .define("ENABLE_DARTS"),
] ]
), ),
], ],
cxxLanguageStandard: .cxx14 cxxLanguageStandard: .cxx14
) )

View File

@ -22,65 +22,65 @@ import copencc
/// However, the string on which it is operating should not be mutated /// However, the string on which it is operating should not be mutated
/// during the course of a conversion. /// during the course of a conversion.
public class ChineseConverter { public class ChineseConverter {
/// These constants define the ChineseConverter options. /// These constants define the ChineseConverter options.
public struct Options: OptionSet { public struct Options: OptionSet {
public let rawValue: Int public let rawValue: Int
public init(rawValue: Int) { public init(rawValue: Int) {
self.rawValue = rawValue self.rawValue = rawValue
} }
/// Convert to Traditional Chinese. (default) /// Convert to Traditional Chinese. (default)
public static let traditionalize = Options(rawValue: 1 << 0) public static let traditionalize = Options(rawValue: 1 << 0)
/// Convert to Simplified Chinese. /// Convert to Simplified Chinese.
public static let simplify = Options(rawValue: 1 << 1) public static let simplify = Options(rawValue: 1 << 1)
/// Use Taiwan standard. /// Use Taiwan standard.
public static let twStandard = Options(rawValue: 1 << 5) public static let twStandard = Options(rawValue: 1 << 5)
/// Use HongKong standard. /// Use HongKong standard.
public static let hkStandard = Options(rawValue: 1 << 6) public static let hkStandard = Options(rawValue: 1 << 6)
/// Cancel Taiwan standard. /// Cancel Taiwan standard.
public static let twStandardRev = Options(rawValue: 1 << 15) public static let twStandardRev = Options(rawValue: 1 << 15)
/// Cancel HongKong standard. /// Cancel HongKong standard.
public static let hkStandardRev = Options(rawValue: 1 << 16) public static let hkStandardRev = Options(rawValue: 1 << 16)
/// Taiwanese idiom conversion. /// Taiwanese idiom conversion.
public static let twIdiom = Options(rawValue: 1 << 10) public static let twIdiom = Options(rawValue: 1 << 10)
} }
private let seg: ConversionDictionary private let seg: ConversionDictionary
private let chain: [ConversionDictionary] private let chain: [ConversionDictionary]
private let converter: CCConverterRef private let converter: CCConverterRef
private init(loader: DictionaryLoader, options: Options) throws { private init(loader: DictionaryLoader, options: Options) throws {
seg = try loader.segmentation(options: options) seg = try loader.segmentation(options: options)
chain = try loader.conversionChain(options: options) chain = try loader.conversionChain(options: options)
var rawChain = chain.map(\.dict) var rawChain = chain.map(\.dict)
converter = CCConverterCreate("SwiftyOpenCC", seg.dict, &rawChain, rawChain.count) converter = CCConverterCreate("SwiftyOpenCC", seg.dict, &rawChain, rawChain.count)
} }
/// Returns an initialized `ChineseConverter` instance with the specified /// Returns an initialized `ChineseConverter` instance with the specified
/// conversion options. /// conversion options.
/// ///
/// - Parameter options: The converts options. /// - Parameter options: The converts options.
/// - Throws: Throws `ConversionError` if failed. /// - Throws: Throws `ConversionError` if failed.
public convenience init(options: Options) throws { public convenience init(options: Options) throws {
let loader = DictionaryLoader(bundle: .module) let loader = DictionaryLoader(bundle: .module)
try self.init(loader: loader, options: options) try self.init(loader: loader, options: options)
} }
/// Return a converted string using the converts current option. /// Return a converted string using the converts current option.
/// ///
/// - Parameter text: The string to convert. /// - Parameter text: The string to convert.
/// - Returns: A converted string using the converts current option. /// - Returns: A converted string using the converts current option.
public func convert(_ text: String) -> String { public func convert(_ text: String) -> String {
let stlStr = CCConverterCreateConvertedStringFromString(converter, text)! let stlStr = CCConverterCreateConvertedStringFromString(converter, text)!
defer { STLStringDestroy(stlStr) } defer { STLStringDestroy(stlStr) }
return String(utf8String: STLStringGetUTF8String(stlStr))! return String(utf8String: STLStringGetUTF8String(stlStr))!
} }
} }

View File

@ -9,21 +9,21 @@ import Foundation
import copencc import copencc
class ConversionDictionary { class ConversionDictionary {
let group: [ConversionDictionary] let group: [ConversionDictionary]
let dict: CCDictRef let dict: CCDictRef
init(path: String) throws { init(path: String) throws {
guard let dict = CCDictCreateMarisaWithPath(path) else { guard let dict = CCDictCreateMarisaWithPath(path) else {
throw ConversionError(ccErrorno) throw ConversionError(ccErrorno)
} }
group = [] group = []
self.dict = dict self.dict = dict
} }
init(group: [ConversionDictionary]) { init(group: [ConversionDictionary]) {
var rawGroup = group.map(\.dict) var rawGroup = group.map(\.dict)
self.group = group self.group = group
dict = CCDictCreateWithGroup(&rawGroup, rawGroup.count) dict = CCDictCreateWithGroup(&rawGroup, rawGroup.count)
} }
} }

View File

@ -9,28 +9,28 @@ import Foundation
import copencc import copencc
public enum ConversionError: Error { public enum ConversionError: Error {
case fileNotFound case fileNotFound
case invalidFormat case invalidFormat
case invalidTextDictionary case invalidTextDictionary
case invalidUTF8 case invalidUTF8
case unknown case unknown
init(_ code: CCErrorCode) { init(_ code: CCErrorCode) {
switch code { switch code {
case .fileNotFound: case .fileNotFound:
self = .fileNotFound self = .fileNotFound
case .invalidFormat: case .invalidFormat:
self = .invalidFormat self = .invalidFormat
case .invalidTextDictionary: case .invalidTextDictionary:
self = .invalidTextDictionary self = .invalidTextDictionary
case .invalidUTF8: case .invalidUTF8:
self = .invalidUTF8 self = .invalidUTF8
case .unknown, _: case .unknown, _:
self = .unknown self = .unknown
} }
} }
} }

View File

@ -9,49 +9,49 @@ import Foundation
import copencc import copencc
extension ChineseConverter { extension ChineseConverter {
struct DictionaryLoader { struct DictionaryLoader {
private static let subdirectory = "Dictionary" private static let subdirectory = "Dictionary"
private static let dictCache = WeakValueCache<String, ConversionDictionary>() private static let dictCache = WeakValueCache<String, ConversionDictionary>()
private let bundle: Bundle private let bundle: Bundle
init(bundle: Bundle) { init(bundle: Bundle) {
self.bundle = bundle self.bundle = bundle
} }
func dict(_ name: ChineseConverter.DictionaryName) throws -> ConversionDictionary { func dict(_ name: ChineseConverter.DictionaryName) throws -> ConversionDictionary {
guard guard
let path = bundle.path( let path = bundle.path(
forResource: name.description, ofType: "ocd2", forResource: name.description, ofType: "ocd2",
inDirectory: DictionaryLoader.subdirectory inDirectory: DictionaryLoader.subdirectory
) )
else { else {
throw ConversionError.fileNotFound throw ConversionError.fileNotFound
} }
return try DictionaryLoader.dictCache.value(for: path) { return try DictionaryLoader.dictCache.value(for: path) {
try ConversionDictionary(path: path) try ConversionDictionary(path: path)
} }
} }
} }
} }
extension ChineseConverter.DictionaryLoader { extension ChineseConverter.DictionaryLoader {
func segmentation(options: ChineseConverter.Options) throws -> ConversionDictionary { func segmentation(options: ChineseConverter.Options) throws -> ConversionDictionary {
let dictName = options.segmentationDictName let dictName = options.segmentationDictName
return try dict(dictName) return try dict(dictName)
} }
func conversionChain(options: ChineseConverter.Options) throws -> [ConversionDictionary] { func conversionChain(options: ChineseConverter.Options) throws -> [ConversionDictionary] {
try options.conversionChain.compactMap { names in try options.conversionChain.compactMap { names in
switch names.count { switch names.count {
case 0: case 0:
return nil return nil
case 1: case 1:
return try dict(names.first!) return try dict(names.first!)
case _: case _:
let dicts = try names.map(dict) let dicts = try names.map(dict)
return ConversionDictionary(group: dicts) return ConversionDictionary(group: dicts)
} }
} }
} }
} }

View File

@ -8,96 +8,96 @@
import Foundation import Foundation
extension ChineseConverter { extension ChineseConverter {
enum DictionaryName: CustomStringConvertible { enum DictionaryName: CustomStringConvertible {
case hkVariants case hkVariants
case hkVariantsRev case hkVariantsRev
case hkVariantsRevPhrases case hkVariantsRevPhrases
case jpVariants case jpVariants
case stCharacters case stCharacters
case stPhrases case stPhrases
case tsCharacters case tsCharacters
case tsPhrases case tsPhrases
case twPhrases case twPhrases
case twPhrasesRev case twPhrasesRev
case twVariants case twVariants
case twVariantsRev case twVariantsRev
case twVariantsRevPhrases case twVariantsRevPhrases
var description: String { var description: String {
switch self { switch self {
case .hkVariants: return "HKVariants" case .hkVariants: return "HKVariants"
case .hkVariantsRev: return "HKVariantsRev" case .hkVariantsRev: return "HKVariantsRev"
case .hkVariantsRevPhrases: return "HKVariantsRevPhrases" case .hkVariantsRevPhrases: return "HKVariantsRevPhrases"
case .jpVariants: return "JPVariants" case .jpVariants: return "JPVariants"
case .stCharacters: return "STCharacters" case .stCharacters: return "STCharacters"
case .stPhrases: return "STPhrases" case .stPhrases: return "STPhrases"
case .tsCharacters: return "TSCharacters" case .tsCharacters: return "TSCharacters"
case .tsPhrases: return "TSPhrases" case .tsPhrases: return "TSPhrases"
case .twPhrases: return "TWPhrases" case .twPhrases: return "TWPhrases"
case .twPhrasesRev: return "TWPhrasesRev" case .twPhrasesRev: return "TWPhrasesRev"
case .twVariants: return "TWVariants" case .twVariants: return "TWVariants"
case .twVariantsRev: return "TWVariantsRev" case .twVariantsRev: return "TWVariantsRev"
case .twVariantsRevPhrases: return "TWVariantsRevPhrases" case .twVariantsRevPhrases: return "TWVariantsRevPhrases"
} }
} }
} }
} }
extension ChineseConverter.Options { extension ChineseConverter.Options {
var segmentationDictName: ChineseConverter.DictionaryName { var segmentationDictName: ChineseConverter.DictionaryName {
if contains(.traditionalize) { if contains(.traditionalize) {
return .stPhrases return .stPhrases
} else if contains(.simplify) { } else if contains(.simplify) {
return .tsPhrases return .tsPhrases
} else if contains(.hkStandard) { } else if contains(.hkStandard) {
return .hkVariants return .hkVariants
} else if contains(.twStandard) { } else if contains(.twStandard) {
return .twVariants return .twVariants
} else if contains(.hkStandardRev) { } else if contains(.hkStandardRev) {
return .hkVariantsRev return .hkVariantsRev
} else if contains(.twStandardRev) { } else if contains(.twStandardRev) {
return .twVariantsRev return .twVariantsRev
} else { } else {
return .stPhrases return .stPhrases
} }
} }
var conversionChain: [[ChineseConverter.DictionaryName]] { var conversionChain: [[ChineseConverter.DictionaryName]] {
var result: [[ChineseConverter.DictionaryName]] = [] var result: [[ChineseConverter.DictionaryName]] = []
if contains(.traditionalize) { if contains(.traditionalize) {
result.append([.stPhrases, .stCharacters]) result.append([.stPhrases, .stCharacters])
if contains(.twIdiom) { if contains(.twIdiom) {
result.append([.twPhrases]) result.append([.twPhrases])
} }
if contains(.hkStandard) { if contains(.hkStandard) {
result.append([.hkVariants]) result.append([.hkVariants])
} else if contains(.twStandard) { } else if contains(.twStandard) {
result.append([.twVariants]) result.append([.twVariants])
} }
} else if contains(.simplify) { } else if contains(.simplify) {
if contains(.hkStandard) { if contains(.hkStandard) {
result.append([.hkVariantsRevPhrases, .hkVariantsRev]) result.append([.hkVariantsRevPhrases, .hkVariantsRev])
} else if contains(.twStandard) { } else if contains(.twStandard) {
result.append([.twVariantsRevPhrases, .twVariantsRev]) result.append([.twVariantsRevPhrases, .twVariantsRev])
} }
if contains(.twIdiom) { if contains(.twIdiom) {
result.append([.twPhrasesRev]) result.append([.twPhrasesRev])
} }
result.append([.tsPhrases, .tsCharacters]) result.append([.tsPhrases, .tsCharacters])
} else { } else {
if contains(.hkStandard) { if contains(.hkStandard) {
result.append([.hkVariants]) result.append([.hkVariants])
} else if contains(.twStandard) { } else if contains(.twStandard) {
result.append([.twVariants]) result.append([.twVariants])
} else if contains(.hkStandardRev) { } else if contains(.hkStandardRev) {
result.append([.hkVariantsRev]) result.append([.hkVariantsRev])
} else if contains(.twStandardRev) { } else if contains(.twStandardRev) {
result.append([.twVariantsRev]) result.append([.twVariantsRev])
} }
} }
if result.isEmpty { if result.isEmpty {
return [[.stPhrases, .stCharacters]] return [[.stPhrases, .stCharacters]]
} }
return result return result
} }
} }

View File

@ -8,33 +8,33 @@
import Foundation import Foundation
class WeakBox<Value: AnyObject> { class WeakBox<Value: AnyObject> {
private(set) weak var value: Value? private(set) weak var value: Value?
init(_ value: Value) { init(_ value: Value) {
self.value = value self.value = value
} }
} }
class WeakValueCache<Key: Hashable, Value: AnyObject> { class WeakValueCache<Key: Hashable, Value: AnyObject> {
private var storage: [Key: WeakBox<Value>] = [:] private var storage: [Key: WeakBox<Value>] = [:]
private var lock = NSLock() private var lock = NSLock()
func value(for key: Key) -> Value? { func value(for key: Key) -> Value? {
storage[key]?.value storage[key]?.value
} }
func value(for key: Key, make: () throws -> Value) rethrows -> Value { func value(for key: Key, make: () throws -> Value) rethrows -> Value {
if let value = storage[key]?.value { if let value = storage[key]?.value {
return value return value
} }
lock.lock() lock.lock()
defer { lock.unlock() } defer { lock.unlock() }
if let value = storage[key]?.value { if let value = storage[key]?.value {
return value return value
} }
let value = try make() let value = try make()
storage[key] = WeakBox(value) storage[key] = WeakBox(value)
return value return value
} }
} }

View File

@ -3,66 +3,66 @@ import XCTest
@testable import OpenCC @testable import OpenCC
let testCases: [(String, ChineseConverter.Options)] = [ let testCases: [(String, ChineseConverter.Options)] = [
("s2t", [.traditionalize]), ("s2t", [.traditionalize]),
("t2s", [.simplify]), ("t2s", [.simplify]),
("s2hk", [.traditionalize, .hkStandard]), ("s2hk", [.traditionalize, .hkStandard]),
("hk2s", [.simplify, .hkStandard]), ("hk2s", [.simplify, .hkStandard]),
("s2tw", [.traditionalize, .twStandard]), ("s2tw", [.traditionalize, .twStandard]),
("tw2s", [.simplify, .twStandard]), ("tw2s", [.simplify, .twStandard]),
("s2twp", [.traditionalize, .twStandard, .twIdiom]), ("s2twp", [.traditionalize, .twStandard, .twIdiom]),
("tw2sp", [.simplify, .twStandard, .twIdiom]), ("tw2sp", [.simplify, .twStandard, .twIdiom]),
] ]
class OpenCCTests: XCTestCase { class OpenCCTests: XCTestCase {
func converter(option: ChineseConverter.Options) throws -> ChineseConverter { func converter(option: ChineseConverter.Options) throws -> ChineseConverter {
try ChineseConverter(options: option) try ChineseConverter(options: option)
} }
func testConversion() throws { func testConversion() throws {
func testCase(name: String, ext: String) -> String { func testCase(name: String, ext: String) -> String {
let url = Bundle.module.url( let url = Bundle.module.url(
forResource: name, withExtension: ext, subdirectory: "testcases" forResource: name, withExtension: ext, subdirectory: "testcases"
)! )!
return try! String(contentsOf: url) return try! String(contentsOf: url)
} }
for (name, opt) in testCases { for (name, opt) in testCases {
let coverter = try ChineseConverter(options: opt) let coverter = try ChineseConverter(options: opt)
let input = testCase(name: name, ext: "in") let input = testCase(name: name, ext: "in")
let converted = coverter.convert(input) let converted = coverter.convert(input)
let output = testCase(name: name, ext: "ans") let output = testCase(name: name, ext: "ans")
XCTAssertEqual(converted, output, "Conversion \(name) fails") XCTAssertEqual(converted, output, "Conversion \(name) fails")
} }
} }
func testConverterCreationPerformance() { func testConverterCreationPerformance() {
let options: ChineseConverter.Options = [.traditionalize, .twStandard, .twIdiom] let options: ChineseConverter.Options = [.traditionalize, .twStandard, .twIdiom]
measure { measure {
for _ in 0..<10 { for _ in 0..<10 {
_ = try! ChineseConverter(options: options) _ = try! ChineseConverter(options: options)
} }
} }
} }
func testDictionaryCache() { func testDictionaryCache() {
let options: ChineseConverter.Options = [.traditionalize, .twStandard, .twIdiom] let options: ChineseConverter.Options = [.traditionalize, .twStandard, .twIdiom]
let holder = try! ChineseConverter(options: options) let holder = try! ChineseConverter(options: options)
measure { measure {
for _ in 0..<1000 { for _ in 0..<1000 {
_ = try! ChineseConverter(options: options) _ = try! ChineseConverter(options: options)
} }
} }
_ = holder.convert("foo") _ = holder.convert("foo")
} }
func testConversionPerformance() throws { func testConversionPerformance() throws {
let cov = try converter(option: [.traditionalize, .twStandard, .twIdiom]) let cov = try converter(option: [.traditionalize, .twStandard, .twIdiom])
let url = Bundle.module.url( let url = Bundle.module.url(
forResource: "zuozhuan", withExtension: "txt", subdirectory: "benchmark" forResource: "zuozhuan", withExtension: "txt", subdirectory: "benchmark"
)! )!
// 1.9 MB, 624k word // 1.9 MB, 624k word
let str = try String(contentsOf: url) let str = try String(contentsOf: url)
measure { measure {
_ = cov.convert(str) _ = cov.convert(str)
} }
} }
} }

View File

@ -0,0 +1,69 @@
// Copyright (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
import Foundation
public class LineReader {
let encoding: String.Encoding
let chunkSize: Int
var fileHandle: FileHandle
let delimData: Data
var buffer: Data
var atEof: Bool
public init(
file: FileHandle, encoding: String.Encoding = .utf8,
chunkSize: Int = 4096
) throws {
let fileHandle = file
self.encoding = encoding
self.chunkSize = chunkSize
self.fileHandle = fileHandle
delimData = "\n".data(using: encoding)!
buffer = Data(capacity: chunkSize)
atEof = false
}
/// Return next line, or nil on EOF.
public func nextLine() -> String? {
// Read data chunks from file until a line delimiter is found:
while !atEof {
// get a data from the buffer up to the next delimiter
if let range = buffer.range(of: delimData) {
// convert data to a string
let line = String(data: buffer.subdata(in: 0..<range.lowerBound), encoding: encoding)!
// remove that data from the buffer
buffer.removeSubrange(0..<range.upperBound)
return line
}
let nextData = fileHandle.readData(ofLength: chunkSize)
if !nextData.isEmpty {
buffer.append(nextData)
} else {
// End of file or read error
atEof = true
if !buffer.isEmpty {
// Buffer contains last line in file (not terminated by delimiter).
let line = String(data: buffer as Data, encoding: encoding)!
return line
}
}
}
return nil
}
/// Start reading from the beginning of file.
public func rewind() {
fileHandle.seek(toFileOffset: 0)
buffer.count = 0
atEof = false
}
}
extension LineReader: Sequence {
public func makeIterator() -> AnyIterator<String> {
AnyIterator {
self.nextLine()
}
}
}

View File

@ -24,31 +24,20 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
#ifndef USERSYMBOLLM_H #import <Foundation/Foundation.h>
#define USERSYMBOLLM_H
#include "LanguageModel.h" NS_ASSUME_NONNULL_BEGIN
#include "UserPhrasesLM.h"
#include <iostream>
#include <map>
#include <string>
namespace vChewing @interface Composer : NSObject
{ + (BOOL)chkKeyValidity:(UniChar)charCode;
+ (BOOL)isBufferEmpty;
+ (void)clearBuffer;
+ (void)combineReadingKey:(UniChar)charCode;
+ (BOOL)checkWhetherToneMarkerConfirms;
+ (NSString *)getSyllableComposition;
+ (void)doBackSpaceToBuffer;
+ (NSString *)getComposition;
+ (void)ensureParser;
@end
class UserSymbolLM : public UserPhrasesLM NS_ASSUME_NONNULL_END
{
public:
bool allowConsolidation() override
{
return true;
}
float overridedValue() override
{
return -12.0;
}
};
} // namespace vChewing
#endif

117
Source/3rdParty/OVMandarin/Composer.mm vendored Normal file
View File

@ -0,0 +1,117 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "Composer.hh"
#import "Mandarin.h"
#import "vChewing-Swift.h"
static Mandarin::BopomofoReadingBuffer *PhoneticBuffer;
@implementation Composer
+ (BOOL)chkKeyValidity:(UniChar)charCode
{
return PhoneticBuffer->isValidKey((char)charCode);
}
+ (BOOL)isBufferEmpty
{
return PhoneticBuffer->isEmpty();
}
+ (void)clearBuffer
{
PhoneticBuffer->clear();
}
+ (void)combineReadingKey:(UniChar)charCode
{
PhoneticBuffer->combineKey((char)charCode);
}
+ (BOOL)checkWhetherToneMarkerConfirms
{
return PhoneticBuffer->hasToneMarker();
}
+ (NSString *)getSyllableComposition
{
return [NSString stringWithUTF8String:PhoneticBuffer->syllable().composedString().c_str()];
}
+ (void)doBackSpaceToBuffer
{
PhoneticBuffer->backspace();
}
+ (NSString *)getComposition
{
return [NSString stringWithUTF8String:PhoneticBuffer->composedString().c_str()];
}
+ (void)ensureParser
{
if (PhoneticBuffer)
{
switch (mgrPrefs.mandarinParser)
{
case MandarinParserOfStandard:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::StandardLayout());
break;
case MandarinParserOfEten:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::ETenLayout());
break;
case MandarinParserOfHsu:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::HsuLayout());
break;
case MandarinParserOfEen26:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::ETen26Layout());
break;
case MandarinParserOfIBM:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::IBMLayout());
break;
case MandarinParserOfMiTAC:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::MiTACLayout());
break;
case MandarinParserOfFakeSeigyou:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::FakeSeigyouLayout());
break;
case MandarinParserOfHanyuPinyin:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::HanyuPinyinLayout());
break;
default:
PhoneticBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::StandardLayout());
mgrPrefs.mandarinParser = MandarinParserOfStandard;
}
PhoneticBuffer->clear();
}
else
{
PhoneticBuffer = new Mandarin::BopomofoReadingBuffer(Mandarin::BopomofoKeyboardLayout::StandardLayout());
}
}
@end

View File

@ -32,28 +32,28 @@ import OpenCC
/// Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass /// Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass
/// in Swift in order to bridge the Swift classes into our Objective-C++ project. /// in Swift in order to bridge the Swift classes into our Objective-C++ project.
public class OpenCCBridge: NSObject { public class OpenCCBridge: NSObject {
private static let shared = OpenCCBridge() private static let shared = OpenCCBridge()
private var simplify: ChineseConverter? private var simplify: ChineseConverter?
private var traditionalize: ChineseConverter? private var traditionalize: ChineseConverter?
override private init() { override private init() {
try? simplify = ChineseConverter(options: .simplify) try? simplify = ChineseConverter(options: .simplify)
try? traditionalize = ChineseConverter(options: [.traditionalize, .twStandard]) try? traditionalize = ChineseConverter(options: [.traditionalize, .twStandard])
super.init() super.init()
} }
/// CrossConvert. /// CrossConvert.
/// ///
/// - Parameter string: Text in Original Script. /// - Parameter string: Text in Original Script.
/// - Returns: Text converted to Different Script. /// - Returns: Text converted to Different Script.
@objc public static func crossConvert(_ string: String) -> String? { public static func crossConvert(_ string: String) -> String? {
switch ctlInputMethod.currentKeyHandler.inputMode { switch ctlInputMethod.currentKeyHandler.inputMode {
case InputMode.imeModeCHS: case InputMode.imeModeCHS:
return shared.traditionalize?.convert(string) return shared.traditionalize?.convert(string)
case InputMode.imeModeCHT: case InputMode.imeModeCHT:
return shared.simplify?.convert(string) return shared.simplify?.convert(string)
default: default:
return string return string
} }
} }
} }

View File

@ -22,82 +22,82 @@ import SwiftUI
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension Preferences { extension Preferences {
/** /**
Function builder for `Preferences` components used in order to restrict types of child views to be of type `Section`. Function builder for `Preferences` components used in order to restrict types of child views to be of type `Section`.
*/ */
@resultBuilder @resultBuilder
public enum SectionBuilder { public enum SectionBuilder {
public static func buildBlock(_ sections: Section...) -> [Section] { public static func buildBlock(_ sections: Section...) -> [Section] {
sections sections
} }
} }
/** /**
A view which holds `Preferences.Section` views and does all the alignment magic similar to `NSGridView` from AppKit. A view which holds `Preferences.Section` views and does all the alignment magic similar to `NSGridView` from AppKit.
*/ */
public struct Container: View { public struct Container: View {
private let sectionBuilder: () -> [Section] private let sectionBuilder: () -> [Section]
private let contentWidth: Double private let contentWidth: Double
private let minimumLabelWidth: Double private let minimumLabelWidth: Double
@State private var maximumLabelWidth = 0.0 @State private var maximumLabelWidth = 0.0
/** /**
Creates an instance of container component, which handles layout of stacked `Preferences.Section` views. Creates an instance of container component, which handles layout of stacked `Preferences.Section` views.
Custom alignment requires content width to be specified beforehand. Custom alignment requires content width to be specified beforehand.
- Parameters: - Parameters:
- contentWidth: A fixed width of the container's content (excluding paddings). - contentWidth: A fixed width of the container's content (excluding paddings).
- minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label. - minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label.
- builder: A view builder that creates `Preferences.Section`'s of this container. - builder: A view builder that creates `Preferences.Section`'s of this container.
*/ */
public init( public init(
contentWidth: Double, contentWidth: Double,
minimumLabelWidth: Double = 0, minimumLabelWidth: Double = 0,
@SectionBuilder builder: @escaping () -> [Section] @SectionBuilder builder: @escaping () -> [Section]
) { ) {
sectionBuilder = builder sectionBuilder = builder
self.contentWidth = contentWidth self.contentWidth = contentWidth
self.minimumLabelWidth = minimumLabelWidth self.minimumLabelWidth = minimumLabelWidth
} }
public var body: some View { public var body: some View {
let sections = sectionBuilder() let sections = sectionBuilder()
return VStack(alignment: .preferenceSectionLabel) { return VStack(alignment: .preferenceSectionLabel) {
ForEach(0..<sections.count, id: \.self) { index in ForEach(0..<sections.count, id: \.self) { index in
viewForSection(sections, index: index) viewForSection(sections, index: index)
} }
} }
.modifier(Section.LabelWidthModifier(maximumWidth: $maximumLabelWidth)) .modifier(Section.LabelWidthModifier(maximumWidth: $maximumLabelWidth))
.frame(width: CGFloat(contentWidth), alignment: .leading) .frame(width: CGFloat(contentWidth), alignment: .leading)
.padding(.vertical, 20) .padding(.vertical, 20)
.padding(.horizontal, 30) .padding(.horizontal, 30)
} }
@ViewBuilder @ViewBuilder
private func viewForSection(_ sections: [Section], index: Int) -> some View { private func viewForSection(_ sections: [Section], index: Int) -> some View {
sections[index] sections[index]
if index != sections.count - 1, sections[index].bottomDivider { if index != sections.count - 1, sections[index].bottomDivider {
Divider() Divider()
// Strangely doesn't work without width being specified. Probably because of custom alignment. // Strangely doesn't work without width being specified. Probably because of custom alignment.
.frame(width: CGFloat(contentWidth), height: 20) .frame(width: CGFloat(contentWidth), height: 20)
.alignmentGuide(.preferenceSectionLabel) { .alignmentGuide(.preferenceSectionLabel) {
$0[.leading] + CGFloat(max(minimumLabelWidth, maximumLabelWidth)) $0[.leading] + CGFloat(max(minimumLabelWidth, maximumLabelWidth))
} }
} }
} }
} }
} }
/// Extension with custom alignment guide for section title labels. /// Extension with custom alignment guide for section title labels.
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension HorizontalAlignment { extension HorizontalAlignment {
private enum PreferenceSectionLabelAlignment: AlignmentID { private enum PreferenceSectionLabelAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat { static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading] context[HorizontalAlignment.leading]
} }
} }
static let preferenceSectionLabel = HorizontalAlignment(PreferenceSectionLabelAlignment.self) static let preferenceSectionLabel = HorizontalAlignment(PreferenceSectionLabelAlignment.self)
} }

View File

@ -21,135 +21,135 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Foundation import Foundation
struct Localization { struct Localization {
enum Identifier { enum Identifier {
case preferences case preferences
case preferencesEllipsized case preferencesEllipsized
} }
private static let localizedStrings: [Identifier: [String: String]] = [ private static let localizedStrings: [Identifier: [String: String]] = [
.preferences: [ .preferences: [
"ar": "تفضيلات", "ar": "تفضيلات",
"ca": "Preferències", "ca": "Preferències",
"cs": "Předvolby", "cs": "Předvolby",
"da": "Indstillinger", "da": "Indstillinger",
"de": "Einstellungen", "de": "Einstellungen",
"el": "Προτιμήσεις", "el": "Προτιμήσεις",
"en": "Preferences", "en": "Preferences",
"en-AU": "Preferences", "en-AU": "Preferences",
"en-GB": "Preferences", "en-GB": "Preferences",
"es": "Preferencias", "es": "Preferencias",
"es-419": "Preferencias", "es-419": "Preferencias",
"fi": "Asetukset", "fi": "Asetukset",
"fr": "Préférences", "fr": "Préférences",
"fr-CA": "Préférences", "fr-CA": "Préférences",
"he": "העדפות", "he": "העדפות",
"hi": "प्राथमिकता", "hi": "प्राथमिकता",
"hr": "Postavke", "hr": "Postavke",
"hu": "Beállítások", "hu": "Beállítások",
"id": "Preferensi", "id": "Preferensi",
"it": "Preferenze", "it": "Preferenze",
"ja": "環境設定", "ja": "環境設定",
"ko": "환경설정", "ko": "환경설정",
"ms": "Keutamaan", "ms": "Keutamaan",
"nl": "Voorkeuren", "nl": "Voorkeuren",
"no": "Valg", "no": "Valg",
"pl": "Preferencje", "pl": "Preferencje",
"pt": "Preferências", "pt": "Preferências",
"pt-PT": "Preferências", "pt-PT": "Preferências",
"ro": "Preferințe", "ro": "Preferințe",
"ru": "Настройки", "ru": "Настройки",
"sk": "Nastavenia", "sk": "Nastavenia",
"sv": "Inställningar", "sv": "Inställningar",
"th": "การตั้งค่า", "th": "การตั้งค่า",
"tr": "Tercihler", "tr": "Tercihler",
"uk": "Параметри", "uk": "Параметри",
"vi": "Tùy chọn", "vi": "Tùy chọn",
"zh-CN": "偏好设置", "zh-CN": "偏好设置",
"zh-HK": "偏好設定", "zh-HK": "偏好設定",
"zh-TW": "偏好設定", "zh-TW": "偏好設定",
], ],
.preferencesEllipsized: [ .preferencesEllipsized: [
"ar": "تفضيلات…", "ar": "تفضيلات…",
"ca": "Preferències…", "ca": "Preferències…",
"cs": "Předvolby…", "cs": "Předvolby…",
"da": "Indstillinger…", "da": "Indstillinger…",
"de": "Einstellungen…", "de": "Einstellungen…",
"el": "Προτιμήσεις…", "el": "Προτιμήσεις…",
"en": "Preferences…", "en": "Preferences…",
"en-AU": "Preferences…", "en-AU": "Preferences…",
"en-GB": "Preferences…", "en-GB": "Preferences…",
"es": "Preferencias…", "es": "Preferencias…",
"es-419": "Preferencias…", "es-419": "Preferencias…",
"fi": "Asetukset…", "fi": "Asetukset…",
"fr": "Préférences…", "fr": "Préférences…",
"fr-CA": "Préférences…", "fr-CA": "Préférences…",
"he": "העדפות…", "he": "העדפות…",
"hi": "प्राथमिकता…", "hi": "प्राथमिकता…",
"hr": "Postavke…", "hr": "Postavke…",
"hu": "Beállítások…", "hu": "Beállítások…",
"id": "Preferensi…", "id": "Preferensi…",
"it": "Preferenze…", "it": "Preferenze…",
"ja": "環境設定…", "ja": "環境設定…",
"ko": "환경설정...", "ko": "환경설정...",
"ms": "Keutamaan…", "ms": "Keutamaan…",
"nl": "Voorkeuren…", "nl": "Voorkeuren…",
"no": "Valg…", "no": "Valg…",
"pl": "Preferencje…", "pl": "Preferencje…",
"pt": "Preferências…", "pt": "Preferências…",
"pt-PT": "Preferências…", "pt-PT": "Preferências…",
"ro": "Preferințe…", "ro": "Preferințe…",
"ru": "Настройки…", "ru": "Настройки…",
"sk": "Nastavenia…", "sk": "Nastavenia…",
"sv": "Inställningar…", "sv": "Inställningar…",
"th": "การตั้งค่า…", "th": "การตั้งค่า…",
"tr": "Tercihler…", "tr": "Tercihler…",
"uk": "Параметри…", "uk": "Параметри…",
"vi": "Tùy chọn…", "vi": "Tùy chọn…",
"zh-CN": "偏好设置…", "zh-CN": "偏好设置…",
"zh-HK": "偏好設定⋯", "zh-HK": "偏好設定⋯",
"zh-TW": "偏好設定⋯", "zh-TW": "偏好設定⋯",
], ],
] ]
/** /**
Returns the localized version of the given string. Returns the localized version of the given string.
- Parameter identifier: Identifier of the string to localize. - Parameter identifier: Identifier of the string to localize.
- Note: If the system's locale can't be determined, the English localization of the string will be returned. - Note: If the system's locale can't be determined, the English localization of the string will be returned.
*/ */
static subscript(identifier: Identifier) -> String { static subscript(identifier: Identifier) -> String {
// Force-unwrapped since all of the involved code is under our control. // Force-unwrapped since all of the involved code is under our control.
let localizedDict = Localization.localizedStrings[identifier]! let localizedDict = Localization.localizedStrings[identifier]!
let defaultLocalizedString = localizedDict["en"]! let defaultLocalizedString = localizedDict["en"]!
// Iterate through all user-preferred languages until we find one that has a valid language code. // Iterate through all user-preferred languages until we find one that has a valid language code.
let preferredLocale = let preferredLocale =
Locale.preferredLanguages Locale.preferredLanguages
.lazy .lazy
.map { Locale(identifier: $0) } .map { Locale(identifier: $0) }
.first { $0.languageCode != nil } .first { $0.languageCode != nil }
?? .current ?? .current
guard let languageCode = preferredLocale.languageCode else { guard let languageCode = preferredLocale.languageCode else {
return defaultLocalizedString return defaultLocalizedString
} }
// Chinese is the only language where different region codes result in different translations. // Chinese is the only language where different region codes result in different translations.
if languageCode == "zh" { if languageCode == "zh" {
let regionCode = preferredLocale.regionCode ?? "" let regionCode = preferredLocale.regionCode ?? ""
if regionCode == "HK" || regionCode == "TW" { if regionCode == "HK" || regionCode == "TW" {
return localizedDict["\(languageCode)-\(regionCode)"]! return localizedDict["\(languageCode)-\(regionCode)"]!
} else { } else {
// Fall back to "regular" zh-CN if neither the HK or TW region codes are found. // Fall back to "regular" zh-CN if neither the HK or TW region codes are found.
return localizedDict["\(languageCode)-CN"]! return localizedDict["\(languageCode)-CN"]!
} }
} else { } else {
if let localizedString = localizedDict[languageCode] { if let localizedString = localizedDict[languageCode] {
return localizedString return localizedString
} }
} }
return defaultLocalizedString return defaultLocalizedString
} }
} }

View File

@ -24,89 +24,89 @@ import SwiftUI
/// ///
/// Acts as type-eraser for `Preferences.Pane<T>`. /// Acts as type-eraser for `Preferences.Pane<T>`.
public protocol PreferencePaneConvertible { public protocol PreferencePaneConvertible {
/** /**
Convert `self` to equivalent `PreferencePane`. Convert `self` to equivalent `PreferencePane`.
*/ */
func asPreferencePane() -> PreferencePane func asPreferencePane() -> PreferencePane
} }
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension Preferences { extension Preferences {
/** /**
Create a SwiftUI-based preference pane. Create a SwiftUI-based preference pane.
SwiftUI equivalent of the `PreferencePane` protocol. SwiftUI equivalent of the `PreferencePane` protocol.
*/ */
public struct Pane<Content: View>: View, PreferencePaneConvertible { public struct Pane<Content: View>: View, PreferencePaneConvertible {
let identifier: PaneIdentifier let identifier: PaneIdentifier
let title: String let title: String
let toolbarIcon: NSImage let toolbarIcon: NSImage
let content: Content let content: Content
public init( public init(
identifier: PaneIdentifier, identifier: PaneIdentifier,
title: String, title: String,
toolbarIcon: NSImage, toolbarIcon: NSImage,
contentView: () -> Content contentView: () -> Content
) { ) {
self.identifier = identifier self.identifier = identifier
self.title = title self.title = title
self.toolbarIcon = toolbarIcon self.toolbarIcon = toolbarIcon
content = contentView() content = contentView()
} }
public var body: some View { content } public var body: some View { content }
public func asPreferencePane() -> PreferencePane { public func asPreferencePane() -> PreferencePane {
PaneHostingController(pane: self) PaneHostingController(pane: self)
} }
} }
/** /**
Hosting controller enabling `Preferences.Pane` to be used alongside AppKit `NSViewController`'s. Hosting controller enabling `Preferences.Pane` to be used alongside AppKit `NSViewController`'s.
*/ */
public final class PaneHostingController<Content: View>: NSHostingController<Content>, PreferencePane { public final class PaneHostingController<Content: View>: NSHostingController<Content>, PreferencePane {
public let preferencePaneIdentifier: PaneIdentifier public let preferencePaneIdentifier: PaneIdentifier
public let preferencePaneTitle: String public let preferencePaneTitle: String
public let toolbarItemIcon: NSImage public let toolbarItemIcon: NSImage
init( init(
identifier: PaneIdentifier, identifier: PaneIdentifier,
title: String, title: String,
toolbarIcon: NSImage, toolbarIcon: NSImage,
content: Content content: Content
) { ) {
preferencePaneIdentifier = identifier preferencePaneIdentifier = identifier
preferencePaneTitle = title preferencePaneTitle = title
toolbarItemIcon = toolbarIcon toolbarItemIcon = toolbarIcon
super.init(rootView: content) super.init(rootView: content)
} }
public convenience init(pane: Pane<Content>) { public convenience init(pane: Pane<Content>) {
self.init( self.init(
identifier: pane.identifier, identifier: pane.identifier,
title: pane.title, title: pane.title,
toolbarIcon: pane.toolbarIcon, toolbarIcon: pane.toolbarIcon,
content: pane.content content: pane.content
) )
} }
@available(*, unavailable) @available(*, unavailable)
@objc @objc
dynamic required init?(coder _: NSCoder) { dynamic required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
} }
} }
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension View { extension View {
/** /**
Applies font and color for a label used for describing a preference. Applies font and color for a label used for describing a preference.
*/ */
public func preferenceDescription() -> some View { public func preferenceDescription() -> some View {
font(.system(size: 11.0)) font(.system(size: 11.0))
// TODO: Use `.foregroundStyle` when targeting macOS 12. // TODO: Use `.foregroundStyle` when targeting macOS 12.
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }

View File

@ -21,39 +21,39 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension Preferences { extension Preferences {
public struct PaneIdentifier: Hashable, RawRepresentable, Codable { public struct PaneIdentifier: Hashable, RawRepresentable, Codable {
public let rawValue: String public let rawValue: String
public init(rawValue: String) { public init(rawValue: String) {
self.rawValue = rawValue self.rawValue = rawValue
} }
} }
} }
public protocol PreferencePane: NSViewController { public protocol PreferencePane: NSViewController {
var preferencePaneIdentifier: Preferences.PaneIdentifier { get } var preferencePaneIdentifier: Preferences.PaneIdentifier { get }
var preferencePaneTitle: String { get } var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get } var toolbarItemIcon: NSImage { get }
} }
extension PreferencePane { extension PreferencePane {
public var toolbarItemIdentifier: NSToolbarItem.Identifier { public var toolbarItemIdentifier: NSToolbarItem.Identifier {
preferencePaneIdentifier.toolbarItemIdentifier preferencePaneIdentifier.toolbarItemIdentifier
} }
public var toolbarItemIcon: NSImage { .empty } public var toolbarItemIcon: NSImage { .empty }
} }
extension Preferences.PaneIdentifier { extension Preferences.PaneIdentifier {
public init(_ rawValue: String) { public init(_ rawValue: String) {
self.init(rawValue: rawValue) self.init(rawValue: rawValue)
} }
public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) { public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) {
self.init(rawValue: itemIdentifier.rawValue) self.init(rawValue: itemIdentifier.rawValue)
} }
public var toolbarItemIdentifier: NSToolbarItem.Identifier { public var toolbarItemIdentifier: NSToolbarItem.Identifier {
NSToolbarItem.Identifier(rawValue) NSToolbarItem.Identifier(rawValue)
} }
} }

View File

@ -21,8 +21,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension Preferences { extension Preferences {
public enum Style { public enum Style {
case toolbarItems case toolbarItems
case segmentedControl case segmentedControl
} }
} }

View File

@ -21,15 +21,15 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
protocol PreferencesStyleController: AnyObject { protocol PreferencesStyleController: AnyObject {
var delegate: PreferencesStyleControllerDelegate? { get set } var delegate: PreferencesStyleControllerDelegate? { get set }
var isKeepingWindowCentered: Bool { get } var isKeepingWindowCentered: Bool { get }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem?
func selectTab(index: Int) func selectTab(index: Int)
} }
protocol PreferencesStyleControllerDelegate: AnyObject { protocol PreferencesStyleControllerDelegate: AnyObject {
func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool) func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool)
func activateTab(index: Int, animated: Bool) func activateTab(index: Int, animated: Bool)
} }

View File

@ -21,242 +21,242 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate { final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate {
private var activeTab: Int? private var activeTab: Int?
private var preferencePanes = [PreferencePane]() private var preferencePanes = [PreferencePane]()
private var style: Preferences.Style? private var style: Preferences.Style?
internal var preferencePanesCount: Int { preferencePanes.count } internal var preferencePanesCount: Int { preferencePanes.count }
private var preferencesStyleController: PreferencesStyleController! private var preferencesStyleController: PreferencesStyleController!
private var isKeepingWindowCentered: Bool { preferencesStyleController.isKeepingWindowCentered } private var isKeepingWindowCentered: Bool { preferencesStyleController.isKeepingWindowCentered }
private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] { private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] {
preferencesStyleController?.toolbarItemIdentifiers() ?? [] preferencesStyleController?.toolbarItemIdentifiers() ?? []
} }
var window: NSWindow! { view.window } var window: NSWindow! { view.window }
var isAnimated = true var isAnimated = true
var activeViewController: NSViewController? { var activeViewController: NSViewController? {
guard let activeTab = activeTab else { guard let activeTab = activeTab else {
return nil return nil
} }
return preferencePanes[activeTab] return preferencePanes[activeTab]
} }
override func loadView() { override func loadView() {
view = NSView() view = NSView()
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
} }
func configure(preferencePanes: [PreferencePane], style: Preferences.Style) { func configure(preferencePanes: [PreferencePane], style: Preferences.Style) {
self.preferencePanes = preferencePanes self.preferencePanes = preferencePanes
self.style = style self.style = style
children = preferencePanes children = preferencePanes
let toolbar = NSToolbar(identifier: "PreferencesToolbar") let toolbar = NSToolbar(identifier: "PreferencesToolbar")
toolbar.allowsUserCustomization = false toolbar.allowsUserCustomization = false
toolbar.displayMode = .iconAndLabel toolbar.displayMode = .iconAndLabel
toolbar.showsBaselineSeparator = true toolbar.showsBaselineSeparator = true
toolbar.delegate = self toolbar.delegate = self
switch style { switch style {
case .segmentedControl: case .segmentedControl:
preferencesStyleController = SegmentedControlStyleViewController(preferencePanes: preferencePanes) preferencesStyleController = SegmentedControlStyleViewController(preferencePanes: preferencePanes)
case .toolbarItems: case .toolbarItems:
preferencesStyleController = ToolbarItemStyleViewController( preferencesStyleController = ToolbarItemStyleViewController(
preferencePanes: preferencePanes, preferencePanes: preferencePanes,
toolbar: toolbar, toolbar: toolbar,
centerToolbarItems: false centerToolbarItems: false
) )
} }
preferencesStyleController.delegate = self preferencesStyleController.delegate = self
// Called last so that `preferencesStyleController` can be asked for items. // Called last so that `preferencesStyleController` can be asked for items.
window.toolbar = toolbar window.toolbar = toolbar
} }
func activateTab(preferencePane: PreferencePane, animated: Bool) { func activateTab(preferencePane: PreferencePane, animated: Bool) {
activateTab(preferenceIdentifier: preferencePane.preferencePaneIdentifier, animated: animated) activateTab(preferenceIdentifier: preferencePane.preferencePaneIdentifier, animated: animated)
} }
func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool) { func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool) {
guard let index = (preferencePanes.firstIndex { $0.preferencePaneIdentifier == preferenceIdentifier }) else { guard let index = (preferencePanes.firstIndex { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
return activateTab(index: 0, animated: animated) return activateTab(index: 0, animated: animated)
} }
activateTab(index: index, animated: animated) activateTab(index: index, animated: animated)
} }
func activateTab(index: Int, animated: Bool) { func activateTab(index: Int, animated: Bool) {
defer { defer {
activeTab = index activeTab = index
preferencesStyleController.selectTab(index: index) preferencesStyleController.selectTab(index: index)
updateWindowTitle(tabIndex: index) updateWindowTitle(tabIndex: index)
} }
if activeTab == nil { if activeTab == nil {
immediatelyDisplayTab(index: index) immediatelyDisplayTab(index: index)
} else { } else {
guard index != activeTab else { guard index != activeTab else {
return return
} }
animateTabTransition(index: index, animated: animated) animateTabTransition(index: index, animated: animated)
} }
} }
func restoreInitialTab() { func restoreInitialTab() {
if activeTab == nil { if activeTab == nil {
activateTab(index: 0, animated: false) activateTab(index: 0, animated: false)
} }
} }
private func updateWindowTitle(tabIndex _: Int) { private func updateWindowTitle(tabIndex _: Int) {
window.title = { window.title = {
// if preferencePanes.count > 1 { // if preferencePanes.count > 1 {
// return preferencePanes[tabIndex].preferencePaneTitle // return preferencePanes[tabIndex].preferencePaneTitle
// } else { // } else {
// let preferences = Localization[.preferences] // let preferences = Localization[.preferences]
// let appName = Bundle.main.appName // let appName = Bundle.main.appName
// return "\(appName) \(preferences)" // return "\(appName) \(preferences)"
// } // }
var preferencesTitleName = NSLocalizedString("vChewing Preferences…", comment: "") var preferencesTitleName = NSLocalizedString("vChewing Preferences…", comment: "")
preferencesTitleName.removeLast() preferencesTitleName.removeLast()
return preferencesTitleName return preferencesTitleName
}() }()
} }
/// Cached constraints that pin `childViewController` views to the content view. /// Cached constraints that pin `childViewController` views to the content view.
private var activeChildViewConstraints = [NSLayoutConstraint]() private var activeChildViewConstraints = [NSLayoutConstraint]()
private func immediatelyDisplayTab(index: Int) { private func immediatelyDisplayTab(index: Int) {
let toViewController = preferencePanes[index] let toViewController = preferencePanes[index]
view.addSubview(toViewController.view) view.addSubview(toViewController.view)
activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds() activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
setWindowFrame(for: toViewController, animated: false) setWindowFrame(for: toViewController, animated: false)
} }
private func animateTabTransition(index: Int, animated: Bool) { private func animateTabTransition(index: Int, animated: Bool) {
guard let activeTab = activeTab else { guard let activeTab = activeTab else {
assertionFailure( assertionFailure(
"animateTabTransition called before a tab was displayed; transition only works from one tab to another") "animateTabTransition called before a tab was displayed; transition only works from one tab to another")
immediatelyDisplayTab(index: index) immediatelyDisplayTab(index: index)
return return
} }
let fromViewController = preferencePanes[activeTab] let fromViewController = preferencePanes[activeTab]
let toViewController = preferencePanes[index] let toViewController = preferencePanes[index]
// View controller animations only work on macOS 10.14 and newer. // View controller animations only work on macOS 10.14 and newer.
let options: NSViewController.TransitionOptions let options: NSViewController.TransitionOptions
if #available(macOS 10.14, *) { if #available(macOS 10.14, *) {
options = animated && isAnimated ? [.crossfade] : [] options = animated && isAnimated ? [.crossfade] : []
} else { } else {
options = [] options = []
} }
view.removeConstraints(activeChildViewConstraints) view.removeConstraints(activeChildViewConstraints)
transition( transition(
from: fromViewController, from: fromViewController,
to: toViewController, to: toViewController,
options: options options: options
) { [self] in ) { [self] in
activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds() activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
} }
} }
override func transition( override func transition(
from fromViewController: NSViewController, from fromViewController: NSViewController,
to toViewController: NSViewController, to toViewController: NSViewController,
options: NSViewController.TransitionOptions = [], options: NSViewController.TransitionOptions = [],
completionHandler completion: (() -> Void)? = nil completionHandler completion: (() -> Void)? = nil
) { ) {
let isAnimated = let isAnimated =
options options
.intersection([ .intersection([
.crossfade, .crossfade,
.slideUp, .slideUp,
.slideDown, .slideDown,
.slideForward, .slideForward,
.slideBackward, .slideBackward,
.slideLeft, .slideLeft,
.slideRight, .slideRight,
]) ])
.isEmpty == false .isEmpty == false
if isAnimated { if isAnimated {
NSAnimationContext.runAnimationGroup( NSAnimationContext.runAnimationGroup(
{ context in { context in
context.allowsImplicitAnimation = true context.allowsImplicitAnimation = true
context.duration = 0.25 context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
setWindowFrame(for: toViewController, animated: true) setWindowFrame(for: toViewController, animated: true)
super.transition( super.transition(
from: fromViewController, from: fromViewController,
to: toViewController, to: toViewController,
options: options, options: options,
completionHandler: completion completionHandler: completion
) )
}, completionHandler: nil }, completionHandler: nil
) )
} else { } else {
super.transition( super.transition(
from: fromViewController, from: fromViewController,
to: toViewController, to: toViewController,
options: options, options: options,
completionHandler: completion completionHandler: completion
) )
} }
} }
private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) { private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) {
guard let window = window else { guard let window = window else {
preconditionFailure() preconditionFailure()
} }
let contentSize = viewController.view.fittingSize let contentSize = viewController.view.fittingSize
let newWindowSize = window.frameRect(forContentRect: CGRect(origin: .zero, size: contentSize)).size let newWindowSize = window.frameRect(forContentRect: CGRect(origin: .zero, size: contentSize)).size
var frame = window.frame var frame = window.frame
frame.origin.y += frame.height - newWindowSize.height frame.origin.y += frame.height - newWindowSize.height
frame.size = newWindowSize frame.size = newWindowSize
if isKeepingWindowCentered { if isKeepingWindowCentered {
let horizontalDiff = (window.frame.width - newWindowSize.width) / 2 let horizontalDiff = (window.frame.width - newWindowSize.width) / 2
frame.origin.x += horizontalDiff frame.origin.x += horizontalDiff
} }
let animatableWindow = animated ? window.animator() : window let animatableWindow = animated ? window.animator() : window
animatableWindow.setFrame(frame, display: false) animatableWindow.setFrame(frame, display: false)
} }
} }
extension PreferencesTabViewController: NSToolbarDelegate { extension PreferencesTabViewController: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarItemIdentifiers toolbarItemIdentifiers
} }
func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarItemIdentifiers toolbarItemIdentifiers
} }
func toolbarSelectableItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarSelectableItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
style == .segmentedControl ? [] : toolbarItemIdentifiers style == .segmentedControl ? [] : toolbarItemIdentifiers
} }
public func toolbar( public func toolbar(
_: NSToolbar, _: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar _: Bool willBeInsertedIntoToolbar _: Bool
) -> NSToolbarItem? { ) -> NSToolbarItem? {
if itemIdentifier == .flexibleSpace { if itemIdentifier == .flexibleSpace {
return nil return nil
} }
return preferencesStyleController.toolbarItem( return preferencesStyleController.toolbarItem(
preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: itemIdentifier)) preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: itemIdentifier))
} }
} }

View File

@ -21,168 +21,168 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension NSWindow.FrameAutosaveName { extension NSWindow.FrameAutosaveName {
static let preferences: NSWindow.FrameAutosaveName = "com.sindresorhus.Preferences.FrameAutosaveName" static let preferences: NSWindow.FrameAutosaveName = "com.sindresorhus.Preferences.FrameAutosaveName"
} }
public final class PreferencesWindowController: NSWindowController { public final class PreferencesWindowController: NSWindowController {
private let tabViewController = PreferencesTabViewController() private let tabViewController = PreferencesTabViewController()
public var isAnimated: Bool { public var isAnimated: Bool {
get { tabViewController.isAnimated } get { tabViewController.isAnimated }
set { set {
tabViewController.isAnimated = newValue tabViewController.isAnimated = newValue
} }
} }
public var hidesToolbarForSingleItem: Bool { public var hidesToolbarForSingleItem: Bool {
didSet { didSet {
updateToolbarVisibility() updateToolbarVisibility()
} }
} }
private func updateToolbarVisibility() { private func updateToolbarVisibility() {
window?.toolbar?.isVisible = window?.toolbar?.isVisible =
(hidesToolbarForSingleItem == false) (hidesToolbarForSingleItem == false)
|| (tabViewController.preferencePanesCount > 1) || (tabViewController.preferencePanesCount > 1)
} }
public init( public init(
preferencePanes: [PreferencePane], preferencePanes: [PreferencePane],
style: Preferences.Style = .toolbarItems, style: Preferences.Style = .toolbarItems,
animated: Bool = true, animated: Bool = true,
hidesToolbarForSingleItem: Bool = true hidesToolbarForSingleItem: Bool = true
) { ) {
precondition(!preferencePanes.isEmpty, "You need to set at least one view controller") precondition(!preferencePanes.isEmpty, "You need to set at least one view controller")
let window = UserInteractionPausableWindow( let window = UserInteractionPausableWindow(
contentRect: preferencePanes[0].view.bounds, contentRect: preferencePanes[0].view.bounds,
styleMask: [ styleMask: [
.titled, .titled,
.closable, .closable,
], ],
backing: .buffered, backing: .buffered,
defer: true defer: true
) )
self.hidesToolbarForSingleItem = hidesToolbarForSingleItem self.hidesToolbarForSingleItem = hidesToolbarForSingleItem
super.init(window: window) super.init(window: window)
window.contentViewController = tabViewController window.contentViewController = tabViewController
window.titleVisibility = { window.titleVisibility = {
switch style { switch style {
case .toolbarItems: case .toolbarItems:
return .visible return .visible
case .segmentedControl: case .segmentedControl:
return preferencePanes.count <= 1 ? .visible : .hidden return preferencePanes.count <= 1 ? .visible : .hidden
} }
}() }()
if #available(macOS 11.0, *), style == .toolbarItems { if #available(macOS 11.0, *), style == .toolbarItems {
window.toolbarStyle = .preference window.toolbarStyle = .preference
} }
tabViewController.isAnimated = animated tabViewController.isAnimated = animated
tabViewController.configure(preferencePanes: preferencePanes, style: style) tabViewController.configure(preferencePanes: preferencePanes, style: style)
updateToolbarVisibility() updateToolbarVisibility()
} }
@available(*, unavailable) @available(*, unavailable)
override public init(window _: NSWindow?) { override public init(window _: NSWindow?) {
fatalError("init(window:) is not supported, use init(preferences:style:animated:)") fatalError("init(window:) is not supported, use init(preferences:style:animated:)")
} }
@available(*, unavailable) @available(*, unavailable)
public required init?(coder _: NSCoder) { public required init?(coder _: NSCoder) {
fatalError("init(coder:) is not supported, use init(preferences:style:animated:)") fatalError("init(coder:) is not supported, use init(preferences:style:animated:)")
} }
/** /**
Show the preferences window and brings it to front. Show the preferences window and brings it to front.
If you pass a `Preferences.PaneIdentifier`, the window will activate the corresponding tab. If you pass a `Preferences.PaneIdentifier`, the window will activate the corresponding tab.
- Parameter preferencePane: Identifier of the preference pane to display, or `nil` to show the tab that was open when the user last closed the window. - Parameter preferencePane: Identifier of the preference pane to display, or `nil` to show the tab that was open when the user last closed the window.
- Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`. - Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`.
- See `close()` to close the window again. - See `close()` to close the window again.
- See `showWindow(_:)` to show the window without the convenience of activating the app. - See `showWindow(_:)` to show the window without the convenience of activating the app.
*/ */
public func show(preferencePane preferenceIdentifier: Preferences.PaneIdentifier? = nil) { public func show(preferencePane preferenceIdentifier: Preferences.PaneIdentifier? = nil) {
if let preferenceIdentifier = preferenceIdentifier { if let preferenceIdentifier = preferenceIdentifier {
tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false) tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false)
} else { } else {
tabViewController.restoreInitialTab() tabViewController.restoreInitialTab()
} }
showWindow(self) showWindow(self)
restoreWindowPosition() restoreWindowPosition()
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
private func restoreWindowPosition() { private func restoreWindowPosition() {
guard guard
let window = window, let window = window,
let screenContainingWindow = window.screen let screenContainingWindow = window.screen
else { else {
return return
} }
window.setFrameOrigin( window.setFrameOrigin(
CGPoint( CGPoint(
x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2, x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2,
y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2 y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2
)) ))
window.setFrameUsingName(.preferences) window.setFrameUsingName(.preferences)
window.setFrameAutosaveName(.preferences) window.setFrameAutosaveName(.preferences)
} }
} }
extension PreferencesWindowController { extension PreferencesWindowController {
/// Returns the active pane if it responds to the given action. /// Returns the active pane if it responds to the given action.
public override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? { public override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
if let target = super.supplementalTarget(forAction: action, sender: sender) { if let target = super.supplementalTarget(forAction: action, sender: sender) {
return target return target
} }
guard let activeViewController = tabViewController.activeViewController else { guard let activeViewController = tabViewController.activeViewController else {
return nil return nil
} }
if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder, if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder,
target.responds(to: action) target.responds(to: action)
{ {
return target return target
} }
if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder, if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder,
target.responds(to: action) target.responds(to: action)
{ {
return target return target
} }
return nil return nil
} }
} }
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension PreferencesWindowController { extension PreferencesWindowController {
/** /**
Create a preferences window from only SwiftUI-based preference panes. Create a preferences window from only SwiftUI-based preference panes.
*/ */
public convenience init( public convenience init(
panes: [PreferencePaneConvertible], panes: [PreferencePaneConvertible],
style: Preferences.Style = .toolbarItems, style: Preferences.Style = .toolbarItems,
animated: Bool = true, animated: Bool = true,
hidesToolbarForSingleItem: Bool = true hidesToolbarForSingleItem: Bool = true
) { ) {
let preferencePanes = panes.map { $0.asPreferencePane() } let preferencePanes = panes.map { $0.asPreferencePane() }
self.init( self.init(
preferencePanes: preferencePanes, preferencePanes: preferencePanes,
style: style, style: style,
animated: animated, animated: animated,
hidesToolbarForSingleItem: hidesToolbarForSingleItem hidesToolbarForSingleItem: hidesToolbarForSingleItem
) )
} }
} }

View File

@ -22,119 +22,119 @@ import SwiftUI
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension Preferences { extension Preferences {
/** /**
Represents a section with right-aligned title and optional bottom divider. Represents a section with right-aligned title and optional bottom divider.
*/ */
@available(macOS 10.15, *) @available(macOS 10.15, *)
public struct Section: View { public struct Section: View {
/** /**
Preference key holding max width of section labels. Preference key holding max width of section labels.
*/ */
private struct LabelWidthPreferenceKey: PreferenceKey { private struct LabelWidthPreferenceKey: PreferenceKey {
typealias Value = Double typealias Value = Double
static var defaultValue = 0.0 static var defaultValue = 0.0
static func reduce(value: inout Double, nextValue: () -> Double) { static func reduce(value: inout Double, nextValue: () -> Double) {
let next = nextValue() let next = nextValue()
value = next > value ? next : value value = next > value ? next : value
} }
} }
/** /**
Convenience overlay for finding a label's dimensions using `GeometryReader`. Convenience overlay for finding a label's dimensions using `GeometryReader`.
*/ */
private struct LabelOverlay: View { private struct LabelOverlay: View {
var body: some View { var body: some View {
GeometryReader { geometry in GeometryReader { geometry in
Color.clear Color.clear
.preference(key: LabelWidthPreferenceKey.self, value: Double(geometry.size.width)) .preference(key: LabelWidthPreferenceKey.self, value: Double(geometry.size.width))
} }
} }
} }
/** /**
Convenience modifier for applying `LabelWidthPreferenceKey`. Convenience modifier for applying `LabelWidthPreferenceKey`.
*/ */
struct LabelWidthModifier: ViewModifier { struct LabelWidthModifier: ViewModifier {
@Binding var maximumWidth: Double @Binding var maximumWidth: Double
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in .onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in
maximumWidth = Double(newMaximumWidth) maximumWidth = Double(newMaximumWidth)
} }
} }
} }
public let label: AnyView public let label: AnyView
public let content: AnyView public let content: AnyView
public let bottomDivider: Bool public let bottomDivider: Bool
public let verticalAlignment: VerticalAlignment public let verticalAlignment: VerticalAlignment
/** /**
A section is responsible for controlling a single preference. A section is responsible for controlling a single preference.
- Parameters: - Parameters:
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`. - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content. - verticalAlignement: The vertical alignment of the section content.
- verticalAlignment: - verticalAlignment:
- label: A view describing preference handled by this section. - label: A view describing preference handled by this section.
- content: A content view. - content: A content view.
*/ */
public init<Label: View, Content: View>( public init<Label: View, Content: View>(
bottomDivider: Bool = false, bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline, verticalAlignment: VerticalAlignment = .firstTextBaseline,
label: @escaping () -> Label, label: @escaping () -> Label,
@ViewBuilder content: @escaping () -> Content @ViewBuilder content: @escaping () -> Content
) { ) {
self.label = label() self.label = label()
.overlay(LabelOverlay()) .overlay(LabelOverlay())
.eraseToAnyView() // TODO: Remove use of `AnyView`. .eraseToAnyView() // TODO: Remove use of `AnyView`.
self.bottomDivider = bottomDivider self.bottomDivider = bottomDivider
self.verticalAlignment = verticalAlignment self.verticalAlignment = verticalAlignment
let stack = VStack(alignment: .leading) { content() } let stack = VStack(alignment: .leading) { content() }
self.content = stack.eraseToAnyView() self.content = stack.eraseToAnyView()
} }
/** /**
Creates instance of section, responsible for controling single preference with `Text` as a `Label`. Creates instance of section, responsible for controling single preference with `Text` as a `Label`.
- Parameters: - Parameters:
- title: A string describing preference handled by this section. - title: A string describing preference handled by this section.
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`. - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content. - verticalAlignement: The vertical alignment of the section content.
- verticalAlignment: - verticalAlignment:
- content: A content view. - content: A content view.
*/ */
public init<Content: View>( public init<Content: View>(
title: String, title: String,
bottomDivider: Bool = false, bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline, verticalAlignment: VerticalAlignment = .firstTextBaseline,
@ViewBuilder content: @escaping () -> Content @ViewBuilder content: @escaping () -> Content
) { ) {
let textLabel = { let textLabel = {
Text(title) Text(title)
.font(.system(size: 13.0)) .font(.system(size: 13.0))
.overlay(LabelOverlay()) .overlay(LabelOverlay())
.eraseToAnyView() .eraseToAnyView()
} }
self.init( self.init(
bottomDivider: bottomDivider, bottomDivider: bottomDivider,
verticalAlignment: verticalAlignment, verticalAlignment: verticalAlignment,
label: textLabel, label: textLabel,
content: content content: content
) )
} }
public var body: some View { public var body: some View {
HStack(alignment: verticalAlignment) { HStack(alignment: verticalAlignment) {
label label
.alignmentGuide(.preferenceSectionLabel) { $0[.trailing] } .alignmentGuide(.preferenceSectionLabel) { $0[.trailing] }
content content
Spacer() Spacer()
} }
} }
} }
} }

View File

@ -21,139 +21,139 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension NSToolbarItem.Identifier { extension NSToolbarItem.Identifier {
static let toolbarSegmentedControlItem = Self("toolbarSegmentedControlItem") static let toolbarSegmentedControlItem = Self("toolbarSegmentedControlItem")
} }
extension NSUserInterfaceItemIdentifier { extension NSUserInterfaceItemIdentifier {
static let toolbarSegmentedControl = Self("toolbarSegmentedControl") static let toolbarSegmentedControl = Self("toolbarSegmentedControl")
} }
final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController { final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController {
var segmentedControl: NSSegmentedControl! { var segmentedControl: NSSegmentedControl! {
get { view as? NSSegmentedControl } get { view as? NSSegmentedControl }
set { set {
view = newValue view = newValue
} }
} }
var isKeepingWindowCentered: Bool { true } var isKeepingWindowCentered: Bool { true }
weak var delegate: PreferencesStyleControllerDelegate? weak var delegate: PreferencesStyleControllerDelegate?
private var preferencePanes: [PreferencePane]! private var preferencePanes: [PreferencePane]!
required init(preferencePanes: [PreferencePane]) { required init(preferencePanes: [PreferencePane]) {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.preferencePanes = preferencePanes self.preferencePanes = preferencePanes
} }
@available(*, unavailable) @available(*, unavailable)
required init?(coder _: NSCoder) { required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func loadView() {
view = createSegmentedControl(preferencePanes: preferencePanes) view = createSegmentedControl(preferencePanes: preferencePanes)
} }
fileprivate func createSegmentedControl(preferencePanes: [PreferencePane]) -> NSSegmentedControl { fileprivate func createSegmentedControl(preferencePanes: [PreferencePane]) -> NSSegmentedControl {
let segmentedControl = NSSegmentedControl() let segmentedControl = NSSegmentedControl()
segmentedControl.segmentCount = preferencePanes.count segmentedControl.segmentCount = preferencePanes.count
segmentedControl.segmentStyle = .texturedSquare segmentedControl.segmentStyle = .texturedSquare
segmentedControl.target = self segmentedControl.target = self
segmentedControl.action = #selector(segmentedControlAction) segmentedControl.action = #selector(segmentedControlAction)
segmentedControl.identifier = .toolbarSegmentedControl segmentedControl.identifier = .toolbarSegmentedControl
if let cell = segmentedControl.cell as? NSSegmentedCell { if let cell = segmentedControl.cell as? NSSegmentedCell {
cell.controlSize = .regular cell.controlSize = .regular
cell.trackingMode = .selectOne cell.trackingMode = .selectOne
} }
let segmentSize: CGSize = { let segmentSize: CGSize = {
let insets = CGSize(width: 36, height: 12) let insets = CGSize(width: 36, height: 12)
var maxSize = CGSize.zero var maxSize = CGSize.zero
for preferencePane in preferencePanes { for preferencePane in preferencePanes {
let title = preferencePane.preferencePaneTitle let title = preferencePane.preferencePaneTitle
let titleSize = title.size( let titleSize = title.size(
withAttributes: [ withAttributes: [
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)) .font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular))
] ]
) )
maxSize = CGSize( maxSize = CGSize(
width: max(titleSize.width, maxSize.width), width: max(titleSize.width, maxSize.width),
height: max(titleSize.height, maxSize.height) height: max(titleSize.height, maxSize.height)
) )
} }
return CGSize( return CGSize(
width: maxSize.width + insets.width, width: maxSize.width + insets.width,
height: maxSize.height + insets.height height: maxSize.height + insets.height
) )
}() }()
let segmentBorderWidth = CGFloat(preferencePanes.count) + 1 let segmentBorderWidth = CGFloat(preferencePanes.count) + 1
let segmentWidth = segmentSize.width * CGFloat(preferencePanes.count) + segmentBorderWidth let segmentWidth = segmentSize.width * CGFloat(preferencePanes.count) + segmentBorderWidth
let segmentHeight = segmentSize.height let segmentHeight = segmentSize.height
segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight) segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
for (index, preferencePane) in preferencePanes.enumerated() { for (index, preferencePane) in preferencePanes.enumerated() {
segmentedControl.setLabel(preferencePane.preferencePaneTitle, forSegment: index) segmentedControl.setLabel(preferencePane.preferencePaneTitle, forSegment: index)
segmentedControl.setWidth(segmentSize.width, forSegment: index) segmentedControl.setWidth(segmentSize.width, forSegment: index)
if let cell = segmentedControl.cell as? NSSegmentedCell { if let cell = segmentedControl.cell as? NSSegmentedCell {
cell.setTag(index, forSegment: index) cell.setTag(index, forSegment: index)
} }
} }
return segmentedControl return segmentedControl
} }
@IBAction private func segmentedControlAction(_ control: NSSegmentedControl) { @IBAction private func segmentedControlAction(_ control: NSSegmentedControl) {
delegate?.activateTab(index: control.selectedSegment, animated: true) delegate?.activateTab(index: control.selectedSegment, animated: true)
} }
func selectTab(index: Int) { func selectTab(index: Int) {
segmentedControl.selectedSegment = index segmentedControl.selectedSegment = index
} }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] { func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
[ [
.flexibleSpace, .flexibleSpace,
.toolbarSegmentedControlItem, .toolbarSegmentedControlItem,
.flexibleSpace, .flexibleSpace,
] ]
} }
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? { func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? {
let toolbarItemIdentifier = preferenceIdentifier.toolbarItemIdentifier let toolbarItemIdentifier = preferenceIdentifier.toolbarItemIdentifier
precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem) precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem)
// When the segments outgrow the window, we need to provide a group of // When the segments outgrow the window, we need to provide a group of
// NSToolbarItems with custom menu item labels and action handling for the // NSToolbarItems with custom menu item labels and action handling for the
// context menu that pops up at the right edge of the window. // context menu that pops up at the right edge of the window.
let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier) let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier)
toolbarItemGroup.view = segmentedControl toolbarItemGroup.view = segmentedControl
toolbarItemGroup.subitems = preferencePanes.enumerated().map { index, preferenceable -> NSToolbarItem in toolbarItemGroup.subitems = preferencePanes.enumerated().map { index, preferenceable -> NSToolbarItem in
let item = NSToolbarItem(itemIdentifier: .init("segment-\(preferenceable.preferencePaneTitle)")) let item = NSToolbarItem(itemIdentifier: .init("segment-\(preferenceable.preferencePaneTitle)"))
item.label = preferenceable.preferencePaneTitle item.label = preferenceable.preferencePaneTitle
let menuItem = NSMenuItem( let menuItem = NSMenuItem(
title: preferenceable.preferencePaneTitle, title: preferenceable.preferencePaneTitle,
action: #selector(segmentedControlMenuAction), action: #selector(segmentedControlMenuAction),
keyEquivalent: "" keyEquivalent: ""
) )
menuItem.tag = index menuItem.tag = index
menuItem.target = self menuItem.target = self
item.menuFormRepresentation = menuItem item.menuFormRepresentation = menuItem
return item return item
} }
return toolbarItemGroup return toolbarItemGroup
} }
@IBAction private func segmentedControlMenuAction(_ menuItem: NSMenuItem) { @IBAction private func segmentedControlMenuAction(_ menuItem: NSMenuItem) {
delegate?.activateTab(index: menuItem.tag, animated: true) delegate?.activateTab(index: menuItem.tag, animated: true)
} }
} }

View File

@ -21,57 +21,57 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController { final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController {
let toolbar: NSToolbar let toolbar: NSToolbar
let centerToolbarItems: Bool let centerToolbarItems: Bool
let preferencePanes: [PreferencePane] let preferencePanes: [PreferencePane]
var isKeepingWindowCentered: Bool { centerToolbarItems } var isKeepingWindowCentered: Bool { centerToolbarItems }
weak var delegate: PreferencesStyleControllerDelegate? weak var delegate: PreferencesStyleControllerDelegate?
init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) { init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) {
self.preferencePanes = preferencePanes self.preferencePanes = preferencePanes
self.toolbar = toolbar self.toolbar = toolbar
self.centerToolbarItems = centerToolbarItems self.centerToolbarItems = centerToolbarItems
} }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] { func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
var toolbarItemIdentifiers = [NSToolbarItem.Identifier]() var toolbarItemIdentifiers = [NSToolbarItem.Identifier]()
if centerToolbarItems { if centerToolbarItems {
toolbarItemIdentifiers.append(.flexibleSpace) toolbarItemIdentifiers.append(.flexibleSpace)
} }
for preferencePane in preferencePanes { for preferencePane in preferencePanes {
toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier) toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier)
} }
if centerToolbarItems { if centerToolbarItems {
toolbarItemIdentifiers.append(.flexibleSpace) toolbarItemIdentifiers.append(.flexibleSpace)
} }
return toolbarItemIdentifiers return toolbarItemIdentifiers
} }
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? { func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? {
guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else { guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
preconditionFailure() preconditionFailure()
} }
let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier) let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier)
toolbarItem.label = preference.preferencePaneTitle toolbarItem.label = preference.preferencePaneTitle
toolbarItem.image = preference.toolbarItemIcon toolbarItem.image = preference.toolbarItemIcon
toolbarItem.target = self toolbarItem.target = self
toolbarItem.action = #selector(toolbarItemSelected) toolbarItem.action = #selector(toolbarItemSelected)
return toolbarItem return toolbarItem
} }
@IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) { @IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
delegate?.activateTab( delegate?.activateTab(
preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier), preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
animated: true animated: true
) )
} }
func selectTab(index: Int) { func selectTab(index: Int) {
toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier
} }
} }

View File

@ -22,117 +22,117 @@ import Cocoa
import SwiftUI import SwiftUI
extension NSImage { extension NSImage {
static var empty: NSImage { NSImage(size: .zero) } static var empty: NSImage { NSImage(size: .zero) }
} }
extension NSView { extension NSView {
@discardableResult @discardableResult
func constrainToSuperviewBounds() -> [NSLayoutConstraint] { func constrainToSuperviewBounds() -> [NSLayoutConstraint] {
guard let superview = superview else { guard let superview = superview else {
preconditionFailure("superview has to be set first") preconditionFailure("superview has to be set first")
} }
var result = [NSLayoutConstraint]() var result = [NSLayoutConstraint]()
result.append( result.append(
contentsOf: NSLayoutConstraint.constraints( contentsOf: NSLayoutConstraint.constraints(
withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil,
views: ["subview": self] views: ["subview": self]
)) ))
result.append( result.append(
contentsOf: NSLayoutConstraint.constraints( contentsOf: NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil,
views: ["subview": self] views: ["subview": self]
)) ))
translatesAutoresizingMaskIntoConstraints = false translatesAutoresizingMaskIntoConstraints = false
superview.addConstraints(result) superview.addConstraints(result)
return result return result
} }
} }
extension NSEvent { extension NSEvent {
/// Events triggered by user interaction. /// Events triggered by user interaction.
static let userInteractionEvents: [NSEvent.EventType] = { static let userInteractionEvents: [NSEvent.EventType] = {
var events: [NSEvent.EventType] = [ var events: [NSEvent.EventType] = [
.leftMouseDown, .leftMouseDown,
.leftMouseUp, .leftMouseUp,
.rightMouseDown, .rightMouseDown,
.rightMouseUp, .rightMouseUp,
.leftMouseDragged, .leftMouseDragged,
.rightMouseDragged, .rightMouseDragged,
.keyDown, .keyDown,
.keyUp, .keyUp,
.scrollWheel, .scrollWheel,
.tabletPoint, .tabletPoint,
.otherMouseDown, .otherMouseDown,
.otherMouseUp, .otherMouseUp,
.otherMouseDragged, .otherMouseDragged,
.gesture, .gesture,
.magnify, .magnify,
.swipe, .swipe,
.rotate, .rotate,
.beginGesture, .beginGesture,
.endGesture, .endGesture,
.smartMagnify, .smartMagnify,
.quickLook, .quickLook,
.directTouch, .directTouch,
] ]
if #available(macOS 10.10.3, *) { if #available(macOS 10.10.3, *) {
events.append(.pressure) events.append(.pressure)
} }
return events return events
}() }()
/// Whether the event was triggered by user interaction. /// Whether the event was triggered by user interaction.
var isUserInteraction: Bool { NSEvent.userInteractionEvents.contains(type) } var isUserInteraction: Bool { NSEvent.userInteractionEvents.contains(type) }
} }
extension Bundle { extension Bundle {
var appName: String { var appName: String {
string(forInfoDictionaryKey: "CFBundleDisplayName") string(forInfoDictionaryKey: "CFBundleDisplayName")
?? string(forInfoDictionaryKey: "CFBundleName") ?? string(forInfoDictionaryKey: "CFBundleName")
?? string(forInfoDictionaryKey: "CFBundleExecutable") ?? string(forInfoDictionaryKey: "CFBundleExecutable")
?? "<Unknown App Name>" ?? "<Unknown App Name>"
} }
private func string(forInfoDictionaryKey key: String) -> String? { private func string(forInfoDictionaryKey key: String) -> String? {
// `object(forInfoDictionaryKey:)` prefers localized info dictionary over the regular one automatically // `object(forInfoDictionaryKey:)` prefers localized info dictionary over the regular one automatically
object(forInfoDictionaryKey: key) as? String object(forInfoDictionaryKey: key) as? String
} }
} }
/// A window that allows you to disable all user interactions via `isUserInteractionEnabled`. /// A window that allows you to disable all user interactions via `isUserInteractionEnabled`.
/// ///
/// Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set. /// Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set.
class UserInteractionPausableWindow: NSWindow { class UserInteractionPausableWindow: NSWindow {
var isUserInteractionEnabled = true var isUserInteractionEnabled = true
override func sendEvent(_ event: NSEvent) { override func sendEvent(_ event: NSEvent) {
guard isUserInteractionEnabled || !event.isUserInteraction else { guard isUserInteractionEnabled || !event.isUserInteraction else {
return return
} }
super.sendEvent(event) super.sendEvent(event)
} }
override func responds(to selector: Selector!) -> Bool { override func responds(to selector: Selector!) -> Bool {
// Deactivate toolbar interactions from the Main Menu. // Deactivate toolbar interactions from the Main Menu.
if selector == #selector(NSWindow.toggleToolbarShown(_:)) { if selector == #selector(NSWindow.toggleToolbarShown(_:)) {
return false return false
} }
return super.responds(to: selector) return super.responds(to: selector)
} }
} }
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension View { extension View {
/** /**
Equivalent to `.eraseToAnyPublisher()` from the Combine framework. Equivalent to `.eraseToAnyPublisher()` from the Combine framework.
*/ */
func eraseToAnyView() -> AnyView { func eraseToAnyView() -> AnyView {
AnyView(self) AnyView(self)
} }
} }

View File

@ -10,62 +10,62 @@ import SwiftUI
// Ref: https://stackoverflow.com/a/71058587/4162914 // Ref: https://stackoverflow.com/a/71058587/4162914
@available(macOS 11.0, *) @available(macOS 11.0, *)
struct ComboBox: NSViewRepresentable { struct ComboBox: NSViewRepresentable {
// The items that will show up in the pop-up menu: // The items that will show up in the pop-up menu:
var items: [String] var items: [String]
// The property on our parent view that gets synced to the current // The property on our parent view that gets synced to the current
// stringValue of the NSComboBox, whether the user typed it in or // stringValue of the NSComboBox, whether the user typed it in or
// selected it from the list: // selected it from the list:
@Binding var text: String @Binding var text: String
func makeCoordinator() -> Coordinator { func makeCoordinator() -> Coordinator {
Coordinator(self) Coordinator(self)
} }
func makeNSView(context: Context) -> NSComboBox { func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox() let comboBox = NSComboBox()
comboBox.usesDataSource = false comboBox.usesDataSource = false
comboBox.completes = false comboBox.completes = false
comboBox.delegate = context.coordinator comboBox.delegate = context.coordinator
comboBox.intercellSpacing = NSSize(width: 0.0, height: 10.0) comboBox.intercellSpacing = NSSize(width: 0.0, height: 10.0)
return comboBox return comboBox
} }
func updateNSView(_ nsView: NSComboBox, context: Context) { func updateNSView(_ nsView: NSComboBox, context: Context) {
nsView.removeAllItems() nsView.removeAllItems()
nsView.addItems(withObjectValues: items) nsView.addItems(withObjectValues: items)
// ComboBox doesn't automatically select the item matching its text; // ComboBox doesn't automatically select the item matching its text;
// we must do that manually. But we need the delegate to ignore that // we must do that manually. But we need the delegate to ignore that
// selection-change or we'll get a "state modified during view update; // selection-change or we'll get a "state modified during view update;
// will cause undefined behavior" warning. // will cause undefined behavior" warning.
context.coordinator.ignoreSelectionChanges = true context.coordinator.ignoreSelectionChanges = true
nsView.stringValue = text nsView.stringValue = text
nsView.selectItem(withObjectValue: text) nsView.selectItem(withObjectValue: text)
context.coordinator.ignoreSelectionChanges = false context.coordinator.ignoreSelectionChanges = false
} }
class Coordinator: NSObject, NSComboBoxDelegate { class Coordinator: NSObject, NSComboBoxDelegate {
var parent: ComboBox var parent: ComboBox
var ignoreSelectionChanges: Bool = false var ignoreSelectionChanges: Bool = false
init(_ parent: ComboBox) { init(_ parent: ComboBox) {
self.parent = parent self.parent = parent
} }
func comboBoxSelectionDidChange(_ notification: Notification) { func comboBoxSelectionDidChange(_ notification: Notification) {
if !ignoreSelectionChanges, if !ignoreSelectionChanges,
let box: NSComboBox = notification.object as? NSComboBox, let box: NSComboBox = notification.object as? NSComboBox,
let newStringValue: String = box.objectValueOfSelectedItem as? String let newStringValue: String = box.objectValueOfSelectedItem as? String
{ {
parent.text = newStringValue parent.text = newStringValue
} }
} }
func controlTextDidEndEditing(_ obj: Notification) { func controlTextDidEndEditing(_ obj: Notification) {
if let textField = obj.object as? NSTextField { if let textField = obj.object as? NSTextField {
parent.text = textField.stringValue parent.text = textField.stringValue
} }
} }
} }
} }

@ -1 +1 @@
Subproject commit 8199254d3abbf63e3b7535bbc975f8519a2d6834 Subproject commit 4065cb727373ab12a3401eb3526e4a6208671e59

View File

@ -30,5 +30,5 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@import Foundation; @import Foundation;
#import "KeyHandler.h" #import "CTools.h"
#import "mgrLangModel.h" #import "Composer.hh"

View File

@ -29,216 +29,218 @@ import InputMethodKit
@objc(AppDelegate) @objc(AppDelegate)
class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate, class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate,
FSEventStreamHelperDelegate FSEventStreamHelperDelegate
{ {
func helper(_: FSEventStreamHelper, didReceive _: [FSEventStreamHelper.Event]) { func helper(_: FSEventStreamHelper, didReceive _: [FSEventStreamHelper.Event]) {
// 100ms 使使 // 100ms 使使
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
if mgrPrefs.shouldAutoReloadUserDataFiles { if mgrPrefs.shouldAutoReloadUserDataFiles {
IME.initLangModels(userOnly: true) IME.initLangModels(userOnly: true)
} }
} }
} }
// let vChewingKeyLayoutBundle = Bundle.init(path: URL(fileURLWithPath: Bundle.main.resourcePath ?? "").appendingPathComponent("vChewingKeyLayout.bundle").path) // let vChewingKeyLayoutBundle = Bundle.init(path: URL(fileURLWithPath: Bundle.main.resourcePath ?? "").appendingPathComponent("vChewingKeyLayout.bundle").path)
@IBOutlet var window: NSWindow? @IBOutlet var window: NSWindow?
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? private var updateNextStepURL: URL?
private 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")
) )
private var currentAlertType: String = "" private var currentAlertType: String = ""
// dealloc // dealloc
deinit { deinit {
ctlPrefWindowInstance = nil ctlPrefWindowInstance = nil
ctlAboutWindowInstance = nil ctlAboutWindowInstance = nil
checkTask = nil checkTask = nil
updateNextStepURL = nil updateNextStepURL = nil
fsStreamHelper.stop() fsStreamHelper.stop()
fsStreamHelper.delegate = nil fsStreamHelper.delegate = nil
} }
func applicationDidFinishLaunching(_: Notification) { func applicationDidFinishLaunching(_: Notification) {
IME.initLangModels(userOnly: false) IME.initLangModels(userOnly: false)
fsStreamHelper.delegate = self fsStreamHelper.delegate = self
_ = fsStreamHelper.start() _ = fsStreamHelper.start()
mgrPrefs.setMissingDefaults() mgrPrefs.setMissingDefaults()
// 使 // 使
if (UserDefaults.standard.object(forKey: VersionUpdateApi.kCheckUpdateAutomatically) != nil) == true { if (UserDefaults.standard.object(forKey: VersionUpdateApi.kCheckUpdateAutomatically) != nil) == true {
checkForUpdate() checkForUpdate()
} }
} }
@objc func showPreferences() { func updateStreamHelperPath() {
if ctlPrefWindowInstance == nil { fsStreamHelper.path = mgrPrefs.userDataFolderSpecified
ctlPrefWindowInstance = ctlPrefWindow.init(windowNibName: "frmPrefWindow") }
}
ctlPrefWindowInstance?.window?.center()
ctlPrefWindowInstance?.window?.orderFrontRegardless() //
ctlPrefWindowInstance?.window?.level = .statusBar
ctlPrefWindowInstance?.window?.titlebarAppearsTransparent = true
NSApp.setActivationPolicy(.accessory)
}
// New About Window func showPreferences() {
@objc func showAbout() { if ctlPrefWindowInstance == nil {
if ctlAboutWindowInstance == nil { ctlPrefWindowInstance = ctlPrefWindow.init(windowNibName: "frmPrefWindow")
ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow") }
} ctlPrefWindowInstance?.window?.center()
ctlAboutWindowInstance?.window?.center() ctlPrefWindowInstance?.window?.orderFrontRegardless() //
ctlAboutWindowInstance?.window?.orderFrontRegardless() // ctlPrefWindowInstance?.window?.level = .statusBar
ctlAboutWindowInstance?.window?.level = .statusBar ctlPrefWindowInstance?.window?.titlebarAppearsTransparent = true
NSApp.setActivationPolicy(.accessory) NSApp.setActivationPolicy(.accessory)
} }
@objc(checkForUpdate) // New About Window
func checkForUpdate() { func showAbout() {
checkForUpdate(forced: false) if ctlAboutWindowInstance == nil {
} ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow")
}
ctlAboutWindowInstance?.window?.center()
ctlAboutWindowInstance?.window?.orderFrontRegardless() //
ctlAboutWindowInstance?.window?.level = .statusBar
NSApp.setActivationPolicy(.accessory)
}
@objc(checkForUpdateForced:) func checkForUpdate() {
func checkForUpdate(forced: Bool) { checkForUpdate(forced: false)
if checkTask != nil { }
// busy
return
}
// time for update? func checkForUpdate(forced: Bool) {
if !forced { if checkTask != nil {
if UserDefaults.standard.bool(forKey: VersionUpdateApi.kCheckUpdateAutomatically) == false { // busy
return return
} }
let now = Date()
let date = UserDefaults.standard.object(forKey: VersionUpdateApi.kNextUpdateCheckDateKey) as? Date ?? now
if now.compare(date) == .orderedAscending {
return
}
}
let nextUpdateDate = Date(timeInterval: VersionUpdateApi.kNextCheckInterval, since: Date()) // time for update?
UserDefaults.standard.set(nextUpdateDate, forKey: VersionUpdateApi.kNextUpdateCheckDateKey) if !forced {
if UserDefaults.standard.bool(forKey: VersionUpdateApi.kCheckUpdateAutomatically) == false {
return
}
let now = Date()
let date = UserDefaults.standard.object(forKey: VersionUpdateApi.kNextUpdateCheckDateKey) as? Date ?? now
if now.compare(date) == .orderedAscending {
return
}
}
checkTask = VersionUpdateApi.check(forced: forced) { [self] result in let nextUpdateDate = Date(timeInterval: VersionUpdateApi.kNextCheckInterval, since: Date())
defer { UserDefaults.standard.set(nextUpdateDate, forKey: VersionUpdateApi.kNextUpdateCheckDateKey)
checkTask = nil
}
switch result {
case .success(let apiResult):
switch apiResult {
case .shouldUpdate(let report):
updateNextStepURL = report.siteUrl
let content = String(
format: NSLocalizedString(
"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@",
comment: ""
),
report.currentShortVersion,
report.currentVersion,
report.remoteShortVersion,
report.remoteVersion,
report.versionDescription
)
IME.prtDebugIntel("vChewingDebug: \(content)")
currentAlertType = "Update"
ctlNonModalAlertWindow.shared.show(
title: NSLocalizedString(
"New Version Available", comment: ""
),
content: content,
confirmButtonTitle: NSLocalizedString(
"Visit Website", comment: ""
),
cancelButtonTitle: NSLocalizedString(
"Not Now", comment: ""
),
cancelAsDefault: false,
delegate: self
)
NSApp.setActivationPolicy(.accessory)
case .noNeedToUpdate, .ignored:
break
}
case .failure(let error):
switch error {
case VersionUpdateApiError.connectionError(let message):
let title = NSLocalizedString(
"Update Check Failed", comment: ""
)
let content = String(
format: NSLocalizedString(
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
comment: ""
), message
)
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
IME.prtDebugIntel("vChewingDebug: \(content)")
currentAlertType = "Update"
ctlNonModalAlertWindow.shared.show(
title: title, content: content,
confirmButtonTitle: buttonTitle,
cancelButtonTitle: nil,
cancelAsDefault: false, delegate: nil
)
NSApp.setActivationPolicy(.accessory)
default:
break
}
}
}
}
func selfUninstall() { checkTask = VersionUpdateApi.check(forced: forced) { [self] result in
currentAlertType = "Uninstall" defer {
let content = String( checkTask = nil
format: NSLocalizedString( }
"This will remove vChewing Input Method from this user account, requiring your confirmation.", switch result {
comment: "" case .success(let apiResult):
)) switch apiResult {
ctlNonModalAlertWindow.shared.show( case .shouldUpdate(let report):
title: NSLocalizedString("Uninstallation", comment: ""), content: content, updateNextStepURL = report.siteUrl
confirmButtonTitle: NSLocalizedString("OK", comment: ""), let content = String(
cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, format: NSLocalizedString(
delegate: self "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@",
) comment: ""
NSApp.setActivationPolicy(.accessory) ),
} report.currentShortVersion,
report.currentVersion,
report.remoteShortVersion,
report.remoteVersion,
report.versionDescription
)
IME.prtDebugIntel("vChewingDebug: \(content)")
currentAlertType = "Update"
ctlNonModalAlertWindow.shared.show(
title: NSLocalizedString(
"New Version Available", comment: ""
),
content: content,
confirmButtonTitle: NSLocalizedString(
"Visit Website", comment: ""
),
cancelButtonTitle: NSLocalizedString(
"Not Now", comment: ""
),
cancelAsDefault: false,
delegate: self
)
NSApp.setActivationPolicy(.accessory)
case .noNeedToUpdate, .ignored:
break
}
case .failure(let error):
switch error {
case VersionUpdateApiError.connectionError(let message):
let title = NSLocalizedString(
"Update Check Failed", comment: ""
)
let content = String(
format: NSLocalizedString(
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
comment: ""
), message
)
let buttonTitle = NSLocalizedString("Dismiss", comment: "")
IME.prtDebugIntel("vChewingDebug: \(content)")
currentAlertType = "Update"
ctlNonModalAlertWindow.shared.show(
title: title, content: content,
confirmButtonTitle: buttonTitle,
cancelButtonTitle: nil,
cancelAsDefault: false, delegate: nil
)
NSApp.setActivationPolicy(.accessory)
default:
break
}
}
}
}
func ctlNonModalAlertWindowDidConfirm(_: ctlNonModalAlertWindow) { func selfUninstall() {
switch currentAlertType { currentAlertType = "Uninstall"
case "Uninstall": let content = String(
NSWorkspace.shared.openFile( format: NSLocalizedString(
mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder" "This will remove vChewing Input Method from this user account, requiring your confirmation.",
) comment: ""
IME.uninstall(isSudo: false, selfKill: true) ))
case "Update": ctlNonModalAlertWindow.shared.show(
if let updateNextStepURL = updateNextStepURL { title: NSLocalizedString("Uninstallation", comment: ""), content: content,
NSWorkspace.shared.open(updateNextStepURL) confirmButtonTitle: NSLocalizedString("OK", comment: ""),
} cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false,
updateNextStepURL = nil delegate: self
default: )
break NSApp.setActivationPolicy(.accessory)
} }
}
func ctlNonModalAlertWindowDidCancel(_: ctlNonModalAlertWindow) { func ctlNonModalAlertWindowDidConfirm(_: ctlNonModalAlertWindow) {
switch currentAlertType { switch currentAlertType {
case "Update": case "Uninstall":
updateNextStepURL = nil NSWorkspace.shared.openFile(
default: mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder"
break )
} IME.uninstall(isSudo: false, selfKill: true)
} case "Update":
if let updateNextStepURL = updateNextStepURL {
NSWorkspace.shared.open(updateNextStepURL)
}
updateNextStepURL = nil
default:
break
}
}
// New About Window func ctlNonModalAlertWindowDidCancel(_: ctlNonModalAlertWindow) {
@IBAction func about(_: Any) { switch currentAlertType {
(NSApp.delegate as? AppDelegate)?.showAbout() case "Update":
NSApplication.shared.activate(ignoringOtherApps: true) updateNextStepURL = nil
} default:
break
}
}
// New About Window
@IBAction func about(_: Any) {
(NSApp.delegate as? AppDelegate)?.showAbout()
NSApplication.shared.activate(ignoringOtherApps: true)
}
} }

View File

@ -25,303 +25,303 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
class AppleKeyboardConverter: NSObject { class AppleKeyboardConverter: NSObject {
static let arrDynamicBasicKeyLayout: [String] = [ static let arrDynamicBasicKeyLayout: [String] = [
"com.apple.keylayout.ZhuyinBopomofo", "com.apple.keylayout.ZhuyinBopomofo",
"com.apple.keylayout.ZhuyinEten", "com.apple.keylayout.ZhuyinEten",
"org.atelierInmu.vChewing.keyLayouts.vchewingdachen", "org.atelierInmu.vChewing.keyLayouts.vchewingdachen",
"org.atelierInmu.vChewing.keyLayouts.vchewingmitac", "org.atelierInmu.vChewing.keyLayouts.vchewingmitac",
"org.atelierInmu.vChewing.keyLayouts.vchewingibm", "org.atelierInmu.vChewing.keyLayouts.vchewingibm",
"org.atelierInmu.vChewing.keyLayouts.vchewingseigyou", "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou",
"org.atelierInmu.vChewing.keyLayouts.vchewingeten", "org.atelierInmu.vChewing.keyLayouts.vchewingeten",
"org.unknown.keylayout.vChewingDachen", "org.unknown.keylayout.vChewingDachen",
"org.unknown.keylayout.vChewingFakeSeigyou", "org.unknown.keylayout.vChewingFakeSeigyou",
"org.unknown.keylayout.vChewingETen", "org.unknown.keylayout.vChewingETen",
"org.unknown.keylayout.vChewingIBM", "org.unknown.keylayout.vChewingIBM",
"org.unknown.keylayout.vChewingMiTAC", "org.unknown.keylayout.vChewingMiTAC",
] ]
@objc class func isDynamicBasicKeyboardLayoutEnabled() -> Bool { class func isDynamicBasicKeyboardLayoutEnabled() -> Bool {
AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout) AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout)
} }
// Apple // Apple
@objc class func cnvApple2ABC(_ charCode: UniChar) -> UniChar { class func cnvApple2ABC(_ charCode: UniChar) -> UniChar {
var charCode = charCode var charCode = charCode
// OVMandarin OVMandarin // OVMandarin OVMandarin
if isDynamicBasicKeyboardLayoutEnabled() { if isDynamicBasicKeyboardLayoutEnabled() {
// Apple // Apple
switch mgrPrefs.basicKeyboardLayout { switch mgrPrefs.basicKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo": case "com.apple.keylayout.ZhuyinBopomofo":
do { do {
if charCode == 97 { charCode = UniChar(65) } if charCode == 97 { charCode = UniChar(65) }
if charCode == 98 { charCode = UniChar(66) } if charCode == 98 { charCode = UniChar(66) }
if charCode == 99 { charCode = UniChar(67) } if charCode == 99 { charCode = UniChar(67) }
if charCode == 100 { charCode = UniChar(68) } if charCode == 100 { charCode = UniChar(68) }
if charCode == 101 { charCode = UniChar(69) } if charCode == 101 { charCode = UniChar(69) }
if charCode == 102 { charCode = UniChar(70) } if charCode == 102 { charCode = UniChar(70) }
if charCode == 103 { charCode = UniChar(71) } if charCode == 103 { charCode = UniChar(71) }
if charCode == 104 { charCode = UniChar(72) } if charCode == 104 { charCode = UniChar(72) }
if charCode == 105 { charCode = UniChar(73) } if charCode == 105 { charCode = UniChar(73) }
if charCode == 106 { charCode = UniChar(74) } if charCode == 106 { charCode = UniChar(74) }
if charCode == 107 { charCode = UniChar(75) } if charCode == 107 { charCode = UniChar(75) }
if charCode == 108 { charCode = UniChar(76) } if charCode == 108 { charCode = UniChar(76) }
if charCode == 109 { charCode = UniChar(77) } if charCode == 109 { charCode = UniChar(77) }
if charCode == 110 { charCode = UniChar(78) } if charCode == 110 { charCode = UniChar(78) }
if charCode == 111 { charCode = UniChar(79) } if charCode == 111 { charCode = UniChar(79) }
if charCode == 112 { charCode = UniChar(80) } if charCode == 112 { charCode = UniChar(80) }
if charCode == 113 { charCode = UniChar(81) } if charCode == 113 { charCode = UniChar(81) }
if charCode == 114 { charCode = UniChar(82) } if charCode == 114 { charCode = UniChar(82) }
if charCode == 115 { charCode = UniChar(83) } if charCode == 115 { charCode = UniChar(83) }
if charCode == 116 { charCode = UniChar(84) } if charCode == 116 { charCode = UniChar(84) }
if charCode == 117 { charCode = UniChar(85) } if charCode == 117 { charCode = UniChar(85) }
if charCode == 118 { charCode = UniChar(86) } if charCode == 118 { charCode = UniChar(86) }
if charCode == 119 { charCode = UniChar(87) } if charCode == 119 { charCode = UniChar(87) }
if charCode == 120 { charCode = UniChar(88) } if charCode == 120 { charCode = UniChar(88) }
if charCode == 121 { charCode = UniChar(89) } if charCode == 121 { charCode = UniChar(89) }
if charCode == 122 { charCode = UniChar(90) } if charCode == 122 { charCode = UniChar(90) }
} }
case "com.apple.keylayout.ZhuyinEten": case "com.apple.keylayout.ZhuyinEten":
do { do {
if charCode == 65345 { charCode = UniChar(65) } if charCode == 65345 { charCode = UniChar(65) }
if charCode == 65346 { charCode = UniChar(66) } if charCode == 65346 { charCode = UniChar(66) }
if charCode == 65347 { charCode = UniChar(67) } if charCode == 65347 { charCode = UniChar(67) }
if charCode == 65348 { charCode = UniChar(68) } if charCode == 65348 { charCode = UniChar(68) }
if charCode == 65349 { charCode = UniChar(69) } if charCode == 65349 { charCode = UniChar(69) }
if charCode == 65350 { charCode = UniChar(70) } if charCode == 65350 { charCode = UniChar(70) }
if charCode == 65351 { charCode = UniChar(71) } if charCode == 65351 { charCode = UniChar(71) }
if charCode == 65352 { charCode = UniChar(72) } if charCode == 65352 { charCode = UniChar(72) }
if charCode == 65353 { charCode = UniChar(73) } if charCode == 65353 { charCode = UniChar(73) }
if charCode == 65354 { charCode = UniChar(74) } if charCode == 65354 { charCode = UniChar(74) }
if charCode == 65355 { charCode = UniChar(75) } if charCode == 65355 { charCode = UniChar(75) }
if charCode == 65356 { charCode = UniChar(76) } if charCode == 65356 { charCode = UniChar(76) }
if charCode == 65357 { charCode = UniChar(77) } if charCode == 65357 { charCode = UniChar(77) }
if charCode == 65358 { charCode = UniChar(78) } if charCode == 65358 { charCode = UniChar(78) }
if charCode == 65359 { charCode = UniChar(79) } if charCode == 65359 { charCode = UniChar(79) }
if charCode == 65360 { charCode = UniChar(80) } if charCode == 65360 { charCode = UniChar(80) }
if charCode == 65361 { charCode = UniChar(81) } if charCode == 65361 { charCode = UniChar(81) }
if charCode == 65362 { charCode = UniChar(82) } if charCode == 65362 { charCode = UniChar(82) }
if charCode == 65363 { charCode = UniChar(83) } if charCode == 65363 { charCode = UniChar(83) }
if charCode == 65364 { charCode = UniChar(84) } if charCode == 65364 { charCode = UniChar(84) }
if charCode == 65365 { charCode = UniChar(85) } if charCode == 65365 { charCode = UniChar(85) }
if charCode == 65366 { charCode = UniChar(86) } if charCode == 65366 { charCode = UniChar(86) }
if charCode == 65367 { charCode = UniChar(87) } if charCode == 65367 { charCode = UniChar(87) }
if charCode == 65368 { charCode = UniChar(88) } if charCode == 65368 { charCode = UniChar(88) }
if charCode == 65369 { charCode = UniChar(89) } if charCode == 65369 { charCode = UniChar(89) }
if charCode == 65370 { charCode = UniChar(90) } if charCode == 65370 { charCode = UniChar(90) }
} }
default: break default: break
} }
// //
if charCode == 12573 { charCode = UniChar(44) } if charCode == 12573 { charCode = UniChar(44) }
if charCode == 12582 { charCode = UniChar(45) } if charCode == 12582 { charCode = UniChar(45) }
if charCode == 12577 { charCode = UniChar(46) } if charCode == 12577 { charCode = UniChar(46) }
if charCode == 12581 { charCode = UniChar(47) } if charCode == 12581 { charCode = UniChar(47) }
if charCode == 12578 { charCode = UniChar(48) } if charCode == 12578 { charCode = UniChar(48) }
if charCode == 12549 { charCode = UniChar(49) } if charCode == 12549 { charCode = UniChar(49) }
if charCode == 12553 { charCode = UniChar(50) } if charCode == 12553 { charCode = UniChar(50) }
if charCode == 711 { charCode = UniChar(51) } if charCode == 711 { charCode = UniChar(51) }
if charCode == 715 { charCode = UniChar(52) } if charCode == 715 { charCode = UniChar(52) }
if charCode == 12563 { charCode = UniChar(53) } if charCode == 12563 { charCode = UniChar(53) }
if charCode == 714 { charCode = UniChar(54) } if charCode == 714 { charCode = UniChar(54) }
if charCode == 729 { charCode = UniChar(55) } if charCode == 729 { charCode = UniChar(55) }
if charCode == 12570 { charCode = UniChar(56) } if charCode == 12570 { charCode = UniChar(56) }
if charCode == 12574 { charCode = UniChar(57) } if charCode == 12574 { charCode = UniChar(57) }
if charCode == 12580 { charCode = UniChar(59) } if charCode == 12580 { charCode = UniChar(59) }
if charCode == 12551 { charCode = UniChar(97) } if charCode == 12551 { charCode = UniChar(97) }
if charCode == 12566 { charCode = UniChar(98) } if charCode == 12566 { charCode = UniChar(98) }
if charCode == 12559 { charCode = UniChar(99) } if charCode == 12559 { charCode = UniChar(99) }
if charCode == 12558 { charCode = UniChar(100) } if charCode == 12558 { charCode = UniChar(100) }
if charCode == 12557 { charCode = UniChar(101) } if charCode == 12557 { charCode = UniChar(101) }
if charCode == 12561 { charCode = UniChar(102) } if charCode == 12561 { charCode = UniChar(102) }
if charCode == 12565 { charCode = UniChar(103) } if charCode == 12565 { charCode = UniChar(103) }
if charCode == 12568 { charCode = UniChar(104) } if charCode == 12568 { charCode = UniChar(104) }
if charCode == 12571 { charCode = UniChar(105) } if charCode == 12571 { charCode = UniChar(105) }
if charCode == 12584 { charCode = UniChar(106) } if charCode == 12584 { charCode = UniChar(106) }
if charCode == 12572 { charCode = UniChar(107) } if charCode == 12572 { charCode = UniChar(107) }
if charCode == 12576 { charCode = UniChar(108) } if charCode == 12576 { charCode = UniChar(108) }
if charCode == 12585 { charCode = UniChar(109) } if charCode == 12585 { charCode = UniChar(109) }
if charCode == 12569 { charCode = UniChar(110) } if charCode == 12569 { charCode = UniChar(110) }
if charCode == 12575 { charCode = UniChar(111) } if charCode == 12575 { charCode = UniChar(111) }
if charCode == 12579 { charCode = UniChar(112) } if charCode == 12579 { charCode = UniChar(112) }
if charCode == 12550 { charCode = UniChar(113) } if charCode == 12550 { charCode = UniChar(113) }
if charCode == 12560 { charCode = UniChar(114) } if charCode == 12560 { charCode = UniChar(114) }
if charCode == 12555 { charCode = UniChar(115) } if charCode == 12555 { charCode = UniChar(115) }
if charCode == 12564 { charCode = UniChar(116) } if charCode == 12564 { charCode = UniChar(116) }
if charCode == 12583 { charCode = UniChar(117) } if charCode == 12583 { charCode = UniChar(117) }
if charCode == 12562 { charCode = UniChar(118) } if charCode == 12562 { charCode = UniChar(118) }
if charCode == 12554 { charCode = UniChar(119) } if charCode == 12554 { charCode = UniChar(119) }
if charCode == 12556 { charCode = UniChar(120) } if charCode == 12556 { charCode = UniChar(120) }
if charCode == 12567 { charCode = UniChar(121) } if charCode == 12567 { charCode = UniChar(121) }
if charCode == 12552 { charCode = UniChar(122) } if charCode == 12552 { charCode = UniChar(122) }
// //
if charCode == 12289 { charCode = UniChar(92) } if charCode == 12289 { charCode = UniChar(92) }
if charCode == 12300 { charCode = UniChar(91) } if charCode == 12300 { charCode = UniChar(91) }
if charCode == 12301 { charCode = UniChar(93) } if charCode == 12301 { charCode = UniChar(93) }
if charCode == 12302 { charCode = UniChar(123) } if charCode == 12302 { charCode = UniChar(123) }
if charCode == 12303 { charCode = UniChar(125) } if charCode == 12303 { charCode = UniChar(125) }
if charCode == 65292 { charCode = UniChar(60) } if charCode == 65292 { charCode = UniChar(60) }
if charCode == 12290 { charCode = UniChar(62) } if charCode == 12290 { charCode = UniChar(62) }
// SHIFT // SHIFT
if charCode == 65281 { charCode = UniChar(33) } if charCode == 65281 { charCode = UniChar(33) }
if charCode == 65312 { charCode = UniChar(64) } if charCode == 65312 { charCode = UniChar(64) }
if charCode == 65283 { charCode = UniChar(35) } if charCode == 65283 { charCode = UniChar(35) }
if charCode == 65284 { charCode = UniChar(36) } if charCode == 65284 { charCode = UniChar(36) }
if charCode == 65285 { charCode = UniChar(37) } if charCode == 65285 { charCode = UniChar(37) }
if charCode == 65087 { charCode = UniChar(94) } if charCode == 65087 { charCode = UniChar(94) }
if charCode == 65286 { charCode = UniChar(38) } if charCode == 65286 { charCode = UniChar(38) }
if charCode == 65290 { charCode = UniChar(42) } if charCode == 65290 { charCode = UniChar(42) }
if charCode == 65288 { charCode = UniChar(40) } if charCode == 65288 { charCode = UniChar(40) }
if charCode == 65289 { charCode = UniChar(41) } if charCode == 65289 { charCode = UniChar(41) }
// Alt // Alt
if charCode == 8212 { charCode = UniChar(45) } if charCode == 8212 { charCode = UniChar(45) }
// Apple // Apple
if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if charCode == 65343 { charCode = UniChar(95) } if charCode == 65343 { charCode = UniChar(95) }
if charCode == 65306 { charCode = UniChar(58) } if charCode == 65306 { charCode = UniChar(58) }
if charCode == 65311 { charCode = UniChar(63) } if charCode == 65311 { charCode = UniChar(63) }
if charCode == 65291 { charCode = UniChar(43) } if charCode == 65291 { charCode = UniChar(43) }
if charCode == 65372 { charCode = UniChar(124) } if charCode == 65372 { charCode = UniChar(124) }
} }
} }
return charCode return charCode
} }
@objc class func cnvStringApple2ABC(_ strProcessed: String) -> String { class func cnvStringApple2ABC(_ strProcessed: String) -> String {
var strProcessed = strProcessed var strProcessed = strProcessed
if isDynamicBasicKeyboardLayoutEnabled() { if isDynamicBasicKeyboardLayoutEnabled() {
// Apple // Apple
switch mgrPrefs.basicKeyboardLayout { switch mgrPrefs.basicKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo": case "com.apple.keylayout.ZhuyinBopomofo":
do { do {
if strProcessed == "a" { strProcessed = "A" } if strProcessed == "a" { strProcessed = "A" }
if strProcessed == "b" { strProcessed = "B" } if strProcessed == "b" { strProcessed = "B" }
if strProcessed == "c" { strProcessed = "C" } if strProcessed == "c" { strProcessed = "C" }
if strProcessed == "d" { strProcessed = "D" } if strProcessed == "d" { strProcessed = "D" }
if strProcessed == "e" { strProcessed = "E" } if strProcessed == "e" { strProcessed = "E" }
if strProcessed == "f" { strProcessed = "F" } if strProcessed == "f" { strProcessed = "F" }
if strProcessed == "g" { strProcessed = "G" } if strProcessed == "g" { strProcessed = "G" }
if strProcessed == "h" { strProcessed = "H" } if strProcessed == "h" { strProcessed = "H" }
if strProcessed == "i" { strProcessed = "I" } if strProcessed == "i" { strProcessed = "I" }
if strProcessed == "j" { strProcessed = "J" } if strProcessed == "j" { strProcessed = "J" }
if strProcessed == "k" { strProcessed = "K" } if strProcessed == "k" { strProcessed = "K" }
if strProcessed == "l" { strProcessed = "L" } if strProcessed == "l" { strProcessed = "L" }
if strProcessed == "m" { strProcessed = "M" } if strProcessed == "m" { strProcessed = "M" }
if strProcessed == "n" { strProcessed = "N" } if strProcessed == "n" { strProcessed = "N" }
if strProcessed == "o" { strProcessed = "O" } if strProcessed == "o" { strProcessed = "O" }
if strProcessed == "p" { strProcessed = "P" } if strProcessed == "p" { strProcessed = "P" }
if strProcessed == "q" { strProcessed = "Q" } if strProcessed == "q" { strProcessed = "Q" }
if strProcessed == "r" { strProcessed = "R" } if strProcessed == "r" { strProcessed = "R" }
if strProcessed == "s" { strProcessed = "S" } if strProcessed == "s" { strProcessed = "S" }
if strProcessed == "t" { strProcessed = "T" } if strProcessed == "t" { strProcessed = "T" }
if strProcessed == "u" { strProcessed = "U" } if strProcessed == "u" { strProcessed = "U" }
if strProcessed == "v" { strProcessed = "V" } if strProcessed == "v" { strProcessed = "V" }
if strProcessed == "w" { strProcessed = "W" } if strProcessed == "w" { strProcessed = "W" }
if strProcessed == "x" { strProcessed = "X" } if strProcessed == "x" { strProcessed = "X" }
if strProcessed == "y" { strProcessed = "Y" } if strProcessed == "y" { strProcessed = "Y" }
if strProcessed == "z" { strProcessed = "Z" } if strProcessed == "z" { strProcessed = "Z" }
} }
case "com.apple.keylayout.ZhuyinEten": case "com.apple.keylayout.ZhuyinEten":
do { do {
if strProcessed == "" { strProcessed = "A" } if strProcessed == "" { strProcessed = "A" }
if strProcessed == "" { strProcessed = "B" } if strProcessed == "" { strProcessed = "B" }
if strProcessed == "" { strProcessed = "C" } if strProcessed == "" { strProcessed = "C" }
if strProcessed == "" { strProcessed = "D" } if strProcessed == "" { strProcessed = "D" }
if strProcessed == "" { strProcessed = "E" } if strProcessed == "" { strProcessed = "E" }
if strProcessed == "" { strProcessed = "F" } if strProcessed == "" { strProcessed = "F" }
if strProcessed == "" { strProcessed = "G" } if strProcessed == "" { strProcessed = "G" }
if strProcessed == "" { strProcessed = "H" } if strProcessed == "" { strProcessed = "H" }
if strProcessed == "" { strProcessed = "I" } if strProcessed == "" { strProcessed = "I" }
if strProcessed == "" { strProcessed = "J" } if strProcessed == "" { strProcessed = "J" }
if strProcessed == "" { strProcessed = "K" } if strProcessed == "" { strProcessed = "K" }
if strProcessed == "" { strProcessed = "L" } if strProcessed == "" { strProcessed = "L" }
if strProcessed == "" { strProcessed = "M" } if strProcessed == "" { strProcessed = "M" }
if strProcessed == "" { strProcessed = "N" } if strProcessed == "" { strProcessed = "N" }
if strProcessed == "" { strProcessed = "O" } if strProcessed == "" { strProcessed = "O" }
if strProcessed == "" { strProcessed = "P" } if strProcessed == "" { strProcessed = "P" }
if strProcessed == "" { strProcessed = "Q" } if strProcessed == "" { strProcessed = "Q" }
if strProcessed == "" { strProcessed = "R" } if strProcessed == "" { strProcessed = "R" }
if strProcessed == "" { strProcessed = "S" } if strProcessed == "" { strProcessed = "S" }
if strProcessed == "" { strProcessed = "T" } if strProcessed == "" { strProcessed = "T" }
if strProcessed == "" { strProcessed = "U" } if strProcessed == "" { strProcessed = "U" }
if strProcessed == "" { strProcessed = "V" } if strProcessed == "" { strProcessed = "V" }
if strProcessed == "" { strProcessed = "W" } if strProcessed == "" { strProcessed = "W" }
if strProcessed == "" { strProcessed = "X" } if strProcessed == "" { strProcessed = "X" }
if strProcessed == "" { strProcessed = "Y" } if strProcessed == "" { strProcessed = "Y" }
if strProcessed == "" { strProcessed = "Z" } if strProcessed == "" { strProcessed = "Z" }
} }
default: break default: break
} }
// //
if strProcessed == "" { strProcessed = "," } if strProcessed == "" { strProcessed = "," }
if strProcessed == "" { strProcessed = "-" } if strProcessed == "" { strProcessed = "-" }
if strProcessed == "" { strProcessed = "." } if strProcessed == "" { strProcessed = "." }
if strProcessed == "" { strProcessed = "/" } if strProcessed == "" { strProcessed = "/" }
if strProcessed == "" { strProcessed = "0" } if strProcessed == "" { strProcessed = "0" }
if strProcessed == "" { strProcessed = "1" } if strProcessed == "" { strProcessed = "1" }
if strProcessed == "" { strProcessed = "2" } if strProcessed == "" { strProcessed = "2" }
if strProcessed == "ˇ" { strProcessed = "3" } if strProcessed == "ˇ" { strProcessed = "3" }
if strProcessed == "ˋ" { strProcessed = "4" } if strProcessed == "ˋ" { strProcessed = "4" }
if strProcessed == "" { strProcessed = "5" } if strProcessed == "" { strProcessed = "5" }
if strProcessed == "ˊ" { strProcessed = "6" } if strProcessed == "ˊ" { strProcessed = "6" }
if strProcessed == "˙" { strProcessed = "7" } if strProcessed == "˙" { strProcessed = "7" }
if strProcessed == "" { strProcessed = "8" } if strProcessed == "" { strProcessed = "8" }
if strProcessed == "" { strProcessed = "9" } if strProcessed == "" { strProcessed = "9" }
if strProcessed == "" { strProcessed = ";" } if strProcessed == "" { strProcessed = ";" }
if strProcessed == "" { strProcessed = "a" } if strProcessed == "" { strProcessed = "a" }
if strProcessed == "" { strProcessed = "b" } if strProcessed == "" { strProcessed = "b" }
if strProcessed == "" { strProcessed = "c" } if strProcessed == "" { strProcessed = "c" }
if strProcessed == "" { strProcessed = "d" } if strProcessed == "" { strProcessed = "d" }
if strProcessed == "" { strProcessed = "e" } if strProcessed == "" { strProcessed = "e" }
if strProcessed == "" { strProcessed = "f" } if strProcessed == "" { strProcessed = "f" }
if strProcessed == "" { strProcessed = "g" } if strProcessed == "" { strProcessed = "g" }
if strProcessed == "" { strProcessed = "h" } if strProcessed == "" { strProcessed = "h" }
if strProcessed == "" { strProcessed = "i" } if strProcessed == "" { strProcessed = "i" }
if strProcessed == "" { strProcessed = "j" } if strProcessed == "" { strProcessed = "j" }
if strProcessed == "" { strProcessed = "k" } if strProcessed == "" { strProcessed = "k" }
if strProcessed == "" { strProcessed = "l" } if strProcessed == "" { strProcessed = "l" }
if strProcessed == "" { strProcessed = "m" } if strProcessed == "" { strProcessed = "m" }
if strProcessed == "" { strProcessed = "n" } if strProcessed == "" { strProcessed = "n" }
if strProcessed == "" { strProcessed = "o" } if strProcessed == "" { strProcessed = "o" }
if strProcessed == "" { strProcessed = "p" } if strProcessed == "" { strProcessed = "p" }
if strProcessed == "" { strProcessed = "q" } if strProcessed == "" { strProcessed = "q" }
if strProcessed == "" { strProcessed = "r" } if strProcessed == "" { strProcessed = "r" }
if strProcessed == "" { strProcessed = "s" } if strProcessed == "" { strProcessed = "s" }
if strProcessed == "" { strProcessed = "t" } if strProcessed == "" { strProcessed = "t" }
if strProcessed == "" { strProcessed = "u" } if strProcessed == "" { strProcessed = "u" }
if strProcessed == "" { strProcessed = "v" } if strProcessed == "" { strProcessed = "v" }
if strProcessed == "" { strProcessed = "w" } if strProcessed == "" { strProcessed = "w" }
if strProcessed == "" { strProcessed = "x" } if strProcessed == "" { strProcessed = "x" }
if strProcessed == "" { strProcessed = "y" } if strProcessed == "" { strProcessed = "y" }
if strProcessed == "" { strProcessed = "z" } if strProcessed == "" { strProcessed = "z" }
// //
if strProcessed == "" { strProcessed = "\\" } if strProcessed == "" { strProcessed = "\\" }
if strProcessed == "" { strProcessed = "[" } if strProcessed == "" { strProcessed = "[" }
if strProcessed == "" { strProcessed = "]" } if strProcessed == "" { strProcessed = "]" }
if strProcessed == "" { strProcessed = "{" } if strProcessed == "" { strProcessed = "{" }
if strProcessed == "" { strProcessed = "}" } if strProcessed == "" { strProcessed = "}" }
if strProcessed == "" { strProcessed = "<" } if strProcessed == "" { strProcessed = "<" }
if strProcessed == "" { strProcessed = ">" } if strProcessed == "" { strProcessed = ">" }
// SHIFT // SHIFT
if strProcessed == "" { strProcessed = "!" } if strProcessed == "" { strProcessed = "!" }
if strProcessed == "" { strProcessed = "@" } if strProcessed == "" { strProcessed = "@" }
if strProcessed == "" { strProcessed = "#" } if strProcessed == "" { strProcessed = "#" }
if strProcessed == "" { strProcessed = "$" } if strProcessed == "" { strProcessed = "$" }
if strProcessed == "" { strProcessed = "%" } if strProcessed == "" { strProcessed = "%" }
if strProcessed == "︿" { strProcessed = "^" } if strProcessed == "︿" { strProcessed = "^" }
if strProcessed == "" { strProcessed = "&" } if strProcessed == "" { strProcessed = "&" }
if strProcessed == "" { strProcessed = "*" } if strProcessed == "" { strProcessed = "*" }
if strProcessed == "" { strProcessed = "(" } if strProcessed == "" { strProcessed = "(" }
if strProcessed == "" { strProcessed = ")" } if strProcessed == "" { strProcessed = ")" }
// Alt // Alt
if strProcessed == "" { strProcessed = "-" } if strProcessed == "" { strProcessed = "-" }
// Apple // Apple
if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if strProcessed == "_" { strProcessed = "_" } if strProcessed == "_" { strProcessed = "_" }
if strProcessed == "" { strProcessed = ":" } if strProcessed == "" { strProcessed = ":" }
if strProcessed == "" { strProcessed = "?" } if strProcessed == "" { strProcessed = "?" }
if strProcessed == "" { strProcessed = "+" } if strProcessed == "" { strProcessed = "+" }
if strProcessed == "" { strProcessed = "|" } if strProcessed == "" { strProcessed = "|" }
} }
} }
return strProcessed return strProcessed
} }
} }

View File

@ -1,6 +1,4 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/* /*
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
@ -24,18 +22,12 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
#ifndef GRAMAMBULAR_H_ #import <Foundation/Foundation.h>
#define GRAMAMBULAR_H_
#include "Bigram.h" NS_ASSUME_NONNULL_BEGIN
#include "BlockReadingBuilder.h"
#include "Grid.h"
#include "KeyValuePair.h"
#include "LanguageModel.h"
#include "Node.h"
#include "NodeAnchor.h"
#include "Span.h"
#include "Unigram.h"
#include "Walker.h"
#endif @interface CTools : NSObject
+ (BOOL)isPrintable:(UniChar)charCode;
@end
NS_ASSUME_NONNULL_END

View File

@ -22,30 +22,11 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
#ifndef LMConsolidator_hpp #import "CTools.h"
#define LMConsolidator_hpp
#include <fstream> @implementation CTools
#include <iostream> + (BOOL)isPrintable:(UniChar)charCode
#include <map>
#include <regex>
#include <set>
#include <sstream>
#include <stdio.h>
#include <string>
#include <syslog.h>
using namespace std;
namespace vChewing
{ {
return isprint(charCode);
class LMConsolidator }
{ @end
public:
static bool CheckPragma(const char *path);
static bool FixEOF(const char *path);
static bool ConsolidateContent(const char *path, bool shouldCheckPragma);
};
} // namespace vChewing
#endif /* LMConsolidator_hpp */

View File

@ -29,301 +29,301 @@ import Cocoa
// Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts. // Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts.
// KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes // KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes
// Also: HIToolbox.framework/Versions/A/Headers/Events.h // Also: HIToolbox.framework/Versions/A/Headers/Events.h
@objc enum KeyCode: UInt16 { enum KeyCode: UInt16 {
case kNone = 0 case kNone = 0
case kCarriageReturn = 36 // Renamed from "kReturn" to avoid nomenclatural confusions. case kCarriageReturn = 36 // Renamed from "kReturn" to avoid nomenclatural confusions.
case kTab = 48 case kTab = 48
case kSpace = 49 case kSpace = 49
case kSymbolMenuPhysicalKey = 50 // vChewing Specific case kSymbolMenuPhysicalKey = 50 // vChewing Specific
case kBackSpace = 51 // Renamed from "kDelete" to avoid nomenclatural confusions. case kBackSpace = 51 // Renamed from "kDelete" to avoid nomenclatural confusions.
case kEscape = 53 case kEscape = 53
case kCommand = 55 case kCommand = 55
case kShift = 56 case kShift = 56
case kCapsLock = 57 case kCapsLock = 57
case kOption = 58 case kOption = 58
case kControl = 59 case kControl = 59
case kRightShift = 60 case kRightShift = 60
case kRightOption = 61 case kRightOption = 61
case kRightControl = 62 case kRightControl = 62
case kFunction = 63 case kFunction = 63
case kF17 = 64 case kF17 = 64
case kVolumeUp = 72 case kVolumeUp = 72
case kVolumeDown = 73 case kVolumeDown = 73
case kMute = 74 case kMute = 74
case kLineFeed = 76 // Another keyCode to identify the Enter Key. case kLineFeed = 76 // Another keyCode to identify the Enter Key.
case kF18 = 79 case kF18 = 79
case kF19 = 80 case kF19 = 80
case kF20 = 90 case kF20 = 90
case kF5 = 96 case kF5 = 96
case kF6 = 97 case kF6 = 97
case kF7 = 98 case kF7 = 98
case kF3 = 99 case kF3 = 99
case kF8 = 100 case kF8 = 100
case kF9 = 101 case kF9 = 101
case kF11 = 103 case kF11 = 103
case kF13 = 105 case kF13 = 105
case kF16 = 106 case kF16 = 106
case kF14 = 107 case kF14 = 107
case kF10 = 109 case kF10 = 109
case kF12 = 111 case kF12 = 111
case kF15 = 113 case kF15 = 113
case kHelp = 114 case kHelp = 114
case kHome = 115 case kHome = 115
case kPageUp = 116 case kPageUp = 116
case kWindowDelete = 117 // Renamed from "kForwardDelete" to avoid nomenclatural confusions. case kWindowDelete = 117 // Renamed from "kForwardDelete" to avoid nomenclatural confusions.
case kF4 = 118 case kF4 = 118
case kEnd = 119 case kEnd = 119
case kF2 = 120 case kF2 = 120
case kPageDown = 121 case kPageDown = 121
case kF1 = 122 case kF1 = 122
case kLeftArrow = 123 case kLeftArrow = 123
case kRightArrow = 124 case kRightArrow = 124
case kDownArrow = 125 case kDownArrow = 125
case kUpArrow = 126 case kUpArrow = 126
} }
// CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html // CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html
enum CharCode: UInt /* 16 */ { enum CharCode: UInt /* 16 */ {
case yajuusenpai = 114_514_191_191_810_893 case yajuusenpai = 114_514_191_191_810_893
// CharCode is not reliable at all. KeyCode is the most appropriate choice due to its accuracy. // CharCode is not reliable at all. KeyCode is the most appropriate choice due to its accuracy.
// KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts ... // KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts ...
// ... but only focuses on which physical key is pressed. // ... but only focuses on which physical key is pressed.
} }
class InputHandler: NSObject { class InputHandler: NSObject {
@objc private(set) var useVerticalMode: Bool private(set) var useVerticalMode: Bool
@objc private(set) var inputText: String? private(set) var inputText: String?
@objc private(set) var inputTextIgnoringModifiers: String? private(set) var inputTextIgnoringModifiers: String?
@objc private(set) var charCode: UInt16 private(set) var charCode: UInt16
@objc private(set) var keyCode: UInt16 private(set) var keyCode: UInt16
private var isFlagChanged: Bool private var isFlagChanged: Bool
private var flags: NSEvent.ModifierFlags private var flags: NSEvent.ModifierFlags
private var cursorForwardKey: KeyCode private var cursorForwardKey: KeyCode
private var cursorBackwardKey: KeyCode private var cursorBackwardKey: KeyCode
private var extraChooseCandidateKey: KeyCode private var extraChooseCandidateKey: KeyCode
private var extraChooseCandidateKeyReverse: KeyCode private var extraChooseCandidateKeyReverse: KeyCode
private var absorbedArrowKey: KeyCode private var absorbedArrowKey: KeyCode
private var verticalModeOnlyChooseCandidateKey: KeyCode private var verticalModeOnlyChooseCandidateKey: KeyCode
@objc private(set) var emacsKey: vChewingEmacsKey private(set) var emacsKey: vChewingEmacsKey
@objc init( init(
inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags,
isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil
) { ) {
let inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") let inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "")
let inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC( let inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(
inputTextIgnoringModifiers ?? inputText) inputTextIgnoringModifiers ?? inputText)
self.inputText = inputText self.inputText = inputText
self.inputTextIgnoringModifiers = inputTextIgnoringModifiers self.inputTextIgnoringModifiers = inputTextIgnoringModifiers
self.flags = flags self.flags = flags
isFlagChanged = false isFlagChanged = false
useVerticalMode = isVerticalMode useVerticalMode = isVerticalMode
self.keyCode = keyCode self.keyCode = keyCode
self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
emacsKey = EmacsKeyHelper.detect( emacsKey = EmacsKeyHelper.detect(
charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags
) )
// Define Arrow Keys // Define Arrow Keys
cursorForwardKey = useVerticalMode ? .kDownArrow : .kRightArrow cursorForwardKey = useVerticalMode ? .kDownArrow : .kRightArrow
cursorBackwardKey = useVerticalMode ? .kUpArrow : .kLeftArrow cursorBackwardKey = useVerticalMode ? .kUpArrow : .kLeftArrow
extraChooseCandidateKey = useVerticalMode ? .kLeftArrow : .kDownArrow extraChooseCandidateKey = useVerticalMode ? .kLeftArrow : .kDownArrow
extraChooseCandidateKeyReverse = useVerticalMode ? .kRightArrow : .kUpArrow extraChooseCandidateKeyReverse = useVerticalMode ? .kRightArrow : .kUpArrow
absorbedArrowKey = useVerticalMode ? .kRightArrow : .kUpArrow absorbedArrowKey = useVerticalMode ? .kRightArrow : .kUpArrow
verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .kNone verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .kNone
super.init() super.init()
} }
@objc init(event: NSEvent, isVerticalMode: Bool) { init(event: NSEvent, isVerticalMode: Bool) {
inputText = AppleKeyboardConverter.cnvStringApple2ABC(event.characters ?? "") inputText = AppleKeyboardConverter.cnvStringApple2ABC(event.characters ?? "")
inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC( inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(
event.charactersIgnoringModifiers ?? "") event.charactersIgnoringModifiers ?? "")
keyCode = event.keyCode keyCode = event.keyCode
flags = event.modifierFlags flags = event.modifierFlags
isFlagChanged = (event.type == .flagsChanged) ? true : false isFlagChanged = (event.type == .flagsChanged) ? true : false
useVerticalMode = isVerticalMode useVerticalMode = isVerticalMode
let charCode: UInt16 = { let charCode: UInt16 = {
// count > 0!isEmpty滿 // count > 0!isEmpty滿
guard let inputText = event.characters, !inputText.isEmpty else { guard let inputText = event.characters, !inputText.isEmpty else {
return 0 return 0
} }
let first = inputText[inputText.startIndex].utf16.first! let first = inputText[inputText.startIndex].utf16.first!
return first return first
}() }()
self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
emacsKey = EmacsKeyHelper.detect( emacsKey = EmacsKeyHelper.detect(
charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags
) )
// Define Arrow Keys in the same way above. // Define Arrow Keys in the same way above.
cursorForwardKey = useVerticalMode ? .kDownArrow : .kRightArrow cursorForwardKey = useVerticalMode ? .kDownArrow : .kRightArrow
cursorBackwardKey = useVerticalMode ? .kUpArrow : .kLeftArrow cursorBackwardKey = useVerticalMode ? .kUpArrow : .kLeftArrow
extraChooseCandidateKey = useVerticalMode ? .kLeftArrow : .kDownArrow extraChooseCandidateKey = useVerticalMode ? .kLeftArrow : .kDownArrow
extraChooseCandidateKeyReverse = useVerticalMode ? .kRightArrow : .kUpArrow extraChooseCandidateKeyReverse = useVerticalMode ? .kRightArrow : .kUpArrow
absorbedArrowKey = useVerticalMode ? .kRightArrow : .kUpArrow absorbedArrowKey = useVerticalMode ? .kRightArrow : .kUpArrow
verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .kNone verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .kNone
super.init() super.init()
} }
override var description: String { override var description: String {
charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "")
inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC( inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(
inputTextIgnoringModifiers ?? "") inputTextIgnoringModifiers ?? "")
return return
"<\(super.description) inputText:\(String(describing: inputText)), inputTextIgnoringModifiers:\(String(describing: inputTextIgnoringModifiers)) charCode:\(charCode), keyCode:\(keyCode), flags:\(flags), cursorForwardKey:\(cursorForwardKey), cursorBackwardKey:\(cursorBackwardKey), extraChooseCandidateKey:\(extraChooseCandidateKey), extraChooseCandidateKeyReverse:\(extraChooseCandidateKeyReverse), absorbedArrowKey:\(absorbedArrowKey), verticalModeOnlyChooseCandidateKey:\(verticalModeOnlyChooseCandidateKey), emacsKey:\(emacsKey), useVerticalMode:\(useVerticalMode)>" "<\(super.description) inputText:\(String(describing: inputText)), inputTextIgnoringModifiers:\(String(describing: inputTextIgnoringModifiers)) charCode:\(charCode), keyCode:\(keyCode), flags:\(flags), cursorForwardKey:\(cursorForwardKey), cursorBackwardKey:\(cursorBackwardKey), extraChooseCandidateKey:\(extraChooseCandidateKey), extraChooseCandidateKeyReverse:\(extraChooseCandidateKeyReverse), absorbedArrowKey:\(absorbedArrowKey), verticalModeOnlyChooseCandidateKey:\(verticalModeOnlyChooseCandidateKey), emacsKey:\(emacsKey), useVerticalMode:\(useVerticalMode)>"
} }
@objc var isShiftHold: Bool { var isShiftHold: Bool {
flags.contains([.shift]) flags.contains([.shift])
} }
@objc var isCommandHold: Bool { var isCommandHold: Bool {
flags.contains([.command]) flags.contains([.command])
} }
@objc var isControlHold: Bool { var isControlHold: Bool {
flags.contains([.control]) flags.contains([.control])
} }
@objc var isControlHotKey: Bool { var isControlHotKey: Bool {
flags.contains([.control]) && inputText?.first?.isLetter ?? false flags.contains([.control]) && inputText?.first?.isLetter ?? false
} }
@objc var isOptionHotKey: Bool { var isOptionHotKey: Bool {
flags.contains([.option]) && inputText?.first?.isLetter ?? false flags.contains([.option]) && inputText?.first?.isLetter ?? false
} }
@objc var isOptionHold: Bool { var isOptionHold: Bool {
flags.contains([.option]) flags.contains([.option])
} }
@objc var isCapsLockOn: Bool { var isCapsLockOn: Bool {
flags.contains([.capsLock]) flags.contains([.capsLock])
} }
@objc var isNumericPad: Bool { var isNumericPad: Bool {
flags.contains([.numericPad]) flags.contains([.numericPad])
} }
@objc var isFunctionKeyHold: Bool { var isFunctionKeyHold: Bool {
flags.contains([.function]) flags.contains([.function])
} }
@objc var isReservedKey: Bool { var isReservedKey: Bool {
guard let code = KeyCode(rawValue: keyCode) else { guard let code = KeyCode(rawValue: keyCode) else {
return false return false
} }
return code.rawValue != KeyCode.kNone.rawValue return code.rawValue != KeyCode.kNone.rawValue
} }
@objc var isTab: Bool { var isTab: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kTab KeyCode(rawValue: keyCode) == KeyCode.kTab
} }
@objc var isEnter: Bool { var isEnter: Bool {
(KeyCode(rawValue: keyCode) == KeyCode.kCarriageReturn) (KeyCode(rawValue: keyCode) == KeyCode.kCarriageReturn)
|| (KeyCode(rawValue: keyCode) == KeyCode.kLineFeed) || (KeyCode(rawValue: keyCode) == KeyCode.kLineFeed)
} }
@objc var isUp: Bool { var isUp: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kUpArrow KeyCode(rawValue: keyCode) == KeyCode.kUpArrow
} }
@objc var isDown: Bool { var isDown: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kDownArrow KeyCode(rawValue: keyCode) == KeyCode.kDownArrow
} }
@objc var isLeft: Bool { var isLeft: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kLeftArrow KeyCode(rawValue: keyCode) == KeyCode.kLeftArrow
} }
@objc var isRight: Bool { var isRight: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kRightArrow KeyCode(rawValue: keyCode) == KeyCode.kRightArrow
} }
@objc var isPageUp: Bool { var isPageUp: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kPageUp KeyCode(rawValue: keyCode) == KeyCode.kPageUp
} }
@objc var isPageDown: Bool { var isPageDown: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kPageDown KeyCode(rawValue: keyCode) == KeyCode.kPageDown
} }
@objc var isSpace: Bool { var isSpace: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kSpace KeyCode(rawValue: keyCode) == KeyCode.kSpace
} }
@objc var isBackSpace: Bool { var isBackSpace: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kBackSpace KeyCode(rawValue: keyCode) == KeyCode.kBackSpace
} }
@objc var isESC: Bool { var isESC: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kEscape KeyCode(rawValue: keyCode) == KeyCode.kEscape
} }
@objc var isHome: Bool { var isHome: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kHome KeyCode(rawValue: keyCode) == KeyCode.kHome
} }
@objc var isEnd: Bool { var isEnd: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kEnd KeyCode(rawValue: keyCode) == KeyCode.kEnd
} }
@objc var isDelete: Bool { var isDelete: Bool {
KeyCode(rawValue: keyCode) == KeyCode.kWindowDelete KeyCode(rawValue: keyCode) == KeyCode.kWindowDelete
} }
@objc var isCursorBackward: Bool { var isCursorBackward: Bool {
KeyCode(rawValue: keyCode) == cursorBackwardKey KeyCode(rawValue: keyCode) == cursorBackwardKey
} }
@objc var isCursorForward: Bool { var isCursorForward: Bool {
KeyCode(rawValue: keyCode) == cursorForwardKey KeyCode(rawValue: keyCode) == cursorForwardKey
} }
@objc var isAbsorbedArrowKey: Bool { var isAbsorbedArrowKey: Bool {
KeyCode(rawValue: keyCode) == absorbedArrowKey KeyCode(rawValue: keyCode) == absorbedArrowKey
} }
@objc var isExtraChooseCandidateKey: Bool { var isExtraChooseCandidateKey: Bool {
KeyCode(rawValue: keyCode) == extraChooseCandidateKey KeyCode(rawValue: keyCode) == extraChooseCandidateKey
} }
@objc var isExtraChooseCandidateKeyReverse: Bool { var isExtraChooseCandidateKeyReverse: Bool {
KeyCode(rawValue: keyCode) == extraChooseCandidateKeyReverse KeyCode(rawValue: keyCode) == extraChooseCandidateKeyReverse
} }
@objc var isVerticalModeOnlyChooseCandidateKey: Bool { var isVerticalModeOnlyChooseCandidateKey: Bool {
KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey
} }
@objc var isUpperCaseASCIILetterKey: Bool { var isUpperCaseASCIILetterKey: Bool {
// flags == .shift Shift // flags == .shift Shift
charCode >= 65 && charCode <= 90 && flags == .shift charCode >= 65 && charCode <= 90 && flags == .shift
} }
@objc var isSymbolMenuPhysicalKey: Bool { var isSymbolMenuPhysicalKey: Bool {
// KeyCode macOS Apple // KeyCode macOS Apple
// ![input isShift] 使 Shift // ![input isShift] 使 Shift
KeyCode(rawValue: keyCode) == KeyCode.kSymbolMenuPhysicalKey KeyCode(rawValue: keyCode) == KeyCode.kSymbolMenuPhysicalKey
} }
} }
@objc enum vChewingEmacsKey: UInt16 { enum vChewingEmacsKey: UInt16 {
case none = 0 case none = 0
case forward = 6 // F case forward = 6 // F
case backward = 2 // B case backward = 2 // B
case home = 1 // A case home = 1 // A
case end = 5 // E case end = 5 // E
case delete = 4 // D case delete = 4 // D
case nextPage = 22 // V case nextPage = 22 // V
} }
class EmacsKeyHelper: NSObject { class EmacsKeyHelper: NSObject {
@objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey {
let charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) let charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
if flags.contains(.control) { if flags.contains(.control) {
return vChewingEmacsKey(rawValue: charCode) ?? .none return vChewingEmacsKey(rawValue: charCode) ?? .none
} }
return .none return .none
} }
} }

View File

@ -58,449 +58,439 @@ import Cocoa
/// - Choosing Candidate: The candidate window is open to let the user to choose /// - Choosing Candidate: The candidate window is open to let the user to choose
/// one among the candidates. /// one among the candidates.
class InputState: NSObject { class InputState: NSObject {
/// Represents that the input controller is deactivated. /// Represents that the input controller is deactivated.
@objc(InputStateDeactivated) class Deactivated: InputState {
class Deactivated: InputState { override var description: String {
override var description: String { "<InputState.Deactivated>"
"<InputState.Deactivated>" }
} }
}
// MARK: - // MARK: -
/// Represents that the composing buffer is empty. /// Represents that the composing buffer is empty.
@objc(InputStateEmpty) class Empty: InputState {
class Empty: InputState { var composingBuffer: String {
@objc var composingBuffer: String { ""
"" }
}
override var description: String { override var description: String {
"<InputState.Empty>" "<InputState.Empty>"
} }
} }
// MARK: - // MARK: -
/// Represents that the composing buffer is empty. /// Represents that the composing buffer is empty.
@objc(InputStateEmptyIgnoringPreviousState) class EmptyIgnoringPreviousState: InputState {
class EmptyIgnoringPreviousState: InputState { var composingBuffer: String {
@objc var composingBuffer: String { ""
"" }
}
override var description: String { override var description: String {
"<InputState.EmptyIgnoringPreviousState>" "<InputState.EmptyIgnoringPreviousState>"
} }
} }
// MARK: - // MARK: -
/// Represents that the input controller is committing text into client app. /// Represents that the input controller is committing text into client app.
@objc(InputStateCommitting) class Committing: InputState {
class Committing: InputState { private(set) var poppedText: String = ""
@objc private(set) var poppedText: String = ""
@objc convenience init(poppedText: String) { convenience init(poppedText: String) {
self.init() self.init()
self.poppedText = poppedText self.poppedText = poppedText
} }
override var description: String { override var description: String {
"<InputState.Committing poppedText:\(poppedText)>" "<InputState.Committing poppedText:\(poppedText)>"
} }
} }
// MARK: - // MARK: -
/// Represents that the composing buffer is not empty. /// Represents that the composing buffer is not empty.
@objc(InputStateNotEmpty) class NotEmpty: InputState {
class NotEmpty: InputState { private(set) var composingBuffer: String
@objc private(set) var composingBuffer: String private(set) var cursorIndex: UInt
@objc private(set) var cursorIndex: UInt
@objc init(composingBuffer: String, cursorIndex: UInt) { init(composingBuffer: String, cursorIndex: UInt) {
self.composingBuffer = composingBuffer self.composingBuffer = composingBuffer
self.cursorIndex = cursorIndex self.cursorIndex = cursorIndex
} }
override var description: String { override var description: String {
"<InputState.NotEmpty, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>" "<InputState.NotEmpty, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
} }
} }
// MARK: - // MARK: -
/// Represents that the user is inputting text. /// Represents that the user is inputting text.
@objc(InputStateInputting) class Inputting: NotEmpty {
class Inputting: NotEmpty { var poppedText: String = ""
@objc var poppedText: String = "" var tooltip: String = ""
@objc var tooltip: String = ""
@objc override init(composingBuffer: String, cursorIndex: UInt) { override init(composingBuffer: String, cursorIndex: UInt) {
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
} }
@objc var attributedString: NSAttributedString { var attributedString: NSAttributedString {
let attributedSting = NSAttributedString( let attributedSting = NSAttributedString(
string: composingBuffer, string: composingBuffer,
attributes: [ attributes: [
.underlineStyle: NSUnderlineStyle.single.rawValue, .underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0, .markedClauseSegment: 0,
] ]
) )
return attributedSting return attributedSting
} }
override var description: String { override var description: String {
"<InputState.Inputting, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>, poppedText:\(poppedText)>" "<InputState.Inputting, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>, poppedText:\(poppedText)>"
} }
} }
// MARK: - // MARK: -
private let kMinMarkRangeLength = 2 private let kMinMarkRangeLength = 2
private let kMaxMarkRangeLength = mgrPrefs.maxCandidateLength private let kMaxMarkRangeLength = mgrPrefs.maxCandidateLength
/// Represents that the user is marking a range in the composing buffer. /// Represents that the user is marking a range in the composing buffer.
@objc(InputStateMarking) class Marking: NotEmpty {
class Marking: NotEmpty { private(set) var markerIndex: UInt
@objc private(set) var markerIndex: UInt private(set) var markedRange: NSRange
@objc private(set) var markedRange: NSRange private var deleteTargetExists = false
@objc private var deleteTargetExists = false var tooltip: String {
@objc var tooltip: String { if composingBuffer.count != readings.count {
if composingBuffer.count != readings.count { TooltipController.backgroundColor = NSColor(
TooltipController.backgroundColor = NSColor( red: 0.55, green: 0.00, blue: 0.00, alpha: 1.00
red: 0.55, green: 0.00, blue: 0.00, alpha: 1.00 )
) TooltipController.textColor = NSColor.white
TooltipController.textColor = NSColor.white return NSLocalizedString(
return NSLocalizedString( "⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: ""
"⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: "" )
) }
}
if mgrPrefs.phraseReplacementEnabled { if mgrPrefs.phraseReplacementEnabled {
TooltipController.backgroundColor = NSColor.purple TooltipController.backgroundColor = NSColor.purple
TooltipController.textColor = NSColor.white TooltipController.textColor = NSColor.white
return NSLocalizedString( return NSLocalizedString(
"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: "" "⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: ""
) )
} }
if markedRange.length == 0 { if markedRange.length == 0 {
return "" return ""
} }
let text = (composingBuffer as NSString).substring(with: markedRange) let text = (composingBuffer as NSString).substring(with: markedRange)
if markedRange.length < kMinMarkRangeLength { if markedRange.length < kMinMarkRangeLength {
TooltipController.backgroundColor = NSColor( TooltipController.backgroundColor = NSColor(
red: 0.18, green: 0.18, blue: 0.18, alpha: 1.00 red: 0.18, green: 0.18, blue: 0.18, alpha: 1.00
) )
TooltipController.textColor = NSColor( TooltipController.textColor = NSColor(
red: 0.86, green: 0.86, blue: 0.86, alpha: 1.00 red: 0.86, green: 0.86, blue: 0.86, alpha: 1.00
) )
return String( return String(
format: NSLocalizedString( format: NSLocalizedString(
"\"%@\" length must ≥ 2 for a user phrase.", comment: "" "\"%@\" length must ≥ 2 for a user phrase.", comment: ""
), text ), text
) )
} else if markedRange.length > kMaxMarkRangeLength { } else if markedRange.length > kMaxMarkRangeLength {
TooltipController.backgroundColor = NSColor( TooltipController.backgroundColor = NSColor(
red: 0.26, green: 0.16, blue: 0.00, alpha: 1.00 red: 0.26, green: 0.16, blue: 0.00, alpha: 1.00
) )
TooltipController.textColor = NSColor( TooltipController.textColor = NSColor(
red: 1.00, green: 0.60, blue: 0.00, alpha: 1.00 red: 1.00, green: 0.60, blue: 0.00, alpha: 1.00
) )
return String( return String(
format: NSLocalizedString( format: NSLocalizedString(
"\"%@\" length should ≤ %d for a user phrase.", comment: "" "\"%@\" length should ≤ %d for a user phrase.", comment: ""
), ),
text, kMaxMarkRangeLength text, kMaxMarkRangeLength
) )
} }
let (exactBegin, _) = (composingBuffer as NSString).characterIndex( let (exactBegin, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location) from: markedRange.location)
let (exactEnd, _) = (composingBuffer as NSString).characterIndex( let (exactEnd, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location + markedRange.length) from: markedRange.location + markedRange.length)
let selectedReadings = readings[exactBegin..<exactEnd] let selectedReadings = readings[exactBegin..<exactEnd]
let joined = selectedReadings.joined(separator: "-") let joined = selectedReadings.joined(separator: "-")
let exist = mgrLangModel.checkIfUserPhraseExist( let exist = mgrLangModel.checkIfUserPhraseExist(
userPhrase: text, mode: ctlInputMethod.currentKeyHandler.inputMode, key: joined userPhrase: text, mode: ctlInputMethod.currentKeyHandler.inputMode, key: joined
) )
if exist { if exist {
deleteTargetExists = exist deleteTargetExists = exist
TooltipController.backgroundColor = NSColor( TooltipController.backgroundColor = NSColor(
red: 0.00, green: 0.18, blue: 0.13, alpha: 1.00 red: 0.00, green: 0.18, blue: 0.13, alpha: 1.00
) )
TooltipController.textColor = NSColor( TooltipController.textColor = NSColor(
red: 0.00, green: 1.00, blue: 0.74, alpha: 1.00 red: 0.00, green: 1.00, blue: 0.74, alpha: 1.00
) )
return String( return String(
format: NSLocalizedString( format: NSLocalizedString(
"\"%@\" already exists: ENTER to boost, \n SHIFT+CMD+ENTER to exclude.", comment: "" "\"%@\" already exists: ENTER to boost, \n SHIFT+CMD+ENTER to exclude.", comment: ""
), text ), text
) )
} }
TooltipController.backgroundColor = NSColor( TooltipController.backgroundColor = NSColor(
red: 0.18, green: 0.18, blue: 0.18, alpha: 1.00 red: 0.18, green: 0.18, blue: 0.18, alpha: 1.00
) )
TooltipController.textColor = NSColor.white TooltipController.textColor = NSColor.white
return String( return String(
format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: ""), format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: ""),
text text
) )
} }
@objc var tooltipForInputting: String = "" var tooltipForInputting: String = ""
@objc private(set) var readings: [String] private(set) var readings: [String]
@objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) { init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) {
self.markerIndex = markerIndex self.markerIndex = markerIndex
let begin = min(cursorIndex, markerIndex) let begin = min(cursorIndex, markerIndex)
let end = max(cursorIndex, markerIndex) let end = max(cursorIndex, markerIndex)
markedRange = NSRange(location: Int(begin), length: Int(end - begin)) markedRange = NSRange(location: Int(begin), length: Int(end - begin))
self.readings = readings self.readings = readings
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
} }
@objc var attributedString: NSAttributedString { var attributedString: NSAttributedString {
let attributedSting = NSMutableAttributedString(string: composingBuffer) let attributedSting = NSMutableAttributedString(string: composingBuffer)
let end = markedRange.location + markedRange.length let end = markedRange.location + markedRange.length
attributedSting.setAttributes( attributedSting.setAttributes(
[ [
.underlineStyle: NSUnderlineStyle.single.rawValue, .underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0, .markedClauseSegment: 0,
], range: NSRange(location: 0, length: markedRange.location) ], range: NSRange(location: 0, length: markedRange.location)
) )
attributedSting.setAttributes( attributedSting.setAttributes(
[ [
.underlineStyle: NSUnderlineStyle.thick.rawValue, .underlineStyle: NSUnderlineStyle.thick.rawValue,
.markedClauseSegment: 1, .markedClauseSegment: 1,
], range: markedRange ], range: markedRange
) )
attributedSting.setAttributes( attributedSting.setAttributes(
[ [
.underlineStyle: NSUnderlineStyle.single.rawValue, .underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 2, .markedClauseSegment: 2,
], ],
range: NSRange( range: NSRange(
location: end, location: end,
length: (composingBuffer as NSString).length - end length: (composingBuffer as NSString).length - end
) )
) )
return attributedSting return attributedSting
} }
override var description: String { override var description: String {
"<InputState.Marking, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex), markedRange:\(markedRange)>" "<InputState.Marking, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex), markedRange:\(markedRange)>"
} }
@objc func convertToInputting() -> Inputting { func convertToInputting() -> Inputting {
let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
state.tooltip = tooltipForInputting state.tooltip = tooltipForInputting
return state return state
} }
@objc var validToWrite: Bool { var validToWrite: Bool {
/// vChewing allows users to input a string whose length differs /// vChewing allows users to input a string whose length differs
/// from the amount of Bopomofo readings. In this case, the range /// from the amount of Bopomofo readings. In this case, the range
/// in the composing buffer and the readings could not match, so /// in the composing buffer and the readings could not match, so
/// we disable the function to write user phrases in this case. /// we disable the function to write user phrases in this case.
if composingBuffer.count != readings.count { if composingBuffer.count != readings.count {
return false return false
} }
if markedRange.length < kMinMarkRangeLength { if markedRange.length < kMinMarkRangeLength {
return false return false
} }
if markedRange.length > kMaxMarkRangeLength { if markedRange.length > kMaxMarkRangeLength {
return false return false
} }
if ctlInputMethod.areWeDeleting, !deleteTargetExists { if ctlInputMethod.areWeDeleting, !deleteTargetExists {
return false return false
} }
return markedRange.length >= kMinMarkRangeLength return markedRange.length >= kMinMarkRangeLength
&& markedRange.length <= kMaxMarkRangeLength && markedRange.length <= kMaxMarkRangeLength
} }
@objc var chkIfUserPhraseExists: Bool { var chkIfUserPhraseExists: Bool {
let text = (composingBuffer as NSString).substring(with: markedRange) let text = (composingBuffer as NSString).substring(with: markedRange)
let (exactBegin, _) = (composingBuffer as NSString).characterIndex( let (exactBegin, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location) from: markedRange.location)
let (exactEnd, _) = (composingBuffer as NSString).characterIndex( let (exactEnd, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location + markedRange.length) from: markedRange.location + markedRange.length)
let selectedReadings = readings[exactBegin..<exactEnd] let selectedReadings = readings[exactBegin..<exactEnd]
let joined = selectedReadings.joined(separator: "-") let joined = selectedReadings.joined(separator: "-")
return mgrLangModel.checkIfUserPhraseExist( return mgrLangModel.checkIfUserPhraseExist(
userPhrase: text, mode: ctlInputMethod.currentKeyHandler.inputMode, key: joined userPhrase: text, mode: ctlInputMethod.currentKeyHandler.inputMode, key: joined
) )
== true == true
} }
@objc var userPhrase: String { var userPhrase: String {
let text = (composingBuffer as NSString).substring(with: markedRange) let text = (composingBuffer as NSString).substring(with: markedRange)
let (exactBegin, _) = (composingBuffer as NSString).characterIndex( let (exactBegin, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location) from: markedRange.location)
let (exactEnd, _) = (composingBuffer as NSString).characterIndex( let (exactEnd, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location + markedRange.length) from: markedRange.location + markedRange.length)
let selectedReadings = readings[exactBegin..<exactEnd] let selectedReadings = readings[exactBegin..<exactEnd]
let joined = selectedReadings.joined(separator: "-") let joined = selectedReadings.joined(separator: "-")
return "\(text) \(joined)" return "\(text) \(joined)"
} }
@objc var userPhraseConverted: String { var userPhraseConverted: String {
let text = let text =
OpenCCBridge.crossConvert( OpenCCBridge.crossConvert(
(composingBuffer as NSString).substring(with: markedRange)) ?? "" (composingBuffer as NSString).substring(with: markedRange)) ?? ""
let (exactBegin, _) = (composingBuffer as NSString).characterIndex( let (exactBegin, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location) from: markedRange.location)
let (exactEnd, _) = (composingBuffer as NSString).characterIndex( let (exactEnd, _) = (composingBuffer as NSString).characterIndex(
from: markedRange.location + markedRange.length) from: markedRange.location + markedRange.length)
let selectedReadings = readings[exactBegin..<exactEnd] let selectedReadings = readings[exactBegin..<exactEnd]
let joined = selectedReadings.joined(separator: "-") let joined = selectedReadings.joined(separator: "-")
let convertedMark = "#𝙊𝙥𝙚𝙣𝘾𝘾" let convertedMark = "#𝙊𝙥𝙚𝙣𝘾𝘾"
return "\(text) \(joined)\t\(convertedMark)" return "\(text) \(joined)\t\(convertedMark)"
} }
} }
// MARK: - // MARK: -
/// Represents that the user is choosing in a candidates list. /// Represents that the user is choosing in a candidates list.
@objc(InputStateChoosingCandidate) class ChoosingCandidate: NotEmpty {
class ChoosingCandidate: NotEmpty { private(set) var candidates: [String]
@objc private(set) var candidates: [String] private(set) var useVerticalMode: Bool
@objc private(set) var useVerticalMode: Bool
@objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) { init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) {
self.candidates = candidates self.candidates = candidates
self.useVerticalMode = useVerticalMode self.useVerticalMode = useVerticalMode
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
} }
@objc var attributedString: NSAttributedString { var attributedString: NSAttributedString {
let attributedSting = NSAttributedString( let attributedSting = NSAttributedString(
string: composingBuffer, string: composingBuffer,
attributes: [ attributes: [
.underlineStyle: NSUnderlineStyle.single.rawValue, .underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0, .markedClauseSegment: 0,
] ]
) )
return attributedSting return attributedSting
} }
override var description: String { override var description: String {
"<InputState.ChoosingCandidate, candidates:\(candidates), useVerticalMode:\(useVerticalMode), composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>" "<InputState.ChoosingCandidate, candidates:\(candidates), useVerticalMode:\(useVerticalMode), composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
} }
} }
// MARK: - // MARK: -
/// Represents that the user is choosing in a candidates list /// Represents that the user is choosing in a candidates list
/// in the associated phrases mode. /// in the associated phrases mode.
@objc(InputStateAssociatedPhrases) class AssociatedPhrases: InputState {
class AssociatedPhrases: InputState { private(set) var candidates: [String] = []
@objc private(set) var candidates: [String] = [] private(set) var useVerticalMode: Bool = false
@objc private(set) var useVerticalMode: Bool = false init(candidates: [String], useVerticalMode: Bool) {
@objc init(candidates: [String], useVerticalMode: Bool) { self.candidates = candidates
self.candidates = candidates self.useVerticalMode = useVerticalMode
self.useVerticalMode = useVerticalMode super.init()
super.init() }
}
override var description: String { override var description: String {
"<InputState.AssociatedPhrases, candidates:\(candidates), useVerticalMode:\(useVerticalMode)>" "<InputState.AssociatedPhrases, candidates:\(candidates), useVerticalMode:\(useVerticalMode)>"
} }
} }
@objc(InputStateSymbolTable) class SymbolTable: ChoosingCandidate {
class SymbolTable: ChoosingCandidate { var node: SymbolNode
@objc var node: SymbolNode
@objc init(node: SymbolNode, useVerticalMode: Bool) { init(node: SymbolNode, useVerticalMode: Bool) {
self.node = node self.node = node
let candidates = node.children?.map(\.title) ?? [String]() let candidates = node.children?.map(\.title) ?? [String]()
super.init( super.init(
composingBuffer: "", cursorIndex: 0, candidates: candidates, composingBuffer: "", cursorIndex: 0, candidates: candidates,
useVerticalMode: useVerticalMode useVerticalMode: useVerticalMode
) )
} }
override var description: String { override var description: String {
"<InputState.SymbolTable, candidates:\(candidates), useVerticalMode:\(useVerticalMode), composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>" "<InputState.SymbolTable, candidates:\(candidates), useVerticalMode:\(useVerticalMode), composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
} }
} }
} }
class SymbolNode: NSObject { class SymbolNode: NSObject {
@objc var title: String var title: String
@objc var children: [SymbolNode]? var children: [SymbolNode]?
@objc init(_ title: String, _ children: [SymbolNode]? = nil) { init(_ title: String, _ children: [SymbolNode]? = nil) {
self.title = title self.title = title
self.children = children self.children = children
super.init() super.init()
} }
@objc 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) }
super.init() super.init()
} }
@objc static let catCommonSymbols = String( static let catCommonSymbols = String(
format: NSLocalizedString("catCommonSymbols", comment: "")) format: NSLocalizedString("catCommonSymbols", comment: ""))
@objc static let catHoriBrackets = String( static let catHoriBrackets = String(
format: NSLocalizedString("catHoriBrackets", comment: "")) format: NSLocalizedString("catHoriBrackets", comment: ""))
@objc static let catVertBrackets = String( static let catVertBrackets = String(
format: NSLocalizedString("catVertBrackets", comment: "")) format: NSLocalizedString("catVertBrackets", comment: ""))
@objc static let catGreekLetters = String( static let catGreekLetters = String(
format: NSLocalizedString("catGreekLetters", comment: "")) format: NSLocalizedString("catGreekLetters", comment: ""))
@objc static let catMathSymbols = String( static let catMathSymbols = String(
format: NSLocalizedString("catMathSymbols", comment: "")) format: NSLocalizedString("catMathSymbols", comment: ""))
@objc static let catCurrencyUnits = String( static let catCurrencyUnits = String(
format: NSLocalizedString("catCurrencyUnits", comment: "")) format: NSLocalizedString("catCurrencyUnits", comment: ""))
@objc static let catSpecialSymbols = String( static let catSpecialSymbols = String(
format: NSLocalizedString("catSpecialSymbols", comment: "")) format: NSLocalizedString("catSpecialSymbols", comment: ""))
@objc static let catUnicodeSymbols = String( static let catUnicodeSymbols = String(
format: NSLocalizedString("catUnicodeSymbols", comment: "")) format: NSLocalizedString("catUnicodeSymbols", comment: ""))
@objc static let catCircledKanjis = String( static let catCircledKanjis = String(
format: NSLocalizedString("catCircledKanjis", comment: "")) format: NSLocalizedString("catCircledKanjis", comment: ""))
@objc static let catCircledKataKana = String( static let catCircledKataKana = String(
format: NSLocalizedString("catCircledKataKana", comment: "")) format: NSLocalizedString("catCircledKataKana", comment: ""))
@objc static let catBracketKanjis = String( static let catBracketKanjis = String(
format: NSLocalizedString("catBracketKanjis", comment: "")) format: NSLocalizedString("catBracketKanjis", comment: ""))
@objc static let catSingleTableLines = String( static let catSingleTableLines = String(
format: NSLocalizedString("catSingleTableLines", comment: "")) format: NSLocalizedString("catSingleTableLines", comment: ""))
@objc static let catDoubleTableLines = String( static let catDoubleTableLines = String(
format: NSLocalizedString("catDoubleTableLines", comment: "")) format: NSLocalizedString("catDoubleTableLines", comment: ""))
@objc static let catFillingBlocks = String( static let catFillingBlocks = String(
format: NSLocalizedString("catFillingBlocks", comment: "")) format: NSLocalizedString("catFillingBlocks", comment: ""))
@objc static let catLineSegments = String( static let catLineSegments = String(
format: NSLocalizedString("catLineSegments", comment: "")) format: NSLocalizedString("catLineSegments", comment: ""))
@objc static let root: SymbolNode = .init( static let root: SymbolNode = .init(
"/", "/",
[ [
SymbolNode(""), SymbolNode(""),
SymbolNode(catCommonSymbols, symbols: ",、。.?!;:‧‥﹐﹒˙·‘’“”〝〞‵′〃~$%@&#*"), SymbolNode(catCommonSymbols, symbols: ",、。.?!;:‧‥﹐﹒˙·‘’“”〝〞‵′〃~$%@&#*"),
SymbolNode(catHoriBrackets, symbols: "()「」〔〕{}〈〉『』《》【】﹙﹚﹝﹞﹛﹜"), SymbolNode(catHoriBrackets, symbols: "()「」〔〕{}〈〉『』《》【】﹙﹚﹝﹞﹛﹜"),
SymbolNode(catVertBrackets, symbols: "︵︶﹁﹂︹︺︷︸︿﹀﹃﹄︽︾︻︼"), SymbolNode(catVertBrackets, symbols: "︵︶﹁﹂︹︺︷︸︿﹀﹃﹄︽︾︻︼"),
SymbolNode( SymbolNode(
catGreekLetters, symbols: "αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ" catGreekLetters, symbols: "αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"
), ),
SymbolNode(catMathSymbols, symbols: "+-×÷=≠≒∞±√<>﹤﹥≦≧∩∪ˇ⊥∠∟⊿㏒㏑∫∮∵∴╳﹢"), SymbolNode(catMathSymbols, symbols: "+-×÷=≠≒∞±√<>﹤﹥≦≧∩∪ˇ⊥∠∟⊿㏒㏑∫∮∵∴╳﹢"),
SymbolNode(catCurrencyUnits, symbols: "$€¥¢£₽₨₩฿₺₮₱₭₴₦৲৳૱௹﷼₹₲₪₡₫៛₵₢₸₤₳₥₠₣₰₧₯₶₷"), SymbolNode(catCurrencyUnits, symbols: "$€¥¢£₽₨₩฿₺₮₱₭₴₦৲৳૱௹﷼₹₲₪₡₫៛₵₢₸₤₳₥₠₣₰₧₯₶₷"),
SymbolNode(catSpecialSymbols, symbols: "↑↓←→↖↗↙↘↺⇧⇩⇦⇨⇄⇆⇅⇵↻◎○●⊕⊙※△▲☆★◇◆□■▽▼§¥〒¢£♀♂↯"), SymbolNode(catSpecialSymbols, symbols: "↑↓←→↖↗↙↘↺⇧⇩⇦⇨⇄⇆⇅⇵↻◎○●⊕⊙※△▲☆★◇◆□■▽▼§¥〒¢£♀♂↯"),
SymbolNode(catUnicodeSymbols, symbols: "♨☀☁☂☃♠♥♣♦♩♪♫♬☺☻"), SymbolNode(catUnicodeSymbols, symbols: "♨☀☁☂☃♠♥♣♦♩♪♫♬☺☻"),
SymbolNode(catCircledKanjis, symbols: "㊟㊞㊚㊛㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗︎㊘㊙︎㊜㊝㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰🈚︎🈯︎"), SymbolNode(catCircledKanjis, symbols: "㊟㊞㊚㊛㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗︎㊘㊙︎㊜㊝㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰🈚︎🈯︎"),
SymbolNode( SymbolNode(
catCircledKataKana, symbols: "㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋾" catCircledKataKana, symbols: "㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋾"
), ),
SymbolNode(catBracketKanjis, symbols: "㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃"), SymbolNode(catBracketKanjis, symbols: "㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃"),
SymbolNode(catSingleTableLines, symbols: "├─┼┴┬┤┌┐╞═╪╡│▕└┘╭╮╰╯"), SymbolNode(catSingleTableLines, symbols: "├─┼┴┬┤┌┐╞═╪╡│▕└┘╭╮╰╯"),
SymbolNode(catDoubleTableLines, symbols: "╔╦╗╠═╬╣╓╥╖╒╤╕║╚╩╝╟╫╢╙╨╜╞╪╡╘╧╛"), SymbolNode(catDoubleTableLines, symbols: "╔╦╗╠═╬╣╓╥╖╒╤╕║╚╩╝╟╫╢╙╨╜╞╪╡╘╧╛"),
SymbolNode(catFillingBlocks, symbols: "_ˍ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉◢◣◥◤"), SymbolNode(catFillingBlocks, symbols: "_ˍ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉◢◣◥◤"),
SymbolNode(catLineSegments, symbols: "﹣﹦≡|∣∥–︱—︳╴¯ ̄﹉﹊﹍﹎﹋﹌﹏︴∕﹨╱╲/\"), SymbolNode(catLineSegments, symbols: "﹣﹦≡|∣∥–︱—︳╴¯ ̄﹉﹊﹍﹎﹋﹌﹏︴∕﹨╱╲/\"),
] ]
) )
} }

View File

@ -1,102 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
@class InputHandler;
@class InputState;
NS_ASSUME_NONNULL_BEGIN
typedef NSString *const InputMode NS_TYPED_ENUM;
extern InputMode imeModeCHT;
extern InputMode imeModeCHS;
extern InputMode imeModeNULL;
struct BufferStatePackage
{
NSString *composedText;
NSInteger cursorIndex;
NSString *resultOfRear;
NSString *resultOfFront;
};
@class KeyHandler;
@protocol KeyHandlerDelegate <NSObject>
- (id)ctlCandidateForKeyHandler:(KeyHandler *)keyHandler;
- (void)keyHandler:(KeyHandler *)keyHandler didSelectCandidateAtIndex:(NSInteger)index ctlCandidate:(id)controller;
- (BOOL)keyHandler:(KeyHandler *)keyHandler didRequestWriteUserPhraseWithState:(InputState *)state;
@end
@interface KeyHandler : NSObject
- (BOOL)isBuilderEmpty;
- (void)fixNodeWithValue:(NSString *)value NS_SWIFT_NAME(fixNode(value:));
- (void)clear;
@property(strong, nonatomic) InputMode inputMode;
@property(weak, nonatomic) id<KeyHandlerDelegate> delegate;
// The following items need to be exposed to Swift:
- (void)_walk;
- (NSString *)_popOverflowComposingTextAndWalk;
- (NSArray<NSString *> *)_currentReadings;
- (BOOL)checkWhetherToneMarkerConfirmsPhoneticReadingBuffer;
- (BOOL)chkKeyValidity:(UniChar)value;
- (BOOL)ifLangModelHasUnigramsForKey:(NSString *)reading;
- (BOOL)isPhoneticReadingBufferEmpty;
- (BOOL)isPrintable:(UniChar)charCode;
- (NSArray<NSString *> *)buildAssociatePhraseArrayWithKey:(NSString *)key;
- (NSArray<NSString *> *)getCandidatesArray;
- (NSInteger)getKeyLengthAtIndexZero;
- (NSInteger)getBuilderCursorIndex;
- (NSInteger)getBuilderLength;
- (NSInteger)getPackagedCursorIndex;
- (NSString *)getComposedText;
- (NSString *)getCompositionFromPhoneticReadingBuffer;
- (NSString *)getStrLocationResult:(BOOL)isFront NS_SWIFT_NAME(getStrLocationResult(isFront:));
- (NSString *)getSyllableCompositionFromPhoneticReadingBuffer;
- (void)clearPhoneticReadingBuffer;
- (void)combinePhoneticReadingBufferKey:(UniChar)charCode;
- (void)createNewBuilder;
- (void)dealWithOverrideModelSuggestions;
- (void)deleteBuilderReadingAfterCursor;
- (void)deleteBuilderReadingInFrontOfCursor;
- (void)doBackSpaceToPhoneticReadingBuffer;
- (void)ensurePhoneticParser;
- (void)insertReadingToBuilderAtCursor:(NSString *)reading;
- (void)packageBufferStateMaterials;
- (void)removeBuilderAndReset:(BOOL)shouldReset;
- (void)setBuilderCursorIndex:(NSInteger)value;
- (void)setInputModesToLM:(BOOL)isCHS;
- (void)syncBaseLMPrefs;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,637 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "KeyHandler.h"
#import "Gramambular.h"
#import "LMInstantiator.h"
#import "Mandarin.h"
#import "UserOverrideModel.h"
#import "mgrLangModel_Privates.h"
#import "vChewing-Swift.h"
#import <string>
InputMode imeModeCHS = ctlInputMethod.kIMEModeCHS;
InputMode imeModeCHT = ctlInputMethod.kIMEModeCHT;
InputMode imeModeNULL = ctlInputMethod.kIMEModeNULL;
typedef vChewing::LMInstantiator BaseLM;
typedef vChewing::UserOverrideModel UserOverrideLM;
typedef Gramambular::BlockReadingBuilder BlockBuilder;
typedef Mandarin::BopomofoReadingBuffer PhoneticBuffer;
static const double kEpsilon = 0.000001;
NSString *packagedComposedText;
NSInteger packagedCursorIndex;
NSString *packagedResultOfRear;
NSString *packagedResultOfFront;
// NON-SWIFTIFIABLE
static double FindHighestScore(const std::vector<Gramambular::NodeAnchor> &nodes, double epsilon)
{
double highestScore = 0.0;
for (auto ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni)
{
double score = ni->node->highestUnigramScore();
if (score > highestScore)
highestScore = score;
}
return highestScore + epsilon;
}
// NON-SWIFTIFIABLE
class NodeAnchorDescendingSorter
{
public:
bool operator()(const Gramambular::NodeAnchor &a, const Gramambular::NodeAnchor &b) const
{
return a.node->key().length() > b.node->key().length();
}
};
// if DEBUG is defined, a DOT file (GraphViz format) will be written to the
// specified path every time the grid is walked
#if DEBUG
static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
#endif
// NON-SWIFTIFIABLE
@implementation KeyHandler
{
// the reading buffer that takes user input
PhoneticBuffer *_bpmfReadingBuffer;
// language model
BaseLM *_languageModel;
// user override model
UserOverrideLM *_userOverrideModel;
// the grid (lattice) builder for the unigrams (and bigrams)
BlockBuilder *_builder;
// latest walked path (trellis) using the Viterbi algorithm
std::vector<Gramambular::NodeAnchor> _walkedNodes;
NSString *_inputMode;
}
@synthesize delegate = _delegate;
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
// VARIABLE: "_inputMode"
- (NSString *)inputMode
{
return _inputMode;
}
// NON-SWIFTIFIABLE
- (BOOL)isBuilderEmpty
{
return (_builder->grid().width() == 0);
}
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
// VARIABLE: "_inputMode"
- (void)setInputMode:(NSString *)value
{
// 下面這句的「isKindOfClass」是做類型檢查
// 為了應對出現輸入法 plist 被改壞掉這樣的極端情況。
BOOL isCHS = [value isKindOfClass:[NSString class]] && [value isEqual:imeModeCHS];
// 緊接著將新的簡繁輸入模式提報給 ctlInputMethod:
ctlInputMethod.currentInputMode = isCHS ? imeModeCHS : imeModeCHT;
mgrPrefs.mostRecentInputMode = ctlInputMethod.currentInputMode;
// 拿當前的 _inputMode 與 ctlInputMethod 的提報結果對比,不同的話則套用新設定:
if (![_inputMode isEqualToString:ctlInputMethod.currentInputMode])
{
// Reinitiate language models if necessary
[self setInputModesToLM:isCHS];
// Synchronize the sub-languageModel state settings to the new LM.
[self syncBaseLMPrefs];
[self removeBuilderAndReset:YES];
if (![self isPhoneticReadingBufferEmpty])
[self clearPhoneticReadingBuffer];
}
_inputMode = ctlInputMethod.currentInputMode;
}
// NON-SWIFTIFIABLE: Required by an ObjC(pp)-based class.
- (void)dealloc
{ // clean up everything
if (_bpmfReadingBuffer)
delete _bpmfReadingBuffer;
if (_builder)
[self removeBuilderAndReset:NO];
}
// NON-SWIFTIFIABLE: Not placeable in swift extensions.
- (instancetype)init
{
self = [super init];
if (self)
{
[self ensurePhoneticParser];
[self setInputMode:ctlInputMethod.currentInputMode];
}
return self;
}
// NON-SWIFTIFIABLE
- (void)fixNodeWithValue:(NSString *)value
{
NSInteger cursorIndex = [self getActualCandidateCursorIndex];
std::string stringValue(value.UTF8String);
Gramambular::NodeAnchor selectedNode = _builder->grid().fixNodeSelectedCandidate(cursorIndex, stringValue);
if (!mgrPrefs.useSCPCTypingMode)
{ // 不要針對逐字選字模式啟用臨時半衰記憶模型。
// If the length of the readings and the characters do not match,
// it often means it is a special symbol and it should not be stored
// in the user override model.
BOOL addToOverrideModel = YES;
if (selectedNode.spanningLength != [value count])
addToOverrideModel = NO;
if (addToOverrideModel)
{
double score = selectedNode.node->scoreForCandidate(stringValue);
if (score <= -12) // 威注音的 SymbolLM 的 Score 是 -12。
addToOverrideModel = NO;
}
if (addToOverrideModel)
_userOverrideModel->observe(_walkedNodes, cursorIndex, stringValue, [[NSDate date] timeIntervalSince1970]);
}
[self _walk];
if (mgrPrefs.moveCursorAfterSelectingCandidate)
{
size_t nextPosition = 0;
for (auto node : _walkedNodes)
{
if (nextPosition >= cursorIndex)
break;
nextPosition += node.spanningLength;
}
if (nextPosition <= [self getBuilderLength])
[self setBuilderCursorIndex:nextPosition];
}
}
// NON-SWIFTIFIABLE
- (void)clear
{
[self clearPhoneticReadingBuffer];
_builder->clear();
_walkedNodes.clear();
}
#pragma mark - States Building
// NON-SWIFTIFIABLE
- (void)packageBufferStateMaterials
{
// We gather the data through this function, package it,
// and sent it to our Swift extension to build the InputState.Inputting there.
// Otherwise, ObjC++ always bugs for "expecting a type".
// "updating the composing buffer" means to request the client to "refresh" the text input buffer
// with our "composing text"
NSMutableString *composingBuffer = [[NSMutableString alloc] init];
NSInteger composedStringCursorIndex = 0;
// we must do some Unicode codepoint counting to find the actual cursor location for the client
// i.e. we need to take UTF-16 into consideration, for which a surrogate pair takes 2 UniChars
// locations
size_t readingCursorIndex = 0;
size_t builderCursorIndex = [self getBuilderCursorIndex];
NSString *resultOfRear = @"";
NSString *resultOfFront = @"";
for (std::vector<Gramambular::NodeAnchor>::iterator wi = _walkedNodes.begin(), we = _walkedNodes.end(); wi != we;
++wi)
{
if ((*wi).node)
{
std::string nodeStr = (*wi).node->currentKeyValue().value;
NSString *valueString = [NSString stringWithUTF8String:nodeStr.c_str()];
[composingBuffer appendString:valueString];
NSArray<NSString *> *splited = [valueString split];
NSInteger codepointCount = splited.count;
// this re-aligns the cursor index in the composed string
// (the actual cursor on the screen) with the builder's logical
// cursor (reading) cursor; each built node has a "spanning length"
// (e.g. two reading blocks has a spanning length of 2), and we
// accumulate those lengths to calculate the displayed cursor
// index
size_t spanningLength = (*wi).spanningLength;
if (readingCursorIndex + spanningLength <= builderCursorIndex)
{
composedStringCursorIndex += [valueString length];
readingCursorIndex += spanningLength;
}
else
{
if (codepointCount == spanningLength)
{
for (size_t i = 0; i < codepointCount && readingCursorIndex < builderCursorIndex; i++)
{
composedStringCursorIndex += [splited[i] length];
readingCursorIndex++;
}
}
else
{
if (readingCursorIndex < builderCursorIndex)
{
composedStringCursorIndex += [valueString length];
readingCursorIndex += spanningLength;
if (readingCursorIndex > builderCursorIndex)
{
readingCursorIndex = builderCursorIndex;
}
if (builderCursorIndex == 0)
{
resultOfFront =
[NSString stringWithUTF8String:_builder->readings()[builderCursorIndex].c_str()];
}
else if (builderCursorIndex >= _builder->readings().size())
{
resultOfRear = [NSString
stringWithUTF8String:_builder->readings()[_builder->readings().size() - 1].c_str()];
}
else
{
resultOfFront =
[NSString stringWithUTF8String:_builder->readings()[builderCursorIndex].c_str()];
resultOfRear =
[NSString stringWithUTF8String:_builder->readings()[builderCursorIndex - 1].c_str()];
}
}
}
}
}
}
// now we gather all the info, we separate the composing buffer to two parts, head and tail,
// and insert the reading text (the Mandarin syllable) in between them;
// the reading text is what the user is typing
NSString *head = [composingBuffer substringToIndex:composedStringCursorIndex];
NSString *reading = [self getCompositionFromPhoneticReadingBuffer];
NSString *tail = [composingBuffer substringFromIndex:composedStringCursorIndex];
NSString *composedText = [head stringByAppendingString:[reading stringByAppendingString:tail]];
NSInteger cursorIndex = composedStringCursorIndex + [reading length];
packagedComposedText = composedText;
packagedCursorIndex = cursorIndex;
packagedResultOfRear = resultOfRear;
packagedResultOfFront = resultOfFront;
}
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
- (NSString *)getStrLocationResult:(BOOL)isFront
{
if (isFront)
return packagedResultOfFront;
else
return packagedResultOfRear;
}
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
- (NSString *)getComposedText
{
return packagedComposedText;
}
// NON-SWIFTIFIABLE DUE TO VARIABLE AVAILABLE ACCESSIBILITY RANGE.
- (NSInteger)getPackagedCursorIndex
{
return packagedCursorIndex;
}
// NON-SWIFTIFIABLE
- (void)_walk
{
// retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation
// of the best possible Mandarin characters given the input syllables,
// using the Viterbi algorithm implemented in the Gramambular library
Gramambular::Walker walker(&_builder->grid());
// the reverse walk traces the trellis from the end
_walkedNodes = walker.reverseWalk(_builder->grid().width());
// then we reverse the nodes so that we get the forward-walked nodes
reverse(_walkedNodes.begin(), _walkedNodes.end());
// if DEBUG is defined, a GraphViz file is written to kGraphVizOutputfile
#if DEBUG
std::string dotDump = _builder->grid().dumpDOT();
NSString *dotStr = [NSString stringWithUTF8String:dotDump.c_str()];
NSError *error = nil;
BOOL __unused success = [dotStr writeToFile:kGraphVizOutputfile
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
#endif
}
// NON-SWIFTIFIABLE
- (NSString *)_popOverflowComposingTextAndWalk
{
// in an ideal world, we can as well let the user type forever,
// but because the Viterbi algorithm has a complexity of O(N^2),
// the walk will become slower as the number of nodes increase,
// therefore we need to auto-commit overflown texts which usually
// lose their influence over the whole MLE anyway -- so that when
// the user type along, the already composed text in the rear side
// of the buffer will be committed (i.e. "popped out").
NSString *poppedText = @"";
NSInteger composingBufferSize = mgrPrefs.composingBufferSize;
if (_builder->grid().width() > (size_t)composingBufferSize)
{
if (_walkedNodes.size() > 0)
{
Gramambular::NodeAnchor &anchor = _walkedNodes[0];
poppedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()];
_builder->removeHeadReadings(anchor.spanningLength);
}
}
[self _walk];
return poppedText;
}
// NON-SWIFTIFIABLE
- (NSArray<NSString *> *)_currentReadings
{
NSMutableArray<NSString *> *readingsArray = [[NSMutableArray alloc] init];
std::vector<std::string> v = _builder->readings();
for (std::vector<std::string>::iterator it_i = v.begin(); it_i != v.end(); ++it_i)
[readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]];
return readingsArray;
}
// NON-SWIFTIFIABLE
- (NSArray<NSString *> *)buildAssociatePhraseArrayWithKey:(NSString *)key
{
NSMutableArray<NSString *> *array = [NSMutableArray array];
std::string cppKey = std::string(key.UTF8String);
if (_languageModel->hasAssociatedPhrasesForKey(cppKey))
{
std::vector<std::string> phrases = _languageModel->associatedPhrasesForKey(cppKey);
for (auto phrase : phrases)
{
NSString *item = [[NSString alloc] initWithUTF8String:phrase.c_str()];
[array addObject:item];
}
}
return array;
}
#pragma mark - 必須用 ObjCpp 處理的部分: Mandarin
- (BOOL)chkKeyValidity:(UniChar)charCode
{
return _bpmfReadingBuffer->isValidKey((char)charCode);
}
- (BOOL)isPhoneticReadingBufferEmpty
{
return _bpmfReadingBuffer->isEmpty();
}
- (void)clearPhoneticReadingBuffer
{
_bpmfReadingBuffer->clear();
}
- (void)combinePhoneticReadingBufferKey:(UniChar)charCode
{
_bpmfReadingBuffer->combineKey((char)charCode);
}
- (BOOL)checkWhetherToneMarkerConfirmsPhoneticReadingBuffer
{
return _bpmfReadingBuffer->hasToneMarker();
}
- (NSString *)getSyllableCompositionFromPhoneticReadingBuffer
{
return [NSString stringWithUTF8String:_bpmfReadingBuffer->syllable().composedString().c_str()];
}
- (void)doBackSpaceToPhoneticReadingBuffer
{
_bpmfReadingBuffer->backspace();
}
- (NSString *)getCompositionFromPhoneticReadingBuffer
{
return [NSString stringWithUTF8String:_bpmfReadingBuffer->composedString().c_str()];
}
- (void)ensurePhoneticParser
{
if (_bpmfReadingBuffer)
{
switch (mgrPrefs.mandarinParser)
{
case MandarinParserOfStandard:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::StandardLayout());
break;
case MandarinParserOfEten:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::ETenLayout());
break;
case MandarinParserOfHsu:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::HsuLayout());
break;
case MandarinParserOfEen26:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::ETen26Layout());
break;
case MandarinParserOfIBM:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::IBMLayout());
break;
case MandarinParserOfMiTAC:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::MiTACLayout());
break;
case MandarinParserOfFakeSeigyou:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::FakeSeigyouLayout());
break;
case MandarinParserOfHanyuPinyin:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::HanyuPinyinLayout());
break;
default:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::StandardLayout());
mgrPrefs.mandarinParser = MandarinParserOfStandard;
}
}
else
{
_bpmfReadingBuffer = new Mandarin::BopomofoReadingBuffer(Mandarin::BopomofoKeyboardLayout::StandardLayout());
}
}
#pragma mark - 必須用 ObjCpp 處理的部分: Gramambular 等
- (void)removeBuilderAndReset:(BOOL)shouldReset
{
if (_builder)
{
delete _builder;
if (shouldReset)
[self createNewBuilder];
}
else if (shouldReset)
[self createNewBuilder];
}
- (void)createNewBuilder
{
_builder = new Gramambular::BlockReadingBuilder(_languageModel);
// Each Mandarin syllable is separated by a hyphen.
_builder->setJoinSeparator("-");
}
- (void)setInputModesToLM:(BOOL)isCHS
{
_languageModel = isCHS ? [mgrLangModel lmCHS] : [mgrLangModel lmCHT];
_userOverrideModel = isCHS ? [mgrLangModel userOverrideModelCHS] : [mgrLangModel userOverrideModelCHT];
}
- (void)syncBaseLMPrefs
{
if (_languageModel)
{
_languageModel->setPhraseReplacementEnabled(mgrPrefs.phraseReplacementEnabled);
_languageModel->setSymbolEnabled(mgrPrefs.symbolInputEnabled);
_languageModel->setCNSEnabled(mgrPrefs.cns11643Enabled);
}
}
// ----
- (BOOL)ifLangModelHasUnigramsForKey:(NSString *)reading
{
return _languageModel->hasUnigramsForKey((std::string)[reading UTF8String]);
}
- (void)insertReadingToBuilderAtCursor:(NSString *)reading
{
_builder->insertReadingAtCursor((std::string)[reading UTF8String]);
}
- (void)dealWithOverrideModelSuggestions
{
// 這一整段都太 C++ 且只出現一次,就整個端過來了。
// 拆開封裝的話,只會把問題搞得更麻煩而已。
std::string overrideValue = (mgrPrefs.useSCPCTypingMode)
? ""
: _userOverrideModel->suggest(_walkedNodes, [self getBuilderCursorIndex],
[[NSDate date] timeIntervalSince1970]);
if (!overrideValue.empty())
{
NSInteger cursorIndex = [self getActualCandidateCursorIndex];
std::vector<Gramambular::NodeAnchor> nodes = mgrPrefs.setRearCursorMode
? _builder->grid().nodesCrossingOrEndingAt(cursorIndex)
: _builder->grid().nodesEndingAt(cursorIndex);
double highestScore = FindHighestScore(nodes, kEpsilon);
_builder->grid().overrideNodeScoreForSelectedCandidate(cursorIndex, overrideValue,
static_cast<float>(highestScore));
}
}
- (void)setBuilderCursorIndex:(NSInteger)value
{
_builder->setCursorIndex(value);
}
- (NSInteger)getBuilderCursorIndex
{
return _builder->cursorIndex();
}
- (NSInteger)getBuilderLength
{
return _builder->length();
}
- (void)deleteBuilderReadingInFrontOfCursor
{
_builder->deleteReadingBeforeCursor();
}
- (void)deleteBuilderReadingAfterCursor
{
_builder->deleteReadingAfterCursor();
}
- (NSArray<NSString *> *)getCandidatesArray
{
NSMutableArray<NSString *> *candidatesArray = [[NSMutableArray alloc] init];
NSInteger cursorIndex = [self getActualCandidateCursorIndex];
std::vector<Gramambular::NodeAnchor> nodes = mgrPrefs.setRearCursorMode
? _builder->grid().nodesCrossingOrEndingAt(cursorIndex)
: _builder->grid().nodesEndingAt(cursorIndex);
// sort the nodes, so that longer nodes (representing longer phrases) are placed at the top of the candidate list
stable_sort(nodes.begin(), nodes.end(), NodeAnchorDescendingSorter());
// then use the C++ trick to retrieve the candidates for each node at/crossing the cursor
for (std::vector<Gramambular::NodeAnchor>::iterator ni = nodes.begin(), ne = nodes.end(); ni != ne; ++ni)
{
const std::vector<Gramambular::KeyValuePair> &candidates = (*ni).node->candidates();
for (std::vector<Gramambular::KeyValuePair>::const_iterator ci = candidates.begin(), ce = candidates.end();
ci != ce; ++ci)
[candidatesArray addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]];
}
return candidatesArray;
}
- (NSInteger)getKeyLengthAtIndexZero
{
return [NSString stringWithUTF8String:_walkedNodes[0].node->currentKeyValue().value.c_str()].length;
}
#pragma mark - 威注音認為有必要單獨拿出來處理的部分,交給 Swift 則有些困難。
- (BOOL)isPrintable:(UniChar)charCode
{
return isprint(charCode);
}
@end

View File

@ -0,0 +1,324 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
public enum InputMode: String {
case imeModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS"
case imeModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT"
case imeModeNULL = ""
}
// MARK: - Delegate.
protocol KeyHandlerDelegate: NSObjectProtocol {
func ctlCandidate(for _: KeyHandler) -> Any
func keyHandler(
_: KeyHandler, didSelectCandidateAt index: Int,
ctlCandidate controller: Any
)
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputState)
-> Bool
}
// MARK: - Kernel.
class KeyHandler: NSObject {
let kEpsilon: Double = 0.000001
var _inputMode: String = ""
var _languageModel: vChewing.LMInstantiator = .init()
var _userOverrideModel: vChewing.LMUserOverride = .init()
var _builder: Megrez.BlockReadingBuilder
var _walkedNodes: [Megrez.NodeAnchor] = []
weak var delegate: KeyHandlerDelegate?
var inputMode: InputMode {
get {
switch _inputMode {
case "org.atelierInmu.inputmethod.vChewing.IMECHS":
return InputMode.imeModeCHS
case "org.atelierInmu.inputmethod.vChewing.IMECHT":
return InputMode.imeModeCHT
default:
return InputMode.imeModeNULL
}
}
set { setInputMode(newValue.rawValue) }
}
override init() {
_builder = Megrez.BlockReadingBuilder(lm: _languageModel)
super.init()
Composer.ensureParser()
setInputMode(ctlInputMethod.currentInputMode)
}
func clear() {
Composer.clearBuffer()
_builder.clear()
_walkedNodes.removeAll()
}
// ObjC 使
func setInputMode(_ value: String) {
// isKindOfClass
// plist
let isCHS: Bool = (value == InputMode.imeModeCHS.rawValue)
// ctlInputMethod:
ctlInputMethod.currentInputMode = isCHS ? InputMode.imeModeCHS.rawValue : InputMode.imeModeCHT.rawValue
mgrPrefs.mostRecentInputMode = ctlInputMethod.currentInputMode
// _inputMode ctlInputMethod
if _inputMode != ctlInputMethod.currentInputMode {
// Reinitiate language models if necessary
setInputModesToLM(isCHS: isCHS)
// Synchronize the sub-languageModel state settings to the new LM.
syncBaseLMPrefs()
// Create new grid builder.
createNewBuilder()
if !Composer.isBufferEmpty() {
Composer.clearBuffer()
}
}
//
_inputMode = ctlInputMethod.currentInputMode
}
// MARK: - Functions dealing with Megrez.
func walk() {
// Retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation
// of the best possible Mandarin characters given the input syllables,
// using the Viterbi algorithm implemented in the Gramambular library
let walker = Megrez.Walker(grid: _builder.grid())
// the reverse walk traces the trellis from the end
let walked: [Megrez.NodeAnchor] = walker.reverseWalk(at: _builder.grid().width())
// then we use ".reversed()" to reverse the nodes so that we get the forward-walked nodes
_walkedNodes.removeAll()
_walkedNodes.append(contentsOf: walked.reversed())
}
func popOverflowComposingTextAndWalk() -> String {
// In ideal situations we can allow users to type infinitely in a buffer.
// However, Viberti algorithm has a complexity of O(N^2), the walk will
// become slower as the number of nodes increase. Therefore, we need to
// auto-commit overflown texts which usually lose their influence over
// the whole MLE anyway -- so that when the user type along, the already
// composed text in the rear side of the buffer will be committed out.
// (i.e. popped out.)
var poppedText = ""
if _builder.grid().width() > mgrPrefs.composingBufferSize {
if _walkedNodes.count > 0 {
let anchor: Megrez.NodeAnchor = _walkedNodes[0]
if let theNode = anchor.node {
poppedText = theNode.currentKeyValue().value
}
_builder.removeHeadReadings(count: anchor.spanningLength)
}
}
walk()
return poppedText
}
func buildAssociatePhraseArray(withKey key: String) -> [String] {
var arrResult: [String] = []
if _languageModel.hasAssociatedPhrasesForKey(key) {
arrResult.append(contentsOf: _languageModel.associatedPhrasesForKey(key))
}
return arrResult
}
func fixNode(value: String) {
let cursorIndex: Int = getActualCandidateCursorIndex()
let selectedNode: Megrez.NodeAnchor = _builder.grid().fixNodeSelectedCandidate(
location: cursorIndex, value: value
)
//
if !mgrPrefs.useSCPCTypingMode {
// If the length of the readings and the characters do not match,
// it often means it is a special symbol and it should not be stored
// in the user override model.
var addToUserOverrideModel = true
if selectedNode.spanningLength != value.count {
addToUserOverrideModel = false
}
if addToUserOverrideModel {
if let theNode = selectedNode.node {
// SymbolLM Score -12
if theNode.scoreFor(candidate: value) <= -12 {
addToUserOverrideModel = false
}
}
}
if addToUserOverrideModel {
_userOverrideModel.observe(
walkedNodes: _walkedNodes, cursorIndex: cursorIndex, candidate: value,
timestamp: NSDate().timeIntervalSince1970
)
}
}
walk()
if mgrPrefs.moveCursorAfterSelectingCandidate {
var nextPosition = 0
for node in _walkedNodes {
if nextPosition >= cursorIndex { break }
nextPosition += node.spanningLength
}
if nextPosition <= getBuilderLength() {
setBuilderCursorIndex(value: nextPosition)
}
}
}
func getCandidatesArray() -> [String] {
var arrCandidates: [String] = []
var arrNodes: [Megrez.NodeAnchor] = []
arrNodes.append(contentsOf: getRawNodes())
/// nodes
///
///
if !arrNodes.isEmpty {
// sort the nodes, so that longer nodes (representing longer phrases)
// are placed at the top of the candidate list
arrNodes.sort { $0.keyLength > $1.keyLength }
// then use the Swift trick to retrieve the candidates for each node at/crossing the cursor
for currentNodeAnchor in arrNodes {
if let currentNode = currentNodeAnchor.node {
for currentCandidate in currentNode.candidates() {
arrCandidates.append(currentCandidate.value)
}
}
}
}
return arrCandidates
}
func dealWithOverrideModelSuggestions() {
let overrideValue =
mgrPrefs.useSCPCTypingMode
? ""
: _userOverrideModel.suggest(
walkedNodes: _walkedNodes, cursorIndex: getBuilderCursorIndex(),
timestamp: NSDate().timeIntervalSince1970
)
if !overrideValue.isEmpty {
_builder.grid().overrideNodeScoreForSelectedCandidate(
location: getActualCandidateCursorIndex(),
value: overrideValue,
overridingScore: findHighestScore(nodes: getRawNodes(), epsilon: kEpsilon)
)
}
}
func findHighestScore(nodes: [Megrez.NodeAnchor], epsilon: Double) -> Double {
var highestScore: Double = 0
for currentAnchor in nodes {
if let theNode = currentAnchor.node {
let score = theNode.highestUnigramScore()
if score > highestScore {
highestScore = score
}
}
}
return highestScore + epsilon
}
// MARK: - Extracted methods and functions.
func isBuilderEmpty() -> Bool { _builder.grid().width() == 0 }
func getRawNodes() -> [Megrez.NodeAnchor] {
/// 使 nodesCrossing macOS
/// nodeCrossing Megrez
/// Windows
mgrPrefs.setRearCursorMode
? _builder.grid().nodesCrossingOrEndingAt(location: getActualCandidateCursorIndex())
: _builder.grid().nodesEndingAt(location: getActualCandidateCursorIndex())
}
func setInputModesToLM(isCHS: Bool) {
_languageModel = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT
_userOverrideModel = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT
}
func syncBaseLMPrefs() {
_languageModel.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled
_languageModel.isCNSEnabled = mgrPrefs.cns11643Enabled
_languageModel.isSymbolEnabled = mgrPrefs.symbolInputEnabled
}
func createNewBuilder() {
_builder = Megrez.BlockReadingBuilder(lm: _languageModel)
// Each Mandarin syllable is separated by a hyphen.
_builder.setJoinSeparator(separator: "-")
}
func currentReadings() -> [String] { _builder.readings() }
func ifLangModelHasUnigrams(forKey reading: String) -> Bool {
_languageModel.hasUnigramsFor(key: reading)
}
func insertReadingToBuilderAtCursor(reading: String) {
_builder.insertReadingAtCursor(reading: reading)
}
func setBuilderCursorIndex(value: Int) {
_builder.setCursorIndex(newIndex: value)
}
func getBuilderCursorIndex() -> Int {
_builder.cursorIndex()
}
func getBuilderLength() -> Int {
_builder.length()
}
func deleteBuilderReadingInFrontOfCursor() {
_builder.deleteReadingBeforeCursor()
}
func deleteBuilderReadingAfterCursor() {
_builder.deleteReadingAfterCursor()
}
func getKeyLengthAtIndexZero() -> Int {
_walkedNodes[0].node?.currentKeyValue().value.count ?? 0
}
}

View File

@ -28,339 +28,339 @@ import Cocoa
// MARK: - § Handle Candidate State. // MARK: - § Handle Candidate State.
@objc extension KeyHandler { extension KeyHandler {
func handleCandidate( func handleCandidate(
state: InputState, state: InputState,
input: InputHandler, input: InputHandler,
stateCallback: @escaping (InputState) -> Void, stateCallback: @escaping (InputState) -> Void,
errorCallback: @escaping () -> Void errorCallback: @escaping () -> Void
) -> Bool { ) -> Bool {
let inputText = input.inputText let inputText = input.inputText
let charCode: UniChar = input.charCode let charCode: UniChar = input.charCode
if let ctlCandidateCurrent = delegate!.ctlCandidate(for: self) as? ctlCandidate { if let ctlCandidateCurrent = delegate!.ctlCandidate(for: self) as? ctlCandidate {
// MARK: Cancel Candidate // MARK: Cancel Candidate
let cancelCandidateKey = let cancelCandidateKey =
input.isBackSpace || input.isESC || input.isDelete input.isBackSpace || input.isESC || input.isDelete
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold) || ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
if cancelCandidateKey { if cancelCandidateKey {
if (state is InputState.AssociatedPhrases) if (state is InputState.AssociatedPhrases)
|| mgrPrefs.useSCPCTypingMode || mgrPrefs.useSCPCTypingMode
|| isBuilderEmpty() || isBuilderEmpty()
{ {
// //
// //
// 使 BackSpace // 使 BackSpace
// isBuilderEmpty() // isBuilderEmpty()
clear() clear()
stateCallback(InputState.EmptyIgnoringPreviousState()) stateCallback(InputState.EmptyIgnoringPreviousState())
} else { } else {
stateCallback(buildInputtingState()) stateCallback(buildInputtingState())
} }
return true return true
} }
// MARK: Enter // MARK: Enter
if input.isEnter { if input.isEnter {
if state is InputState.AssociatedPhrases { if state is InputState.AssociatedPhrases {
clear() clear()
stateCallback(InputState.EmptyIgnoringPreviousState()) stateCallback(InputState.EmptyIgnoringPreviousState())
return true return true
} }
delegate!.keyHandler( delegate!.keyHandler(
self, self,
didSelectCandidateAt: Int(ctlCandidateCurrent.selectedCandidateIndex), didSelectCandidateAt: Int(ctlCandidateCurrent.selectedCandidateIndex),
ctlCandidate: ctlCandidateCurrent ctlCandidate: ctlCandidateCurrent
) )
return true return true
} }
// MARK: Tab // MARK: Tab
if input.isTab { if input.isTab {
let updated: Bool = let updated: Bool =
mgrPrefs.specifyShiftTabKeyBehavior mgrPrefs.specifyShiftTabKeyBehavior
? (input.isShiftHold ? (input.isShiftHold
? ctlCandidateCurrent.showPreviousPage() ? ctlCandidateCurrent.showPreviousPage()
: ctlCandidateCurrent.showNextPage()) : ctlCandidateCurrent.showNextPage())
: (input.isShiftHold : (input.isShiftHold
? ctlCandidateCurrent.highlightPreviousCandidate() ? ctlCandidateCurrent.highlightPreviousCandidate()
: ctlCandidateCurrent.highlightNextCandidate()) : ctlCandidateCurrent.highlightNextCandidate())
if !updated { if !updated {
IME.prtDebugIntel("9B691919") IME.prtDebugIntel("9B691919")
errorCallback() errorCallback()
} }
return true return true
} }
// MARK: Space // MARK: Space
if input.isSpace { if input.isSpace {
let updated: Bool = let updated: Bool =
mgrPrefs.specifyShiftSpaceKeyBehavior mgrPrefs.specifyShiftSpaceKeyBehavior
? (input.isShiftHold ? (input.isShiftHold
? ctlCandidateCurrent.highlightNextCandidate() ? ctlCandidateCurrent.highlightNextCandidate()
: ctlCandidateCurrent.showNextPage()) : ctlCandidateCurrent.showNextPage())
: (input.isShiftHold : (input.isShiftHold
? ctlCandidateCurrent.showNextPage() ? ctlCandidateCurrent.showNextPage()
: ctlCandidateCurrent.highlightNextCandidate()) : ctlCandidateCurrent.highlightNextCandidate())
if !updated { if !updated {
IME.prtDebugIntel("A11C781F") IME.prtDebugIntel("A11C781F")
errorCallback() errorCallback()
} }
return true return true
} }
// MARK: PgDn // MARK: PgDn
if input.isPageDown || input.emacsKey == vChewingEmacsKey.nextPage { if input.isPageDown || input.emacsKey == vChewingEmacsKey.nextPage {
let updated: Bool = ctlCandidateCurrent.showNextPage() let updated: Bool = ctlCandidateCurrent.showNextPage()
if !updated { if !updated {
IME.prtDebugIntel("9B691919") IME.prtDebugIntel("9B691919")
errorCallback() errorCallback()
} }
return true return true
} }
// MARK: PgUp // MARK: PgUp
if input.isPageUp { if input.isPageUp {
let updated: Bool = ctlCandidateCurrent.showPreviousPage() let updated: Bool = ctlCandidateCurrent.showPreviousPage()
if !updated { if !updated {
IME.prtDebugIntel("9569955D") IME.prtDebugIntel("9569955D")
errorCallback() errorCallback()
} }
return true return true
} }
// MARK: Left Arrow // MARK: Left Arrow
if input.isLeft { if input.isLeft {
if ctlCandidateCurrent is ctlCandidateHorizontal { if ctlCandidateCurrent is ctlCandidateHorizontal {
let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate() let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate()
if !updated { if !updated {
IME.prtDebugIntel("1145148D") IME.prtDebugIntel("1145148D")
errorCallback() errorCallback()
} }
} else { } else {
let updated: Bool = ctlCandidateCurrent.showPreviousPage() let updated: Bool = ctlCandidateCurrent.showPreviousPage()
if !updated { if !updated {
IME.prtDebugIntel("1919810D") IME.prtDebugIntel("1919810D")
errorCallback() errorCallback()
} }
} }
return true return true
} }
// MARK: EmacsKey Backward // MARK: EmacsKey Backward
if input.emacsKey == vChewingEmacsKey.backward { if input.emacsKey == vChewingEmacsKey.backward {
let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate() let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate()
if !updated { if !updated {
IME.prtDebugIntel("9B89308D") IME.prtDebugIntel("9B89308D")
errorCallback() errorCallback()
} }
return true return true
} }
// MARK: Right Arrow // MARK: Right Arrow
if input.isRight { if input.isRight {
if ctlCandidateCurrent is ctlCandidateHorizontal { if ctlCandidateCurrent is ctlCandidateHorizontal {
let updated: Bool = ctlCandidateCurrent.highlightNextCandidate() let updated: Bool = ctlCandidateCurrent.highlightNextCandidate()
if !updated { if !updated {
IME.prtDebugIntel("9B65138D") IME.prtDebugIntel("9B65138D")
errorCallback() errorCallback()
} }
} else { } else {
let updated: Bool = ctlCandidateCurrent.showNextPage() let updated: Bool = ctlCandidateCurrent.showNextPage()
if !updated { if !updated {
IME.prtDebugIntel("9244908D") IME.prtDebugIntel("9244908D")
errorCallback() errorCallback()
} }
} }
return true return true
} }
// MARK: EmacsKey Forward // MARK: EmacsKey Forward
if input.emacsKey == vChewingEmacsKey.forward { if input.emacsKey == vChewingEmacsKey.forward {
let updated: Bool = ctlCandidateCurrent.highlightNextCandidate() let updated: Bool = ctlCandidateCurrent.highlightNextCandidate()
if !updated { if !updated {
IME.prtDebugIntel("9B2428D") IME.prtDebugIntel("9B2428D")
errorCallback() errorCallback()
} }
return true return true
} }
// MARK: Up Arrow // MARK: Up Arrow
if input.isUp { if input.isUp {
if ctlCandidateCurrent is ctlCandidateHorizontal { if ctlCandidateCurrent is ctlCandidateHorizontal {
let updated: Bool = ctlCandidateCurrent.showPreviousPage() let updated: Bool = ctlCandidateCurrent.showPreviousPage()
if !updated { if !updated {
IME.prtDebugIntel("9B614524") IME.prtDebugIntel("9B614524")
errorCallback() errorCallback()
} }
} else { } else {
let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate() let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate()
if !updated { if !updated {
IME.prtDebugIntel("ASD9908D") IME.prtDebugIntel("ASD9908D")
errorCallback() errorCallback()
} }
} }
return true return true
} }
// MARK: Down Arrow // MARK: Down Arrow
if input.isDown { if input.isDown {
if ctlCandidateCurrent is ctlCandidateHorizontal { if ctlCandidateCurrent is ctlCandidateHorizontal {
let updated: Bool = ctlCandidateCurrent.showNextPage() let updated: Bool = ctlCandidateCurrent.showNextPage()
if !updated { if !updated {
IME.prtDebugIntel("92B990DD") IME.prtDebugIntel("92B990DD")
errorCallback() errorCallback()
} }
} else { } else {
let updated: Bool = ctlCandidateCurrent.highlightNextCandidate() let updated: Bool = ctlCandidateCurrent.highlightNextCandidate()
if !updated { if !updated {
IME.prtDebugIntel("6B99908D") IME.prtDebugIntel("6B99908D")
errorCallback() errorCallback()
} }
} }
return true return true
} }
// MARK: Home Key // MARK: Home Key
if input.isHome || input.emacsKey == vChewingEmacsKey.home { if input.isHome || input.emacsKey == vChewingEmacsKey.home {
if ctlCandidateCurrent.selectedCandidateIndex == 0 { if ctlCandidateCurrent.selectedCandidateIndex == 0 {
IME.prtDebugIntel("9B6EDE8D") IME.prtDebugIntel("9B6EDE8D")
errorCallback() errorCallback()
} else { } else {
ctlCandidateCurrent.selectedCandidateIndex = 0 ctlCandidateCurrent.selectedCandidateIndex = 0
} }
return true return true
} }
// MARK: End Key // MARK: End Key
var candidates: [String]! var candidates: [String]!
if let state = state as? InputState.ChoosingCandidate { if let state = state as? InputState.ChoosingCandidate {
candidates = state.candidates candidates = state.candidates
} else if let state = state as? InputState.AssociatedPhrases { } else if let state = state as? InputState.AssociatedPhrases {
candidates = state.candidates candidates = state.candidates
} }
if candidates.isEmpty { if candidates.isEmpty {
return false return false
} else { // count > 0!isEmpty滿 } else { // count > 0!isEmpty滿
if input.isEnd || input.emacsKey == vChewingEmacsKey.end { if input.isEnd || input.emacsKey == vChewingEmacsKey.end {
if ctlCandidateCurrent.selectedCandidateIndex == UInt(candidates.count - 1) { if ctlCandidateCurrent.selectedCandidateIndex == UInt(candidates.count - 1) {
IME.prtDebugIntel("9B69AAAD") IME.prtDebugIntel("9B69AAAD")
errorCallback() errorCallback()
} else { } else {
ctlCandidateCurrent.selectedCandidateIndex = UInt(candidates.count - 1) ctlCandidateCurrent.selectedCandidateIndex = UInt(candidates.count - 1)
} }
} }
} }
// MARK: - Associated Phrases // MARK: - Associated Phrases
if state is InputState.AssociatedPhrases { if state is InputState.AssociatedPhrases {
if !input.isShiftHold { return false } if !input.isShiftHold { return false }
} }
var index: Int = NSNotFound var index: Int = NSNotFound
var match: String! var match: String!
if state is InputState.AssociatedPhrases { if state is InputState.AssociatedPhrases {
match = input.inputTextIgnoringModifiers match = input.inputTextIgnoringModifiers
} else { } else {
match = inputText match = inputText
} }
var j = 0 var j = 0
while j < ctlCandidateCurrent.keyLabels.count { while j < ctlCandidateCurrent.keyLabels.count {
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j] let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
if match.compare(label.key, options: .caseInsensitive, range: nil, locale: .current) == .orderedSame { if match.compare(label.key, options: .caseInsensitive, range: nil, locale: .current) == .orderedSame {
index = j index = j
break break
} }
j += 1 j += 1
} }
if index != NSNotFound { if index != NSNotFound {
let candidateIndex: UInt = ctlCandidateCurrent.candidateIndexAtKeyLabelIndex(UInt(index)) let candidateIndex: UInt = ctlCandidateCurrent.candidateIndexAtKeyLabelIndex(UInt(index))
if candidateIndex != UInt.max { if candidateIndex != UInt.max {
delegate!.keyHandler( delegate!.keyHandler(
self, didSelectCandidateAt: Int(candidateIndex), ctlCandidate: ctlCandidateCurrent self, didSelectCandidateAt: Int(candidateIndex), ctlCandidate: ctlCandidateCurrent
) )
return true return true
} }
} }
if state is InputState.AssociatedPhrases { return false } if state is InputState.AssociatedPhrases { return false }
// MARK: SCPC Mode Processing // MARK: SCPC Mode Processing
if mgrPrefs.useSCPCTypingMode { if mgrPrefs.useSCPCTypingMode {
var punctuationNamePrefix = "" var punctuationNamePrefix = ""
if input.isOptionHold { if input.isOptionHold {
punctuationNamePrefix = "_alt_punctuation_" punctuationNamePrefix = "_alt_punctuation_"
} else if input.isControlHold { } else if input.isControlHold {
punctuationNamePrefix = "_ctrl_punctuation_" punctuationNamePrefix = "_ctrl_punctuation_"
} else if mgrPrefs.halfWidthPunctuationEnabled { } else if mgrPrefs.halfWidthPunctuationEnabled {
punctuationNamePrefix = "_half_punctuation_" punctuationNamePrefix = "_half_punctuation_"
} else { } else {
punctuationNamePrefix = "_punctuation_" punctuationNamePrefix = "_punctuation_"
} }
let parser = getCurrentMandarinParser() let parser = getCurrentMandarinParser()
let arrCustomPunctuations: [String] = [ let arrCustomPunctuations: [String] = [
punctuationNamePrefix, parser, String(format: "%c", CChar(charCode)), punctuationNamePrefix, parser, String(format: "%c", CChar(charCode)),
] ]
let customPunctuation: String = arrCustomPunctuations.joined(separator: "") let customPunctuation: String = arrCustomPunctuations.joined(separator: "")
let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))] let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))]
let punctuation: String = arrPunctuations.joined(separator: "") let punctuation: String = arrPunctuations.joined(separator: "")
var shouldAutoSelectCandidate: Bool = var shouldAutoSelectCandidate: Bool =
chkKeyValidity(charCode) || ifLangModelHasUnigrams(forKey: customPunctuation) Composer.chkKeyValidity(charCode) || ifLangModelHasUnigrams(forKey: customPunctuation)
|| ifLangModelHasUnigrams(forKey: punctuation) || ifLangModelHasUnigrams(forKey: punctuation)
if !shouldAutoSelectCandidate, input.isUpperCaseASCIILetterKey { if !shouldAutoSelectCandidate, input.isUpperCaseASCIILetterKey {
let letter: String! = String(format: "%@%c", "_letter_", CChar(charCode)) let letter: String! = String(format: "%@%c", "_letter_", CChar(charCode))
if ifLangModelHasUnigrams(forKey: letter) { shouldAutoSelectCandidate = true } if ifLangModelHasUnigrams(forKey: letter) { shouldAutoSelectCandidate = true }
} }
if shouldAutoSelectCandidate { if shouldAutoSelectCandidate {
let candidateIndex: UInt = ctlCandidateCurrent.candidateIndexAtKeyLabelIndex(0) let candidateIndex: UInt = ctlCandidateCurrent.candidateIndexAtKeyLabelIndex(0)
if candidateIndex != UInt.max { if candidateIndex != UInt.max {
delegate!.keyHandler( delegate!.keyHandler(
self, self,
didSelectCandidateAt: Int(candidateIndex), didSelectCandidateAt: Int(candidateIndex),
ctlCandidate: ctlCandidateCurrent ctlCandidate: ctlCandidateCurrent
) )
clear() clear()
let empty = InputState.EmptyIgnoringPreviousState() let empty = InputState.EmptyIgnoringPreviousState()
stateCallback(empty) stateCallback(empty)
return handle( return handle(
input: input, state: empty, stateCallback: stateCallback, errorCallback: errorCallback input: input, state: empty, stateCallback: stateCallback, errorCallback: errorCallback
) )
} }
return true return true
} }
} }
} // END: "if let ctlCandidateCurrent" } // END: "if let ctlCandidateCurrent"
IME.prtDebugIntel("172A0F81") IME.prtDebugIntel("172A0F81")
errorCallback() errorCallback()
return true return true
} }
} }

View File

@ -28,405 +28,405 @@ import Cocoa
// MARK: - § Handle Input with States. // MARK: - § Handle Input with States.
@objc extension KeyHandler { extension KeyHandler {
func handle( func handle(
input: InputHandler, input: InputHandler,
state: InputState, state: InputState,
stateCallback: @escaping (InputState) -> Void, stateCallback: @escaping (InputState) -> Void,
errorCallback: @escaping () -> Void errorCallback: @escaping () -> Void
) -> Bool { ) -> Bool {
let charCode: UniChar = input.charCode let charCode: UniChar = input.charCode
var state = state // Turn this incoming constant into variable. var state = state // Turn this incoming constant into variable.
let inputText: String = input.inputText ?? "" let inputText: String = input.inputText ?? ""
// Ignore the input if its inputText is empty. // Ignore the input if its inputText is empty.
// Reason: such inputs may be functional key combinations. // Reason: such inputs may be functional key combinations.
if inputText.isEmpty { if inputText.isEmpty {
return false return false
} }
// Ignore the input if the composing buffer is empty with no reading // Ignore the input if the composing buffer is empty with no reading
// and there is some function key combination. // and there is some function key combination.
let isFunctionKey: Bool = let isFunctionKey: Bool =
input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNumericPad) input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNumericPad)
if !(state is InputState.NotEmpty) && !(state is InputState.AssociatedPhrases) && isFunctionKey { if !(state is InputState.NotEmpty) && !(state is InputState.AssociatedPhrases) && isFunctionKey {
return false return false
} }
// MARK: Caps Lock processing. // MARK: Caps Lock processing.
// If Caps Lock is ON, temporarily disable bopomofo. // If Caps Lock is ON, temporarily disable bopomofo.
// Note: Alphanumerical mode processing. // Note: Alphanumerical mode processing.
if input.isBackSpace || input.isEnter || input.isAbsorbedArrowKey || input.isExtraChooseCandidateKey if input.isBackSpace || input.isEnter || input.isAbsorbedArrowKey || input.isExtraChooseCandidateKey
|| input.isExtraChooseCandidateKeyReverse || input.isCursorForward || input.isCursorBackward || input.isExtraChooseCandidateKeyReverse || input.isCursorForward || input.isCursorBackward
{ {
// Do nothing if backspace is pressed -- we ignore the key // Do nothing if backspace is pressed -- we ignore the key
} else if input.isCapsLockOn { } else if input.isCapsLockOn {
// Process all possible combination, we hope. // Process all possible combination, we hope.
clear() clear()
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
// When shift is pressed, don't do further processing... // When shift is pressed, don't do further processing...
// ...since it outputs capital letter anyway. // ...since it outputs capital letter anyway.
if input.isShiftHold { if input.isShiftHold {
return false return false
} }
// If ASCII but not printable, don't use insertText:replacementRange: // If ASCII but not printable, don't use insertText:replacementRange:
// Certain apps don't handle non-ASCII char insertions. // Certain apps don't handle non-ASCII char insertions.
if charCode < 0x80, !isPrintable(charCode) { if charCode < 0x80, !CTools.isPrintable(charCode) {
return false return false
} }
// Commit the entire input buffer. // Commit the entire input buffer.
stateCallback(InputState.Committing(poppedText: inputText.lowercased())) stateCallback(InputState.Committing(poppedText: inputText.lowercased()))
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
return true return true
} }
// MARK: Numeric Pad Processing. // MARK: Numeric Pad Processing.
if input.isNumericPad { if input.isNumericPad {
if !input.isLeft, !input.isRight, !input.isDown, if !input.isLeft, !input.isRight, !input.isDown,
!input.isUp, !input.isSpace, isPrintable(charCode) !input.isUp, !input.isSpace, CTools.isPrintable(charCode)
{ {
clear() clear()
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
stateCallback(InputState.Committing(poppedText: inputText.lowercased())) stateCallback(InputState.Committing(poppedText: inputText.lowercased()))
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
return true return true
} }
} }
// MARK: Handle Candidates. // MARK: Handle Candidates.
if state is InputState.ChoosingCandidate { if state is InputState.ChoosingCandidate {
return handleCandidate( return handleCandidate(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
) )
} }
// MARK: Handle Associated Phrases. // MARK: Handle Associated Phrases.
if state is InputState.AssociatedPhrases { if state is InputState.AssociatedPhrases {
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(InputState.Empty())
} }
} }
// MARK: Handle Marking. // MARK: Handle Marking.
if let marking = state as? InputState.Marking { if let marking = state as? InputState.Marking {
if handleMarkingState( if handleMarkingState(
marking, input: input, stateCallback: stateCallback, marking, input: input, stateCallback: stateCallback,
errorCallback: errorCallback errorCallback: errorCallback
) { ) {
return true return true
} }
state = marking.convertToInputting() state = marking.convertToInputting()
stateCallback(state) stateCallback(state)
} }
// MARK: Handle BPMF Keys. // MARK: Handle BPMF Keys.
var composeReading = false var composeReading = false
let skipPhoneticHandling = input.isReservedKey || input.isControlHold || input.isOptionHold let skipPhoneticHandling = input.isReservedKey || input.isControlHold || input.isOptionHold
// See if Phonetic reading is valid. // See if Phonetic reading is valid.
if !skipPhoneticHandling && chkKeyValidity(charCode) { if !skipPhoneticHandling && Composer.chkKeyValidity(charCode) {
combinePhoneticReadingBufferKey(charCode) Composer.combineReadingKey(charCode)
// If we have a tone marker, we have to insert the reading to the // If we have a tone marker, we have to insert the reading to the
// builder in other words, if we don't have a tone marker, we just // builder in other words, if we don't have a tone marker, we just
// update the composing buffer. // update the composing buffer.
composeReading = checkWhetherToneMarkerConfirmsPhoneticReadingBuffer() composeReading = Composer.checkWhetherToneMarkerConfirms()
if !composeReading { if !composeReading {
stateCallback(buildInputtingState()) stateCallback(buildInputtingState())
return true return true
} }
} }
// See if we have composition if Enter/Space is hit and buffer is not empty. // See if we have composition if Enter/Space is hit and buffer is not empty.
// We use "|=" conditioning so that the tone marker key is also taken into account. // We use "|=" conditioning so that the tone marker key is also taken into account.
// However, Swift does not support "|=". // However, Swift does not support "|=".
composeReading = composeReading || (!isPhoneticReadingBufferEmpty() && (input.isSpace || input.isEnter)) composeReading = composeReading || (!Composer.isBufferEmpty() && (input.isSpace || input.isEnter))
if composeReading { if composeReading {
let reading = getSyllableCompositionFromPhoneticReadingBuffer() let reading = Composer.getSyllableComposition()
if !ifLangModelHasUnigrams(forKey: reading) { if !ifLangModelHasUnigrams(forKey: reading) {
IME.prtDebugIntel("B49C0979") IME.prtDebugIntel("B49C0979:語彙庫內無「\(reading)」的匹配記錄。")
errorCallback() errorCallback()
stateCallback(buildInputtingState()) stateCallback(buildInputtingState())
return true return true
} }
// ... and insert it into the lattice grid... // ... and insert it into the lattice grid...
insertReadingToBuilder(atCursor: reading) insertReadingToBuilderAtCursor(reading: reading)
// ... then walk the lattice grid... // ... then walk the lattice grid...
let poppedText = _popOverflowComposingTextAndWalk() let poppedText = popOverflowComposingTextAndWalk()
// ... get and tweak override model suggestion if possible... // ... get and tweak override model suggestion if possible...
dealWithOverrideModelSuggestions() dealWithOverrideModelSuggestions()
// ... then update the text. // ... then update the text.
clearPhoneticReadingBuffer() Composer.clearBuffer()
let inputting = buildInputtingState() let inputting = buildInputtingState()
inputting.poppedText = poppedText inputting.poppedText = poppedText
stateCallback(inputting) stateCallback(inputting)
if mgrPrefs.useSCPCTypingMode { if mgrPrefs.useSCPCTypingMode {
let choosingCandidates: InputState.ChoosingCandidate = buildCandidate( let choosingCandidates: InputState.ChoosingCandidate = buildCandidate(
state: inputting, state: inputting,
useVerticalMode: input.useVerticalMode useVerticalMode: input.useVerticalMode
) )
if choosingCandidates.candidates.count == 1 { if choosingCandidates.candidates.count == 1 {
clear() clear()
let text: String = choosingCandidates.candidates.first ?? "" let text: String = choosingCandidates.candidates.first ?? ""
stateCallback(InputState.Committing(poppedText: text)) stateCallback(InputState.Committing(poppedText: text))
if !mgrPrefs.associatedPhrasesEnabled { if !mgrPrefs.associatedPhrasesEnabled {
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
} else { } else {
if let associatedPhrases = if let associatedPhrases =
buildAssociatePhraseState( buildAssociatePhraseState(
withKey: text, withKey: text,
useVerticalMode: input.useVerticalMode useVerticalMode: input.useVerticalMode
), !associatedPhrases.candidates.isEmpty ), !associatedPhrases.candidates.isEmpty
{ {
stateCallback(associatedPhrases) stateCallback(associatedPhrases)
} else { } else {
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
} }
} }
} else { } else {
stateCallback(choosingCandidates) stateCallback(choosingCandidates)
} }
} }
return true return true
} }
// MARK: Calling candidate window using Space or Down or PageUp / PageDn. // MARK: Calling candidate window using Space or Down or PageUp / PageDn.
if let currentState = state as? InputState.NotEmpty { if let currentState = state as? InputState.NotEmpty {
if isPhoneticReadingBufferEmpty(), if Composer.isBufferEmpty(),
input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse || input.isSpace input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse || input.isSpace
|| input.isPageDown || input.isPageUp || input.isTab || input.isPageDown || input.isPageUp || input.isTab
|| (input.useVerticalMode && (input.isVerticalModeOnlyChooseCandidateKey)) || (input.useVerticalMode && (input.isVerticalModeOnlyChooseCandidateKey))
{ {
if input.isSpace { if input.isSpace {
// If the Space key is NOT set to be a selection key // If the Space key is NOT set to be a selection key
if input.isShiftHold || !mgrPrefs.chooseCandidateUsingSpace { if input.isShiftHold || !mgrPrefs.chooseCandidateUsingSpace {
if getBuilderCursorIndex() >= getBuilderLength() { if getBuilderCursorIndex() >= getBuilderLength() {
let composingBuffer = currentState.composingBuffer let composingBuffer = currentState.composingBuffer
if (composingBuffer.count) != 0 { if (composingBuffer.count) != 0 {
stateCallback(InputState.Committing(poppedText: composingBuffer)) stateCallback(InputState.Committing(poppedText: composingBuffer))
} }
clear() clear()
stateCallback(InputState.Committing(poppedText: " ")) stateCallback(InputState.Committing(poppedText: " "))
stateCallback(InputState.Empty()) stateCallback(InputState.Empty())
} else if ifLangModelHasUnigrams(forKey: " ") { } else if ifLangModelHasUnigrams(forKey: " ") {
insertReadingToBuilder(atCursor: " ") insertReadingToBuilderAtCursor(reading: " ")
let poppedText = _popOverflowComposingTextAndWalk() let poppedText = popOverflowComposingTextAndWalk()
let inputting = buildInputtingState() let inputting = buildInputtingState()
inputting.poppedText = poppedText inputting.poppedText = poppedText
stateCallback(inputting) stateCallback(inputting)
} }
return true return true
} }
} }
stateCallback(buildCandidate(state: currentState, useVerticalMode: input.useVerticalMode)) stateCallback(buildCandidate(state: currentState, useVerticalMode: input.useVerticalMode))
return true return true
} }
} }
// MARK: - // MARK: -
// MARK: Esc // MARK: Esc
if input.isESC { return handleEsc(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } if input.isESC { return handleEsc(state: state, stateCallback: stateCallback, errorCallback: errorCallback) }
// MARK: Cursor backward // MARK: Cursor backward
if input.isCursorBackward || input.emacsKey == vChewingEmacsKey.backward { if input.isCursorBackward || input.emacsKey == vChewingEmacsKey.backward {
return handleBackward( return handleBackward(
state: state, state: state,
input: input, input: input,
stateCallback: stateCallback, stateCallback: stateCallback,
errorCallback: errorCallback errorCallback: errorCallback
) )
} }
// MARK: Cursor forward // MARK: Cursor forward
if input.isCursorForward || input.emacsKey == vChewingEmacsKey.forward { if input.isCursorForward || input.emacsKey == vChewingEmacsKey.forward {
return handleForward( return handleForward(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
) )
} }
// MARK: Home // MARK: Home
if input.isHome || input.emacsKey == vChewingEmacsKey.home { if input.isHome || input.emacsKey == vChewingEmacsKey.home {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: End // MARK: End
if input.isEnd || input.emacsKey == vChewingEmacsKey.end { if input.isEnd || input.emacsKey == vChewingEmacsKey.end {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: Ctrl+PgLf or Shift+PgLf // MARK: Ctrl+PgLf or Shift+PgLf
if (input.isControlHold || input.isShiftHold) && (input.isOptionHold && input.isLeft) { if (input.isControlHold || input.isShiftHold) && (input.isOptionHold && input.isLeft) {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: Ctrl+PgRt or Shift+PgRt // MARK: Ctrl+PgRt or Shift+PgRt
if (input.isControlHold || input.isShiftHold) && (input.isOptionHold && input.isRight) { if (input.isControlHold || input.isShiftHold) && (input.isOptionHold && input.isRight) {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: AbsorbedArrowKey // MARK: AbsorbedArrowKey
if input.isAbsorbedArrowKey || input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse { if input.isAbsorbedArrowKey || input.isExtraChooseCandidateKey || input.isExtraChooseCandidateKeyReverse {
return handleAbsorbedArrowKey(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleAbsorbedArrowKey(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: Backspace // MARK: Backspace
if input.isBackSpace { if input.isBackSpace {
return handleBackspace(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleBackspace(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: Delete // MARK: Delete
if input.isDelete || input.emacsKey == vChewingEmacsKey.delete { if input.isDelete || input.emacsKey == vChewingEmacsKey.delete {
return handleDelete(state: state, stateCallback: stateCallback, errorCallback: errorCallback) return handleDelete(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: Enter // MARK: Enter
if input.isEnter { if input.isEnter {
return (input.isCommandHold && input.isControlHold) return (input.isCommandHold && input.isControlHold)
? handleCtrlCommandEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback) ? handleCtrlCommandEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
: handleEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback) : handleEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
} }
// MARK: - // MARK: -
// MARK: Punctuation list // MARK: Punctuation list
if input.isSymbolMenuPhysicalKey && !input.isShiftHold { if input.isSymbolMenuPhysicalKey && !input.isShiftHold {
if !input.isOptionHold { if !input.isOptionHold {
if ifLangModelHasUnigrams(forKey: "_punctuation_list") { if ifLangModelHasUnigrams(forKey: "_punctuation_list") {
if isPhoneticReadingBufferEmpty() { if Composer.isBufferEmpty() {
insertReadingToBuilder(atCursor: "_punctuation_list") insertReadingToBuilderAtCursor(reading: "_punctuation_list")
let poppedText: String! = _popOverflowComposingTextAndWalk() let poppedText: String! = popOverflowComposingTextAndWalk()
let inputting = buildInputtingState() let inputting = buildInputtingState()
inputting.poppedText = poppedText inputting.poppedText = poppedText
stateCallback(inputting) stateCallback(inputting)
stateCallback(buildCandidate(state: inputting, useVerticalMode: input.useVerticalMode)) stateCallback(buildCandidate(state: inputting, useVerticalMode: input.useVerticalMode))
} else { // If there is still unfinished bpmf reading, ignore the punctuation } else { // If there is still unfinished bpmf reading, ignore the punctuation
IME.prtDebugIntel("17446655") IME.prtDebugIntel("17446655")
errorCallback() errorCallback()
} }
return true return true
} }
} else { } else {
// commit buffer ESC // commit buffer ESC
// Enter 使 commit buffer // Enter 使 commit buffer
// bool _ = // bool _ =
_ = handleEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback) _ = handleEnter(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
stateCallback(InputState.SymbolTable(node: SymbolNode.root, useVerticalMode: input.useVerticalMode)) stateCallback(InputState.SymbolTable(node: SymbolNode.root, useVerticalMode: input.useVerticalMode))
return true return true
} }
} }
// MARK: Punctuation // MARK: Punctuation
// if nothing is matched, see if it's a punctuation key for current layout. // If nothing is matched, see if it's a punctuation key for current layout.
var punctuationNamePrefix = "" var punctuationNamePrefix = ""
if input.isOptionHold { if input.isOptionHold {
punctuationNamePrefix = "_alt_punctuation_" punctuationNamePrefix = "_alt_punctuation_"
} else if input.isControlHold { } else if input.isControlHold {
punctuationNamePrefix = "_ctrl_punctuation_" punctuationNamePrefix = "_ctrl_punctuation_"
} else if mgrPrefs.halfWidthPunctuationEnabled { } else if mgrPrefs.halfWidthPunctuationEnabled {
punctuationNamePrefix = "_half_punctuation_" punctuationNamePrefix = "_half_punctuation_"
} else { } else {
punctuationNamePrefix = "_punctuation_" punctuationNamePrefix = "_punctuation_"
} }
let parser = getCurrentMandarinParser() let parser = getCurrentMandarinParser()
let arrCustomPunctuations: [String] = [ let arrCustomPunctuations: [String] = [
punctuationNamePrefix, parser, String(format: "%c", CChar(charCode)), punctuationNamePrefix, parser, String(format: "%c", CChar(charCode)),
] ]
let customPunctuation: String = arrCustomPunctuations.joined(separator: "") let customPunctuation: String = arrCustomPunctuations.joined(separator: "")
if handlePunctuation( if handlePunctuation(
customPunctuation, customPunctuation,
state: state, state: state,
usingVerticalMode: input.useVerticalMode, usingVerticalMode: input.useVerticalMode,
stateCallback: stateCallback, stateCallback: stateCallback,
errorCallback: errorCallback errorCallback: errorCallback
) { ) {
return true return true
} }
// if nothing is matched, see if it's a punctuation key. // if nothing is matched, see if it's a punctuation key.
let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))] let arrPunctuations: [String] = [punctuationNamePrefix, String(format: "%c", CChar(charCode))]
let punctuation: String = arrPunctuations.joined(separator: "") let punctuation: String = arrPunctuations.joined(separator: "")
if handlePunctuation( if handlePunctuation(
punctuation, punctuation,
state: state, state: state,
usingVerticalMode: input.useVerticalMode, usingVerticalMode: input.useVerticalMode,
stateCallback: stateCallback, stateCallback: stateCallback,
errorCallback: errorCallback errorCallback: errorCallback
) { ) {
return true return true
} }
// 使 2.2 // 使 2.2
if input.isUpperCaseASCIILetterKey { if input.isUpperCaseASCIILetterKey {
let letter: String! = String(format: "%@%c", "_letter_", CChar(charCode)) let letter: String! = String(format: "%@%c", "_letter_", CChar(charCode))
if handlePunctuation( if handlePunctuation(
letter, letter,
state: state, state: state,
usingVerticalMode: input.useVerticalMode, usingVerticalMode: input.useVerticalMode,
stateCallback: stateCallback, stateCallback: stateCallback,
errorCallback: errorCallback errorCallback: errorCallback
) { ) {
return true return true
} }
} }
// MARK: - Still Nothing. // MARK: - Still Nothing.
// Still nothing? Then we update the composing buffer. // Still nothing? Then we update the composing buffer.
// Note that some app has strange behavior if we don't do this, // Note that some app has strange behavior if we don't do this,
// "thinking" that the key is not actually consumed. // "thinking" that the key is not actually consumed.
// F1-F12 // F1-F12
// 便 // 便
if (state is InputState.NotEmpty) || !isPhoneticReadingBufferEmpty() { if (state is InputState.NotEmpty) || !Composer.isBufferEmpty() {
IME.prtDebugIntel( IME.prtDebugIntel(
"Blocked data: charCode: \(charCode), keyCode: \(input.keyCode)") "Blocked data: charCode: \(charCode), keyCode: \(input.keyCode)")
IME.prtDebugIntel("A9BFF20E") IME.prtDebugIntel("A9BFF20E")
errorCallback() errorCallback()
stateCallback(state) stateCallback(state)
return true return true
} }
return false return false
} }
} }

View File

@ -28,27 +28,27 @@ import Cocoa
// MARK: - § Misc functions. // MARK: - § Misc functions.
@objc extension KeyHandler { extension KeyHandler {
func getCurrentMandarinParser() -> String { func getCurrentMandarinParser() -> String {
mgrPrefs.mandarinParserName + "_" mgrPrefs.mandarinParserName + "_"
} }
func getActualCandidateCursorIndex() -> Int { func getActualCandidateCursorIndex() -> Int {
var cursorIndex = getBuilderCursorIndex() var cursorIndex = getBuilderCursorIndex()
// Windows Yahoo Kimo IME style, phrase is *at the rear of* the cursor. // Windows Yahoo Kimo IME style, phrase is *at the rear of* the cursor.
// (i.e. the cursor is always *before* the phrase.) // (i.e. the cursor is always *before* the phrase.)
// This is different from MS Phonetics IME style ... // This is different from MS Phonetics IME style ...
// ... since Windows Yahoo Kimo allows "node crossing". // ... since Windows Yahoo Kimo allows "node crossing".
if (mgrPrefs.setRearCursorMode if (mgrPrefs.setRearCursorMode
&& (cursorIndex < getBuilderLength())) && (cursorIndex < getBuilderLength()))
|| cursorIndex == 0 || cursorIndex == 0
{ {
if cursorIndex == 0 && !mgrPrefs.setRearCursorMode { if cursorIndex == 0, !mgrPrefs.setRearCursorMode {
cursorIndex += getKeyLengthAtIndexZero() cursorIndex += getKeyLengthAtIndexZero()
} else { } else {
cursorIndex += 1 cursorIndex += 1
} }
} }
return cursorIndex return cursorIndex
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,155 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "KeyValueBlobReader.h"
namespace vChewing
{
KeyValueBlobReader::State KeyValueBlobReader::Next(KeyValue *out)
{
static auto new_line = [](char c) { return c == '\n' || c == '\r'; };
static auto blank = [](char c) { return c == ' ' || c == '\t'; };
static auto blank_or_newline = [](char c) { return blank(c) || new_line(c); };
static auto content_char = [](char c) { return !blank(c) && !new_line(c); };
if (state_ == State::ERROR)
{
return state_;
}
const char *key_begin = nullptr;
size_t key_length = 0;
const char *value_begin = nullptr;
size_t value_length = 0;
while (true)
{
state_ = SkipUntilNot(blank_or_newline);
if (state_ != State::CAN_CONTINUE)
{
return state_;
}
// Check if it's a comment line; if so, read until end of line.
if (*current_ != '#')
{
break;
}
state_ = SkipUntil(new_line);
if (state_ != State::CAN_CONTINUE)
{
return state_;
}
}
// No need to check whether* current_ is a content_char, since content_char
// is defined as not blank and not new_line.
key_begin = current_;
state_ = SkipUntilNot(content_char);
if (state_ != State::CAN_CONTINUE)
{
goto error;
}
key_length = current_ - key_begin;
// There should be at least one blank character after the key string.
if (!blank(*current_))
{
goto error;
}
state_ = SkipUntilNot(blank);
if (state_ != State::CAN_CONTINUE)
{
goto error;
}
if (!content_char(*current_))
{
goto error;
}
value_begin = current_;
// value must only contain content characters, blanks not are allowed.
// also, there's no need to check the state after this, since we will always
// emit the value. This also avoids the situation where trailing spaces in a
// line would become part of the value.
SkipUntilNot(content_char);
value_length = current_ - value_begin;
// Unconditionally skip until the end of the line. This prevents the case
// like "foo bar baz\n" where baz should not be treated as the Next key.
SkipUntil(new_line);
if (out != nullptr)
{
*out = KeyValue{std::string_view{key_begin, key_length}, std::string_view{value_begin, value_length}};
}
state_ = State::HAS_PAIR;
return state_;
error:
state_ = State::ERROR;
return state_;
}
KeyValueBlobReader::State KeyValueBlobReader::SkipUntilNot(const std::function<bool(char)> &f)
{
while (current_ != end_ && *current_)
{
if (!f(*current_))
{
return State::CAN_CONTINUE;
}
++current_;
}
return State::END;
}
KeyValueBlobReader::State KeyValueBlobReader::SkipUntil(const std::function<bool(char)> &f)
{
while (current_ != end_ && *current_)
{
if (f(*current_))
{
return State::CAN_CONTINUE;
}
++current_;
}
return State::END;
}
std::ostream &operator<<(std::ostream &os, const KeyValueBlobReader::KeyValue &kv)
{
os << "(key: " << kv.key << ", value: " << kv.value << ")";
return os;
}
} // namespace vChewing

View File

@ -1,107 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef SOURCE_ENGINE_KEYVALUEBLOBREADER_H_
#define SOURCE_ENGINE_KEYVALUEBLOBREADER_H_
#include <cstddef>
#include <functional>
#include <iostream>
#include <string_view>
// A reader for text-based, blank-separated key-value pairs in a binary blob.
//
// This reader is suitable for reading language model files that entirely
// consist of key-value pairs. Leading or trailing spaces are ignored.
// Lines that start with "#" are treated as comments. Values cannot contain
// spaces. Any space after the value string is parsed is ignored. This implies
// that after a blank, anything that comes after the value can be used as
// comment. Both ' ' and '\t' are treated as blank characters, and the parser
// is agnostic to how lines are ended, and so LF, CR LF, and CR are all valid
// line endings.
//
// std::string_view is used to allow returning results efficiently. As a result,
// the blob is a const char* and will never be mutated. This implies, for
// example, read-only mmap can be used to parse large files.
namespace vChewing
{
class KeyValueBlobReader
{
public:
enum class State : int
{
// There are no more key-value pairs in this blob.
END = 0,
// The reader has produced a new key-value pair.
HAS_PAIR = 1,
// An error is encountered and the parsing stopped.
ERROR = -1,
// Internal-only state: the parser can continue parsing.
CAN_CONTINUE = 2
};
struct KeyValue
{
constexpr KeyValue() : key(""), value("")
{
}
constexpr KeyValue(std::string_view k, std::string_view v) : key(k), value(v)
{
}
bool operator==(const KeyValue &another) const
{
return key == another.key && value == another.value;
}
std::string_view key;
std::string_view value;
};
KeyValueBlobReader(const char *blob, size_t size) : current_(blob), end_(blob + size)
{
}
// Parse the next key-value pair and return the state of the reader. If
// `out` is passed, out will be set to the produced key-value pair if there
// is one.
State Next(KeyValue *out = nullptr);
private:
State SkipUntil(const std::function<bool(char)> &f);
State SkipUntilNot(const std::function<bool(char)> &f);
const char *current_;
const char *end_;
State state_ = State::CAN_CONTINUE;
};
std::ostream &operator<<(std::ostream &, const KeyValueBlobReader::KeyValue &);
} // namespace vChewing
#endif // SOURCE_ENGINE_KEYVALUEBLOBREADER_H_

View File

@ -27,49 +27,49 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension NSString { extension NSString {
/// Converts the index in an NSString to the index in a Swift string. /// Converts the index in an NSString to the index in a Swift string.
/// ///
/// An Emoji might be compose by more than one UTF-16 code points, however /// 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 /// 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 /// causes that the NSString and Swift string representation of the same
/// string have different lengths once the string contains such Emoji. The /// 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 /// method helps to find the index in a Swift string by passing the index
/// in an NSString. /// in an NSString.
public func characterIndex(from utf16Index: Int) -> (Int, String) { public func characterIndex(from utf16Index: Int) -> (Int, String) {
let string = (self as String) let string = (self as String)
var length = 0 var length = 0
for (i, character) in string.enumerated() { for (i, character) in string.enumerated() {
length += character.utf16.count length += character.utf16.count
if length > utf16Index { if length > utf16Index {
return (i, string) return (i, string)
} }
} }
return (string.count, string) return (string.count, string)
} }
@objc public func nextUtf16Position(for index: Int) -> Int { public func nextUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index) var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex < string.count { if fixedIndex < string.count {
fixedIndex += 1 fixedIndex += 1
} }
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
} }
@objc public func previousUtf16Position(for index: Int) -> Int { public func previousUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index) var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex > 0 { if fixedIndex > 0 {
fixedIndex -= 1 fixedIndex -= 1
} }
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
} }
@objc public var count: Int { public var count: Int {
(self as String).count (self as String).count
} }
@objc public func split() -> [NSString] { public func split() -> [NSString] {
Array(self as String).map { Array(self as String).map {
NSString(string: String($0)) NSString(string: String($0))
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -27,78 +27,78 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
public protocol FSEventStreamHelperDelegate: AnyObject { public protocol FSEventStreamHelperDelegate: AnyObject {
func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event])
} }
public class FSEventStreamHelper: NSObject { public class FSEventStreamHelper: NSObject {
public struct Event { public struct Event {
var path: String var path: String
var flags: FSEventStreamEventFlags var flags: FSEventStreamEventFlags
var id: FSEventStreamEventId var id: FSEventStreamEventId
} }
public let path: String public var path: String
public let dispatchQueue: DispatchQueue public let dispatchQueue: DispatchQueue
public weak var delegate: FSEventStreamHelperDelegate? public weak var delegate: FSEventStreamHelperDelegate?
@objc public init(path: String, queue: DispatchQueue) { @objc public init(path: String, queue: DispatchQueue) {
self.path = path self.path = path
dispatchQueue = queue dispatchQueue = queue
} }
private var stream: FSEventStreamRef? private var stream: FSEventStreamRef?
public func start() -> Bool { public func start() -> Bool {
if stream != nil { if stream != nil {
return false return false
} }
var context = FSEventStreamContext() var context = FSEventStreamContext()
context.info = Unmanaged.passUnretained(self).toOpaque() context.info = Unmanaged.passUnretained(self).toOpaque()
guard guard
let stream = FSEventStreamCreate( let stream = FSEventStreamCreate(
nil, nil,
{ {
_, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds in _, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds in
let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!) let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!)
.takeUnretainedValue() .takeUnretainedValue()
let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self) let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount) let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount)
let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount) let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount)
let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount) let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount)
let events = (0..<eventCount).map { let events = (0..<eventCount).map {
FSEventStreamHelper.Event( FSEventStreamHelper.Event(
path: String(cString: pathsPtr[$0]), path: String(cString: pathsPtr[$0]),
flags: flagsPtr[$0], flags: flagsPtr[$0],
id: eventIDsPtr[$0] id: eventIDsPtr[$0]
) )
} }
helper.delegate?.helper(helper, didReceive: events) helper.delegate?.helper(helper, didReceive: events)
}, },
&context, &context,
[path] as CFArray, [path] as CFArray,
UInt64(kFSEventStreamEventIdSinceNow), UInt64(kFSEventStreamEventIdSinceNow),
1.0, 1.0,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone) FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
) )
else { else {
return false return false
} }
FSEventStreamSetDispatchQueue(stream, dispatchQueue) FSEventStreamSetDispatchQueue(stream, dispatchQueue)
if !FSEventStreamStart(stream) { if !FSEventStreamStart(stream) {
FSEventStreamInvalidate(stream) FSEventStreamInvalidate(stream)
return false return false
} }
self.stream = stream self.stream = stream
return true return true
} }
func stop() { func stop() {
guard let stream = stream else { guard let stream = stream else {
return return
} }
FSEventStreamStop(stream) FSEventStreamStop(stream)
FSEventStreamInvalidate(stream) FSEventStreamInvalidate(stream)
self.stream = nil self.stream = nil
} }
} }

View File

@ -1,176 +0,0 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "LMConsolidator.h"
#include "vChewing-Swift.h"
namespace vChewing
{
constexpr std::string_view FORMATTED_PRAGMA_HEADER =
"# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍";
// HEADER VERIFIER. CREDIT: Shiki Suen
bool LMConsolidator::CheckPragma(const char *path)
{
ifstream zfdCheckPragma(path);
if (zfdCheckPragma.good())
{
string firstLine;
getline(zfdCheckPragma, firstLine);
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "HEADER SEEN ||%s", firstLine.c_str());
if (firstLine != FORMATTED_PRAGMA_HEADER)
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "HEADER VERIFICATION FAILED. START IN-PLACE CONSOLIDATING PROCESS.");
return false;
}
}
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "HEADER VERIFICATION SUCCESSFUL.");
return true;
}
// EOF FIXER. CREDIT: Shiki Suen.
bool LMConsolidator::FixEOF(const char *path)
{
std::fstream zfdEOFFixerIncomingStream(path);
zfdEOFFixerIncomingStream.seekg(-1, std::ios_base::end);
char z;
zfdEOFFixerIncomingStream.get(z);
if (z != '\n')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// REPORT: Data File not ended with a new line.\n");
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// DATA FILE: %s", path);
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// PROCEDURE: Trying to insert a new line as EOF before per-line check process.\n");
std::ofstream zfdEOFFixerOutput(path, std::ios_base::app);
zfdEOFFixerOutput << std::endl;
zfdEOFFixerOutput.close();
if (zfdEOFFixerOutput.fail())
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// REPORT: Failed to append a newline to the data file. Insufficient Privileges?\n");
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// DATA FILE: %s", path);
return false;
}
}
zfdEOFFixerIncomingStream.close();
if (zfdEOFFixerIncomingStream.fail())
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS,
"// REPORT: Failed to read lines through the data file for EOF check. Insufficient Privileges?\n");
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// DATA FILE: %s", path);
return false;
}
return true;
} // END: EOF FIXER.
// CONTENT CONSOLIDATOR. CREDIT: Shiki Suen.
bool LMConsolidator::ConsolidateContent(const char *path, bool shouldCheckPragma)
{
bool pragmaCheckResult = LMConsolidator::CheckPragma(path);
if (pragmaCheckResult && shouldCheckPragma)
{
return true;
}
ifstream zfdContentConsolidatorIncomingStream(path);
vector<string> vecEntry;
while (!zfdContentConsolidatorIncomingStream.eof())
{ // Xcode 13 能用的 ObjCpp 與 Cpp 並無原生支援「\h」這個 Regex 參數的能力,只能逐行處理。
string zfdBuffer;
getline(zfdContentConsolidatorIncomingStream, zfdBuffer);
vecEntry.push_back(zfdBuffer);
}
// 第一遍 for 用來統整每行內的內容。
// regex sedCJKWhiteSpace("\\x{3000}"), sedNonBreakWhiteSpace("\\x{A0}"), sedWhiteSpace("\\s+"),
// sedLeadingSpace("^\\s"), sedTrailingSpace("\\s$"); // 這樣寫會導致輸入法敲不了任何字,推測 Xcode 13 支援的 cpp /
// objCpp 可能對某些 Regex 寫法有相容性問題。 regex sedCJKWhiteSpace(" "), sedNonBreakWhiteSpace(" "),
// sedWhiteSpace("\\s+"), sedLeadingSpace("^\\s"), sedTrailingSpace("\\s$"); // RegEx 先定義好。
regex sedToConsolidate("( +| +| +|\t+)+"), sedToTrim("(^\\s|\\s$)");
for (int i = 0; i < vecEntry.size(); i++)
{ // 第一遍 for 用來統整每行內的內容。
if (vecEntry[i].size() != 0)
{ // 不要理會空行,否則給空行加上 endl 等於再加空行。
// RegEx 處理順序:先將全形空格換成西文空格,然後合併任何意義上的連續空格(包括 tab
// 等),最後去除每行首尾空格。 vecEntry[i] = regex_replace(vecEntry[i], sedCJKWhiteSpace, " ").c_str(); //
// 中日韓全形空格轉為 ASCII 空格。 vecEntry[i] = regex_replace(vecEntry[i], sedNonBreakWhiteSpace, "
// ").c_str(); // Non-Break 型空格轉為 ASCII 空格。 vecEntry[i] = regex_replace(vecEntry[i], sedWhiteSpace,
// " ").c_str(); // 所有意義上的連續的 \s 型空格都轉為單個 ASCII 空格。 vecEntry[i] =
// regex_replace(vecEntry[i], sedLeadingSpace, "").c_str(); // 去掉行首空格。 vecEntry[i] =
// regex_replace(vecEntry[i], sedTrailingSpace, "").c_str(); // 去掉行尾空格。
// 上述命令分步驟執行容易產生效能問題,故濃縮為下述兩句。
vecEntry[i] = regex_replace(vecEntry[i], sedToConsolidate, " ").c_str();
vecEntry[i] = regex_replace(vecEntry[i], sedToTrim, "").c_str();
}
}
// 在第二遍 for 運算之前,針對 vecEntry 去除重複條目。
std::reverse(vecEntry.begin(), vecEntry.end()); // 先首尾顛倒,免得破壞最新的 override 資訊。
vecEntry.erase(unique(vecEntry.begin(), vecEntry.end()), vecEntry.end()); // 去重複。
std::reverse(vecEntry.begin(), vecEntry.end()); // 再顛倒回來。
// 統整完畢。開始將統整過的內容寫入檔案。
ofstream zfdContentConsolidatorOutput(path); // 這裡是要從頭開始重寫檔案內容,所以不需要「 ios_base::app 」。
if (!pragmaCheckResult)
{
zfdContentConsolidatorOutput << FORMATTED_PRAGMA_HEADER << endl; // 寫入經過整理處理的 HEADER。
}
for (int i = 0; i < vecEntry.size(); i++)
{ // 第二遍 for 用來寫入統整過的內容。
if (vecEntry[i].size() != 0)
{ // 這句很重要,不然還是會把經過 RegEx 處理後出現的空行搞到檔案裡。
zfdContentConsolidatorOutput << vecEntry[i]
<< endl; // 這裡是必須得加上 endl 的,不然所有行都變成一個整合行。
}
}
zfdContentConsolidatorOutput.close();
if (zfdContentConsolidatorOutput.fail())
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS,
"// REPORT: Failed to write content-consolidated data to the file. Insufficient Privileges?\n");
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// DATA FILE: %s", path);
return false;
}
zfdContentConsolidatorIncomingStream.close();
if (zfdContentConsolidatorIncomingStream.fail())
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// REPORT: Failed to read lines through the data file for content-consolidation. "
"Insufficient Privileges?\n");
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "// DATA FILE: %s", path);
return false;
}
return true;
} // END: CONTENT CONSOLIDATOR.
} // namespace vChewing

View File

@ -25,320 +25,325 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Carbon import Carbon
import Cocoa import Cocoa
// The namespace of this input method.
public enum vChewing {}
public class IME: NSObject { public class IME: NSObject {
static let arrSupportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"] static let arrSupportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"]
static let dlgOpenPath = NSOpenPanel() static let dlgOpenPath = NSOpenPanel()
// MARK: - // MARK: -
@objc static var areWeUsingOurOwnPhraseEditor: Bool = false static var areWeUsingOurOwnPhraseEditor: Bool = false
// MARK: - ctlInputMethod // MARK: - ctlInputMethod
static func getInputMode(isReversed: Bool = false) -> InputMode { static func getInputMode(isReversed: Bool = false) -> InputMode {
if isReversed { if isReversed {
return (ctlInputMethod.currentKeyHandler.inputMode == InputMode.imeModeCHT) return (ctlInputMethod.currentKeyHandler.inputMode == InputMode.imeModeCHT)
? InputMode.imeModeCHS : InputMode.imeModeCHT ? InputMode.imeModeCHS : InputMode.imeModeCHT
} else { } else {
return ctlInputMethod.currentKeyHandler.inputMode return ctlInputMethod.currentKeyHandler.inputMode
} }
} }
// MARK: - Print debug information to the console. // MARK: - Print debug information to the console.
@objc static func prtDebugIntel(_ strPrint: String) { static func prtDebugIntel(_ strPrint: String) {
if mgrPrefs.isDebugModeEnabled { if mgrPrefs.isDebugModeEnabled {
NSLog("vChewingErrorCallback: %@", strPrint) NSLog("vChewingErrorCallback: %@", strPrint)
} }
} }
// MARK: - Tell whether this IME is running with Root privileges. // MARK: - Tell whether this IME is running with Root privileges.
@objc static var isSudoMode: Bool { static var isSudoMode: Bool {
NSUserName() == "root" NSUserName() == "root"
} }
// MARK: - Initializing Language Models. // MARK: - Initializing Language Models.
@objc static func initLangModels(userOnly: Bool) { static func initLangModels(userOnly: Bool) {
if !userOnly { DispatchQueue.global(qos: .userInitiated).async {
mgrLangModel.loadDataModels() // // mgrLangModel loadUserPhrases dataFolderPath
} //
// mgrLangModel loadUserPhrases dataFolderPath //
// mgrLangModel.loadUserAssociatedPhrases()
// mgrLangModel.loadUserPhraseReplacement()
mgrLangModel.loadUserPhrases() mgrLangModel.loadUserPhrases()
mgrLangModel.loadUserPhraseReplacement() }
mgrLangModel.loadUserAssociatedPhrases() if !userOnly {
} // mgrLangModel.loadDataModels()
}
}
// MARK: - System Dark Mode Status Detector. // MARK: - System Dark Mode Status Detector.
@objc static func isDarkMode() -> Bool { static func isDarkMode() -> Bool {
if #available(macOS 10.15, *) { if #available(macOS 10.15, *) {
let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription
.lowercased() .lowercased()
if appearanceDescription.contains("dark") { if appearanceDescription.contains("dark") {
return true return true
} }
} else if #available(macOS 10.14, *) { } else if #available(macOS 10.14, *) {
if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle") if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle")
as? String as? String
{ {
if appleInterfaceStyle.lowercased().contains("dark") { if appleInterfaceStyle.lowercased().contains("dark") {
return true return true
} }
} }
} }
return false return false
} }
// MARK: - Open a phrase data file. // MARK: - Open a phrase data file.
static func openPhraseFile(userFileAt path: String) { static func openPhraseFile(userFileAt path: String) {
func checkIfUserFilesExist() -> Bool { func checkIfUserFilesExist() -> Bool {
if !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHS) if !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHS)
|| !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHT) || !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHT)
{ {
let content = String( let content = String(
format: NSLocalizedString( format: NSLocalizedString(
"Please check the permission at \"%@\".", comment: "" "Please check the permission at \"%@\".", comment: ""
), ),
mgrLangModel.dataFolderPath(isDefaultFolder: false) mgrLangModel.dataFolderPath(isDefaultFolder: false)
) )
ctlNonModalAlertWindow.shared.show( ctlNonModalAlertWindow.shared.show(
title: NSLocalizedString("Unable to create the user phrase file.", comment: ""), title: NSLocalizedString("Unable to create the user phrase file.", comment: ""),
content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""),
cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil
) )
NSApp.setActivationPolicy(.accessory) NSApp.setActivationPolicy(.accessory)
return false return false
} }
return true return true
} }
if !checkIfUserFilesExist() { if !checkIfUserFilesExist() {
return return
} }
NSWorkspace.shared.openFile(path, withApplication: "vChewingPhraseEditor") NSWorkspace.shared.openFile(path, withApplication: "vChewingPhraseEditor")
} }
// MARK: - Trash a file if it exists. // MARK: - Trash a file if it exists.
@discardableResult static func trashTargetIfExists(_ path: String) -> Bool { @discardableResult static func trashTargetIfExists(_ path: String) -> Bool {
do { do {
if FileManager.default.fileExists(atPath: path) { if FileManager.default.fileExists(atPath: path) {
// //
try FileManager.default.trashItem( try FileManager.default.trashItem(
at: URL(fileURLWithPath: path), resultingItemURL: nil at: URL(fileURLWithPath: path), resultingItemURL: nil
) )
} else { } else {
NSLog("Item doesn't exist: \(path)") NSLog("Item doesn't exist: \(path)")
} }
} catch let error as NSError { } catch let error as NSError {
NSLog("Failed from removing this object: \(path) || Error: \(error)") NSLog("Failed from removing this object: \(path) || Error: \(error)")
return false return false
} }
return true return true
} }
// MARK: - Uninstall the input method. // MARK: - Uninstall the input method.
@discardableResult static func uninstall(isSudo: Bool = false, selfKill: Bool = true) -> Int32 { @discardableResult static func uninstall(isSudo: Bool = false, selfKill: Bool = true) -> Int32 {
// Bundle.main.bundleURL便使 sudo // Bundle.main.bundleURL便使 sudo
guard let bundleID = Bundle.main.bundleIdentifier else { guard let bundleID = Bundle.main.bundleIdentifier else {
NSLog("Failed to ensure the bundle identifier.") NSLog("Failed to ensure the bundle identifier.")
return -1 return -1
} }
let kTargetBin = "vChewing" let kTargetBin = "vChewing"
let kTargetBundle = "/vChewing.app" let kTargetBundle = "/vChewing.app"
let pathLibrary = let pathLibrary =
isSudo isSudo
? "/Library" ? "/Library"
: FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].path : FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].path
let pathIMELibrary = let pathIMELibrary =
isSudo isSudo
? "/Library/Input Methods" ? "/Library/Input Methods"
: FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0].path : FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0].path
let pathUnitKeyboardLayouts = "/Keyboard Layouts" let pathUnitKeyboardLayouts = "/Keyboard Layouts"
let arrKeyLayoutFiles = [ let arrKeyLayoutFiles = [
"/vChewing ETen.keylayout", "/vChewingKeyLayout.bundle", "/vChewing MiTAC.keylayout", "/vChewing ETen.keylayout", "/vChewingKeyLayout.bundle", "/vChewing MiTAC.keylayout",
"/vChewing IBM.keylayout", "/vChewing FakeSeigyou.keylayout", "/vChewing IBM.keylayout", "/vChewing FakeSeigyou.keylayout",
"/vChewing Dachen.keylayout", "/vChewing Dachen.keylayout",
] ]
// //
for objPath in arrKeyLayoutFiles { for objPath in arrKeyLayoutFiles {
let objFullPath = pathLibrary + pathUnitKeyboardLayouts + objPath let objFullPath = pathLibrary + pathUnitKeyboardLayouts + objPath
if !IME.trashTargetIfExists(objFullPath) { return -1 } if !IME.trashTargetIfExists(objFullPath) { return -1 }
} }
if CommandLine.arguments.count > 2, CommandLine.arguments[2] == "--all", if CommandLine.arguments.count > 2, CommandLine.arguments[2] == "--all",
CommandLine.arguments[1] == "uninstall" CommandLine.arguments[1] == "uninstall"
{ {
// 使 // 使
// 使 symbol link // 使 symbol link
// symbol link // symbol link
IME.trashTargetIfExists(mgrLangModel.dataFolderPath(isDefaultFolder: true)) IME.trashTargetIfExists(mgrLangModel.dataFolderPath(isDefaultFolder: true))
IME.trashTargetIfExists(pathLibrary + "/Preferences/" + bundleID + ".plist") // App IME.trashTargetIfExists(pathLibrary + "/Preferences/" + bundleID + ".plist") // App
} }
if !IME.trashTargetIfExists(pathIMELibrary + kTargetBundle) { return -1 } // App if !IME.trashTargetIfExists(pathIMELibrary + kTargetBundle) { return -1 } // App
// //
if selfKill { if selfKill {
let killTask = Process() let killTask = Process()
killTask.launchPath = "/usr/bin/killall" killTask.launchPath = "/usr/bin/killall"
killTask.arguments = ["-9", kTargetBin] killTask.arguments = ["-9", kTargetBin]
killTask.launch() killTask.launch()
killTask.waitUntilExit() killTask.waitUntilExit()
} }
return 0 return 0
} }
// MARK: - Registering the input method. // MARK: - Registering the input method.
@discardableResult static func registerInputMethod() -> Int32 { @discardableResult static func registerInputMethod() -> Int32 {
guard let bundleID = Bundle.main.bundleIdentifier else { guard let bundleID = Bundle.main.bundleIdentifier else {
return -1 return -1
} }
let bundleUrl = Bundle.main.bundleURL let bundleUrl = Bundle.main.bundleURL
var maybeInputSource = InputSourceHelper.inputSource(for: bundleID) var maybeInputSource = InputSourceHelper.inputSource(for: bundleID)
if maybeInputSource == nil { if maybeInputSource == nil {
NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)") NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)")
// then register // then register
let status = InputSourceHelper.registerTnputSource(at: bundleUrl) let status = InputSourceHelper.registerTnputSource(at: bundleUrl)
if !status { if !status {
NSLog( NSLog(
"Fatal error: Cannot register input source \(bundleID) at \(bundleUrl.absoluteString)." "Fatal error: Cannot register input source \(bundleID) at \(bundleUrl.absoluteString)."
) )
return -1 return -1
} }
maybeInputSource = InputSourceHelper.inputSource(for: bundleID) maybeInputSource = InputSourceHelper.inputSource(for: bundleID)
} }
guard let inputSource = maybeInputSource else { guard let inputSource = maybeInputSource else {
NSLog("Fatal error: Cannot find input source \(bundleID) after registration.") NSLog("Fatal error: Cannot find input source \(bundleID) after registration.")
return -1 return -1
} }
if !InputSourceHelper.inputSourceEnabled(for: inputSource) { if !InputSourceHelper.inputSourceEnabled(for: inputSource) {
NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).") NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).")
let status = InputSourceHelper.enable(inputSource: inputSource) let status = InputSourceHelper.enable(inputSource: inputSource)
if !status { if !status {
NSLog("Fatal error: Cannot enable input source \(bundleID).") NSLog("Fatal error: Cannot enable input source \(bundleID).")
return -1 return -1
} }
if !InputSourceHelper.inputSourceEnabled(for: inputSource) { if !InputSourceHelper.inputSourceEnabled(for: inputSource) {
NSLog("Fatal error: Cannot enable input source \(bundleID).") NSLog("Fatal error: Cannot enable input source \(bundleID).")
return -1 return -1
} }
} }
if CommandLine.arguments.count > 2, CommandLine.arguments[2] == "--all" { if CommandLine.arguments.count > 2, CommandLine.arguments[2] == "--all" {
let enabled = InputSourceHelper.enableAllInputMode(for: bundleID) let enabled = InputSourceHelper.enableAllInputMode(for: bundleID)
NSLog( NSLog(
enabled enabled
? "All input sources enabled for \(bundleID)" ? "All input sources enabled for \(bundleID)"
: "Cannot enable all input sources for \(bundleID), but this is ignored") : "Cannot enable all input sources for \(bundleID), but this is ignored")
} }
return 0 return 0
} }
// MARK: - ASCII // MARK: - ASCII
struct CarbonKeyboardLayout { struct CarbonKeyboardLayout {
var strName: String = "" var strName: String = ""
var strValue: String = "" var strValue: String = ""
} }
static let arrWhitelistedKeyLayoutsASCII: [String] = [ static let arrWhitelistedKeyLayoutsASCII: [String] = [
"com.apple.keylayout.ABC", "com.apple.keylayout.ABC",
"com.apple.keylayout.ABC-AZERTY", "com.apple.keylayout.ABC-AZERTY",
"com.apple.keylayout.ABC-QWERTZ", "com.apple.keylayout.ABC-QWERTZ",
"com.apple.keylayout.British", "com.apple.keylayout.British",
"com.apple.keylayout.Colemak", "com.apple.keylayout.Colemak",
"com.apple.keylayout.Dvorak", "com.apple.keylayout.Dvorak",
"com.apple.keylayout.Dvorak-Left", "com.apple.keylayout.Dvorak-Left",
"com.apple.keylayout.DVORAK-QWERTYCMD", "com.apple.keylayout.DVORAK-QWERTYCMD",
"com.apple.keylayout.Dvorak-Right", "com.apple.keylayout.Dvorak-Right",
] ]
static var arrEnumerateSystemKeyboardLayouts: [IME.CarbonKeyboardLayout] { static var arrEnumerateSystemKeyboardLayouts: [IME.CarbonKeyboardLayout] {
// macOS // macOS
var arrKeyLayouts: [IME.CarbonKeyboardLayout] = [] var arrKeyLayouts: [IME.CarbonKeyboardLayout] = []
arrKeyLayouts += [ arrKeyLayouts += [
IME.CarbonKeyboardLayout( IME.CarbonKeyboardLayout(
strName: NSLocalizedString("Apple Chewing - Dachen", comment: ""), strName: NSLocalizedString("Apple Chewing - Dachen", comment: ""),
strValue: "com.apple.keylayout.ZhuyinBopomofo" strValue: "com.apple.keylayout.ZhuyinBopomofo"
), ),
IME.CarbonKeyboardLayout( IME.CarbonKeyboardLayout(
strName: NSLocalizedString("Apple Chewing - Eten Traditional", comment: ""), strName: NSLocalizedString("Apple Chewing - Eten Traditional", comment: ""),
strValue: "com.apple.keylayout.ZhuyinEten" strValue: "com.apple.keylayout.ZhuyinEten"
), ),
] ]
// ASCII // ASCII
var arrKeyLayoutsMACV: [IME.CarbonKeyboardLayout] = [] var arrKeyLayoutsMACV: [IME.CarbonKeyboardLayout] = []
var arrKeyLayoutsASCII: [IME.CarbonKeyboardLayout] = [] var arrKeyLayoutsASCII: [IME.CarbonKeyboardLayout] = []
let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
for source in list { for source in list {
if let ptrCategory = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { if let ptrCategory = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) {
let category = Unmanaged<CFString>.fromOpaque(ptrCategory).takeUnretainedValue() let category = Unmanaged<CFString>.fromOpaque(ptrCategory).takeUnretainedValue()
if category != kTISCategoryKeyboardInputSource { if category != kTISCategoryKeyboardInputSource {
continue continue
} }
} else { } else {
continue continue
} }
if let ptrASCIICapable = TISGetInputSourceProperty( if let ptrASCIICapable = TISGetInputSourceProperty(
source, kTISPropertyInputSourceIsASCIICapable source, kTISPropertyInputSourceIsASCIICapable
) { ) {
let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(ptrASCIICapable) let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(ptrASCIICapable)
.takeUnretainedValue() .takeUnretainedValue()
if asciiCapable != kCFBooleanTrue { if asciiCapable != kCFBooleanTrue {
continue continue
} }
} else { } else {
continue continue
} }
if let ptrSourceType = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { if let ptrSourceType = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) {
let sourceType = Unmanaged<CFString>.fromOpaque(ptrSourceType).takeUnretainedValue() let sourceType = Unmanaged<CFString>.fromOpaque(ptrSourceType).takeUnretainedValue()
if sourceType != kTISTypeKeyboardLayout { if sourceType != kTISTypeKeyboardLayout {
continue continue
} }
} else { } else {
continue continue
} }
guard let ptrSourceID = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), guard let ptrSourceID = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName)
else { else {
continue continue
} }
let sourceID = String(Unmanaged<CFString>.fromOpaque(ptrSourceID).takeUnretainedValue()) let sourceID = String(Unmanaged<CFString>.fromOpaque(ptrSourceID).takeUnretainedValue())
let localizedName = String( let localizedName = String(
Unmanaged<CFString>.fromOpaque(localizedNamePtr).takeUnretainedValue()) Unmanaged<CFString>.fromOpaque(localizedNamePtr).takeUnretainedValue())
if sourceID.contains("vChewing") { if sourceID.contains("vChewing") {
arrKeyLayoutsMACV += [ arrKeyLayoutsMACV += [
IME.CarbonKeyboardLayout(strName: localizedName, strValue: sourceID) IME.CarbonKeyboardLayout(strName: localizedName, strValue: sourceID)
] ]
} }
if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) { if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) {
arrKeyLayoutsASCII += [ arrKeyLayoutsASCII += [
IME.CarbonKeyboardLayout(strName: localizedName, strValue: sourceID) IME.CarbonKeyboardLayout(strName: localizedName, strValue: sourceID)
] ]
} }
} }
arrKeyLayouts += arrKeyLayoutsMACV arrKeyLayouts += arrKeyLayoutsMACV
arrKeyLayouts += arrKeyLayoutsASCII arrKeyLayouts += arrKeyLayoutsASCII
return arrKeyLayouts return arrKeyLayouts
} }
} }
// MARK: - Root Extensions // MARK: - Root Extensions
@ -346,27 +351,27 @@ public class IME: NSObject {
// Extend the RangeReplaceableCollection to allow it clean duplicated characters. // Extend the RangeReplaceableCollection to allow it clean duplicated characters.
// Ref: https://stackoverflow.com/questions/25738817/ // Ref: https://stackoverflow.com/questions/25738817/
extension RangeReplaceableCollection where Element: Hashable { extension RangeReplaceableCollection where Element: Hashable {
var charDeDuplicate: Self { var charDeDuplicate: Self {
var set = Set<Element>() var set = Set<Element>()
return filter { set.insert($0).inserted } return filter { set.insert($0).inserted }
} }
} }
// MARK: - Error Extension // MARK: - Error Extension
extension String: Error {} extension String: Error {}
extension String: LocalizedError { extension String: LocalizedError {
public var errorDescription: String? { public var errorDescription: String? {
self self
} }
} }
// MARK: - Ensuring trailing slash of a string: // MARK: - Ensuring trailing slash of a string:
extension String { extension String {
mutating func ensureTrailingSlash() { mutating func ensureTrailingSlash() {
if !hasSuffix("/") { if !hasSuffix("/") {
self += "/" self += "/"
} }
} }
} }

View File

@ -28,109 +28,101 @@ import Carbon
import Cocoa import Cocoa
public class InputSourceHelper: NSObject { public class InputSourceHelper: NSObject {
@available(*, unavailable) @available(*, unavailable)
override public init() { override public init() {
super.init() super.init()
} }
public static func allInstalledInputSources() -> [TISInputSource] { public static func allInstalledInputSources() -> [TISInputSource] {
TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
} }
@objc(inputSourceForProperty:stringValue:) public static func inputSource(for propertyKey: CFString, stringValue: String)
public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource?
-> TISInputSource? {
{ let stringID = CFStringGetTypeID()
let stringID = CFStringGetTypeID() for source in allInstalledInputSources() {
for source in allInstalledInputSources() { if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) {
if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) { let property = Unmanaged<CFTypeRef>.fromOpaque(propertyPtr).takeUnretainedValue()
let property = Unmanaged<CFTypeRef>.fromOpaque(propertyPtr).takeUnretainedValue() let typeID = CFGetTypeID(property)
let typeID = CFGetTypeID(property) if typeID != stringID {
if typeID != stringID { continue
continue }
} if stringValue == property as? String {
if stringValue == property as? String { return source
return source }
} }
} }
} return nil
return nil }
}
@objc(inputSourceForInputSourceID:) public static func inputSource(for sourceID: String) -> TISInputSource? {
public static func inputSource(for sourceID: String) -> TISInputSource? { inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID)
inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) }
}
@objc(inputSourceEnabled:) public static func inputSourceEnabled(for source: TISInputSource) -> Bool {
public static func inputSourceEnabled(for source: TISInputSource) -> Bool { if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) {
if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue()
let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue() return value == kCFBooleanTrue
return value == kCFBooleanTrue }
} return false
return false }
}
@objc(enableInputSource:) public static func enable(inputSource: TISInputSource) -> Bool {
public static func enable(inputSource: TISInputSource) -> Bool { let status = TISEnableInputSource(inputSource)
let status = TISEnableInputSource(inputSource) return status == noErr
return status == noErr }
}
@objc(enableAllInputModesForInputSourceBundleID:) public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool {
public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { var enabled = false
var enabled = false for source in allInstalledInputSources() {
for source in allInstalledInputSources() { guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID),
guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID)
let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else {
else { continue
continue }
} let bundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr).takeUnretainedValue()
let bundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr).takeUnretainedValue() if String(bundleID) == inputSourceBundleD {
if String(bundleID) == inputSourceBundleD { let modeEnabled = enable(inputSource: source)
let modeEnabled = enable(inputSource: source) if !modeEnabled {
if !modeEnabled { return false
return false }
} enabled = true
enabled = true }
} }
}
return enabled return enabled
} }
@objc(enableInputMode:forInputSourceBundleID:) public static func enable(inputMode modeID: String, for bundleID: String) -> Bool {
public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { for source in allInstalledInputSources() {
for source in allInstalledInputSources() { guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID),
guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID)
let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else {
else { continue
continue }
} let inputsSourceBundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr)
let inputsSourceBundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr) .takeUnretainedValue()
.takeUnretainedValue() let inputsSourceModeID = Unmanaged<CFString>.fromOpaque(modePtr).takeUnretainedValue()
let inputsSourceModeID = Unmanaged<CFString>.fromOpaque(modePtr).takeUnretainedValue() if modeID == String(inputsSourceModeID), bundleID == String(inputsSourceBundleID) {
if modeID == String(inputsSourceModeID), bundleID == String(inputsSourceBundleID) { let enabled = enable(inputSource: source)
let enabled = enable(inputSource: source) print(
print( "Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)"
"Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)" )
) return enabled
return enabled }
} }
} print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)")
print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)") return false
return false }
}
@objc(disableInputSource:) public static func disable(inputSource: TISInputSource) -> Bool {
public static func disable(inputSource: TISInputSource) -> Bool { let status = TISDisableInputSource(inputSource)
let status = TISDisableInputSource(inputSource) return status == noErr
return status == noErr }
}
@objc(registerInputSource:) public static func registerTnputSource(at url: URL) -> Bool {
public static func registerTnputSource(at url: URL) -> Bool { let status = TISRegisterInputSource(url as CFURL)
let status = TISRegisterInputSource(url as CFURL) return status == noErr
return status == noErr }
}
} }

View File

@ -27,154 +27,154 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
struct VersionUpdateReport { struct VersionUpdateReport {
var siteUrl: URL? var siteUrl: URL?
var currentShortVersion: String = "" var currentShortVersion: String = ""
var currentVersion: String = "" var currentVersion: String = ""
var remoteShortVersion: String = "" var remoteShortVersion: String = ""
var remoteVersion: String = "" var remoteVersion: String = ""
var versionDescription: String = "" var versionDescription: String = ""
} }
enum VersionUpdateApiResult { enum VersionUpdateApiResult {
case shouldUpdate(report: VersionUpdateReport) case shouldUpdate(report: VersionUpdateReport)
case noNeedToUpdate case noNeedToUpdate
case ignored case ignored
} }
enum VersionUpdateApiError: Error, LocalizedError { enum VersionUpdateApiError: Error, LocalizedError {
case connectionError(message: String) case connectionError(message: String)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .connectionError(let message): case .connectionError(let message):
return String( return 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: %@",
comment: "" comment: ""
), message ), message
) )
} }
} }
} }
enum VersionUpdateApi { enum VersionUpdateApi {
static let kCheckUpdateAutomatically = "CheckUpdateAutomatically" static let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
static let kNextUpdateCheckDateKey = "NextUpdateCheckDate" static let kNextUpdateCheckDateKey = "NextUpdateCheckDate"
static let kUpdateInfoEndpointKey = "UpdateInfoEndpoint" static let kUpdateInfoEndpointKey = "UpdateInfoEndpoint"
static let kUpdateInfoSiteKey = "UpdateInfoSite" static let kUpdateInfoSiteKey = "UpdateInfoSite"
static let kVersionDescription = "VersionDescription" static let kVersionDescription = "VersionDescription"
static let kNextCheckInterval: TimeInterval = 86400.0 static let kNextCheckInterval: TimeInterval = 86400.0
static let kTimeoutInterval: TimeInterval = 60.0 static let kTimeoutInterval: TimeInterval = 60.0
static func check( static func check(
forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void
) -> URLSessionTask? { ) -> URLSessionTask? {
guard let infoDict = Bundle.main.infoDictionary, guard let infoDict = Bundle.main.infoDictionary,
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
let updateInfoURL = URL(string: updateInfoURLString) let updateInfoURL = URL(string: updateInfoURLString)
else { else {
return nil return nil
} }
let request = URLRequest( let request = URLRequest(
url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData,
timeoutInterval: kTimeoutInterval timeoutInterval: kTimeoutInterval
) )
let task = URLSession.shared.dataTask(with: request) { data, _, error in let task = URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error { if let error = error {
DispatchQueue.main.async { DispatchQueue.main.async {
forced forced
? callback( ? callback(
.failure( .failure(
VersionUpdateApiError.connectionError( VersionUpdateApiError.connectionError(
message: error.localizedDescription))) message: error.localizedDescription)))
: callback(.success(.ignored)) : callback(.success(.ignored))
} }
return return
} }
do { do {
guard guard
let plist = try PropertyListSerialization.propertyList( let plist = try PropertyListSerialization.propertyList(
from: data ?? Data(), options: [], format: nil from: data ?? Data(), options: [], format: nil
) as? [AnyHashable: Any], ) as? [AnyHashable: Any],
let remoteVersion = plist[kCFBundleVersionKey] as? String, let remoteVersion = plist[kCFBundleVersionKey] as? String,
let infoDict = Bundle.main.infoDictionary let infoDict = Bundle.main.infoDictionary
else { else {
DispatchQueue.main.async { DispatchQueue.main.async {
forced forced
? callback(.success(.noNeedToUpdate)) ? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored)) : callback(.success(.ignored))
} }
return return
} }
// TODO: Validate info (e.g. bundle identifier) // TODO: Validate info (e.g. bundle identifier)
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
let result = currentVersion.compare( let result = currentVersion.compare(
remoteVersion, options: .numeric, range: nil, locale: nil remoteVersion, options: .numeric, range: nil, locale: nil
) )
if result != .orderedAscending { if result != .orderedAscending {
DispatchQueue.main.async { DispatchQueue.main.async {
forced forced
? callback(.success(.noNeedToUpdate)) ? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored)) : callback(.success(.ignored))
} }
IME.prtDebugIntel( IME.prtDebugIntel(
"vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available." "vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available."
) )
return return
} }
IME.prtDebugIntel( IME.prtDebugIntel(
"vChewingDebug: Update // New version detected, proceeding to the next phase.") "vChewingDebug: Update // New version detected, proceeding to the next phase.")
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
let siteInfoURL = URL(string: siteInfoURLString) let siteInfoURL = URL(string: siteInfoURLString)
else { else {
DispatchQueue.main.async { DispatchQueue.main.async {
forced forced
? callback(.success(.noNeedToUpdate)) ? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored)) : callback(.success(.ignored))
} }
IME.prtDebugIntel( IME.prtDebugIntel(
"vChewingDebug: Update // Failed from retrieving / parsing URL intel.") "vChewingDebug: Update // Failed from retrieving / parsing URL intel.")
return return
} }
IME.prtDebugIntel( IME.prtDebugIntel(
"vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.") "vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.")
var report = VersionUpdateReport(siteUrl: siteInfoURL) var report = VersionUpdateReport(siteUrl: siteInfoURL)
var versionDescription = "" var versionDescription = ""
let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any] let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any]
if let versionDescriptions = versionDescriptions { if let versionDescriptions = versionDescriptions {
var locale = "en" var locale = "en"
let preferredTags = Bundle.preferredLocalizations(from: IME.arrSupportedLocales) let preferredTags = Bundle.preferredLocalizations(from: IME.arrSupportedLocales)
if let first = preferredTags.first { if let first = preferredTags.first {
locale = first locale = first
} }
versionDescription = versionDescription =
versionDescriptions[locale] as? String ?? versionDescriptions["en"] versionDescriptions[locale] as? String ?? versionDescriptions["en"]
as? String ?? "" as? String ?? ""
if !versionDescription.isEmpty { if !versionDescription.isEmpty {
versionDescription = "\n\n" + versionDescription versionDescription = "\n\n" + versionDescription
} }
} }
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
report.currentVersion = currentVersion report.currentVersion = currentVersion
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? "" report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
report.remoteVersion = remoteVersion report.remoteVersion = remoteVersion
report.versionDescription = versionDescription report.versionDescription = versionDescription
DispatchQueue.main.async { DispatchQueue.main.async {
callback(.success(.shouldUpdate(report: report))) callback(.success(.shouldUpdate(report: report)))
} }
IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.") IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.")
} catch { } catch {
DispatchQueue.main.async { DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
} }
} }
} }
task.resume() task.resume()
return task return task
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
extension Bool { extension Bool {
fileprivate var state: NSControl.StateValue { fileprivate var state: NSControl.StateValue {
self ? .on : .off self ? .on : .off
} }
} }
// MARK: - IME Menu Manager // MARK: - IME Menu Manager
@ -37,320 +37,321 @@ extension Bool {
// //
extension ctlInputMethod { extension ctlInputMethod {
override func menu() -> NSMenu! { override func menu() -> NSMenu! {
let optionKeyPressed = NSEvent.modifierFlags.contains(.option) let optionKeyPressed = NSEvent.modifierFlags.contains(.option)
let menu = NSMenu(title: "Input Method Menu") let menu = NSMenu(title: "Input Method Menu")
let useSCPCTypingModeItem = menu.addItem( let useSCPCTypingModeItem = menu.addItem(
withTitle: NSLocalizedString("Per-Char Select Mode", comment: ""), withTitle: NSLocalizedString("Per-Char Select Mode", comment: ""),
action: #selector(toggleSCPCTypingMode(_:)), keyEquivalent: "P" action: #selector(toggleSCPCTypingMode(_:)), keyEquivalent: "P"
) )
useSCPCTypingModeItem.keyEquivalentModifierMask = [.command, .control] useSCPCTypingModeItem.keyEquivalentModifierMask = [.command, .control]
useSCPCTypingModeItem.state = mgrPrefs.useSCPCTypingMode.state useSCPCTypingModeItem.state = mgrPrefs.useSCPCTypingMode.state
let userAssociatedPhrasesItem = menu.addItem( let userAssociatedPhrasesItem = menu.addItem(
withTitle: NSLocalizedString("Per-Char Associated Phrases", comment: ""), withTitle: NSLocalizedString("Per-Char Associated Phrases", comment: ""),
action: #selector(toggleAssociatedPhrasesEnabled(_:)), keyEquivalent: "O" action: #selector(toggleAssociatedPhrasesEnabled(_:)), keyEquivalent: "O"
) )
userAssociatedPhrasesItem.keyEquivalentModifierMask = [.command, .control] userAssociatedPhrasesItem.keyEquivalentModifierMask = [.command, .control]
userAssociatedPhrasesItem.state = mgrPrefs.associatedPhrasesEnabled.state userAssociatedPhrasesItem.state = mgrPrefs.associatedPhrasesEnabled.state
let useCNS11643SupportItem = menu.addItem( let useCNS11643SupportItem = menu.addItem(
withTitle: NSLocalizedString("CNS11643 Mode", comment: ""), withTitle: NSLocalizedString("CNS11643 Mode", comment: ""),
action: #selector(toggleCNS11643Enabled(_:)), keyEquivalent: "L" action: #selector(toggleCNS11643Enabled(_:)), keyEquivalent: "L"
) )
useCNS11643SupportItem.keyEquivalentModifierMask = [.command, .control] useCNS11643SupportItem.keyEquivalentModifierMask = [.command, .control]
useCNS11643SupportItem.state = mgrPrefs.cns11643Enabled.state useCNS11643SupportItem.state = mgrPrefs.cns11643Enabled.state
if IME.getInputMode() == InputMode.imeModeCHT { if IME.getInputMode() == InputMode.imeModeCHT {
let chineseConversionItem = menu.addItem( let chineseConversionItem = menu.addItem(
withTitle: NSLocalizedString("Force KangXi Writing", comment: ""), withTitle: NSLocalizedString("Force KangXi Writing", comment: ""),
action: #selector(toggleChineseConverter(_:)), keyEquivalent: "K" action: #selector(toggleChineseConverter(_:)), keyEquivalent: "K"
) )
chineseConversionItem.keyEquivalentModifierMask = [.command, .control] chineseConversionItem.keyEquivalentModifierMask = [.command, .control]
chineseConversionItem.state = mgrPrefs.chineseConversionEnabled.state chineseConversionItem.state = mgrPrefs.chineseConversionEnabled.state
let shiftJISConversionItem = menu.addItem( let shiftJISConversionItem = menu.addItem(
withTitle: NSLocalizedString("JIS Shinjitai Output", comment: ""), withTitle: NSLocalizedString("JIS Shinjitai Output", comment: ""),
action: #selector(toggleShiftJISShinjitaiOutput(_:)), keyEquivalent: "J" action: #selector(toggleShiftJISShinjitaiOutput(_:)), keyEquivalent: "J"
) )
shiftJISConversionItem.keyEquivalentModifierMask = [.command, .control] shiftJISConversionItem.keyEquivalentModifierMask = [.command, .control]
shiftJISConversionItem.state = mgrPrefs.shiftJISShinjitaiOutputEnabled.state shiftJISConversionItem.state = mgrPrefs.shiftJISShinjitaiOutputEnabled.state
} }
let halfWidthPunctuationItem = menu.addItem( let halfWidthPunctuationItem = menu.addItem(
withTitle: NSLocalizedString("Half-Width Punctuation Mode", comment: ""), withTitle: NSLocalizedString("Half-Width Punctuation Mode", comment: ""),
action: #selector(toggleHalfWidthPunctuation(_:)), keyEquivalent: "H" action: #selector(toggleHalfWidthPunctuation(_:)), keyEquivalent: "H"
) )
halfWidthPunctuationItem.keyEquivalentModifierMask = [.command, .control] halfWidthPunctuationItem.keyEquivalentModifierMask = [.command, .control]
halfWidthPunctuationItem.state = mgrPrefs.halfWidthPunctuationEnabled.state halfWidthPunctuationItem.state = mgrPrefs.halfWidthPunctuationEnabled.state
if optionKeyPressed { if optionKeyPressed || mgrPrefs.phraseReplacementEnabled {
let phaseReplacementItem = menu.addItem( let phaseReplacementItem = menu.addItem(
withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""), withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""),
action: #selector(togglePhraseReplacement(_:)), keyEquivalent: "" action: #selector(togglePhraseReplacement(_:)), keyEquivalent: ""
) )
phaseReplacementItem.state = mgrPrefs.phraseReplacementEnabled.state phaseReplacementItem.state = mgrPrefs.phraseReplacementEnabled.state
}
let toggleSymbolInputItem = menu.addItem( if optionKeyPressed {
withTitle: NSLocalizedString("Symbol & Emoji Input", comment: ""), let toggleSymbolInputItem = menu.addItem(
action: #selector(toggleSymbolEnabled(_:)), keyEquivalent: "" withTitle: NSLocalizedString("Symbol & Emoji Input", comment: ""),
) action: #selector(toggleSymbolEnabled(_:)), keyEquivalent: ""
toggleSymbolInputItem.state = mgrPrefs.symbolInputEnabled.state )
} toggleSymbolInputItem.state = mgrPrefs.symbolInputEnabled.state
}
menu.addItem(NSMenuItem.separator()) // --------------------- menu.addItem(NSMenuItem.separator()) // ---------------------
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Open User Data Folder", comment: ""), withTitle: NSLocalizedString("Open User Data Folder", comment: ""),
action: #selector(openUserDataFolder(_:)), keyEquivalent: "" action: #selector(openUserDataFolder(_:)), keyEquivalent: ""
) )
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Edit User Phrases…", comment: ""), withTitle: NSLocalizedString("Edit User Phrases…", comment: ""),
action: #selector(openUserPhrases(_:)), keyEquivalent: "" action: #selector(openUserPhrases(_:)), keyEquivalent: ""
) )
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Edit Excluded Phrases…", comment: ""), withTitle: NSLocalizedString("Edit Excluded Phrases…", comment: ""),
action: #selector(openExcludedPhrases(_:)), keyEquivalent: "" action: #selector(openExcludedPhrases(_:)), keyEquivalent: ""
) )
if optionKeyPressed { if optionKeyPressed {
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Edit Phrase Replacement Table…", comment: ""), withTitle: NSLocalizedString("Edit Phrase Replacement Table…", comment: ""),
action: #selector(openPhraseReplacement(_:)), keyEquivalent: "" action: #selector(openPhraseReplacement(_:)), keyEquivalent: ""
) )
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Edit Associated Phrases…", comment: ""), withTitle: NSLocalizedString("Edit Associated Phrases…", comment: ""),
action: #selector(openAssociatedPhrases(_:)), keyEquivalent: "" action: #selector(openAssociatedPhrases(_:)), keyEquivalent: ""
) )
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Edit User Symbol & Emoji Data…", comment: ""), withTitle: NSLocalizedString("Edit User Symbol & Emoji Data…", comment: ""),
action: #selector(openUserSymbols(_:)), keyEquivalent: "" action: #selector(openUserSymbols(_:)), keyEquivalent: ""
) )
} }
if optionKeyPressed || !mgrPrefs.shouldAutoReloadUserDataFiles { if optionKeyPressed || !mgrPrefs.shouldAutoReloadUserDataFiles {
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Reload User Phrases", comment: ""), withTitle: NSLocalizedString("Reload User Phrases", comment: ""),
action: #selector(reloadUserPhrases(_:)), keyEquivalent: "" action: #selector(reloadUserPhrases(_:)), keyEquivalent: ""
) )
} }
menu.addItem(NSMenuItem.separator()) // --------------------- menu.addItem(NSMenuItem.separator()) // ---------------------
if optionKeyPressed { if optionKeyPressed {
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("vChewing Preferences…", comment: ""), withTitle: NSLocalizedString("vChewing Preferences…", comment: ""),
action: #selector(showLegacyPreferences(_:)), keyEquivalent: "" action: #selector(showLegacyPreferences(_:)), keyEquivalent: ""
) )
} else { } else {
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("vChewing Preferences…", comment: ""), withTitle: NSLocalizedString("vChewing Preferences…", comment: ""),
action: #selector(showPreferences(_:)), keyEquivalent: "" action: #selector(showPreferences(_:)), keyEquivalent: ""
) )
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Check for Updates…", comment: ""), withTitle: NSLocalizedString("Check for Updates…", comment: ""),
action: #selector(checkForUpdate(_:)), keyEquivalent: "" action: #selector(checkForUpdate(_:)), keyEquivalent: ""
) )
} }
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Reboot vChewing…", comment: ""), withTitle: NSLocalizedString("Reboot vChewing…", comment: ""),
action: #selector(selfTerminate(_:)), keyEquivalent: "" action: #selector(selfTerminate(_:)), keyEquivalent: ""
) )
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("About vChewing…", comment: ""), withTitle: NSLocalizedString("About vChewing…", comment: ""),
action: #selector(showAbout(_:)), keyEquivalent: "" action: #selector(showAbout(_:)), keyEquivalent: ""
) )
if optionKeyPressed { if optionKeyPressed {
menu.addItem( menu.addItem(
withTitle: NSLocalizedString("Uninstall vChewing…", comment: ""), withTitle: NSLocalizedString("Uninstall vChewing…", comment: ""),
action: #selector(selfUninstall(_:)), keyEquivalent: "" action: #selector(selfUninstall(_:)), keyEquivalent: ""
) )
} }
// NSMenu modified key // NSMenu modified key
setKeyLayout() setKeyLayout()
return menu return menu
} }
// MARK: - IME Menu Items // MARK: - IME Menu Items
@objc override func showPreferences(_: Any?) { @objc override func showPreferences(_: Any?) {
if #available(macOS 11.0, *) { if #available(macOS 11.0, *) {
NSApp.setActivationPolicy(.accessory) NSApp.setActivationPolicy(.accessory)
ctlPrefUI.shared.controller.show(preferencePane: Preferences.PaneIdentifier(rawValue: "General")) ctlPrefUI.shared.controller.show(preferencePane: Preferences.PaneIdentifier(rawValue: "General"))
ctlPrefUI.shared.controller.window?.level = .floating ctlPrefUI.shared.controller.window?.level = .floating
} else { } else {
showPrefWindowTraditional() showPrefWindowTraditional()
} }
} }
@objc func showLegacyPreferences(_: Any?) { @objc func showLegacyPreferences(_: Any?) {
showPrefWindowTraditional() showPrefWindowTraditional()
} }
private func showPrefWindowTraditional() { private func showPrefWindowTraditional() {
(NSApp.delegate as? AppDelegate)?.showPreferences() (NSApp.delegate as? AppDelegate)?.showPreferences()
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@objc func toggleSCPCTypingMode(_: Any?) { @objc func toggleSCPCTypingMode(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("Per-Char Select Mode", comment: ""), "\n", format: "%@%@%@", NSLocalizedString("Per-Char Select Mode", comment: ""), "\n",
mgrPrefs.toggleSCPCTypingModeEnabled() mgrPrefs.toggleSCPCTypingModeEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func toggleChineseConverter(_: Any?) { @objc func toggleChineseConverter(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("Force KangXi Writing", comment: ""), "\n", format: "%@%@%@", NSLocalizedString("Force KangXi Writing", comment: ""), "\n",
mgrPrefs.toggleChineseConversionEnabled() mgrPrefs.toggleChineseConversionEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func toggleShiftJISShinjitaiOutput(_: Any?) { @objc func toggleShiftJISShinjitaiOutput(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("JIS Shinjitai Output", comment: ""), "\n", format: "%@%@%@", NSLocalizedString("JIS Shinjitai Output", comment: ""), "\n",
mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func toggleHalfWidthPunctuation(_: Any?) { @objc func toggleHalfWidthPunctuation(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("Half-Width Punctuation Mode", comment: ""), format: "%@%@%@", NSLocalizedString("Half-Width Punctuation Mode", comment: ""),
"\n", "\n",
mgrPrefs.toggleHalfWidthPunctuationEnabled() mgrPrefs.toggleHalfWidthPunctuationEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func toggleCNS11643Enabled(_: Any?) { @objc func toggleCNS11643Enabled(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("CNS11643 Mode", comment: ""), "\n", format: "%@%@%@", NSLocalizedString("CNS11643 Mode", comment: ""), "\n",
mgrPrefs.toggleCNS11643Enabled() mgrPrefs.toggleCNS11643Enabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func toggleSymbolEnabled(_: Any?) { @objc func toggleSymbolEnabled(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("Symbol & Emoji Input", comment: ""), "\n", format: "%@%@%@", NSLocalizedString("Symbol & Emoji Input", comment: ""), "\n",
mgrPrefs.toggleSymbolInputEnabled() mgrPrefs.toggleSymbolInputEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func toggleAssociatedPhrasesEnabled(_: Any?) { @objc func toggleAssociatedPhrasesEnabled(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("Per-Char Associated Phrases", comment: ""), format: "%@%@%@", NSLocalizedString("Per-Char Associated Phrases", comment: ""),
"\n", "\n",
mgrPrefs.toggleAssociatedPhrasesEnabled() mgrPrefs.toggleAssociatedPhrasesEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func togglePhraseReplacement(_: Any?) { @objc func togglePhraseReplacement(_: Any?) {
NotifierController.notify( NotifierController.notify(
message: String( message: String(
format: "%@%@%@", NSLocalizedString("Use Phrase Replacement", comment: ""), "\n", format: "%@%@%@", NSLocalizedString("Use Phrase Replacement", comment: ""), "\n",
mgrPrefs.togglePhraseReplacementEnabled() mgrPrefs.togglePhraseReplacementEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "") ? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: "")
)) ))
resetKeyHandler() resetKeyHandler()
} }
@objc func selfUninstall(_: Any?) { @objc func selfUninstall(_: Any?) {
(NSApp.delegate as? AppDelegate)?.selfUninstall() (NSApp.delegate as? AppDelegate)?.selfUninstall()
} }
@objc func selfTerminate(_: Any?) { @objc func selfTerminate(_: Any?) {
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
NSApp.terminate(nil) NSApp.terminate(nil)
} }
@objc func checkForUpdate(_: Any?) { @objc func checkForUpdate(_: Any?) {
(NSApp.delegate as? AppDelegate)?.checkForUpdate(forced: true) (NSApp.delegate as? AppDelegate)?.checkForUpdate(forced: true)
} }
@objc func openUserDataFolder(_: Any?) { @objc func openUserDataFolder(_: Any?) {
if !mgrLangModel.checkIfUserDataFolderExists() { if !mgrLangModel.checkIfUserDataFolderExists() {
return return
} }
NSWorkspace.shared.openFile( NSWorkspace.shared.openFile(
mgrLangModel.dataFolderPath(isDefaultFolder: false), withApplication: "Finder" mgrLangModel.dataFolderPath(isDefaultFolder: false), withApplication: "Finder"
) )
} }
@objc func openUserPhrases(_: Any?) { @objc func openUserPhrases(_: Any?) {
IME.openPhraseFile(userFileAt: mgrLangModel.userPhrasesDataPath(IME.getInputMode())) IME.openPhraseFile(userFileAt: mgrLangModel.userPhrasesDataPath(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled { if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled {
IME.openPhraseFile(userFileAt: mgrLangModel.userPhrasesDataPath(IME.getInputMode(isReversed: true))) IME.openPhraseFile(userFileAt: mgrLangModel.userPhrasesDataPath(IME.getInputMode(isReversed: true)))
} }
} }
@objc func openExcludedPhrases(_: Any?) { @objc func openExcludedPhrases(_: Any?) {
IME.openPhraseFile(userFileAt: mgrLangModel.excludedPhrasesDataPath(IME.getInputMode())) IME.openPhraseFile(userFileAt: mgrLangModel.excludedPhrasesDataPath(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled { if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled {
IME.openPhraseFile(userFileAt: mgrLangModel.excludedPhrasesDataPath(IME.getInputMode(isReversed: true))) IME.openPhraseFile(userFileAt: mgrLangModel.excludedPhrasesDataPath(IME.getInputMode(isReversed: true)))
} }
} }
@objc func openUserSymbols(_: Any?) { @objc func openUserSymbols(_: Any?) {
IME.openPhraseFile(userFileAt: mgrLangModel.userSymbolDataPath(IME.getInputMode())) IME.openPhraseFile(userFileAt: mgrLangModel.userSymbolDataPath(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled { if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled {
IME.openPhraseFile(userFileAt: mgrLangModel.userSymbolDataPath(IME.getInputMode(isReversed: true))) IME.openPhraseFile(userFileAt: mgrLangModel.userSymbolDataPath(IME.getInputMode(isReversed: true)))
} }
} }
@objc func openPhraseReplacement(_: Any?) { @objc func openPhraseReplacement(_: Any?) {
IME.openPhraseFile(userFileAt: mgrLangModel.phraseReplacementDataPath(IME.getInputMode())) IME.openPhraseFile(userFileAt: mgrLangModel.phraseReplacementDataPath(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled { if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled {
IME.openPhraseFile(userFileAt: mgrLangModel.phraseReplacementDataPath(IME.getInputMode(isReversed: true))) IME.openPhraseFile(userFileAt: mgrLangModel.phraseReplacementDataPath(IME.getInputMode(isReversed: true)))
} }
} }
@objc func openAssociatedPhrases(_: Any?) { @objc func openAssociatedPhrases(_: Any?) {
IME.openPhraseFile(userFileAt: mgrLangModel.userAssociatedPhrasesDataPath(IME.getInputMode())) IME.openPhraseFile(userFileAt: mgrLangModel.userAssociatedPhrasesDataPath(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled { if NSEvent.modifierFlags.contains(.option), mgrPrefs.isDebugModeEnabled {
IME.openPhraseFile( IME.openPhraseFile(
userFileAt: mgrLangModel.userAssociatedPhrasesDataPath(IME.getInputMode(isReversed: true))) userFileAt: mgrLangModel.userAssociatedPhrasesDataPath(IME.getInputMode(isReversed: true)))
} }
} }
@objc func reloadUserPhrases(_: Any?) { @objc func reloadUserPhrases(_: Any?) {
mgrLangModel.loadUserPhrases() IME.initLangModels(userOnly: true)
mgrLangModel.loadUserPhraseReplacement() }
}
@objc func showAbout(_: Any?) { @objc func showAbout(_: Any?) {
(NSApp.delegate as? AppDelegate)?.showAbout() (NSApp.delegate as? AppDelegate)?.showAbout()
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
} }

View File

@ -27,39 +27,39 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
struct UserDef { struct UserDef {
static let kIsDebugModeEnabled = "_DebugMode" static let kIsDebugModeEnabled = "_DebugMode"
static let kMostRecentInputMode = "MostRecentInputMode" static let kMostRecentInputMode = "MostRecentInputMode"
static let kUserDataFolderSpecified = "UserDataFolderSpecified" static let kUserDataFolderSpecified = "UserDataFolderSpecified"
static let kCheckUpdateAutomatically = "CheckUpdateAutomatically" static let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
static let kMandarinParser = "MandarinParser" static let kMandarinParser = "MandarinParser"
static let kBasicKeyboardLayout = "BasicKeyboardLayout" static let kBasicKeyboardLayout = "BasicKeyboardLayout"
static let kShowPageButtonsInCandidateWindow = "ShowPageButtonsInCandidateWindow" static let kShowPageButtonsInCandidateWindow = "ShowPageButtonsInCandidateWindow"
static let kCandidateListTextSize = "CandidateListTextSize" static let kCandidateListTextSize = "CandidateListTextSize"
static let kAppleLanguages = "AppleLanguages" static let kAppleLanguages = "AppleLanguages"
static let kShouldAutoReloadUserDataFiles = "ShouldAutoReloadUserDataFiles" static let kShouldAutoReloadUserDataFiles = "ShouldAutoReloadUserDataFiles"
static let kSetRearCursorMode = "SetRearCursorMode" static let kSetRearCursorMode = "SetRearCursorMode"
static let kUseHorizontalCandidateList = "UseHorizontalCandidateList" static let kUseHorizontalCandidateList = "UseHorizontalCandidateList"
static let kComposingBufferSize = "ComposingBufferSize" static let kComposingBufferSize = "ComposingBufferSize"
static let kChooseCandidateUsingSpace = "ChooseCandidateUsingSpace" static let kChooseCandidateUsingSpace = "ChooseCandidateUsingSpace"
static let kCNS11643Enabled = "CNS11643Enabled" static let kCNS11643Enabled = "CNS11643Enabled"
static let kSymbolInputEnabled = "SymbolInputEnabled" static let kSymbolInputEnabled = "SymbolInputEnabled"
static let kChineseConversionEnabled = "ChineseConversionEnabled" static let kChineseConversionEnabled = "ChineseConversionEnabled"
static let kShiftJISShinjitaiOutputEnabled = "ShiftJISShinjitaiOutputEnabled" static let kShiftJISShinjitaiOutputEnabled = "ShiftJISShinjitaiOutputEnabled"
static let kHalfWidthPunctuationEnabled = "HalfWidthPunctuationEnable" static let kHalfWidthPunctuationEnabled = "HalfWidthPunctuationEnable"
static let kMoveCursorAfterSelectingCandidate = "MoveCursorAfterSelectingCandidate" static let kMoveCursorAfterSelectingCandidate = "MoveCursorAfterSelectingCandidate"
static let kEscToCleanInputBuffer = "EscToCleanInputBuffer" static let kEscToCleanInputBuffer = "EscToCleanInputBuffer"
static let kSpecifyShiftTabKeyBehavior = "SpecifyShiftTabKeyBehavior" static let kSpecifyShiftTabKeyBehavior = "SpecifyShiftTabKeyBehavior"
static let kSpecifyShiftSpaceKeyBehavior = "SpecifyShiftSpaceKeyBehavior" static let kSpecifyShiftSpaceKeyBehavior = "SpecifyShiftSpaceKeyBehavior"
static let kUseSCPCTypingMode = "UseSCPCTypingMode" static let kUseSCPCTypingMode = "UseSCPCTypingMode"
static let kMaxCandidateLength = "MaxCandidateLength" static let kMaxCandidateLength = "MaxCandidateLength"
static let kShouldNotFartInLieuOfBeep = "ShouldNotFartInLieuOfBeep" static let kShouldNotFartInLieuOfBeep = "ShouldNotFartInLieuOfBeep"
static let kCandidateTextFontName = "CandidateTextFontName" static let kCandidateTextFontName = "CandidateTextFontName"
static let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName" static let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName"
static let kCandidateKeys = "CandidateKeys" static let kCandidateKeys = "CandidateKeys"
static let kAssociatedPhrasesEnabled = "AssociatedPhrasesEnabled" static let kAssociatedPhrasesEnabled = "AssociatedPhrasesEnabled"
static let kPhraseReplacementEnabled = "PhraseReplacementEnabled" static let kPhraseReplacementEnabled = "PhraseReplacementEnabled"
} }
private let kDefaultCandidateListTextSize: CGFloat = 18 private let kDefaultCandidateListTextSize: CGFloat = 18
@ -80,441 +80,441 @@ private let kDefaultKeys = "123456789"
// MARK: - UserDefaults extension. // MARK: - UserDefaults extension.
@objc extension UserDefaults { extension UserDefaults {
func setDefault(_ value: Any?, forKey defaultName: String) { func setDefault(_ value: Any?, forKey defaultName: String) {
if object(forKey: defaultName) == nil { if object(forKey: defaultName) == nil {
set(value, forKey: defaultName) set(value, forKey: defaultName)
} }
} }
} }
// MARK: - Property wrappers // MARK: - Property wrappers
@propertyWrapper @propertyWrapper
struct UserDefault<Value> { struct UserDefault<Value> {
let key: String let key: String
let defaultValue: Value let defaultValue: Value
var container: UserDefaults = .standard var container: UserDefaults = .standard
var wrappedValue: Value { var wrappedValue: Value {
get { get {
container.object(forKey: key) as? Value ?? defaultValue container.object(forKey: key) as? Value ?? defaultValue
} }
set { set {
container.set(newValue, forKey: key) container.set(newValue, forKey: key)
} }
} }
} }
@propertyWrapper @propertyWrapper
struct CandidateListTextSize { struct CandidateListTextSize {
let key: String let key: String
let defaultValue: CGFloat = kDefaultCandidateListTextSize let defaultValue: CGFloat = kDefaultCandidateListTextSize
lazy var container: UserDefault = .init(key: key, defaultValue: defaultValue) lazy var container: UserDefault = .init(key: key, defaultValue: defaultValue)
var wrappedValue: CGFloat { var wrappedValue: CGFloat {
mutating get { mutating get {
var value = container.wrappedValue var value = container.wrappedValue
if value < kMinCandidateListTextSize { if value < kMinCandidateListTextSize {
value = kMinCandidateListTextSize value = kMinCandidateListTextSize
} else if value > kMaxCandidateListTextSize { } else if value > kMaxCandidateListTextSize {
value = kMaxCandidateListTextSize value = kMaxCandidateListTextSize
} }
return value return value
} }
set { set {
var value = newValue var value = newValue
if value < kMinCandidateListTextSize { if value < kMinCandidateListTextSize {
value = kMinCandidateListTextSize value = kMinCandidateListTextSize
} else if value > kMaxCandidateListTextSize { } else if value > kMaxCandidateListTextSize {
value = kMaxCandidateListTextSize value = kMaxCandidateListTextSize
} }
container.wrappedValue = value container.wrappedValue = value
} }
} }
} }
@propertyWrapper @propertyWrapper
struct ComposingBufferSize { struct ComposingBufferSize {
let key: String let key: String
let defaultValue: Int = kDefaultComposingBufferSize let defaultValue: Int = kDefaultComposingBufferSize
lazy var container: UserDefault = .init(key: key, defaultValue: defaultValue) lazy var container: UserDefault = .init(key: key, defaultValue: defaultValue)
var wrappedValue: Int { var wrappedValue: Int {
mutating get { mutating get {
let currentValue = container.wrappedValue let currentValue = container.wrappedValue
if currentValue < kMinComposingBufferSize { if currentValue < kMinComposingBufferSize {
return kMinComposingBufferSize return kMinComposingBufferSize
} else if currentValue > kMaxComposingBufferSize { } else if currentValue > kMaxComposingBufferSize {
return kMaxComposingBufferSize return kMaxComposingBufferSize
} }
return currentValue return currentValue
} }
set { set {
var value = newValue var value = newValue
if value < kMinComposingBufferSize { if value < kMinComposingBufferSize {
value = kMinComposingBufferSize value = kMinComposingBufferSize
} else if value > kMaxComposingBufferSize { } else if value > kMaxComposingBufferSize {
value = kMaxComposingBufferSize value = kMaxComposingBufferSize
} }
container.wrappedValue = value container.wrappedValue = value
} }
} }
} }
// MARK: - // MARK: -
@objc enum MandarinParser: Int { @objc enum MandarinParser: Int {
case ofStandard = 0 case ofStandard = 0
case ofEten = 1 case ofEten = 1
case ofHsu = 2 case ofHsu = 2
case ofEen26 = 3 case ofEen26 = 3
case ofIBM = 4 case ofIBM = 4
case ofMiTAC = 5 case ofMiTAC = 5
case ofFakeSeigyou = 6 case ofFakeSeigyou = 6
case ofHanyuPinyin = 10 case ofHanyuPinyin = 10
var name: String { var name: String {
switch self { switch self {
case .ofStandard: case .ofStandard:
return "Standard" return "Standard"
case .ofEten: case .ofEten:
return "ETen" return "ETen"
case .ofHsu: case .ofHsu:
return "Hsu" return "Hsu"
case .ofEen26: case .ofEen26:
return "ETen26" return "ETen26"
case .ofIBM: case .ofIBM:
return "IBM" return "IBM"
case .ofMiTAC: case .ofMiTAC:
return "MiTAC" return "MiTAC"
case .ofFakeSeigyou: case .ofFakeSeigyou:
return "FakeSeigyou" return "FakeSeigyou"
case .ofHanyuPinyin: case .ofHanyuPinyin:
return "HanyuPinyin" return "HanyuPinyin"
} }
} }
} }
// MARK: - // MARK: -
public class mgrPrefs: NSObject { public class mgrPrefs: NSObject {
static var allKeys: [String] { static var allKeys: [String] {
[ [
UserDef.kIsDebugModeEnabled, UserDef.kIsDebugModeEnabled,
UserDef.kMostRecentInputMode, UserDef.kMostRecentInputMode,
UserDef.kUserDataFolderSpecified, UserDef.kUserDataFolderSpecified,
UserDef.kMandarinParser, UserDef.kMandarinParser,
UserDef.kBasicKeyboardLayout, UserDef.kBasicKeyboardLayout,
UserDef.kShowPageButtonsInCandidateWindow, UserDef.kShowPageButtonsInCandidateWindow,
UserDef.kCandidateListTextSize, UserDef.kCandidateListTextSize,
UserDef.kAppleLanguages, UserDef.kAppleLanguages,
UserDef.kShouldAutoReloadUserDataFiles, UserDef.kShouldAutoReloadUserDataFiles,
UserDef.kSetRearCursorMode, UserDef.kSetRearCursorMode,
UserDef.kUseHorizontalCandidateList, UserDef.kUseHorizontalCandidateList,
UserDef.kComposingBufferSize, UserDef.kComposingBufferSize,
UserDef.kChooseCandidateUsingSpace, UserDef.kChooseCandidateUsingSpace,
UserDef.kCNS11643Enabled, UserDef.kCNS11643Enabled,
UserDef.kSymbolInputEnabled, UserDef.kSymbolInputEnabled,
UserDef.kChineseConversionEnabled, UserDef.kChineseConversionEnabled,
UserDef.kShiftJISShinjitaiOutputEnabled, UserDef.kShiftJISShinjitaiOutputEnabled,
UserDef.kHalfWidthPunctuationEnabled, UserDef.kHalfWidthPunctuationEnabled,
UserDef.kSpecifyShiftTabKeyBehavior, UserDef.kSpecifyShiftTabKeyBehavior,
UserDef.kSpecifyShiftSpaceKeyBehavior, UserDef.kSpecifyShiftSpaceKeyBehavior,
UserDef.kEscToCleanInputBuffer, UserDef.kEscToCleanInputBuffer,
UserDef.kCandidateTextFontName, UserDef.kCandidateTextFontName,
UserDef.kCandidateKeyLabelFontName, UserDef.kCandidateKeyLabelFontName,
UserDef.kCandidateKeys, UserDef.kCandidateKeys,
UserDef.kMoveCursorAfterSelectingCandidate, UserDef.kMoveCursorAfterSelectingCandidate,
UserDef.kPhraseReplacementEnabled, UserDef.kPhraseReplacementEnabled,
UserDef.kUseSCPCTypingMode, UserDef.kUseSCPCTypingMode,
UserDef.kMaxCandidateLength, UserDef.kMaxCandidateLength,
UserDef.kShouldNotFartInLieuOfBeep, UserDef.kShouldNotFartInLieuOfBeep,
UserDef.kAssociatedPhrasesEnabled, UserDef.kAssociatedPhrasesEnabled,
] ]
} }
// MARK: - Preferences Module plist // MARK: - Preferences Module plist
@objc public static func setMissingDefaults() { public static func setMissingDefaults() {
UserDefaults.standard.setDefault(mgrPrefs.isDebugModeEnabled, forKey: UserDef.kIsDebugModeEnabled) UserDefaults.standard.setDefault(mgrPrefs.isDebugModeEnabled, forKey: UserDef.kIsDebugModeEnabled)
UserDefaults.standard.setDefault(mgrPrefs.mostRecentInputMode, forKey: UserDef.kMostRecentInputMode) UserDefaults.standard.setDefault(mgrPrefs.mostRecentInputMode, forKey: UserDef.kMostRecentInputMode)
UserDefaults.standard.setDefault(mgrPrefs.checkUpdateAutomatically, forKey: UserDef.kCheckUpdateAutomatically) UserDefaults.standard.setDefault(mgrPrefs.checkUpdateAutomatically, forKey: UserDef.kCheckUpdateAutomatically)
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.showPageButtonsInCandidateWindow, forKey: UserDef.kShowPageButtonsInCandidateWindow mgrPrefs.showPageButtonsInCandidateWindow, forKey: UserDef.kShowPageButtonsInCandidateWindow
) )
UserDefaults.standard.setDefault(mgrPrefs.symbolInputEnabled, forKey: UserDef.kSymbolInputEnabled) UserDefaults.standard.setDefault(mgrPrefs.symbolInputEnabled, forKey: UserDef.kSymbolInputEnabled)
UserDefaults.standard.setDefault(mgrPrefs.candidateListTextSize, forKey: UserDef.kCandidateListTextSize) UserDefaults.standard.setDefault(mgrPrefs.candidateListTextSize, forKey: UserDef.kCandidateListTextSize)
UserDefaults.standard.setDefault(mgrPrefs.chooseCandidateUsingSpace, forKey: UserDef.kChooseCandidateUsingSpace) UserDefaults.standard.setDefault(mgrPrefs.chooseCandidateUsingSpace, forKey: UserDef.kChooseCandidateUsingSpace)
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.shouldAutoReloadUserDataFiles, forKey: UserDef.kShouldAutoReloadUserDataFiles mgrPrefs.shouldAutoReloadUserDataFiles, forKey: UserDef.kShouldAutoReloadUserDataFiles
) )
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.specifyShiftTabKeyBehavior, forKey: UserDef.kSpecifyShiftTabKeyBehavior mgrPrefs.specifyShiftTabKeyBehavior, forKey: UserDef.kSpecifyShiftTabKeyBehavior
) )
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.specifyShiftSpaceKeyBehavior, forKey: UserDef.kSpecifyShiftSpaceKeyBehavior mgrPrefs.specifyShiftSpaceKeyBehavior, forKey: UserDef.kSpecifyShiftSpaceKeyBehavior
) )
UserDefaults.standard.setDefault(mgrPrefs.useSCPCTypingMode, forKey: UserDef.kUseSCPCTypingMode) UserDefaults.standard.setDefault(mgrPrefs.useSCPCTypingMode, forKey: UserDef.kUseSCPCTypingMode)
UserDefaults.standard.setDefault(mgrPrefs.associatedPhrasesEnabled, forKey: UserDef.kAssociatedPhrasesEnabled) UserDefaults.standard.setDefault(mgrPrefs.associatedPhrasesEnabled, forKey: UserDef.kAssociatedPhrasesEnabled)
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.setRearCursorMode, forKey: UserDef.kSetRearCursorMode mgrPrefs.setRearCursorMode, forKey: UserDef.kSetRearCursorMode
) )
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.moveCursorAfterSelectingCandidate, forKey: UserDef.kMoveCursorAfterSelectingCandidate mgrPrefs.moveCursorAfterSelectingCandidate, forKey: UserDef.kMoveCursorAfterSelectingCandidate
) )
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.useHorizontalCandidateList, forKey: UserDef.kUseHorizontalCandidateList mgrPrefs.useHorizontalCandidateList, forKey: UserDef.kUseHorizontalCandidateList
) )
UserDefaults.standard.setDefault(mgrPrefs.cns11643Enabled, forKey: UserDef.kCNS11643Enabled) UserDefaults.standard.setDefault(mgrPrefs.cns11643Enabled, forKey: UserDef.kCNS11643Enabled)
UserDefaults.standard.setDefault(mgrPrefs.chineseConversionEnabled, forKey: UserDef.kChineseConversionEnabled) UserDefaults.standard.setDefault(mgrPrefs.chineseConversionEnabled, forKey: UserDef.kChineseConversionEnabled)
UserDefaults.standard.setDefault( UserDefaults.standard.setDefault(
mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled
) )
UserDefaults.standard.setDefault(mgrPrefs.phraseReplacementEnabled, forKey: UserDef.kPhraseReplacementEnabled) UserDefaults.standard.setDefault(mgrPrefs.phraseReplacementEnabled, forKey: UserDef.kPhraseReplacementEnabled)
UserDefaults.standard.setDefault(mgrPrefs.shouldNotFartInLieuOfBeep, forKey: UserDef.kShouldNotFartInLieuOfBeep) UserDefaults.standard.setDefault(mgrPrefs.shouldNotFartInLieuOfBeep, forKey: UserDef.kShouldNotFartInLieuOfBeep)
UserDefaults.standard.synchronize() UserDefaults.standard.synchronize()
} }
@UserDefault(key: UserDef.kIsDebugModeEnabled, defaultValue: false) @UserDefault(key: UserDef.kIsDebugModeEnabled, defaultValue: false)
@objc static var isDebugModeEnabled: Bool static var isDebugModeEnabled: Bool
@UserDefault(key: UserDef.kMostRecentInputMode, defaultValue: "") @UserDefault(key: UserDef.kMostRecentInputMode, defaultValue: "")
@objc static var mostRecentInputMode: String static var mostRecentInputMode: String
@UserDefault(key: UserDef.kCheckUpdateAutomatically, defaultValue: false) @UserDefault(key: UserDef.kCheckUpdateAutomatically, defaultValue: false)
@objc static var checkUpdateAutomatically: Bool static var checkUpdateAutomatically: Bool
@UserDefault(key: UserDef.kUserDataFolderSpecified, defaultValue: "") @UserDefault(key: UserDef.kUserDataFolderSpecified, defaultValue: "")
@objc static var userDataFolderSpecified: String static var userDataFolderSpecified: String
@objc static func ifSpecifiedUserDataPathExistsInPlist() -> Bool { static func ifSpecifiedUserDataPathExistsInPlist() -> Bool {
UserDefaults.standard.object(forKey: UserDef.kUserDataFolderSpecified) != nil UserDefaults.standard.object(forKey: UserDef.kUserDataFolderSpecified) != nil
} }
@objc static func resetSpecifiedUserDataFolder() { static func resetSpecifiedUserDataFolder() {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified") UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified")
IME.initLangModels(userOnly: true) IME.initLangModels(userOnly: true)
} }
@UserDefault(key: UserDef.kAppleLanguages, defaultValue: []) @UserDefault(key: UserDef.kAppleLanguages, defaultValue: [])
@objc static var appleLanguages: [String] static var appleLanguages: [String]
@UserDefault(key: UserDef.kMandarinParser, defaultValue: 0) @UserDefault(key: UserDef.kMandarinParser, defaultValue: 0)
@objc static var mandarinParser: Int @objc static var mandarinParser: Int
@objc static var mandarinParserName: String { static var mandarinParserName: String {
(MandarinParser(rawValue: mandarinParser) ?? MandarinParser.ofStandard).name (MandarinParser(rawValue: mandarinParser) ?? MandarinParser.ofStandard).name
} }
@UserDefault( @UserDefault(
key: UserDef.kBasicKeyboardLayout, defaultValue: "com.apple.keylayout.ZhuyinBopomofo" key: UserDef.kBasicKeyboardLayout, defaultValue: "com.apple.keylayout.ZhuyinBopomofo"
) )
@objc static var basicKeyboardLayout: String static var basicKeyboardLayout: String
@UserDefault(key: UserDef.kShowPageButtonsInCandidateWindow, defaultValue: true) @UserDefault(key: UserDef.kShowPageButtonsInCandidateWindow, defaultValue: true)
@objc static var showPageButtonsInCandidateWindow: Bool static var showPageButtonsInCandidateWindow: Bool
@CandidateListTextSize(key: UserDef.kCandidateListTextSize) @CandidateListTextSize(key: UserDef.kCandidateListTextSize)
@objc static var candidateListTextSize: CGFloat static var candidateListTextSize: CGFloat
@UserDefault(key: UserDef.kShouldAutoReloadUserDataFiles, defaultValue: true) @UserDefault(key: UserDef.kShouldAutoReloadUserDataFiles, defaultValue: true)
@objc static var shouldAutoReloadUserDataFiles: Bool static var shouldAutoReloadUserDataFiles: Bool
@UserDefault(key: UserDef.kSetRearCursorMode, defaultValue: false) @UserDefault(key: UserDef.kSetRearCursorMode, defaultValue: false)
@objc static var setRearCursorMode: Bool static var setRearCursorMode: Bool
@UserDefault(key: UserDef.kMoveCursorAfterSelectingCandidate, defaultValue: true) @UserDefault(key: UserDef.kMoveCursorAfterSelectingCandidate, defaultValue: true)
@objc static var moveCursorAfterSelectingCandidate: Bool static var moveCursorAfterSelectingCandidate: Bool
@UserDefault(key: UserDef.kUseHorizontalCandidateList, defaultValue: true) @UserDefault(key: UserDef.kUseHorizontalCandidateList, defaultValue: true)
@objc static var useHorizontalCandidateList: Bool static var useHorizontalCandidateList: Bool
@ComposingBufferSize(key: UserDef.kComposingBufferSize) @ComposingBufferSize(key: UserDef.kComposingBufferSize)
@objc static var composingBufferSize: Int static var composingBufferSize: Int
@UserDefault(key: UserDef.kChooseCandidateUsingSpace, defaultValue: true) @UserDefault(key: UserDef.kChooseCandidateUsingSpace, defaultValue: true)
@objc static var chooseCandidateUsingSpace: Bool static var chooseCandidateUsingSpace: Bool
@UserDefault(key: UserDef.kUseSCPCTypingMode, defaultValue: false) @UserDefault(key: UserDef.kUseSCPCTypingMode, defaultValue: false)
@objc static var useSCPCTypingMode: Bool static var useSCPCTypingMode: Bool
@objc static func toggleSCPCTypingModeEnabled() -> Bool { static func toggleSCPCTypingModeEnabled() -> Bool {
useSCPCTypingMode = !useSCPCTypingMode useSCPCTypingMode = !useSCPCTypingMode
UserDefaults.standard.set(useSCPCTypingMode, forKey: UserDef.kUseSCPCTypingMode) UserDefaults.standard.set(useSCPCTypingMode, forKey: UserDef.kUseSCPCTypingMode)
return useSCPCTypingMode return useSCPCTypingMode
} }
@UserDefault(key: UserDef.kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2) @UserDefault(key: UserDef.kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2)
@objc static var maxCandidateLength: Int static var maxCandidateLength: Int
@UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep, defaultValue: true) @UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep, defaultValue: true)
@objc static var shouldNotFartInLieuOfBeep: Bool static var shouldNotFartInLieuOfBeep: Bool
@objc static func toggleShouldNotFartInLieuOfBeep() -> Bool { static func toggleShouldNotFartInLieuOfBeep() -> Bool {
shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep
UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: UserDef.kShouldNotFartInLieuOfBeep) UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: UserDef.kShouldNotFartInLieuOfBeep)
return shouldNotFartInLieuOfBeep return shouldNotFartInLieuOfBeep
} }
@UserDefault(key: UserDef.kCNS11643Enabled, defaultValue: false) @UserDefault(key: UserDef.kCNS11643Enabled, defaultValue: false)
@objc static var cns11643Enabled: Bool static var cns11643Enabled: Bool
@objc static func toggleCNS11643Enabled() -> Bool { static func toggleCNS11643Enabled() -> Bool {
cns11643Enabled = !cns11643Enabled cns11643Enabled = !cns11643Enabled
mgrLangModel.setCNSEnabled(cns11643Enabled) // mgrLangModel.setCNSEnabled(cns11643Enabled) //
UserDefaults.standard.set(cns11643Enabled, forKey: UserDef.kCNS11643Enabled) UserDefaults.standard.set(cns11643Enabled, forKey: UserDef.kCNS11643Enabled)
return cns11643Enabled return cns11643Enabled
} }
@UserDefault(key: UserDef.kSymbolInputEnabled, defaultValue: true) @UserDefault(key: UserDef.kSymbolInputEnabled, defaultValue: true)
@objc static var symbolInputEnabled: Bool static var symbolInputEnabled: Bool
@objc static func toggleSymbolInputEnabled() -> Bool { static func toggleSymbolInputEnabled() -> Bool {
symbolInputEnabled = !symbolInputEnabled symbolInputEnabled = !symbolInputEnabled
mgrLangModel.setSymbolEnabled(symbolInputEnabled) // mgrLangModel.setSymbolEnabled(symbolInputEnabled) //
UserDefaults.standard.set(symbolInputEnabled, forKey: UserDef.kSymbolInputEnabled) UserDefaults.standard.set(symbolInputEnabled, forKey: UserDef.kSymbolInputEnabled)
return symbolInputEnabled return symbolInputEnabled
} }
@UserDefault(key: UserDef.kChineseConversionEnabled, defaultValue: false) @UserDefault(key: UserDef.kChineseConversionEnabled, defaultValue: false)
@objc static var chineseConversionEnabled: Bool static var chineseConversionEnabled: Bool
@objc @discardableResult static func toggleChineseConversionEnabled() -> Bool { @discardableResult static func toggleChineseConversionEnabled() -> Bool {
chineseConversionEnabled = !chineseConversionEnabled chineseConversionEnabled = !chineseConversionEnabled
// JIS // JIS
if chineseConversionEnabled, shiftJISShinjitaiOutputEnabled { if chineseConversionEnabled, shiftJISShinjitaiOutputEnabled {
toggleShiftJISShinjitaiOutputEnabled() toggleShiftJISShinjitaiOutputEnabled()
UserDefaults.standard.set( UserDefaults.standard.set(
shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled
) )
} }
UserDefaults.standard.set(chineseConversionEnabled, forKey: UserDef.kChineseConversionEnabled) UserDefaults.standard.set(chineseConversionEnabled, forKey: UserDef.kChineseConversionEnabled)
return chineseConversionEnabled return chineseConversionEnabled
} }
@UserDefault(key: UserDef.kShiftJISShinjitaiOutputEnabled, defaultValue: false) @UserDefault(key: UserDef.kShiftJISShinjitaiOutputEnabled, defaultValue: false)
@objc static var shiftJISShinjitaiOutputEnabled: Bool static var shiftJISShinjitaiOutputEnabled: Bool
@objc @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool { @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool {
shiftJISShinjitaiOutputEnabled = !shiftJISShinjitaiOutputEnabled shiftJISShinjitaiOutputEnabled = !shiftJISShinjitaiOutputEnabled
// JIS // JIS
if shiftJISShinjitaiOutputEnabled, chineseConversionEnabled { if shiftJISShinjitaiOutputEnabled, chineseConversionEnabled {
toggleChineseConversionEnabled() toggleChineseConversionEnabled()
} }
UserDefaults.standard.set( UserDefaults.standard.set(
shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled
) )
return shiftJISShinjitaiOutputEnabled return shiftJISShinjitaiOutputEnabled
} }
@UserDefault(key: UserDef.kHalfWidthPunctuationEnabled, defaultValue: false) @UserDefault(key: UserDef.kHalfWidthPunctuationEnabled, defaultValue: false)
@objc static var halfWidthPunctuationEnabled: Bool static var halfWidthPunctuationEnabled: Bool
@objc static func toggleHalfWidthPunctuationEnabled() -> Bool { static func toggleHalfWidthPunctuationEnabled() -> Bool {
halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled
return halfWidthPunctuationEnabled return halfWidthPunctuationEnabled
} }
@UserDefault(key: UserDef.kEscToCleanInputBuffer, defaultValue: true) @UserDefault(key: UserDef.kEscToCleanInputBuffer, defaultValue: true)
@objc static var escToCleanInputBuffer: Bool static var escToCleanInputBuffer: Bool
@UserDefault(key: UserDef.kSpecifyShiftTabKeyBehavior, defaultValue: false) @UserDefault(key: UserDef.kSpecifyShiftTabKeyBehavior, defaultValue: false)
@objc static var specifyShiftTabKeyBehavior: Bool static var specifyShiftTabKeyBehavior: Bool
@UserDefault(key: UserDef.kSpecifyShiftSpaceKeyBehavior, defaultValue: false) @UserDefault(key: UserDef.kSpecifyShiftSpaceKeyBehavior, defaultValue: false)
@objc static var specifyShiftSpaceKeyBehavior: Bool static var specifyShiftSpaceKeyBehavior: Bool
// MARK: - Optional settings // MARK: - Optional settings
@UserDefault(key: UserDef.kCandidateTextFontName, defaultValue: nil) @UserDefault(key: UserDef.kCandidateTextFontName, defaultValue: nil)
@objc static var candidateTextFontName: String? static var candidateTextFontName: String?
@UserDefault(key: UserDef.kCandidateKeyLabelFontName, defaultValue: nil) @UserDefault(key: UserDef.kCandidateKeyLabelFontName, defaultValue: nil)
@objc static var candidateKeyLabelFontName: String? static var candidateKeyLabelFontName: String?
@UserDefault(key: UserDef.kCandidateKeys, defaultValue: kDefaultKeys) @UserDefault(key: UserDef.kCandidateKeys, defaultValue: kDefaultKeys)
@objc static var candidateKeys: String static var candidateKeys: String
@objc static var defaultCandidateKeys: String { static var defaultCandidateKeys: String {
kDefaultKeys kDefaultKeys
} }
@objc static var suggestedCandidateKeys: [String] { static var suggestedCandidateKeys: [String] {
[kDefaultKeys, "234567890", "QWERTYUIO", "QWERTASDF", "ASDFGHJKL", "ASDFZXCVB"] [kDefaultKeys, "234567890", "QWERTYUIO", "QWERTASDF", "ASDFGHJKL", "ASDFZXCVB"]
} }
@objc static func validate(candidateKeys: String) throws { static func validate(candidateKeys: String) throws {
let trimmed = candidateKeys.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = candidateKeys.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { if trimmed.isEmpty {
throw CandidateKeyError.empty throw CandidateKeyError.empty
} }
if !trimmed.canBeConverted(to: .ascii) { if !trimmed.canBeConverted(to: .ascii) {
throw CandidateKeyError.invalidCharacters throw CandidateKeyError.invalidCharacters
} }
if trimmed.contains(" ") { if trimmed.contains(" ") {
throw CandidateKeyError.containSpace throw CandidateKeyError.containSpace
} }
if trimmed.count < 4 { if trimmed.count < 4 {
throw CandidateKeyError.tooShort throw CandidateKeyError.tooShort
} }
if trimmed.count > 15 { if trimmed.count > 15 {
throw CandidateKeyError.tooLong throw CandidateKeyError.tooLong
} }
let set = Set(Array(trimmed)) let set = Set(Array(trimmed))
if set.count != trimmed.count { if set.count != trimmed.count {
throw CandidateKeyError.duplicatedCharacters throw CandidateKeyError.duplicatedCharacters
} }
} }
enum CandidateKeyError: Error, LocalizedError { enum CandidateKeyError: Error, LocalizedError {
case empty case empty
case invalidCharacters case invalidCharacters
case containSpace case containSpace
case duplicatedCharacters case duplicatedCharacters
case tooShort case tooShort
case tooLong case tooLong
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .empty: case .empty:
return NSLocalizedString("Candidates keys cannot be empty.", comment: "") return NSLocalizedString("Candidates keys cannot be empty.", comment: "")
case .invalidCharacters: case .invalidCharacters:
return NSLocalizedString( return NSLocalizedString(
"Candidate keys can only contain ASCII characters like alphanumericals.", "Candidate keys can only contain ASCII characters like alphanumericals.",
comment: "" comment: ""
) )
case .containSpace: case .containSpace:
return NSLocalizedString("Candidate keys cannot contain space.", comment: "") return NSLocalizedString("Candidate keys cannot contain space.", comment: "")
case .duplicatedCharacters: case .duplicatedCharacters:
return NSLocalizedString("There should not be duplicated keys.", comment: "") return NSLocalizedString("There should not be duplicated keys.", comment: "")
case .tooShort: case .tooShort:
return NSLocalizedString( return NSLocalizedString(
"Please specify at least 4 candidate keys.", comment: "" "Please specify at least 4 candidate keys.", comment: ""
) )
case .tooLong: case .tooLong:
return NSLocalizedString("Maximum 15 candidate keys allowed.", comment: "") return NSLocalizedString("Maximum 15 candidate keys allowed.", comment: "")
} }
} }
} }
@UserDefault(key: UserDef.kPhraseReplacementEnabled, defaultValue: false) @UserDefault(key: UserDef.kPhraseReplacementEnabled, defaultValue: false)
@objc static var phraseReplacementEnabled: Bool static var phraseReplacementEnabled: Bool
@objc static func togglePhraseReplacementEnabled() -> Bool { static func togglePhraseReplacementEnabled() -> Bool {
phraseReplacementEnabled = !phraseReplacementEnabled phraseReplacementEnabled = !phraseReplacementEnabled
mgrLangModel.setPhraseReplacementEnabled(phraseReplacementEnabled) mgrLangModel.setPhraseReplacementEnabled(phraseReplacementEnabled)
UserDefaults.standard.set(phraseReplacementEnabled, forKey: UserDef.kPhraseReplacementEnabled) UserDefaults.standard.set(phraseReplacementEnabled, forKey: UserDef.kPhraseReplacementEnabled)
return phraseReplacementEnabled return phraseReplacementEnabled
} }
@UserDefault(key: UserDef.kAssociatedPhrasesEnabled, defaultValue: false) @UserDefault(key: UserDef.kAssociatedPhrasesEnabled, defaultValue: false)
@objc static var associatedPhrasesEnabled: Bool static var associatedPhrasesEnabled: Bool
@objc static func toggleAssociatedPhrasesEnabled() -> Bool { static func toggleAssociatedPhrasesEnabled() -> Bool {
associatedPhrasesEnabled = !associatedPhrasesEnabled associatedPhrasesEnabled = !associatedPhrasesEnabled
UserDefaults.standard.set(associatedPhrasesEnabled, forKey: UserDef.kAssociatedPhrasesEnabled) UserDefaults.standard.set(associatedPhrasesEnabled, forKey: UserDef.kAssociatedPhrasesEnabled)
return associatedPhrasesEnabled return associatedPhrasesEnabled
} }
} }

View File

@ -0,0 +1,167 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
extension vChewing {
public enum LMConsolidator {
public static let kPragmaHeader = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍"
public static func checkPragma(path: String) -> Bool {
if FileManager.default.fileExists(atPath: path) {
let fileHandle = FileHandle(forReadingAtPath: path)!
do {
let lineReader = try LineReader(file: fileHandle)
for strLine in lineReader { // i=0
if strLine != kPragmaHeader {
IME.prtDebugIntel("Header Mismatch, Starting In-Place Consolidation.")
return false
} else {
IME.prtDebugIntel("Header Verification Succeeded: \(strLine).")
return true
}
}
} catch {
IME.prtDebugIntel("Header Verification Failed: File Access Error.")
return false
}
}
IME.prtDebugIntel("Header Verification Failed: File Missing.")
return false
}
@discardableResult public static func fixEOF(path: String) -> Bool {
let urlPath = URL(fileURLWithPath: path)
if FileManager.default.fileExists(atPath: path) {
var strIncoming = ""
do {
strIncoming += try String(contentsOf: urlPath, encoding: .utf8)
if !strIncoming.hasSuffix("\n") {
IME.prtDebugIntel("EOF Fix Necessity Confirmed, Start Fixing.")
if let writeFile = FileHandle(forUpdatingAtPath: path),
let endl = "\n".data(using: .utf8)
{
writeFile.seekToEndOfFile()
writeFile.write(endl)
writeFile.closeFile()
} else {
return false
}
}
} catch {
IME.prtDebugIntel("EOF Fix Failed w/ File: \(path)")
IME.prtDebugIntel("EOF Fix Failed w/ Error: \(error).")
return false
}
IME.prtDebugIntel("EOF Successfully Ensured (with possible autofixes performed).")
return true
}
IME.prtDebugIntel("EOF Fix Failed: File Missing at \(path).")
return false
}
@discardableResult public static func consolidate(path: String, pragma shouldCheckPragma: Bool) -> Bool {
var pragmaResult = false
if shouldCheckPragma {
pragmaResult = checkPragma(path: path)
if pragmaResult {
return true
}
}
let urlPath = URL(fileURLWithPath: path)
if FileManager.default.fileExists(atPath: path) {
var strProcessed = ""
do {
strProcessed += try String(contentsOf: urlPath, encoding: .utf8)
// Step 1: Consolidating formats per line.
// -------
// CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space
// ASCII
strProcessed.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
//
strProcessed.regReplace(pattern: #"(^ | $)"#, replaceWith: "")
// CR & FF to LF,
strProcessed.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n")
if strProcessed.prefix(1) == " " { //
strProcessed.removeFirst()
}
if strProcessed.suffix(1) == " " { //
strProcessed.removeLast()
}
// Step 3: Add Formatted Pragma, the Sorted Header:
if !pragmaResult {
strProcessed = kPragmaHeader + "\n" + strProcessed // Add Sorted Header
}
// Step 4: Deduplication.
let arrData = strProcessed.components(separatedBy: "\n")
strProcessed = "" // Reset its value
// reversed override
let arrDataDeduplicated = Array(NSOrderedSet(array: arrData.reversed()).array as! [String])
for lineData in arrDataDeduplicated.reversed() {
strProcessed += lineData
strProcessed += "\n"
}
// Step 5: Remove duplicated newlines at the end of the file.
strProcessed.regReplace(pattern: "\\n+", replaceWith: "\n")
// Step 6: Write consolidated file contents.
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
} catch {
IME.prtDebugIntel("Consolidation Failed w/ File: \(path)")
IME.prtDebugIntel("Consolidation Failed w/ Error: \(error).")
return false
}
IME.prtDebugIntel("Either Consolidation Successful Or No-Need-To-Consolidate.")
return true
}
IME.prtDebugIntel("Consolidation Failed: File Missing at \(path).")
return false
}
}
}
// MARK: - String Extension
extension String {
fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do {
let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
)
let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith
)
} catch { return }
}
}

View File

@ -1,167 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef LMInstantiator_H
#define LMInstantiator_H
#include "AssociatedPhrases.h"
#include "CNSLM.h"
#include "CoreLM.h"
#include "ParselessLM.h"
#include "PhraseReplacementMap.h"
#include "SymbolLM.h"
#include "UserPhrasesLM.h"
#include "UserSymbolLM.h"
#include <stdio.h>
#include <unordered_set>
namespace vChewing
{
using namespace Gramambular;
/// LMInstantiator is a facade for managing a set of models including
/// the input method language model, user phrases and excluded phrases.
///
/// It is the primary model class that the input controller and grammar builder
/// of vChewing talks to. When the grammar builder starts to build a sentence
/// from a series of BPMF readings, it passes the readings to the model to see
/// if there are valid unigrams, and use returned unigrams to produce the final
/// results.
///
/// LMInstantiator combine and transform the unigrams from the primary language
/// model and user phrases. The process is
///
/// 1) Get the original unigrams.
/// 2) Drop the unigrams whose value is contained in the exclusion map.
/// 3) Replace the values of the unigrams using the phrase replacement map.
/// 4) Replace the values of the unigrams using an external converter lambda.
/// 5) Drop the duplicated phrases.
///
/// The controller can ask the model to load the primary input method language
/// model while launching and to load the user phrases anytime if the custom
/// files are modified. It does not keep the reference of the data pathes but
/// you have to pass the paths when you ask it to do loading.
class LMInstantiator : public Gramambular::LanguageModel
{
public:
LMInstantiator();
~LMInstantiator();
/// Asks to load the primary language model at the given path.
/// @param languageModelPath The path of the language model.
void loadLanguageModel(const char *languageModelPath);
/// If the data model is already loaded.
bool isDataModelLoaded();
/// Asks to load the primary language model at the given path.
/// @param miscDataPath The path of the misc data model.
void loadMiscData(const char *miscDataPath);
/// If the data model is already loaded.
bool isMiscDataLoaded();
/// Asks to load the primary language model at the given path.
/// @param symbolDataPath The path of the symbol data model.
void loadSymbolData(const char *symbolDataPath);
/// If the data model is already loaded.
bool isSymbolDataLoaded();
/// Asks to load the primary language model at the given path.
/// @param cnsDataPath The path of the CNS data model.
void loadCNSData(const char *cnsDataPath);
/// If the data model is already loaded.
bool isCNSDataLoaded();
/// Asks to load the user phrases and excluded phrases at the given path.
/// @param userPhrasesPath The path of user phrases.
/// @param excludedPhrasesPath The path of excluded phrases.
void loadUserPhrases(const char *userPhrasesPath, const char *excludedPhrasesPath);
/// Asks to load the user symbol data at the given path.
/// @param userSymbolDataPath The path of user symbol data.
void loadUserSymbolData(const char *userPhrasesPath);
/// Asks to load the user associated phrases at the given path.
/// @param userAssociatedPhrasesPath The path of the user associated phrases.
void loadUserAssociatedPhrases(const char *userAssociatedPhrasesPath);
/// Asks to load the phrase replacement table at the given path.
/// @param phraseReplacementPath The path of the phrase replacement table.
void loadPhraseReplacementMap(const char *phraseReplacementPath);
/// Not implemented since we do not have data to provide bigram function.
const std::vector<Gramambular::Bigram> bigramsForKeys(const std::string &preceedingKey, const std::string &key);
/// Returns a list of available unigram for the given key.
/// @param key A std::string represents the BPMF reading or a symbol key. For
/// example, it you pass "ㄇㄚ", it returns "嗎", "媽", and so on.
const std::vector<Gramambular::Unigram> unigramsForKey(const std::string &key);
/// If the model has unigrams for the given key.
/// @param key The key.
bool hasUnigramsForKey(const std::string &key);
/// Enables or disables phrase replacement.
void setPhraseReplacementEnabled(bool enabled);
/// If phrase replacement is enabled or not.
bool phraseReplacementEnabled();
/// Enables or disables symbol input.
void setSymbolEnabled(bool enabled);
/// If symbol input is enabled or not.
bool symbolEnabled();
/// Enables or disables CNS11643 input.
void setCNSEnabled(bool enabled);
/// If CNS11643 input is enabled or not.
bool cnsEnabled();
const std::vector<std::string> associatedPhrasesForKey(const std::string &key);
bool hasAssociatedPhrasesForKey(const std::string &key);
protected:
/// Filters and converts the input unigrams and return a new list of unigrams.
///
/// @param unigrams The unigrams to be processed.
/// @param excludedValues The values to excluded unigrams.
/// @param insertedValues The values for unigrams already in the results.
/// It helps to prevent duplicated unigrams. Please note that the method
/// has a side effect that it inserts values to `insertedValues`.
const std::vector<Gramambular::Unigram> filterAndTransformUnigrams(
const std::vector<Gramambular::Unigram> unigrams, const std::unordered_set<std::string> &excludedValues,
std::unordered_set<std::string> &insertedValues);
ParselessLM m_languageModel;
CoreLM m_miscModel;
SymbolLM m_symbolModel;
CNSLM m_cnsModel;
UserPhrasesLM m_userPhrases;
UserPhrasesLM m_excludedPhrases;
UserSymbolLM m_userSymbolModel;
PhraseReplacementMap m_phraseReplacement;
AssociatedPhrases m_associatedPhrases;
bool m_phraseReplacementEnabled;
bool m_cnsEnabled;
bool m_symbolEnabled;
};
}; // namespace vChewing
#endif

View File

@ -1,323 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "LMInstantiator.h"
#include <algorithm>
#include <iterator>
namespace vChewing
{
LMInstantiator::LMInstantiator()
{
}
LMInstantiator::~LMInstantiator()
{
m_languageModel.close();
m_miscModel.close();
m_userPhrases.close();
m_userSymbolModel.close();
m_cnsModel.close();
m_excludedPhrases.close();
m_phraseReplacement.close();
m_associatedPhrases.close();
}
void LMInstantiator::loadLanguageModel(const char *languageModelDataPath)
{
if (languageModelDataPath)
{
m_languageModel.close();
m_languageModel.open(languageModelDataPath);
}
}
bool LMInstantiator::isDataModelLoaded()
{
return m_languageModel.isLoaded();
}
void LMInstantiator::loadCNSData(const char *cnsDataPath)
{
if (cnsDataPath)
{
m_cnsModel.close();
m_cnsModel.open(cnsDataPath);
}
}
bool LMInstantiator::isCNSDataLoaded()
{
return m_cnsModel.isLoaded();
}
void LMInstantiator::loadMiscData(const char *miscDataPath)
{
if (miscDataPath)
{
m_miscModel.close();
m_miscModel.open(miscDataPath);
}
}
bool LMInstantiator::isMiscDataLoaded()
{
return m_miscModel.isLoaded();
}
void LMInstantiator::loadSymbolData(const char *symbolDataPath)
{
if (symbolDataPath)
{
m_symbolModel.close();
m_symbolModel.open(symbolDataPath);
}
}
bool LMInstantiator::isSymbolDataLoaded()
{
return m_symbolModel.isLoaded();
}
void LMInstantiator::loadUserPhrases(const char *userPhrasesDataPath, const char *excludedPhrasesDataPath)
{
if (userPhrasesDataPath)
{
m_userPhrases.close();
m_userPhrases.open(userPhrasesDataPath);
}
if (excludedPhrasesDataPath)
{
m_excludedPhrases.close();
m_excludedPhrases.open(excludedPhrasesDataPath);
}
}
void LMInstantiator::loadUserSymbolData(const char *userSymbolDataPath)
{
if (userSymbolDataPath)
{
m_userSymbolModel.close();
m_userSymbolModel.open(userSymbolDataPath);
}
}
void LMInstantiator::loadUserAssociatedPhrases(const char *userAssociatedPhrasesPath)
{
if (userAssociatedPhrasesPath)
{
m_associatedPhrases.close();
m_associatedPhrases.open(userAssociatedPhrasesPath);
}
}
void LMInstantiator::loadPhraseReplacementMap(const char *phraseReplacementPath)
{
if (phraseReplacementPath)
{
m_phraseReplacement.close();
m_phraseReplacement.open(phraseReplacementPath);
}
}
const std::vector<Gramambular::Bigram> LMInstantiator::bigramsForKeys(const std::string &preceedingKey,
const std::string &key)
{
return std::vector<Gramambular::Bigram>();
}
const std::vector<Gramambular::Unigram> LMInstantiator::unigramsForKey(const std::string &key)
{
if (key == " ")
{
std::vector<Gramambular::Unigram> spaceUnigrams;
Gramambular::Unigram g;
g.keyValue.key = " ";
g.keyValue.value = " ";
g.score = 0;
spaceUnigrams.push_back(g);
return spaceUnigrams;
}
std::vector<Gramambular::Unigram> allUnigrams;
std::vector<Gramambular::Unigram> miscUnigrams;
std::vector<Gramambular::Unigram> symbolUnigrams;
std::vector<Gramambular::Unigram> userUnigrams;
std::vector<Gramambular::Unigram> userSymbolUnigrams;
std::vector<Gramambular::Unigram> cnsUnigrams;
std::unordered_set<std::string> excludedValues;
std::unordered_set<std::string> insertedValues;
if (m_excludedPhrases.hasUnigramsForKey(key))
{
std::vector<Gramambular::Unigram> excludedUnigrams = m_excludedPhrases.unigramsForKey(key);
transform(excludedUnigrams.begin(), excludedUnigrams.end(), inserter(excludedValues, excludedValues.end()),
[](const Gramambular::Unigram &u) { return u.keyValue.value; });
}
if (m_userPhrases.hasUnigramsForKey(key))
{
std::vector<Gramambular::Unigram> rawUserUnigrams = m_userPhrases.unigramsForKey(key);
// 用這句指令讓使用者語彙檔案內的詞條優先順序隨著行數增加而逐漸增高。
// 這樣一來就可以在就地新增語彙時徹底複寫優先權。
std::reverse(rawUserUnigrams.begin(), rawUserUnigrams.end());
userUnigrams = filterAndTransformUnigrams(rawUserUnigrams, excludedValues, insertedValues);
}
if (m_languageModel.hasUnigramsForKey(key))
{
std::vector<Gramambular::Unigram> rawGlobalUnigrams = m_languageModel.unigramsForKey(key);
allUnigrams = filterAndTransformUnigrams(rawGlobalUnigrams, excludedValues, insertedValues);
}
if (m_miscModel.hasUnigramsForKey(key))
{
std::vector<Gramambular::Unigram> rawMiscUnigrams = m_miscModel.unigramsForKey(key);
miscUnigrams = filterAndTransformUnigrams(rawMiscUnigrams, excludedValues, insertedValues);
}
if (m_symbolModel.hasUnigramsForKey(key) && m_symbolEnabled)
{
std::vector<Gramambular::Unigram> rawSymbolUnigrams = m_symbolModel.unigramsForKey(key);
symbolUnigrams = filterAndTransformUnigrams(rawSymbolUnigrams, excludedValues, insertedValues);
}
if (m_userSymbolModel.hasUnigramsForKey(key) && m_symbolEnabled)
{
std::vector<Gramambular::Unigram> rawUserSymbolUnigrams = m_userSymbolModel.unigramsForKey(key);
userSymbolUnigrams = filterAndTransformUnigrams(rawUserSymbolUnigrams, excludedValues, insertedValues);
}
if (m_cnsModel.hasUnigramsForKey(key) && m_cnsEnabled)
{
std::vector<Gramambular::Unigram> rawCNSUnigrams = m_cnsModel.unigramsForKey(key);
cnsUnigrams = filterAndTransformUnigrams(rawCNSUnigrams, excludedValues, insertedValues);
}
allUnigrams.insert(allUnigrams.begin(), userUnigrams.begin(), userUnigrams.end());
allUnigrams.insert(allUnigrams.end(), cnsUnigrams.begin(), cnsUnigrams.end());
allUnigrams.insert(allUnigrams.begin(), miscUnigrams.begin(), miscUnigrams.end());
allUnigrams.insert(allUnigrams.end(), userSymbolUnigrams.begin(), userSymbolUnigrams.end());
allUnigrams.insert(allUnigrams.end(), symbolUnigrams.begin(), symbolUnigrams.end());
return allUnigrams;
}
bool LMInstantiator::hasUnigramsForKey(const std::string &key)
{
if (key == " ")
{
return true;
}
if (!m_excludedPhrases.hasUnigramsForKey(key))
{
return m_userPhrases.hasUnigramsForKey(key) || m_languageModel.hasUnigramsForKey(key);
}
return unigramsForKey(key).size() > 0;
}
void LMInstantiator::setPhraseReplacementEnabled(bool enabled)
{
m_phraseReplacementEnabled = enabled;
}
bool LMInstantiator::phraseReplacementEnabled()
{
return m_phraseReplacementEnabled;
}
void LMInstantiator::setCNSEnabled(bool enabled)
{
m_cnsEnabled = enabled;
}
bool LMInstantiator::cnsEnabled()
{
return m_cnsEnabled;
}
void LMInstantiator::setSymbolEnabled(bool enabled)
{
m_symbolEnabled = enabled;
}
bool LMInstantiator::symbolEnabled()
{
return m_symbolEnabled;
}
const std::vector<Gramambular::Unigram> LMInstantiator::filterAndTransformUnigrams(
const std::vector<Gramambular::Unigram> unigrams, const std::unordered_set<std::string> &excludedValues,
std::unordered_set<std::string> &insertedValues)
{
std::vector<Gramambular::Unigram> results;
for (auto &&unigram : unigrams)
{
// excludedValues filters out the unigrams with the original value.
// insertedValues filters out the ones with the converted value
std::string originalValue = unigram.keyValue.value;
if (excludedValues.find(originalValue) != excludedValues.end())
{
continue;
}
std::string value = originalValue;
if (m_phraseReplacementEnabled)
{
std::string replacement = m_phraseReplacement.valueForKey(value);
if (replacement != "")
{
value = replacement;
}
}
if (insertedValues.find(value) == insertedValues.end())
{
Gramambular::Unigram g;
g.keyValue.value = value;
g.keyValue.key = unigram.keyValue.key;
g.score = unigram.score;
results.push_back(g);
insertedValues.insert(value);
}
}
return results;
}
const std::vector<std::string> LMInstantiator::associatedPhrasesForKey(const std::string &key)
{
return m_associatedPhrases.valuesForKey(key);
}
bool LMInstantiator::hasAssociatedPhrasesForKey(const std::string &key)
{
return m_associatedPhrases.hasValuesForKey(key);
}
} // namespace vChewing

View File

@ -0,0 +1,301 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// NOTE: We still keep some of the comments left by Zonble,
// regardless that he is not in charge of this Swift module
import Foundation
//
// LMInstantiator 100MB
private var lmCNS = vChewing.LMLite(consolidate: false)
private var lmSymbols = vChewing.LMCore(reverse: true, consolidate: false, defaultScore: -13.0, forceDefaultScore: true)
extension vChewing {
/// LMInstantiator is a facade for managing a set of models including
/// the input method language model, user phrases and excluded phrases.
///
/// It is the primary model class that the input controller and grammar builder
/// of vChewing talks to. When the grammar builder starts to build a sentence
/// from a series of BPMF readings, it passes the readings to the model to see
/// if there are valid unigrams, and use returned unigrams to produce the final
/// results.
///
/// LMInstantiator combine and transform the unigrams from the primary language
/// model and user phrases. The process is
///
/// 1) Get the original unigrams.
/// 2) Drop the unigrams whose value is contained in the exclusion map.
/// 3) Replace the values of the unigrams using the phrase replacement map.
/// 4) Drop the duplicated phrases from the generated unigram array.
///
/// The controller can ask the model to load the primary input method language
/// model while launching and to load the user phrases anytime if the custom
/// files are modified. It does not keep the reference of the data pathes but
/// you have to pass the paths when you ask it to load.
public class LMInstantiator: Megrez.LanguageModel {
//
public var isPhraseReplacementEnabled = false
public var isCNSEnabled = false
public var isSymbolEnabled = false
///
/// LMCore key [Unigram]
///
///
/// 使 LMLite
/// LMLite
/// LMLite LMCore
/// LMReplacements LMAssociates 使
//
/// Reverse
/// Reverse
var lmCore = LMCore(reverse: false, consolidate: false, defaultScore: -9.5, forceDefaultScore: false)
var lmMisc = LMCore(reverse: true, consolidate: false, defaultScore: -1, forceDefaultScore: false)
// 使
// 使使
var lmUserPhrases = LMLite(consolidate: true)
var lmFiltered = LMLite(consolidate: true)
var lmUserSymbols = LMLite(consolidate: true)
var lmReplacements = LMReplacments()
var lmAssociates = LMAssociates()
//
override init() {}
// 調
public func isDataModelLoaded() -> Bool { lmCore.isLoaded() }
public func loadLanguageModel(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmCore.open(path)
IME.prtDebugIntel("lmCore: \(lmCore.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmCore: File access failure: \(path)")
}
}
public func isCNSDataLoaded() -> Bool { lmCNS.isLoaded() }
public func loadCNSData(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmCNS.open(path)
IME.prtDebugIntel("lmCNS: \(lmCNS.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmCNS: File access failure: \(path)")
}
}
public func isMiscDataLoaded() -> Bool { lmMisc.isLoaded() }
public func loadMiscData(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmMisc.open(path)
IME.prtDebugIntel("lmMisc: \(lmMisc.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmMisc: File access failure: \(path)")
}
}
public func isSymbolDataLoaded() -> Bool { lmSymbols.isLoaded() }
public func loadSymbolData(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmSymbols.open(path)
IME.prtDebugIntel("lmSymbol: \(lmSymbols.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmSymbols: File access failure: \(path)")
}
}
public func loadUserPhrases(path: String, filterPath: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmUserPhrases.close()
lmUserPhrases.open(path)
IME.prtDebugIntel("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmUserPhrases: File access failure: \(path)")
}
if FileManager.default.isReadableFile(atPath: filterPath) {
lmFiltered.close()
lmFiltered.open(filterPath)
IME.prtDebugIntel("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmFiltered: File access failure: \(path)")
}
}
public func loadUserSymbolData(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmUserSymbols.close()
lmUserSymbols.open(path)
IME.prtDebugIntel("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmUserSymbol: File access failure: \(path)")
}
}
public func loadUserAssociatedPhrases(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmAssociates.close()
lmAssociates.open(path)
IME.prtDebugIntel("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmAssociates: File access failure: \(path)")
}
}
public func loadPhraseReplacementMap(path: String) {
if FileManager.default.isReadableFile(atPath: path) {
lmReplacements.close()
lmReplacements.open(path)
IME.prtDebugIntel("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
} else {
IME.prtDebugIntel("lmReplacements: File access failure: \(path)")
}
}
// MARK: - Core Functions (Public)
/// Not implemented since we do not have data to provide bigram function.
// public func bigramsForKeys(preceedingKey: String, key: String) -> [Megrez.Bigram] { }
/// Returns a list of available unigram for the given key.
/// @param key:String represents the BPMF reading or a symbol key.
/// For instance, it you pass "ˇ", it returns "" and other possible candidates.
override open func unigramsFor(key: String) -> [Megrez.Unigram] {
if key == " " {
///
let spaceUnigram = Megrez.Unigram(
keyValue: Megrez.KeyValuePair(key: " ", value: " "),
score: 0
)
return [spaceUnigram]
}
///
var rawAllUnigrams: [Megrez.Unigram] = []
// reversed 使
//
// rawUserUnigrams
rawAllUnigrams += lmUserPhrases.unigramsFor(key: key, score: 0.0).reversed()
if lmUserPhrases.unigramsFor(key: key).isEmpty {
IME.prtDebugIntel("Not found in UserPhrasesUnigram(\(lmUserPhrases.count)): \(key)")
}
// LMMisc LMCore score (-10.0, 0.0)
rawAllUnigrams += lmMisc.unigramsFor(key: key)
rawAllUnigrams += lmCore.unigramsFor(key: key)
if isCNSEnabled {
rawAllUnigrams += lmCNS.unigramsFor(key: key, score: -11)
}
if isSymbolEnabled {
rawAllUnigrams += lmUserSymbols.unigramsFor(key: key, score: -12.0)
if lmUserSymbols.unigramsFor(key: key).isEmpty {
IME.prtDebugIntel("Not found in UserSymbolUnigram(\(lmUserSymbols.count)): \(key)")
}
rawAllUnigrams += lmSymbols.unigramsFor(key: key)
}
//
var insertedPairs: Set<Megrez.KeyValuePair> = [] //
var filteredPairs: Set<Megrez.KeyValuePair> = [] //
// KeyValuePair
for unigram in lmFiltered.unigramsFor(key: key) {
filteredPairs.insert(unigram.keyValue)
}
var debugOutput = "\n"
for neta in rawAllUnigrams {
debugOutput += "RAW: \(neta.keyValue.key) \(neta.keyValue.value) \(neta.score)\n"
}
if debugOutput == "\n" {
debugOutput = "RAW: No match found in all unigrams."
}
IME.prtDebugIntel(debugOutput)
return filterAndTransform(
unigrams: rawAllUnigrams,
filter: filteredPairs, inserted: &insertedPairs
)
}
/// If the model has unigrams for the given key.
/// @param key The key.
override open func hasUnigramsFor(key: String) -> Bool {
if key == " " { return true }
if !lmFiltered.hasUnigramsFor(key: key) {
return lmUserPhrases.hasUnigramsFor(key: key) || lmCore.hasUnigramsFor(key: key)
}
return !unigramsFor(key: key).isEmpty
}
public func associatedPhrasesForKey(_ key: String) -> [String] {
lmAssociates.valuesFor(key: key) ?? []
}
public func hasAssociatedPhrasesForKey(_ key: String) -> Bool {
lmAssociates.hasValuesFor(key: key)
}
// MARK: - Core Functions (Private)
func filterAndTransform(
unigrams: [Megrez.Unigram],
filter filteredPairs: Set<Megrez.KeyValuePair>,
inserted insertedPairs: inout Set<Megrez.KeyValuePair>
) -> [Megrez.Unigram] {
var results: [Megrez.Unigram] = []
for unigram in unigrams {
var pair: Megrez.KeyValuePair = unigram.keyValue
if filteredPairs.contains(pair) {
continue
}
if isPhraseReplacementEnabled {
let replacement = lmReplacements.valuesFor(key: pair.value)
if !replacement.isEmpty {
IME.prtDebugIntel("\(pair.value) -> \(replacement)")
pair.value = replacement
}
}
if !insertedPairs.contains(pair) {
results.append(Megrez.Unigram(keyValue: pair, score: unigram.score))
insertedPairs.insert(pair)
}
}
return results
}
}
}

View File

@ -34,7 +34,7 @@ namespace vChewing
{ {
// About 20 generations. // About 20 generations.
static const double DecayThreshould = 1.0 / 1048576.0; static const double DecayThreshold = 1.0 / 1048576.0;
static double Score(size_t eventCount, size_t totalCount, double eventTimestamp, double timestamp, double lambda); static double Score(size_t eventCount, size_t totalCount, double eventTimestamp, double timestamp, double lambda);
static bool IsEndingPunctuation(const std::string &value); static bool IsEndingPunctuation(const std::string &value);
@ -126,7 +126,7 @@ void UserOverrideModel::Observation::update(const std::string &candidate, double
static double Score(size_t eventCount, size_t totalCount, double eventTimestamp, double timestamp, double lambda) static double Score(size_t eventCount, size_t totalCount, double eventTimestamp, double timestamp, double lambda)
{ {
double decay = exp((timestamp - eventTimestamp) * lambda); double decay = exp((timestamp - eventTimestamp) * lambda);
if (decay < DecayThreshould) if (decay < DecayThreshold)
{ {
return 0.0; return 0.0;
} }

View File

@ -0,0 +1,120 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
extension vChewing {
@frozen public struct LMAssociates {
var keyValueMap: [String: [Megrez.KeyValuePair]] = [:]
public var count: Int {
keyValueMap.count
}
public init() {
keyValueMap = [:]
}
public func isLoaded() -> Bool {
!keyValueMap.isEmpty
}
@discardableResult public mutating func open(_ path: String) -> Bool {
if isLoaded() {
return false
}
LMConsolidator.fixEOF(path: path)
LMConsolidator.consolidate(path: path, pragma: true)
var arrData: [String] = []
do {
arrData = try String(contentsOfFile: path, encoding: .utf8).components(separatedBy: "\n")
} catch {
IME.prtDebugIntel("\(error)")
IME.prtDebugIntel("↑ Exception happened when reading Associated Phrases data.")
return false
}
for (lineID, lineContent) in arrData.enumerated() {
if !lineContent.hasPrefix("#") {
let lineContent = lineContent.replacingOccurrences(of: "\t", with: " ")
if lineContent.components(separatedBy: " ").count < 2 {
if lineContent != "", lineContent != " " {
IME.prtDebugIntel("Line #\(lineID + 1) Wrecked: \(lineContent)")
}
continue
}
var currentKV = Megrez.KeyValuePair()
for (unitID, unitContent) in lineContent.components(separatedBy: " ").enumerated() {
switch unitID {
case 0:
currentKV.key = unitContent
case 1:
currentKV.value = unitContent
default: break
}
}
keyValueMap[currentKV.key, default: []].append(currentKV)
}
}
return true
}
public mutating func close() {
if isLoaded() {
keyValueMap.removeAll()
}
}
public func dump() {
var strDump = ""
for entry in keyValueMap {
let rows: [Megrez.KeyValuePair] = entry.value
for row in rows {
let addline = row.key + " " + row.value + "\n"
strDump += addline
}
}
IME.prtDebugIntel(strDump)
}
public func valuesFor(key: String) -> [String]? {
var v: [String] = []
if let matched = keyValueMap[key] {
for entry in matched as [Megrez.KeyValuePair] {
v.append(entry.value)
}
}
return v
}
public func hasValuesFor(key: String) -> Bool {
keyValueMap[key] != nil
}
}
}

View File

@ -0,0 +1,155 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// 使 Swift String
import Foundation
extension vChewing {
@frozen public struct LMCore {
var keyValueScoreMap: [String: [Megrez.Unigram]] = [:]
var shouldReverse: Bool = false
var allowConsolidation: Bool = false
var defaultScore: Double = 0
var shouldForceDefaultScore: Bool = false
public var count: Int {
keyValueScoreMap.count
}
public init(
reverse: Bool = false, consolidate: Bool = false, defaultScore scoreDefault: Double = 0,
forceDefaultScore: Bool = false
) {
keyValueScoreMap = [:]
allowConsolidation = consolidate
shouldReverse = reverse
defaultScore = scoreDefault
shouldForceDefaultScore = forceDefaultScore
}
public func isLoaded() -> Bool {
!keyValueScoreMap.isEmpty
}
@discardableResult public mutating func open(_ path: String) -> Bool {
if isLoaded() {
return false
}
if allowConsolidation {
LMConsolidator.fixEOF(path: path)
LMConsolidator.consolidate(path: path, pragma: true)
}
var arrData: [String] = []
do {
arrData = try String(contentsOfFile: path, encoding: .utf8).components(separatedBy: "\n")
} catch {
IME.prtDebugIntel("\(error)")
IME.prtDebugIntel("↑ Exception happened when reading Associated Phrases data.")
return false
}
for (lineID, lineContent) in arrData.enumerated() {
if !lineContent.hasPrefix("#") {
let lineContent = lineContent.replacingOccurrences(of: "\t", with: " ")
if lineContent.components(separatedBy: " ").count < 2 {
if lineContent != "", lineContent != " " {
IME.prtDebugIntel("Line #\(lineID + 1) Wrecked: \(lineContent)")
}
continue
}
var currentUnigram = Megrez.Unigram(keyValue: Megrez.KeyValuePair(), score: defaultScore)
var columnOne = ""
var columnTwo = ""
for (unitID, unitContent) in lineContent.components(separatedBy: " ").enumerated() {
switch unitID {
case 0:
columnOne = unitContent
case 1:
columnTwo = unitContent
case 2:
if !shouldForceDefaultScore {
if let unitContentConverted = Double(unitContent) {
currentUnigram.score = unitContentConverted
} else {
IME.prtDebugIntel("Line #\(lineID) Score Data Wrecked: \(lineContent)")
}
}
default: break
}
}
//
if columnOne.contains("_punctuation_") {
currentUnigram.score -= (Double(lineID) * 0.000001)
}
let kvPair =
shouldReverse
? Megrez.KeyValuePair(key: columnTwo, value: columnOne)
: Megrez.KeyValuePair(key: columnOne, value: columnTwo)
currentUnigram.keyValue = kvPair
let key = shouldReverse ? columnTwo : columnOne
keyValueScoreMap[key, default: []].append(currentUnigram)
}
}
return true
}
public mutating func close() {
if isLoaded() {
keyValueScoreMap.removeAll()
}
}
// MARK: - Advanced features
public func dump() {
var strDump = ""
for entry in keyValueScoreMap {
let rows: [Megrez.Unigram] = entry.value
for row in rows {
let addline = row.keyValue.key + " " + row.keyValue.value + " " + String(row.score) + "\n"
strDump += addline
}
}
IME.prtDebugIntel(strDump)
}
public func bigramsForKeys(precedingKey: String, key: String) -> [Megrez.Bigram] {
// Swift
// [Megrez.Bigram]()
precedingKey == key ? [Megrez.Bigram]() : [Megrez.Bigram]()
}
public func unigramsFor(key: String) -> [Megrez.Unigram] {
keyValueScoreMap[key] ?? [Megrez.Unigram]()
}
public func hasUnigramsFor(key: String) -> Bool {
keyValueScoreMap[key] != nil
}
}
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
extension vChewing {
@frozen public struct LMLite {
var keyValueMap: [String: [Megrez.KeyValuePair]] = [:]
var allowConsolidation = false
public var count: Int {
keyValueMap.count
}
public init(consolidate: Bool = false) {
keyValueMap = [:]
allowConsolidation = consolidate
}
public func isLoaded() -> Bool {
!keyValueMap.isEmpty
}
@discardableResult public mutating func open(_ path: String) -> Bool {
if isLoaded() {
return false
}
if allowConsolidation {
LMConsolidator.fixEOF(path: path)
LMConsolidator.consolidate(path: path, pragma: true)
}
var arrData: [String] = []
do {
arrData = try String(contentsOfFile: path, encoding: .utf8).components(separatedBy: "\n")
} catch {
IME.prtDebugIntel("\(error)")
IME.prtDebugIntel("↑ Exception happened when reading Associated Phrases data.")
return false
}
for (lineID, lineContent) in arrData.enumerated() {
if !lineContent.hasPrefix("#") {
let lineContent = lineContent.replacingOccurrences(of: "\t", with: " ")
if lineContent.components(separatedBy: " ").count < 2 {
if lineContent != "", lineContent != " " {
IME.prtDebugIntel("Line #\(lineID + 1) Wrecked: \(lineContent)")
}
continue
}
var currentKV = Megrez.KeyValuePair()
for (unitID, unitContent) in lineContent.components(separatedBy: " ").enumerated() {
switch unitID {
case 0:
currentKV.value = unitContent
case 1:
currentKV.key = unitContent
default: break
}
}
keyValueMap[currentKV.key, default: []].append(currentKV)
}
}
return true
}
public mutating func close() {
if isLoaded() {
keyValueMap.removeAll()
}
}
public func dump() {
var strDump = ""
for entry in keyValueMap {
let rows: [Megrez.KeyValuePair] = entry.value
for row in rows {
let addline = row.key + " " + row.value + "\n"
strDump += addline
}
}
IME.prtDebugIntel(strDump)
}
public func unigramsFor(key: String, score givenScore: Double = 0.0) -> [Megrez.Unigram] {
var v: [Megrez.Unigram] = []
if let matched = keyValueMap[key] {
for entry in matched as [Megrez.KeyValuePair] {
v.append(Megrez.Unigram(keyValue: entry, score: givenScore))
}
}
return v
}
public func hasUnigramsFor(key: String) -> Bool {
keyValueMap[key] != nil
}
}
}

View File

@ -0,0 +1,107 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
extension vChewing {
@frozen public struct LMReplacments {
var keyValueMap: [String: String] = [:]
public var count: Int {
keyValueMap.count
}
public init() {
keyValueMap = [:]
}
public func isLoaded() -> Bool {
!keyValueMap.isEmpty
}
@discardableResult public mutating func open(_ path: String) -> Bool {
if isLoaded() {
return false
}
LMConsolidator.fixEOF(path: path)
LMConsolidator.consolidate(path: path, pragma: true)
var arrData: [String] = []
do {
arrData = try String(contentsOfFile: path, encoding: .utf8).components(separatedBy: "\n")
} catch {
IME.prtDebugIntel("\(error)")
IME.prtDebugIntel("↑ Exception happened when reading Associated Phrases data.")
return false
}
for (lineID, lineContent) in arrData.enumerated() {
if !lineContent.hasPrefix("#") {
let lineContent = lineContent.replacingOccurrences(of: "\t", with: " ")
if lineContent.components(separatedBy: " ").count < 2 {
if lineContent != "", lineContent != " " {
IME.prtDebugIntel("Line #\(lineID + 1) Wrecked: \(lineContent)")
}
continue
}
var currentKV = Megrez.KeyValuePair()
for (unitID, unitContent) in lineContent.components(separatedBy: " ").enumerated() {
switch unitID {
case 0:
currentKV.key = unitContent
case 1:
currentKV.value = unitContent
default: break
}
}
keyValueMap[currentKV.key] = currentKV.value
}
}
return true
}
public mutating func close() {
if isLoaded() {
keyValueMap.removeAll()
}
}
public func dump() {
var strDump = ""
for entry in keyValueMap {
strDump += entry.key + " " + entry.value + "\n"
}
IME.prtDebugIntel(strDump)
}
public func valuesFor(key: String) -> String {
keyValueMap[key] ?? ""
}
}
}

View File

@ -0,0 +1,223 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
extension vChewing {
public class LMUserOverride {
// MARK: - Private Structures
struct Override {
var count: Int = 0
var timestamp: Double = 0.0
}
struct Observation {
var count: Int = 0
var overrides: [String: Override] = [:]
mutating func update(candidate: String, timestamp: Double) {
count += 1
if var neta = overrides[candidate] {
neta.timestamp = timestamp
neta.count += 1
}
}
}
struct KeyObservationPair: Equatable {
var key: String
var observation: Observation
var hashValue: Int { key.hashValue }
init(key: String, observation: Observation) {
self.key = key
self.observation = observation
}
static func == (lhs: KeyObservationPair, rhs: KeyObservationPair) -> Bool {
lhs.key == rhs.key
}
}
// MARK: - Main
var mutCapacity: Int
var mutDecayExponent: Double
var mutLRUList = [KeyObservationPair]()
var mutLRUMap: [String: KeyObservationPair] = [:]
let kDecayThreshold: Double = 1.0 / 1_048_576.0
public init(capacity: Int = 500, decayConstant: Double = 5400.0) {
mutCapacity = abs(capacity) // Ensures that this value is always > 0.
mutDecayExponent = log(0.5) / decayConstant
}
public func observe(
walkedNodes: [Megrez.NodeAnchor],
cursorIndex: Int,
candidate: String,
timestamp: Double
) {
let key = getWalkedNodesToKey(walkedNodes: walkedNodes, cursorIndex: cursorIndex)
guard !key.isEmpty
else {
return
}
guard let map = mutLRUMap[key] else {
var observation: Observation = .init()
observation.update(candidate: candidate, timestamp: timestamp)
mutLRUMap[key] = KeyObservationPair(key: key, observation: observation)
mutLRUList.insert(KeyObservationPair(key: key, observation: observation), at: 0)
if mutLRUList.count > mutCapacity {
mutLRUMap[mutLRUList.reversed()[0].key] = nil
mutLRUList.removeLast()
}
return
}
var obs = map.observation
obs.update(candidate: candidate, timestamp: timestamp)
let pair = KeyObservationPair(key: key, observation: obs)
mutLRUList.insert(pair, at: 0)
}
public func suggest(
walkedNodes: [Megrez.NodeAnchor],
cursorIndex: Int,
timestamp: Double
) -> String {
let key = getWalkedNodesToKey(walkedNodes: walkedNodes, cursorIndex: cursorIndex)
guard let keyValuePair = mutLRUMap[key],
!key.isEmpty
else {
return ""
}
IME.prtDebugIntel("Suggest - A: \(key)")
IME.prtDebugIntel("Suggest - B: \(keyValuePair.key)")
let observation = keyValuePair.observation
var candidate = ""
var score = 0.0
for overrideNeta in Array(observation.overrides) {
let overrideScore = getScore(
eventCount: overrideNeta.value.count,
totalCount: observation.count,
eventTimestamp: overrideNeta.value.timestamp,
timestamp: timestamp,
lambda: mutDecayExponent
)
if overrideScore == 0.0 {
continue
}
if overrideScore > score {
candidate = overrideNeta.key
score = overrideScore
}
}
return candidate
}
func isEndingPunctuation(value: String) -> Bool {
["", "", "", "", "", "", "", ""].contains(value)
}
public func getScore(
eventCount: Int,
totalCount: Int,
eventTimestamp: Double,
timestamp: Double,
lambda: Double
) -> Double {
let decay = exp((timestamp - eventTimestamp) * lambda)
if decay < kDecayThreshold {
return 0.0
}
let prob = Double(eventCount) / Double(totalCount)
return prob * decay
}
func getWalkedNodesToKey(
walkedNodes: [Megrez.NodeAnchor], cursorIndex: Int
) -> String {
var strOutput = ""
var arrNodes: [Megrez.NodeAnchor] = []
var intLength = 0
for nodeNeta in walkedNodes {
arrNodes.append(nodeNeta)
intLength += nodeNeta.spanningLength
if intLength >= cursorIndex {
break
}
}
// .reversed 使 Swift
//
var arrNodesReversed: [Megrez.NodeAnchor] = []
arrNodesReversed.append(contentsOf: arrNodes.reversed())
if arrNodesReversed.isEmpty {
return ""
}
var strCurrent = "()"
var strPrev = "()"
var strAnterior = "()"
for (theIndex, theAnchor) in arrNodesReversed.enumerated() {
if strCurrent != "()", let nodeCurrent = theAnchor.node {
let keyCurrent = nodeCurrent.currentKeyValue().key
let valCurrent = nodeCurrent.currentKeyValue().value
strCurrent = "(\(keyCurrent), \(valCurrent))"
if let nodePrev = arrNodesReversed[theIndex + 1].node {
let keyPrev = nodePrev.currentKeyValue().key
let valPrev = nodePrev.currentKeyValue().value
strPrev = "(\(keyPrev), \(valPrev))"
}
if let nodeAnterior = arrNodesReversed[theIndex + 2].node {
let keyAnterior = nodeAnterior.currentKeyValue().key
let valAnterior = nodeAnterior.currentKeyValue().value
strAnterior = "(\(keyAnterior), \(valAnterior))"
}
break //
}
}
strOutput = "(\(strAnterior),\(strPrev),\(strCurrent))"
if strOutput == "((),(),())" {
strOutput = ""
}
return strOutput
}
}
}

View File

@ -1,69 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef ASSOCIATEDPHRASES_H
#define ASSOCIATEDPHRASES_H
#include <iostream>
#include <map>
#include <string>
#include <vector>
namespace vChewing
{
class AssociatedPhrases
{
public:
AssociatedPhrases();
~AssociatedPhrases();
const bool isLoaded();
bool open(const char *path);
void close();
const std::vector<std::string> valuesForKey(const std::string &key);
const bool hasValuesForKey(const std::string &key);
protected:
struct Row
{
Row(std::string_view &k, std::string_view &v) : key(k), value(v)
{
}
std::string_view key;
std::string_view value;
};
std::map<std::string_view, std::vector<Row>> keyRowMap;
int fd;
void *data;
size_t length;
};
} // namespace vChewing
#endif /* AssociatedPhrases_hpp */

View File

@ -1,146 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "AssociatedPhrases.h"
#include "vChewing-Swift.h"
#include <fcntl.h>
#include <fstream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "KeyValueBlobReader.h"
#include "LMConsolidator.h"
namespace vChewing
{
AssociatedPhrases::AssociatedPhrases() : fd(-1), data(0), length(0)
{
}
AssociatedPhrases::~AssociatedPhrases()
{
if (data)
{
close();
}
}
const bool AssociatedPhrases::isLoaded()
{
if (data)
{
return true;
}
return false;
}
bool AssociatedPhrases::open(const char *path)
{
if (data)
{
return false;
}
LMConsolidator::FixEOF(path);
LMConsolidator::ConsolidateContent(path, true);
fd = ::open(path, O_RDONLY);
if (fd == -1)
{
printf("open:: file not exist");
return false;
}
struct stat sb;
if (fstat(fd, &sb) == -1)
{
printf("open:: cannot open file");
return false;
}
length = (size_t)sb.st_size;
data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
if (!data)
{
::close(fd);
return false;
}
KeyValueBlobReader reader(static_cast<char *>(data), length);
KeyValueBlobReader::KeyValue keyValue;
KeyValueBlobReader::State state;
while ((state = reader.Next(&keyValue)) == KeyValueBlobReader::State::HAS_PAIR)
{
keyRowMap[keyValue.key].emplace_back(keyValue.key, keyValue.value);
}
// 下面這一段或許可以做成開關、來詢問是否對使用者語彙採取寬鬆策略(哪怕有行內容寫錯也會放行)
if (state == KeyValueBlobReader::State::ERROR)
{
// close();
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "AssociatedPhrases: Failed at Open Step 5. On Error Resume Next.\n");
// return false;
}
return true;
}
void AssociatedPhrases::close()
{
if (data)
{
munmap(data, length);
::close(fd);
data = 0;
}
keyRowMap.clear();
}
const std::vector<std::string> AssociatedPhrases::valuesForKey(const std::string &key)
{
std::vector<std::string> v;
auto iter = keyRowMap.find(key);
if (iter != keyRowMap.end())
{
const std::vector<Row> &rows = iter->second;
for (const auto &row : rows)
{
std::string_view value = row.value;
v.push_back({value.data(), value.size()});
}
}
return v;
}
const bool AssociatedPhrases::hasValuesForKey(const std::string &key)
{
return keyRowMap.find(key) != keyRowMap.end();
}
}; // namespace vChewing

View File

@ -1,85 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef CoreLM_H
#define CoreLM_H
#include "LanguageModel.h"
#include <iostream>
#include <map>
#include <string>
#include <vector>
// this class relies on the fact that we have a space-separated data
// format, and we use mmap and zero-out the separators and line feeds
// to avoid creating new string objects; the parser is a simple DFA
using namespace std;
using namespace Gramambular;
namespace vChewing
{
class CoreLM : public Gramambular::LanguageModel
{
public:
CoreLM();
~CoreLM();
bool isLoaded();
bool open(const char *path);
void close();
void dump();
virtual const std::vector<Gramambular::Bigram> bigramsForKeys(const string &preceedingKey, const string &key);
virtual const std::vector<Gramambular::Unigram> unigramsForKey(const string &key);
virtual bool hasUnigramsForKey(const string &key);
protected:
struct CStringCmp
{
bool operator()(const char *s1, const char *s2) const
{
return strcmp(s1, s2) < 0;
}
};
struct Row
{
const char *key;
const char *value;
const char *logProbability;
};
map<const char *, vector<Row>, CStringCmp> keyRowMap;
int fd;
void *data;
size_t length;
};
}; // namespace vChewing
#endif

View File

@ -1,365 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "CoreLM.h"
#include "vChewing-Swift.h"
#include <fcntl.h>
#include <fstream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <syslog.h>
#include <unistd.h>
using namespace Gramambular;
vChewing::CoreLM::CoreLM() : fd(-1), data(0), length(0)
{
}
vChewing::CoreLM::~CoreLM()
{
if (data)
{
close();
}
}
bool vChewing::CoreLM::isLoaded()
{
if (data)
{
return true;
}
return false;
}
bool vChewing::CoreLM::open(const char *path)
{
if (data)
{
return false;
}
fd = ::open(path, O_RDONLY);
if (fd == -1)
{
return false;
}
struct stat sb;
if (fstat(fd, &sb) == -1)
{
return false;
}
length = (size_t)sb.st_size;
data = mmap(NULL, length, PROT_WRITE, MAP_PRIVATE, fd, 0);
if (!data)
{
::close(fd);
return false;
}
// Regular expression for parsing:
// (\n*\w\w*\s\w\w*\s\w\w*)*$
//
// Expanded as DFA (in Graphviz):
//
// digraph finite_state_machine {
// rankdir = LR;
// size = "10";
//
// node [shape = doublecircle]; End;
// node [shape = circle];
//
// Start -> End [ label = "EOF"];
// Start -> Error [ label = "\\s" ];
// Start -> Start [ label = "\\n" ];
// Start -> 1 [ label = "\\w" ];
//
// 1 -> Error [ label = "\\n, EOF" ];
// 1 -> 2 [ label = "\\s" ];
// 1 -> 1 [ label = "\\w" ];
//
// 2 -> Error [ label = "\\n, \\s, EOF" ];
// 2 -> 3 [ label = "\\w" ];
//
// 3 -> Error [ label = "\\n, EOF "];
// 3 -> 4 [ label = "\\s" ];
// 3 -> 3 [ label = "\\w" ];
//
// 4 -> Error [ label = "\\n, \\s, EOF" ];
// 4 -> 5 [ label = "\\w" ];
//
// 5 -> Error [ label = "\\s, EOF" ];
// 5 -> Start [ label = "\\n" ];
// 5 -> 5 [ label = "\\w" ];
// }
char *head = (char *)data;
char *end = (char *)data + length;
char c;
Row row;
start:
// EOF -> end
if (head == end)
{
goto end;
}
c = *head;
// \s -> error
if (c == ' ')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // Start: \\s -> error");
goto error;
}
// \n -> start
else if (c == '\n')
{
head++;
goto start;
}
// \w -> record column star, state1
row.value = head;
head++;
// fall through to state 1
state1:
// EOF -> error
if (head == end)
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 1: EOF -> error");
goto error;
}
c = *head;
// \n -> error
if (c == '\n')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 1: \\n -> error");
goto error;
}
// \s -> state2 + zero out ending + record column start
else if (c == ' ')
{
*head = 0;
head++;
row.key = head;
goto state2;
}
// \w -> state1
head++;
goto state1;
state2:
// eof -> error
if (head == end)
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 2: EOF -> error");
goto error;
}
c = *head;
// \n, \s -> error
if (c == '\n' || c == ' ')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 2: \\n \\s -> error");
goto error;
}
// \w -> state3
head++;
// fall through to state 3
state3:
// eof -> error
if (head == end)
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 3: EOF -> error");
goto error;
}
c = *head;
// \n -> error
if (c == '\n')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 3: \\n -> error");
goto error;
}
// \s -> state4 + zero out ending + record column start
else if (c == ' ')
{
*head = 0;
head++;
row.logProbability = head;
goto state4;
}
// \w -> state3
head++;
goto state3;
state4:
// eof -> error
if (head == end)
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 4: EOF -> error");
goto error;
}
c = *head;
// \n, \s -> error
if (c == '\n' || c == ' ')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 4: \\n \\s -> error");
goto error;
}
// \w -> state5
head++;
// fall through to state 5
state5:
// eof -> error
if (head == end)
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 5: EOF -> error");
goto error;
}
c = *head;
// \s -> error
if (c == ' ')
{
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // state 5: \\s -> error");
goto error;
}
// \n -> start
else if (c == '\n')
{
*head = 0;
head++;
keyRowMap[row.key].push_back(row);
goto start;
}
// \w -> state 5
head++;
goto state5;
error:
close();
return false;
end:
static const char *space = " ";
static const char *zero = "0.0";
Row emptyRow;
emptyRow.key = space;
emptyRow.value = space;
emptyRow.logProbability = zero;
keyRowMap[space].push_back(emptyRow);
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "vChewingDebug: CoreLM // File Load Complete.");
return true;
}
void vChewing::CoreLM::close()
{
if (data)
{
munmap(data, length);
::close(fd);
data = 0;
}
keyRowMap.clear();
}
void vChewing::CoreLM::dump()
{
size_t rows = 0;
for (map<const char *, vector<Row>>::const_iterator i = keyRowMap.begin(), e = keyRowMap.end(); i != e; ++i)
{
const vector<Row> &r = (*i).second;
for (vector<Row>::const_iterator ri = r.begin(), re = r.end(); ri != re; ++ri)
{
const Row &row = *ri;
cerr << row.key << " " << row.value << " " << row.logProbability << "\n";
rows++;
}
}
}
const std::vector<Gramambular::Bigram> vChewing::CoreLM::bigramsForKeys(const string &preceedingKey, const string &key)
{
return std::vector<Gramambular::Bigram>();
}
const std::vector<Gramambular::Unigram> vChewing::CoreLM::unigramsForKey(const string &key)
{
std::vector<Gramambular::Unigram> v;
map<const char *, vector<Row>>::const_iterator i = keyRowMap.find(key.c_str());
if (i != keyRowMap.end())
{
for (vector<Row>::const_iterator ri = (*i).second.begin(), re = (*i).second.end(); ri != re; ++ri)
{
Unigram g;
const Row &r = *ri;
g.keyValue.key = r.key;
g.keyValue.value = r.value;
g.score = atof(r.logProbability);
v.push_back(g);
}
}
return v;
}
bool vChewing::CoreLM::hasUnigramsForKey(const string &key)
{
return keyRowMap.find(key.c_str()) != keyRowMap.end();
}

View File

@ -1,56 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef PHRASEREPLACEMENTMAP_H
#define PHRASEREPLACEMENTMAP_H
#include <iostream>
#include <map>
#include <string>
namespace vChewing
{
class PhraseReplacementMap
{
public:
PhraseReplacementMap();
~PhraseReplacementMap();
bool open(const char *path);
void close();
const std::string valueForKey(const std::string &key);
protected:
std::map<std::string_view, std::string_view> keyValueMap;
int fd;
void *data;
size_t length;
};
} // namespace vChewing
#endif

View File

@ -1,130 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "PhraseReplacementMap.h"
#include "vChewing-Swift.h"
#include <fcntl.h>
#include <fstream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <syslog.h>
#include <unistd.h>
#include "KeyValueBlobReader.h"
#include "LMConsolidator.h"
namespace vChewing
{
using std::string;
PhraseReplacementMap::PhraseReplacementMap() : fd(-1), data(0), length(0)
{
}
PhraseReplacementMap::~PhraseReplacementMap()
{
if (data)
{
close();
}
}
bool PhraseReplacementMap::open(const char *path)
{
if (data)
{
return false;
}
LMConsolidator::FixEOF(path);
LMConsolidator::ConsolidateContent(path, true);
fd = ::open(path, O_RDONLY);
if (fd == -1)
{
printf("open:: file not exist");
return false;
}
struct stat sb;
if (fstat(fd, &sb) == -1)
{
printf("open:: cannot open file");
return false;
}
length = (size_t)sb.st_size;
data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
if (!data)
{
::close(fd);
return false;
}
KeyValueBlobReader reader(static_cast<char *>(data), length);
KeyValueBlobReader::KeyValue keyValue;
KeyValueBlobReader::State state;
while ((state = reader.Next(&keyValue)) == KeyValueBlobReader::State::HAS_PAIR)
{
keyValueMap[keyValue.key] = keyValue.value;
}
// 下面這一段或許可以做成開關、來詢問是否對使用者語彙採取寬鬆策略(哪怕有行內容寫錯也會放行)
if (state == KeyValueBlobReader::State::ERROR)
{
// close();
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "PhraseReplacementMap: Failed at Open Step 5. On Error Resume Next.\n");
// return false;
}
return true;
}
void PhraseReplacementMap::close()
{
if (data)
{
munmap(data, length);
::close(fd);
data = 0;
}
keyValueMap.clear();
}
const std::string PhraseReplacementMap::valueForKey(const std::string &key)
{
auto iter = keyValueMap.find(key);
if (iter != keyValueMap.end())
{
const std::string_view v = iter->second;
return {v.data(), v.size()};
}
return string("");
}
}

View File

@ -1,82 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef USERPHRASESLM_H
#define USERPHRASESLM_H
#include "LanguageModel.h"
#include <iostream>
#include <map>
#include <string>
namespace vChewing
{
class UserPhrasesLM : public Gramambular::LanguageModel
{
public:
UserPhrasesLM();
~UserPhrasesLM();
bool isLoaded();
bool open(const char *path);
void close();
void dump();
virtual bool allowConsolidation()
{
return true;
}
virtual float overridedValue()
{
return 0.0;
}
virtual const std::vector<Gramambular::Bigram> bigramsForKeys(const std::string &preceedingKey,
const std::string &key);
virtual const std::vector<Gramambular::Unigram> unigramsForKey(const std::string &key);
virtual bool hasUnigramsForKey(const std::string &key);
protected:
struct Row
{
Row(std::string_view &k, std::string_view &v) : key(k), value(v)
{
}
std::string_view key;
std::string_view value;
};
std::map<std::string_view, std::vector<Row>> keyRowMap;
int fd;
void *data;
size_t length;
};
} // namespace vChewing
#endif

View File

@ -1,174 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "UserPhrasesLM.h"
#include "vChewing-Swift.h"
#include <fcntl.h>
#include <fstream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <syslog.h>
#include <unistd.h>
#include "KeyValueBlobReader.h"
#include "LMConsolidator.h"
namespace vChewing
{
UserPhrasesLM::UserPhrasesLM() : fd(-1), data(0), length(0)
{
}
UserPhrasesLM::~UserPhrasesLM()
{
if (data)
{
close();
}
}
bool UserPhrasesLM::isLoaded()
{
if (data)
{
return true;
}
return false;
}
bool UserPhrasesLM::open(const char *path)
{
if (data)
{
return false;
}
if (allowConsolidation())
{
LMConsolidator::FixEOF(path);
LMConsolidator::ConsolidateContent(path, true);
}
fd = ::open(path, O_RDONLY);
if (fd == -1)
{
printf("open:: file not exist");
return false;
}
struct stat sb;
if (fstat(fd, &sb) == -1)
{
printf("open:: cannot open file");
return false;
}
length = (size_t)sb.st_size;
data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
if (!data)
{
::close(fd);
return false;
}
KeyValueBlobReader reader(static_cast<char *>(data), length);
KeyValueBlobReader::KeyValue keyValue;
KeyValueBlobReader::State state;
while ((state = reader.Next(&keyValue)) == KeyValueBlobReader::State::HAS_PAIR)
{
// We invert the key and value, since in user phrases, "key" is the phrase value, and "value" is the BPMF
// reading.
keyRowMap[keyValue.value].emplace_back(keyValue.value, keyValue.key);
}
// 下面這一段或許可以做成開關、來詢問是否對使用者語彙採取寬鬆策略(哪怕有行內容寫錯也會放行)
if (state == KeyValueBlobReader::State::ERROR)
{
// close();
if (mgrPrefs.isDebugModeEnabled)
syslog(LOG_CONS, "UserPhrasesLM: Failed at Open Step 5. On Error Resume Next.\n");
// return false;
}
return true;
}
void UserPhrasesLM::close()
{
if (data)
{
munmap(data, length);
::close(fd);
data = 0;
}
keyRowMap.clear();
}
void UserPhrasesLM::dump()
{
for (const auto &entry : keyRowMap)
{
const std::vector<Row> &rows = entry.second;
for (const auto &row : rows)
{
std::cerr << row.key << " " << row.value << "\n";
}
}
}
const std::vector<Gramambular::Bigram> UserPhrasesLM::bigramsForKeys(const std::string &preceedingKey,
const std::string &key)
{
return std::vector<Gramambular::Bigram>();
}
const std::vector<Gramambular::Unigram> UserPhrasesLM::unigramsForKey(const std::string &key)
{
std::vector<Gramambular::Unigram> v;
auto iter = keyRowMap.find(key);
if (iter != keyRowMap.end())
{
const std::vector<Row> &rows = iter->second;
for (const auto &row : rows)
{
Gramambular::Unigram g;
g.keyValue.key = row.key;
g.keyValue.value = row.value;
g.score = overridedValue();
v.push_back(g);
}
}
return v;
}
bool UserPhrasesLM::hasUnigramsForKey(const std::string &key)
{
return keyRowMap.find(key) != keyRowMap.end();
}
}; // namespace vChewing

View File

@ -1,54 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "KeyHandler.h"
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface mgrLangModel : NSObject
+ (void)loadDataModel:(InputMode)mode;
+ (void)loadUserPhrases;
+ (void)loadUserAssociatedPhrases;
+ (void)loadUserPhraseReplacement;
+ (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase
inputMode:(InputMode)mode
key:(NSString *)key NS_SWIFT_NAME(checkIfUserPhraseExist(userPhrase:mode:key:));
+ (void)consolidateGivenFile:(NSString *)path shouldCheckPragma:(BOOL)shouldCheckPragma;
+ (void)setPhraseReplacementEnabled:(BOOL)phraseReplacementEnabled;
+ (void)setCNSEnabled:(BOOL)cnsEnabled;
+ (void)setSymbolEnabled:(BOOL)symbolEnabled;
@end
/// The following methods are merely for testing.
@interface mgrLangModel ()
+ (void)loadDataModels;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,195 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "mgrLangModel.h"
#import "LMConsolidator.h"
#import "mgrLangModel_Privates.h"
#import "vChewing-Swift.h"
static const int kUserOverrideModelCapacity = 500;
static const double kObservedOverrideHalflife = 5400.0;
static vChewing::LMInstantiator gLangModelCHT;
static vChewing::LMInstantiator gLangModelCHS;
static vChewing::UserOverrideModel gUserOverrideModelCHT(kUserOverrideModelCapacity, kObservedOverrideHalflife);
static vChewing::UserOverrideModel gUserOverrideModelCHS(kUserOverrideModelCapacity, kObservedOverrideHalflife);
@implementation mgrLangModel
// 這個函數無法遷移至 Swift
static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing::LMInstantiator &lm)
{
NSString *dataPath = [mgrLangModel getBundleDataPath:filenameWithoutExtension];
lm.loadLanguageModel([dataPath UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (void)loadDataModels
{
if (!gLangModelCHT.isDataModelLoaded())
LTLoadLanguageModelFile(@"data-cht", gLangModelCHT);
if (!gLangModelCHT.isMiscDataLoaded())
gLangModelCHT.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
if (!gLangModelCHT.isSymbolDataLoaded())
gLangModelCHT.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
if (!gLangModelCHT.isCNSDataLoaded())
gLangModelCHT.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
// -----------------
if (!gLangModelCHS.isDataModelLoaded())
LTLoadLanguageModelFile(@"data-chs", gLangModelCHS);
if (!gLangModelCHS.isMiscDataLoaded())
gLangModelCHS.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
if (!gLangModelCHS.isSymbolDataLoaded())
gLangModelCHS.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
if (!gLangModelCHS.isCNSDataLoaded())
gLangModelCHS.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (void)loadDataModel:(InputMode)mode
{
if ([mode isEqualToString:imeModeCHT])
{
if (!gLangModelCHT.isDataModelLoaded())
LTLoadLanguageModelFile(@"data-cht", gLangModelCHT);
if (!gLangModelCHT.isMiscDataLoaded())
gLangModelCHT.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
if (!gLangModelCHT.isSymbolDataLoaded())
gLangModelCHT.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
if (!gLangModelCHT.isCNSDataLoaded())
gLangModelCHT.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
if ([mode isEqualToString:imeModeCHS])
{
if (!gLangModelCHS.isDataModelLoaded())
LTLoadLanguageModelFile(@"data-chs", gLangModelCHS);
if (!gLangModelCHS.isMiscDataLoaded())
gLangModelCHS.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
if (!gLangModelCHS.isSymbolDataLoaded())
gLangModelCHS.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
if (!gLangModelCHS.isCNSDataLoaded())
gLangModelCHS.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
}
// 這個函數無法遷移至 Swift
+ (void)loadUserPhrases
{
gLangModelCHT.loadUserPhrases([[self userPhrasesDataPath:imeModeCHT] UTF8String],
[[self excludedPhrasesDataPath:imeModeCHT] UTF8String]);
gLangModelCHS.loadUserPhrases([[self userPhrasesDataPath:imeModeCHS] UTF8String],
[[self excludedPhrasesDataPath:imeModeCHS] UTF8String]);
gLangModelCHT.loadUserSymbolData([[self userSymbolDataPath:imeModeCHT] UTF8String]);
gLangModelCHS.loadUserSymbolData([[self userSymbolDataPath:imeModeCHS] UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (void)loadUserAssociatedPhrases
{
gLangModelCHT.loadUserAssociatedPhrases([[self userAssociatedPhrasesDataPath:imeModeCHT] UTF8String]);
gLangModelCHS.loadUserAssociatedPhrases([[self userAssociatedPhrasesDataPath:imeModeCHS] UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (void)loadUserPhraseReplacement
{
gLangModelCHT.loadPhraseReplacementMap([[self phraseReplacementDataPath:imeModeCHT] UTF8String]);
gLangModelCHS.loadPhraseReplacementMap([[self phraseReplacementDataPath:imeModeCHS] UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase
inputMode:(InputMode)mode
key:(NSString *)key NS_SWIFT_NAME(checkIfUserPhraseExist(userPhrase:mode:key:))
{
string unigramKey = string(key.UTF8String);
vector<vChewing::Unigram> unigrams = [mode isEqualToString:imeModeCHT] ? gLangModelCHT.unigramsForKey(unigramKey)
: gLangModelCHS.unigramsForKey(unigramKey);
string userPhraseString = string(userPhrase.UTF8String);
for (auto unigram : unigrams)
{
if (unigram.keyValue.value == userPhraseString)
{
return YES;
}
}
return NO;
}
// 這個函數無法遷移至 Swift
+ (void)consolidateGivenFile:(NSString *)path shouldCheckPragma:(BOOL)shouldCheckPragma
{
vChewing::LMConsolidator::ConsolidateContent([path UTF8String], shouldCheckPragma);
}
// 這個函數無法遷移至 Swift
+ (vChewing::LMInstantiator *)lmCHT
{
return &gLangModelCHT;
}
// 這個函數無法遷移至 Swift
+ (vChewing::LMInstantiator *)lmCHS
{
return &gLangModelCHS;
}
// 這個函數無法遷移至 Swift
+ (vChewing::UserOverrideModel *)userOverrideModelCHT
{
return &gUserOverrideModelCHT;
}
// 這個函數無法遷移至 Swift
+ (vChewing::UserOverrideModel *)userOverrideModelCHS
{
return &gUserOverrideModelCHS;
}
// 這個函數無法遷移至 Swift
+ (void)setPhraseReplacementEnabled:(BOOL)phraseReplacementEnabled
{
gLangModelCHT.setPhraseReplacementEnabled(phraseReplacementEnabled);
gLangModelCHS.setPhraseReplacementEnabled(phraseReplacementEnabled);
}
// 這個函數無法遷移至 Swift
+ (void)setCNSEnabled:(BOOL)cnsEnabled
{
gLangModelCHT.setCNSEnabled(cnsEnabled);
gLangModelCHS.setCNSEnabled(cnsEnabled);
}
// 這個函數無法遷移至 Swift
+ (void)setSymbolEnabled:(BOOL)symbolEnabled
{
gLangModelCHT.setSymbolEnabled(symbolEnabled);
gLangModelCHS.setSymbolEnabled(symbolEnabled);
}
@end

View File

@ -26,222 +26,414 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa import Cocoa
@objc extension mgrLangModel { /// mgrLangModel
// MARK: - /// mgrLangModel
///
/// mgrLangModel
static func getBundleDataPath(_ filenameSansExt: String) -> String { private var gLangModelCHS = vChewing.LMInstantiator()
Bundle.main.path(forResource: filenameSansExt, ofType: "txt")! private var gLangModelCHT = vChewing.LMInstantiator()
} private var gUserOverrideModelCHS = vChewing.LMUserOverride()
private var gUserOverrideModelCHT = vChewing.LMUserOverride()
// MARK: - 使 class mgrLangModel: NSObject {
/// fileprivate
public static var lmCHS: vChewing.LMInstantiator { gLangModelCHS }
public static var lmCHT: vChewing.LMInstantiator { gLangModelCHT }
public static var uomCHS: vChewing.LMUserOverride { gUserOverrideModelCHS }
public static var uomCHT: vChewing.LMUserOverride { gUserOverrideModelCHT }
// Swift appendingPathComponent URL .path // MARK: - Functions reacting directly with language models.
static func userPhrasesDataPath(_ mode: InputMode) -> String { static func loadCoreLanguageModelFile(filenameSansExtension: String, langModel lm: inout vChewing.LMInstantiator) {
let fileName = (mode == InputMode.imeModeCHT) ? "userdata-cht.txt" : "userdata-chs.txt" let dataPath: String = mgrLangModel.getBundleDataPath(filenameSansExtension)
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path lm.loadLanguageModel(path: dataPath)
} }
static func userSymbolDataPath(_ mode: InputMode) -> String { public static func loadDataModels() {
let fileName = (mode == InputMode.imeModeCHT) ? "usersymbolphrases-cht.txt" : "usersymbolphrases-chs.txt" DispatchQueue.global(qos: .userInitiated).async {
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path if !gLangModelCHT.isCNSDataLoaded() {
} gLangModelCHT.loadCNSData(path: getBundleDataPath("char-kanji-cns"))
}
if !gLangModelCHT.isMiscDataLoaded() {
gLangModelCHT.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !gLangModelCHT.isSymbolDataLoaded() {
gLangModelCHT.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
if !gLangModelCHS.isCNSDataLoaded() {
gLangModelCHS.loadCNSData(path: getBundleDataPath("char-kanji-cns"))
}
if !gLangModelCHS.isMiscDataLoaded() {
gLangModelCHS.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !gLangModelCHS.isSymbolDataLoaded() {
gLangModelCHS.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
}
if !gLangModelCHT.isDataModelLoaded() {
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Loading CHT Core Dict...", comment: "")
)
)
loadCoreLanguageModelFile(filenameSansExtension: "data-cht", langModel: &gLangModelCHT)
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Core Dict loading complete.", comment: "")
)
)
}
if !gLangModelCHS.isDataModelLoaded() {
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Loading CHS Core Dict...", comment: "")
)
)
loadCoreLanguageModelFile(filenameSansExtension: "data-chs", langModel: &gLangModelCHS)
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Core Dict loading complete.", comment: "")
)
)
}
}
static func userAssociatedPhrasesDataPath(_ mode: InputMode) -> String { public static func loadDataModel(_ mode: InputMode) {
let fileName = (mode == InputMode.imeModeCHT) ? "associatedPhrases-cht.txt" : "associatedPhrases-chs.txt" if mode == InputMode.imeModeCHS {
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path DispatchQueue.global(qos: .userInitiated).async {
} if !gLangModelCHS.isMiscDataLoaded() {
gLangModelCHS.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !gLangModelCHS.isSymbolDataLoaded() {
gLangModelCHS.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
if !gLangModelCHS.isCNSDataLoaded() {
gLangModelCHS.loadCNSData(path: getBundleDataPath("char-kanji-cns"))
}
}
if !gLangModelCHS.isDataModelLoaded() {
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Loading CHS Core Dict...", comment: "")
)
)
loadCoreLanguageModelFile(filenameSansExtension: "data-chs", langModel: &gLangModelCHS)
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Core Dict loading complete.", comment: "")
)
)
}
} else if mode == InputMode.imeModeCHT {
DispatchQueue.global(qos: .userInitiated).async {
if !gLangModelCHT.isMiscDataLoaded() {
gLangModelCHT.loadMiscData(path: getBundleDataPath("data-zhuyinwen"))
}
if !gLangModelCHT.isSymbolDataLoaded() {
gLangModelCHT.loadSymbolData(path: getBundleDataPath("data-symbols"))
}
if !gLangModelCHT.isCNSDataLoaded() {
gLangModelCHT.loadCNSData(path: getBundleDataPath("char-kanji-cns"))
}
}
if !gLangModelCHT.isDataModelLoaded() {
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Loading CHT Core Dict...", comment: "")
)
)
loadCoreLanguageModelFile(filenameSansExtension: "data-cht", langModel: &gLangModelCHT)
NotifierController.notify(
message: String(
format: "%@", NSLocalizedString("Core Dict loading complete.", comment: "")
)
)
}
}
}
static func excludedPhrasesDataPath(_ mode: InputMode) -> String { public static func loadUserPhrases() {
let fileName = (mode == InputMode.imeModeCHT) ? "exclude-phrases-cht.txt" : "exclude-phrases-chs.txt" gLangModelCHT.loadUserPhrases(
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path path: userPhrasesDataPath(InputMode.imeModeCHT),
} filterPath: excludedPhrasesDataPath(InputMode.imeModeCHT)
)
gLangModelCHS.loadUserPhrases(
path: userPhrasesDataPath(InputMode.imeModeCHS),
filterPath: excludedPhrasesDataPath(InputMode.imeModeCHS)
)
gLangModelCHT.loadUserSymbolData(path: userSymbolDataPath(InputMode.imeModeCHT))
gLangModelCHS.loadUserSymbolData(path: userSymbolDataPath(InputMode.imeModeCHS))
}
static func phraseReplacementDataPath(_ mode: InputMode) -> String { public static func loadUserAssociatedPhrases() {
let fileName = (mode == InputMode.imeModeCHT) ? "phrases-replacement-cht.txt" : "phrases-replacement-chs.txt" gLangModelCHT.loadUserAssociatedPhrases(
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path path: mgrLangModel.userAssociatedPhrasesDataPath(InputMode.imeModeCHT)
} )
gLangModelCHS.loadUserAssociatedPhrases(
path: mgrLangModel.userAssociatedPhrasesDataPath(InputMode.imeModeCHS)
)
}
// MARK: - 使 public static func loadUserPhraseReplacement() {
gLangModelCHT.loadPhraseReplacementMap(
path: mgrLangModel.phraseReplacementDataPath(InputMode.imeModeCHT)
)
gLangModelCHS.loadPhraseReplacementMap(
path: mgrLangModel.phraseReplacementDataPath(InputMode.imeModeCHS)
)
}
static func ensureFileExists( public static func checkIfUserPhraseExist(
_ filePath: String, populateWithTemplate templateBasename: String = "1145141919810", userPhrase: String,
extension ext: String = "txt" mode: InputMode,
) -> Bool { key unigramKey: String
if !FileManager.default.fileExists(atPath: filePath) { ) -> Bool {
let templateURL = Bundle.main.url(forResource: templateBasename, withExtension: ext) let unigrams: [Megrez.Unigram] =
var templateData = Data("".utf8) (mode == InputMode.imeModeCHT)
if templateBasename != "" { ? gLangModelCHT.unigramsFor(key: unigramKey) : gLangModelCHS.unigramsFor(key: unigramKey)
do { for unigram in unigrams {
try templateData = Data(contentsOf: templateURL ?? URL(fileURLWithPath: "")) if unigram.keyValue.value == userPhrase {
} catch { return true
templateData = Data("".utf8) }
} }
do { return false
try templateData.write(to: URL(fileURLWithPath: filePath)) }
} catch {
IME.prtDebugIntel("Failed to write file")
return false
}
}
}
return true
}
static func chkUserLMFilesExist(_ mode: InputMode) -> Bool { public static func setPhraseReplacementEnabled(_ state: Bool) {
if !checkIfUserDataFolderExists() { gLangModelCHT.isPhraseReplacementEnabled = state
return false gLangModelCHS.isPhraseReplacementEnabled = state
} }
if !ensureFileExists(userPhrasesDataPath(mode))
|| !ensureFileExists(userAssociatedPhrasesDataPath(mode))
|| !ensureFileExists(excludedPhrasesDataPath(mode))
|| !ensureFileExists(phraseReplacementDataPath(mode))
|| !ensureFileExists(userSymbolDataPath(mode))
{
return false
}
return true public static func setCNSEnabled(_ state: Bool) {
} gLangModelCHT.isCNSEnabled = state
gLangModelCHS.isCNSEnabled = state
}
// MARK: - 使 public static func setSymbolEnabled(_ state: Bool) {
gLangModelCHT.isSymbolEnabled = state
gLangModelCHS.isSymbolEnabled = state
}
// // MARK: -
static func checkIfSpecifiedUserDataFolderValid(_ folderPath: String?) -> Bool {
var isFolder = ObjCBool(false)
let folderExist = FileManager.default.fileExists(atPath: folderPath ?? "", isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
// static func getBundleDataPath(_ filenameSansExt: String) -> String {
// Bundle.main.path(forResource: filenameSansExt, ofType: "txt")!
var folderPath = folderPath // Convert the incoming constant to a variable. }
if isFolder.boolValue {
folderPath?.ensureTrailingSlash()
}
let isFolderWritable = FileManager.default.isWritableFile(atPath: folderPath ?? "")
if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable { // MARK: - 使
return false
}
return true // Swift appendingPathComponent URL .path
}
// static func userPhrasesDataPath(_ mode: InputMode) -> String {
// let fileName = (mode == InputMode.imeModeCHT) ? "userdata-cht.txt" : "userdata-chs.txt"
static func checkIfUserDataFolderExists() -> Bool { return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
let folderPath = mgrLangModel.dataFolderPath(isDefaultFolder: false) }
var isFolder = ObjCBool(false)
var folderExist = FileManager.default.fileExists(atPath: folderPath, isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
//
//
//
if folderExist, !isFolder.boolValue {
do {
if dataFolderPath(isDefaultFolder: false)
== dataFolderPath(isDefaultFolder: true)
{
let formatter = DateFormatter()
formatter.dateFormat = "YYYYMMDD-HHMM'Hrs'-ss's'"
let dirAlternative = folderPath + formatter.string(from: Date())
try FileManager.default.moveItem(atPath: folderPath, toPath: dirAlternative)
} else {
throw folderPath
}
} catch {
print("Failed to make path available at: \(error)")
return false
}
folderExist = false
}
if !folderExist {
do {
try FileManager.default.createDirectory(
atPath: folderPath,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
print("Failed to create folder: \(error)")
return false
}
}
return true
}
// MARK: - 使 mgrPrefs static func userSymbolDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "usersymbolphrases-cht.txt" : "usersymbolphrases-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
// mgrPrefs static func userAssociatedPhrasesDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "associatedPhrases-cht.txt" : "associatedPhrases-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
static func dataFolderPath(isDefaultFolder: Bool) -> String { static func excludedPhrasesDataPath(_ mode: InputMode) -> String {
let appSupportPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].path let fileName = (mode == InputMode.imeModeCHT) ? "exclude-phrases-cht.txt" : "exclude-phrases-chs.txt"
var userDictPathSpecified = (mgrPrefs.userDataFolderSpecified as NSString).expandingTildeInPath return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
var userDictPathDefault = }
(URL(fileURLWithPath: appSupportPath).appendingPathComponent("vChewing").path as NSString)
.expandingTildeInPath
userDictPathDefault.ensureTrailingSlash() static func phraseReplacementDataPath(_ mode: InputMode) -> String {
userDictPathSpecified.ensureTrailingSlash() let fileName = (mode == InputMode.imeModeCHT) ? "phrases-replacement-cht.txt" : "phrases-replacement-chs.txt"
return URL(fileURLWithPath: dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
if (userDictPathSpecified == userDictPathDefault) // MARK: - 使
|| isDefaultFolder
{
return userDictPathDefault
}
if mgrPrefs.ifSpecifiedUserDataPathExistsInPlist() {
if mgrLangModel.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) {
return userDictPathSpecified
} else {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified")
}
}
return userDictPathDefault
}
// MARK: - 使 static func ensureFileExists(
_ filePath: String, populateWithTemplate templateBasename: String = "1145141919810",
extension ext: String = "txt"
) -> Bool {
if !FileManager.default.fileExists(atPath: filePath) {
let templateURL = Bundle.main.url(forResource: templateBasename, withExtension: ext)
var templateData = Data("".utf8)
if templateBasename != "" {
do {
try templateData = Data(contentsOf: templateURL ?? URL(fileURLWithPath: ""))
} catch {
templateData = Data("".utf8)
}
do {
try templateData.write(to: URL(fileURLWithPath: filePath))
} catch {
IME.prtDebugIntel("Failed to write template data to: \(filePath)")
return false
}
}
}
return true
}
static func writeUserPhrase( static func chkUserLMFilesExist(_ mode: InputMode) -> Bool {
_ userPhrase: String?, inputMode mode: InputMode, areWeDuplicating: Bool, areWeDeleting: Bool if !checkIfUserDataFolderExists() {
) -> Bool { return false
if var currentMarkedPhrase: String = userPhrase { }
if !chkUserLMFilesExist(InputMode.imeModeCHS) if !ensureFileExists(userPhrasesDataPath(mode))
|| !chkUserLMFilesExist(InputMode.imeModeCHT) || !ensureFileExists(userAssociatedPhrasesDataPath(mode))
{ || !ensureFileExists(excludedPhrasesDataPath(mode))
return false || !ensureFileExists(phraseReplacementDataPath(mode))
} || !ensureFileExists(userSymbolDataPath(mode))
{
return false
}
let path = areWeDeleting ? excludedPhrasesDataPath(mode) : userPhrasesDataPath(mode) return true
}
if areWeDuplicating, !areWeDeleting { // MARK: - 使
// Do not use ASCII characters to comment here.
// Otherwise, it will be scrambled by cnvHYPYtoBPMF
// module shipped in the vChewing Phrase Editor.
currentMarkedPhrase += "\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"
}
currentMarkedPhrase += "\n"
if let writeFile = FileHandle(forUpdatingAtPath: path), //
let data = currentMarkedPhrase.data(using: .utf8) static func checkIfSpecifiedUserDataFolderValid(_ folderPath: String?) -> Bool {
{ var isFolder = ObjCBool(false)
writeFile.seekToEndOfFile() let folderExist = FileManager.default.fileExists(atPath: folderPath ?? "", isDirectory: &isFolder)
writeFile.write(data) // The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
writeFile.closeFile()
} else {
return false
}
// We enforce the format consolidation here, since the pragma header //
// will let the UserPhraseLM bypasses the consolidating process on load. //
consolidate(givenFile: path, shouldCheckPragma: false) var folderPath = folderPath // Convert the incoming constant to a variable.
if isFolder.boolValue {
folderPath?.ensureTrailingSlash()
}
let isFolderWritable = FileManager.default.isWritableFile(atPath: folderPath ?? "")
// We use FSEventStream to monitor possible changes of the user phrase folder, hence the if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable {
// lack of the needs of manually load data here unless FSEventStream is disabled by user. return false
if !mgrPrefs.shouldAutoReloadUserDataFiles { }
loadUserPhrases()
} return true
return true }
}
return false //
} //
static func checkIfUserDataFolderExists() -> Bool {
let folderPath = mgrLangModel.dataFolderPath(isDefaultFolder: false)
var isFolder = ObjCBool(false)
var folderExist = FileManager.default.fileExists(atPath: folderPath, isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
//
//
//
if folderExist, !isFolder.boolValue {
do {
if dataFolderPath(isDefaultFolder: false)
== dataFolderPath(isDefaultFolder: true)
{
let formatter = DateFormatter()
formatter.dateFormat = "YYYYMMDD-HHMM'Hrs'-ss's'"
let dirAlternative = folderPath + formatter.string(from: Date())
try FileManager.default.moveItem(atPath: folderPath, toPath: dirAlternative)
} else {
throw folderPath
}
} catch {
print("Failed to make path available at: \(error)")
return false
}
folderExist = false
}
if !folderExist {
do {
try FileManager.default.createDirectory(
atPath: folderPath,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
print("Failed to create folder: \(error)")
return false
}
}
return true
}
// MARK: - 使 mgrPrefs
// mgrPrefs
static func dataFolderPath(isDefaultFolder: Bool) -> String {
let appSupportPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].path
var userDictPathSpecified = (mgrPrefs.userDataFolderSpecified as NSString).expandingTildeInPath
var userDictPathDefault =
(URL(fileURLWithPath: appSupportPath).appendingPathComponent("vChewing").path as NSString)
.expandingTildeInPath
userDictPathDefault.ensureTrailingSlash()
userDictPathSpecified.ensureTrailingSlash()
if (userDictPathSpecified == userDictPathDefault)
|| isDefaultFolder
{
return userDictPathDefault
}
if mgrPrefs.ifSpecifiedUserDataPathExistsInPlist() {
if mgrLangModel.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) {
return userDictPathSpecified
} else {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified")
}
}
return userDictPathDefault
}
// MARK: - 使
static func writeUserPhrase(
_ userPhrase: String?, inputMode mode: InputMode, areWeDuplicating: Bool, areWeDeleting: Bool
) -> Bool {
if var currentMarkedPhrase: String = userPhrase {
if !chkUserLMFilesExist(InputMode.imeModeCHS)
|| !chkUserLMFilesExist(InputMode.imeModeCHT)
{
return false
}
let path = areWeDeleting ? excludedPhrasesDataPath(mode) : userPhrasesDataPath(mode)
if areWeDuplicating, !areWeDeleting {
// Do not use ASCII characters to comment here.
// Otherwise, it will be scrambled by cnvHYPYtoBPMF
// module shipped in the vChewing Phrase Editor.
currentMarkedPhrase += "\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"
}
if let writeFile = FileHandle(forUpdatingAtPath: path),
let data = currentMarkedPhrase.data(using: .utf8),
let endl = "\n".data(using: .utf8)
{
writeFile.seekToEndOfFile()
writeFile.write(endl)
writeFile.write(data)
writeFile.write(endl)
writeFile.closeFile()
} else {
return false
}
// We enforce the format consolidation here, since the pragma header
// will let the UserPhraseLM bypasses the consolidating process on load.
if !vChewing.LMConsolidator.consolidate(path: path, pragma: false) {
return false
}
// We use FSEventStream to monitor possible changes of the user phrase folder, hence the
// lack of the needs of manually load data here unless FSEventStream is disabled by user.
if !mgrPrefs.shouldAutoReloadUserDataFiles {
loadUserPhrases()
}
return true
}
return false
}
} }

View File

@ -1,40 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "LMInstantiator.h"
#import "UserOverrideModel.h"
#import "mgrLangModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface mgrLangModel ()
@property(class, readonly, nonatomic) vChewing::LMInstantiator *lmCHT;
@property(class, readonly, nonatomic) vChewing::LMInstantiator *lmCHS;
@property(class, readonly, nonatomic) vChewing::UserOverrideModel *userOverrideModelCHS;
@property(class, readonly, nonatomic) vChewing::UserOverrideModel *userOverrideModelCHT;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,110 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef BIGRAM_H_
#define BIGRAM_H_
#include <vector>
#include "KeyValuePair.h"
namespace Gramambular
{
class Bigram
{
public:
Bigram();
KeyValuePair preceedingKeyValue;
KeyValuePair keyValue;
double score;
bool operator==(const Bigram &another) const;
bool operator<(const Bigram &another) const;
};
inline std::ostream &operator<<(std::ostream &stream, const Bigram &gram)
{
std::streamsize p = stream.precision();
stream.precision(6);
stream << "(" << gram.keyValue << "|" << gram.preceedingKeyValue << "," << gram.score << ")";
stream.precision(p);
return stream;
}
inline std::ostream &operator<<(std::ostream &stream, const std::vector<Bigram> &grams)
{
stream << "[" << grams.size() << "]=>{";
size_t index = 0;
for (std::vector<Bigram>::const_iterator gi = grams.begin(); gi != grams.end(); ++gi, ++index)
{
stream << index << "=>";
stream << *gi;
if (gi + 1 != grams.end())
{
stream << ",";
}
}
stream << "}";
return stream;
}
inline Bigram::Bigram() : score(0.0)
{
}
inline bool Bigram::operator==(const Bigram &another) const
{
return preceedingKeyValue == another.preceedingKeyValue && keyValue == another.keyValue && score == another.score;
}
inline bool Bigram::operator<(const Bigram &another) const
{
if (preceedingKeyValue < another.preceedingKeyValue)
{
return true;
}
else if (preceedingKeyValue == another.preceedingKeyValue)
{
if (keyValue < another.keyValue)
{
return true;
}
else if (keyValue == another.keyValue)
{
return score < another.score;
}
return false;
}
return false;
}
} // namespace Gramambular
#endif

View File

@ -1,242 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef BLOCKREADINGBUILDER_H_
#define BLOCKREADINGBUILDER_H_
#include <string>
#include <vector>
#include "Grid.h"
#include "LanguageModel.h"
namespace Gramambular
{
class BlockReadingBuilder
{
public:
explicit BlockReadingBuilder(LanguageModel *lm);
void clear();
size_t length() const;
size_t cursorIndex() const;
void setCursorIndex(size_t newIndex);
void insertReadingAtCursor(const std::string &reading);
bool deleteReadingBeforeCursor(); // backspace
bool deleteReadingAfterCursor(); // delete
bool removeHeadReadings(size_t count);
void setJoinSeparator(const std::string &separator);
const std::string joinSeparator() const;
std::vector<std::string> readings() const;
Grid &grid();
protected:
void build();
static const std::string Join(std::vector<std::string>::const_iterator begin,
std::vector<std::string>::const_iterator end, const std::string &separator);
// 規定最多可以組成的詞的字數上限為 10
static const size_t MaximumBuildSpanLength = 10;
size_t m_cursorIndex;
std::vector<std::string> m_readings;
Grid m_grid;
LanguageModel *m_LM;
std::string m_joinSeparator;
};
inline BlockReadingBuilder::BlockReadingBuilder(LanguageModel *lm) : m_LM(lm), m_cursorIndex(0)
{
}
inline void BlockReadingBuilder::clear()
{
m_cursorIndex = 0;
m_readings.clear();
m_grid.clear();
}
inline size_t BlockReadingBuilder::length() const
{
return m_readings.size();
}
inline size_t BlockReadingBuilder::cursorIndex() const
{
return m_cursorIndex;
}
inline void BlockReadingBuilder::setCursorIndex(size_t newIndex)
{
m_cursorIndex = newIndex > m_readings.size() ? m_readings.size() : newIndex;
}
inline void BlockReadingBuilder::insertReadingAtCursor(const std::string &reading)
{
m_readings.insert(m_readings.begin() + m_cursorIndex, reading);
m_grid.expandGridByOneAtLocation(m_cursorIndex);
build();
m_cursorIndex++;
}
inline std::vector<std::string> BlockReadingBuilder::readings() const
{
return m_readings;
}
inline bool BlockReadingBuilder::deleteReadingBeforeCursor()
{
if (!m_cursorIndex)
{
return false;
}
m_readings.erase(m_readings.begin() + m_cursorIndex - 1, m_readings.begin() + m_cursorIndex);
m_cursorIndex--;
m_grid.shrinkGridByOneAtLocation(m_cursorIndex);
build();
return true;
}
inline bool BlockReadingBuilder::deleteReadingAfterCursor()
{
if (m_cursorIndex == m_readings.size())
{
return false;
}
m_readings.erase(m_readings.begin() + m_cursorIndex, m_readings.begin() + m_cursorIndex + 1);
m_grid.shrinkGridByOneAtLocation(m_cursorIndex);
build();
return true;
}
inline bool BlockReadingBuilder::removeHeadReadings(size_t count)
{
if (count > length())
{
return false;
}
for (size_t i = 0; i < count; i++)
{
if (m_cursorIndex)
{
m_cursorIndex--;
}
m_readings.erase(m_readings.begin(), m_readings.begin() + 1);
m_grid.shrinkGridByOneAtLocation(0);
build();
}
return true;
}
inline void BlockReadingBuilder::setJoinSeparator(const std::string &separator)
{
m_joinSeparator = separator;
}
inline const std::string BlockReadingBuilder::joinSeparator() const
{
return m_joinSeparator;
}
inline Grid &BlockReadingBuilder::grid()
{
return m_grid;
}
inline void BlockReadingBuilder::build()
{
if (!m_LM)
{
return;
}
size_t begin = 0;
size_t end = m_cursorIndex + MaximumBuildSpanLength;
if (m_cursorIndex < MaximumBuildSpanLength)
{
begin = 0;
}
else
{
begin = m_cursorIndex - MaximumBuildSpanLength;
}
if (end > m_readings.size())
{
end = m_readings.size();
}
for (size_t p = begin; p < end; p++)
{
for (size_t q = 1; q <= MaximumBuildSpanLength && p + q <= end; q++)
{
std::string combinedReading = Join(m_readings.begin() + p, m_readings.begin() + p + q, m_joinSeparator);
if (!m_grid.hasNodeAtLocationSpanningLengthMatchingKey(p, q, combinedReading))
{
std::vector<Unigram> unigrams = m_LM->unigramsForKey(combinedReading);
if (unigrams.size() > 0)
{
Node n(combinedReading, unigrams, std::vector<Bigram>());
m_grid.insertNode(n, p, q);
}
}
}
}
}
inline const std::string BlockReadingBuilder::Join(std::vector<std::string>::const_iterator begin,
std::vector<std::string>::const_iterator end,
const std::string &separator)
{
std::string result;
for (std::vector<std::string>::const_iterator iter = begin; iter != end;)
{
result += *iter;
++iter;
if (iter != end)
{
result += separator;
}
}
return result;
}
} // namespace Gramambular
#endif

View File

@ -1,313 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef GRID_H_
#define GRID_H_
#include <map>
#include <string>
#include <vector>
#include "NodeAnchor.h"
#include "Span.h"
namespace Gramambular
{
class Grid
{
public:
void clear();
void insertNode(const Node &node, size_t location, size_t spanningLength);
bool hasNodeAtLocationSpanningLengthMatchingKey(size_t location, size_t spanningLength, const std::string &key);
void expandGridByOneAtLocation(size_t location);
void shrinkGridByOneAtLocation(size_t location);
size_t width() const;
std::vector<NodeAnchor> nodesEndingAt(size_t location);
std::vector<NodeAnchor> nodesCrossingOrEndingAt(size_t location);
// "Freeze" the node with the unigram that represents the selected candidate
// value. After this, the node that contains the unigram will always be
// evaluated to that unigram, while all other overlapping nodes will be reset
// to their initial state (that is, if any of those nodes were "frozen" or
// fixed, they will be unfrozen.)
NodeAnchor fixNodeSelectedCandidate(size_t location, const std::string &value);
// Similar to fixNodeSelectedCandidate, but instead of "freezing" the node,
// only boost the unigram that represents the value with an overriding score.
// This has the same side effect as fixNodeSelectedCandidate, which is that
// all other overlapping nodes will be reset to their initial state.
void overrideNodeScoreForSelectedCandidate(size_t location, const std::string &value, float overridingScore);
std::string dumpDOT()
{
std::stringstream sst;
sst << "digraph {" << std::endl;
sst << "graph [ rankdir=LR ];" << std::endl;
sst << "BOS;" << std::endl;
for (size_t p = 0; p < m_spans.size(); p++)
{
Span &span = m_spans[p];
for (size_t ni = 0; ni <= span.maximumLength(); ni++)
{
Node *np = span.nodeOfLength(ni);
if (np)
{
if (!p)
{
sst << "BOS -> " << np->currentKeyValue().value << ";" << std::endl;
}
sst << np->currentKeyValue().value << ";" << std::endl;
if (p + ni < m_spans.size())
{
Span &dstSpan = m_spans[p + ni];
for (size_t q = 0; q <= dstSpan.maximumLength(); q++)
{
Node *dn = dstSpan.nodeOfLength(q);
if (dn)
{
sst << np->currentKeyValue().value << " -> " << dn->currentKeyValue().value << ";"
<< std::endl;
}
}
}
if (p + ni == m_spans.size())
{
sst << np->currentKeyValue().value << " -> "
<< "EOS;" << std::endl;
}
}
}
}
sst << "EOS;" << std::endl;
sst << "}";
return sst.str();
}
protected:
std::vector<Span> m_spans;
};
inline void Grid::clear()
{
m_spans.clear();
}
inline void Grid::insertNode(const Node &node, size_t location, size_t spanningLength)
{
if (location >= m_spans.size())
{
size_t diff = location - m_spans.size() + 1;
for (size_t i = 0; i < diff; i++)
{
m_spans.push_back(Span());
}
}
m_spans[location].insertNodeOfLength(node, spanningLength);
}
inline bool Grid::hasNodeAtLocationSpanningLengthMatchingKey(size_t location, size_t spanningLength,
const std::string &key)
{
if (location > m_spans.size())
{
return false;
}
const Node *n = m_spans[location].nodeOfLength(spanningLength);
if (!n)
{
return false;
}
return key == n->key();
}
inline void Grid::expandGridByOneAtLocation(size_t location)
{
if (!location || location == m_spans.size())
{
m_spans.insert(m_spans.begin() + location, Span());
}
else
{
m_spans.insert(m_spans.begin() + location, Span());
for (size_t i = 0; i < location; i++)
{
// zaps overlapping spans
m_spans[i].removeNodeOfLengthGreaterThan(location - i);
}
}
}
inline void Grid::shrinkGridByOneAtLocation(size_t location)
{
if (location >= m_spans.size())
{
return;
}
m_spans.erase(m_spans.begin() + location);
for (size_t i = 0; i < location; i++)
{
// zaps overlapping spans
m_spans[i].removeNodeOfLengthGreaterThan(location - i);
}
}
inline size_t Grid::width() const
{
return m_spans.size();
}
// macOS 10.6 開始的內建注音的游標前置選字風格
inline std::vector<NodeAnchor> Grid::nodesEndingAt(size_t location)
{
std::vector<NodeAnchor> result;
if (m_spans.size() && location <= m_spans.size())
{
for (size_t i = 0; i < location; i++)
{
Span &span = m_spans[i];
if (i + span.maximumLength() >= location)
{
Node *np = span.nodeOfLength(location - i);
if (np)
{
NodeAnchor na;
na.node = np;
na.location = i;
na.spanningLength = location - i;
result.push_back(na);
}
}
}
}
return result;
}
// Windows 版奇摩注音輸入法的游標後置的選字風格。
// 與微軟新注音相異的是,這個風格允許在詞的中間叫出候選字窗。
inline std::vector<NodeAnchor> Grid::nodesCrossingOrEndingAt(size_t location)
{
std::vector<NodeAnchor> result;
if (m_spans.size() && location <= m_spans.size())
{
for (size_t i = 0; i < location; i++)
{
Span &span = m_spans[i];
if (i + span.maximumLength() >= location)
{
for (size_t j = 1, m = span.maximumLength(); j <= m; j++)
{
if (i + j < location)
{
continue;
}
Node *np = span.nodeOfLength(j);
if (np)
{
NodeAnchor na;
na.node = np;
na.location = i;
na.spanningLength = location - i;
result.push_back(na);
}
}
}
}
}
return result;
}
// For nodes found at the location, fix their currently-selected candidate using
// the supplied string value.
inline NodeAnchor Grid::fixNodeSelectedCandidate(size_t location, const std::string &value)
{
std::vector<NodeAnchor> nodes = nodesCrossingOrEndingAt(location);
NodeAnchor node;
for (auto nodeAnchor : nodes)
{
auto candidates = nodeAnchor.node->candidates();
// Reset the candidate-fixed state of every node at the location.
const_cast<Node *>(nodeAnchor.node)->resetCandidate();
for (size_t i = 0, c = candidates.size(); i < c; ++i)
{
if (candidates[i].value == value)
{
const_cast<Node *>(nodeAnchor.node)->selectCandidateAtIndex(i);
node = nodeAnchor;
break;
}
}
}
return node;
}
inline void Grid::overrideNodeScoreForSelectedCandidate(size_t location, const std::string &value,
float overridingScore)
{
std::vector<NodeAnchor> nodes = nodesCrossingOrEndingAt(location);
for (auto nodeAnchor : nodes)
{
auto candidates = nodeAnchor.node->candidates();
// Reset the candidate-fixed state of every node at the location.
const_cast<Node *>(nodeAnchor.node)->resetCandidate();
for (size_t i = 0, c = candidates.size(); i < c; ++i)
{
if (candidates[i].value == value)
{
const_cast<Node *>(nodeAnchor.node)->selectFloatingCandidateAtIndex(i, overridingScore);
break;
}
}
}
}
} // namespace Gramambular
#endif

View File

@ -1,71 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef KEYVALUEPAIR_H_
#define KEYVALUEPAIR_H_
#include <ostream>
#include <string>
namespace Gramambular
{
class KeyValuePair
{
public:
std::string key;
std::string value;
bool operator==(const KeyValuePair &another) const;
bool operator<(const KeyValuePair &another) const;
};
inline std::ostream &operator<<(std::ostream &stream, const KeyValuePair &pair)
{
stream << "(" << pair.key << "," << pair.value << ")";
return stream;
}
inline bool KeyValuePair::operator==(const KeyValuePair &another) const
{
return key == another.key && value == another.value;
}
inline bool KeyValuePair::operator<(const KeyValuePair &another) const
{
if (key < another.key)
{
return true;
}
else if (key == another.key)
{
return value < another.value;
}
return false;
}
} // namespace Gramambular
#endif

View File

@ -1,249 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef NODE_H_
#define NODE_H_
#include <limits>
#include <map>
#include <string>
#include <vector>
#include "LanguageModel.h"
namespace Gramambular
{
class Node
{
public:
Node();
Node(const std::string &key, const std::vector<Unigram> &unigrams, const std::vector<Bigram> &bigrams);
void primeNodeWithPreceedingKeyValues(const std::vector<KeyValuePair> &keyValues);
bool isCandidateFixed() const;
const std::vector<KeyValuePair> &candidates() const;
void selectCandidateAtIndex(size_t index = 0, bool fix = true);
void resetCandidate();
void selectFloatingCandidateAtIndex(size_t index, double score);
const std::string &key() const;
double score() const;
double scoreForCandidate(const std::string &candidate) const;
const KeyValuePair currentKeyValue() const;
double highestUnigramScore() const;
protected:
const LanguageModel *m_LM;
std::string m_key;
double m_score;
std::vector<Unigram> m_unigrams;
std::vector<KeyValuePair> m_candidates;
std::map<std::string, size_t> m_valueUnigramIndexMap;
std::map<KeyValuePair, std::vector<Bigram>> m_preceedingGramBigramMap;
bool m_candidateFixed;
size_t m_selectedUnigramIndex;
friend std::ostream &operator<<(std::ostream &stream, const Node &node);
};
inline std::ostream &operator<<(std::ostream &stream, const Node &node)
{
stream << "(node,key:" << node.m_key << ",fixed:" << (node.m_candidateFixed ? "true" : "false")
<< ",selected:" << node.m_selectedUnigramIndex << "," << node.m_unigrams << ")";
return stream;
}
inline Node::Node() : m_candidateFixed(false), m_selectedUnigramIndex(0), m_score(0.0)
{
}
inline Node::Node(const std::string &key, const std::vector<Unigram> &unigrams, const std::vector<Bigram> &bigrams)
: m_key(key), m_unigrams(unigrams), m_candidateFixed(false), m_selectedUnigramIndex(0), m_score(0.0)
{
stable_sort(m_unigrams.begin(), m_unigrams.end(), Unigram::ScoreCompare);
if (m_unigrams.size())
{
m_score = m_unigrams[0].score;
}
size_t i = 0;
for (std::vector<Unigram>::const_iterator ui = m_unigrams.begin(); ui != m_unigrams.end(); ++ui)
{
m_valueUnigramIndexMap[(*ui).keyValue.value] = i;
i++;
m_candidates.push_back((*ui).keyValue);
}
for (std::vector<Bigram>::const_iterator bi = bigrams.begin(); bi != bigrams.end(); ++bi)
{
m_preceedingGramBigramMap[(*bi).preceedingKeyValue].push_back(*bi);
}
}
inline void Node::primeNodeWithPreceedingKeyValues(const std::vector<KeyValuePair> &keyValues)
{
size_t newIndex = m_selectedUnigramIndex;
double max = m_score;
if (!isCandidateFixed())
{
for (std::vector<KeyValuePair>::const_iterator kvi = keyValues.begin(); kvi != keyValues.end(); ++kvi)
{
std::map<KeyValuePair, std::vector<Bigram>>::const_iterator f = m_preceedingGramBigramMap.find(*kvi);
if (f != m_preceedingGramBigramMap.end())
{
const std::vector<Bigram> &bigrams = (*f).second;
for (std::vector<Bigram>::const_iterator bi = bigrams.begin(); bi != bigrams.end(); ++bi)
{
const Bigram &bigram = *bi;
if (bigram.score > max)
{
std::map<std::string, size_t>::const_iterator uf =
m_valueUnigramIndexMap.find((*bi).keyValue.value);
if (uf != m_valueUnigramIndexMap.end())
{
newIndex = (*uf).second;
max = bigram.score;
}
}
}
}
}
}
if (m_score != max)
{
m_score = max;
}
if (newIndex != m_selectedUnigramIndex)
{
m_selectedUnigramIndex = newIndex;
}
}
inline bool Node::isCandidateFixed() const
{
return m_candidateFixed;
}
inline const std::vector<KeyValuePair> &Node::candidates() const
{
return m_candidates;
}
inline void Node::selectCandidateAtIndex(size_t index, bool fix)
{
if (index >= m_unigrams.size())
{
m_selectedUnigramIndex = 0;
}
else
{
m_selectedUnigramIndex = index;
}
m_candidateFixed = fix;
m_score = 99;
}
inline void Node::resetCandidate()
{
m_selectedUnigramIndex = 0;
m_candidateFixed = 0;
if (m_unigrams.size())
{
m_score = m_unigrams[0].score;
}
}
inline void Node::selectFloatingCandidateAtIndex(size_t index, double score)
{
if (index >= m_unigrams.size())
{
m_selectedUnigramIndex = 0;
}
else
{
m_selectedUnigramIndex = index;
}
m_candidateFixed = false;
m_score = score;
}
inline const std::string &Node::key() const
{
return m_key;
}
inline double Node::score() const
{
return m_score;
}
inline double Node::scoreForCandidate(const std::string &candidate) const
{
for (auto unigram : m_unigrams)
{
if (unigram.keyValue.value == candidate)
{
return unigram.score;
}
}
return 0.0;
}
inline double Node::highestUnigramScore() const
{
if (m_unigrams.empty())
{
return 0.0;
}
return m_unigrams[0].score;
}
inline const KeyValuePair Node::currentKeyValue() const
{
if (m_selectedUnigramIndex >= m_unigrams.size())
{
return KeyValuePair();
}
else
{
return m_candidates[m_selectedUnigramIndex];
}
}
} // namespace Gramambular
#endif

View File

@ -1,75 +0,0 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef NODEANCHOR_H_
#define NODEANCHOR_H_
#include <vector>
#include "Node.h"
namespace Gramambular
{
struct NodeAnchor
{
const Node *node = nullptr;
size_t location = 0;
size_t spanningLength = 0;
double accumulatedScore = 0.0;
};
inline std::ostream &operator<<(std::ostream &stream, const NodeAnchor &anchor)
{
stream << "{@(" << anchor.location << "," << anchor.spanningLength << "),";
if (anchor.node)
{
stream << *(anchor.node);
}
else
{
stream << "null";
}
stream << "}";
return stream;
}
inline std::ostream &operator<<(std::ostream &stream, const std::vector<NodeAnchor> &anchor)
{
for (std::vector<NodeAnchor>::const_iterator i = anchor.begin(); i != anchor.end(); ++i)
{
stream << *i;
if (i + 1 != anchor.end())
{
stream << "<-";
}
}
return stream;
}
} // namespace Gramambular
#endif

Some files were not shown because too many files have changed in this diff Show More