From 5af66f7480ea117553a08a6a0fef93e7f0a0dbda Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sun, 3 Apr 2022 11:23:50 +0800 Subject: [PATCH] Swift // Clang-Format. --- .clang-format-swift.json | 56 + DataCompiler/dataCompiler.swift | 668 ++++---- Installer/AppDelegate.swift | 518 +++--- Installer/ArchiveUtil.swift | 207 +-- .../3rdParty/OpenCCBridge/OpenCCBridge.swift | 77 +- Source/Modules/AppDelegate.swift | 588 ++++--- .../AppleKeyboardConverter.swift | 644 +++---- .../ControllerModules/InputState.swift | 698 ++++---- .../Modules/ControllerModules/KeyParser.swift | 445 ++--- .../ControllerModules/NSStringUtils.swift | 115 +- .../vChewingKanjiConverter.swift | 1516 +++++++++-------- .../FileHandlers/FSEventStreamHelper.swift | 156 +- Source/Modules/IME.swift | 307 ++-- .../IMEModules/InputSourceHelper.swift | 217 +-- .../Modules/IMEModules/ctlInputMethod.swift | 1437 +++++++++------- Source/Modules/IMEModules/mgrPrefs.swift | 935 +++++----- .../Modules/LangModelRelated/mgrLangModel.mm | 2 +- Source/Modules/SFX/clsSFX.swift | 100 +- Source/Modules/main.swift | 63 +- .../UI/CandidateUI/CandidateController.swift | 263 +-- .../HorizontalCandidateController.swift | 741 ++++---- .../VerticalCandidateController.swift | 750 ++++---- Source/UI/NotifierUI/NotifierController.swift | 333 ++-- Source/UI/TooltipUI/TooltipController.swift | 191 ++- Source/WindowControllers/ctlAboutWindow.swift | 89 +- .../ctlNonModalAlertWindow.swift | 188 +- Source/WindowControllers/ctlPrefWindow.swift | 480 +++--- UserPhraseEditor/AppDelegate.swift | 78 +- UserPhraseEditor/Content.swift | 60 +- UserPhraseEditor/Document.swift | 254 +-- UserPhraseEditor/StringExtension.swift | 1016 +++++------ UserPhraseEditor/ViewController.swift | 86 +- UserPhraseEditor/WindowController.swift | 50 +- UserPhraseEditor/ctlAboutWindow.swift | 89 +- 34 files changed, 7137 insertions(+), 6280 deletions(-) create mode 100644 .clang-format-swift.json diff --git a/.clang-format-swift.json b/.clang-format-swift.json new file mode 100644 index 00000000..7c9dc985 --- /dev/null +++ b/.clang-format-swift.json @@ -0,0 +1,56 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "tabs" : 1 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : true, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 120, + "maximumBlankLines" : 1, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoBlockComments" : false, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "tabWidth" : 4, + "version" : 1 +} diff --git a/DataCompiler/dataCompiler.swift b/DataCompiler/dataCompiler.swift index bb2fc46d..bc183687 100644 --- a/DataCompiler/dataCompiler.swift +++ b/DataCompiler/dataCompiler.swift @@ -1,383 +1,421 @@ +#!/usr/bin/env swift + // 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: +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. +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. +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. +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 // MARK: - 前導工作 -fileprivate extension String { - 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(self.startIndex..., in: self) - self = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) - } catch { return } - } +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(self.startIndex..., in: self) + self = regex.stringByReplacingMatches( + in: self, options: [], range: range, withTemplate: replaceWith) + } catch { return } + } } -fileprivate func getDocumentsDirectory() -> URL { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - return paths[0] +private func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] } // MARK: - 引入小數點位數控制函數 // Ref: https://stackoverflow.com/a/32581409/4162914 -fileprivate extension Float { - func rounded(toPlaces places:Int) -> Float { - let divisor = pow(10.0, Float(places)) - return (self * divisor).rounded() / divisor - } +extension Float { + fileprivate func rounded(toPlaces places: Int) -> Float { + let divisor = pow(10.0, Float(places)) + return (self * divisor).rounded() / divisor + } } // MARK: - 引入幂乘函數 // Ref: https://stackoverflow.com/a/41581695/4162914 precedencegroup ExponentiationPrecedence { - associativity: right - higherThan: MultiplicationPrecedence + associativity: right + higherThan: MultiplicationPrecedence } -infix operator ** : ExponentiationPrecedence +infix operator **: ExponentiationPrecedence func ** (_ base: Double, _ exp: Double) -> Double { - return pow(base, exp) + return pow(base, exp) } func ** (_ base: Float, _ exp: Float) -> Float { - return pow(base, exp) + return pow(base, exp) } // MARK: - 定義檔案結構 struct Entry { - var valPhone: String = "" - var valPhrase: String = "" - var valWeight: Float = -1.0 - var valCount: Int = 0 + var valPhone: String = "" + var valPhrase: String = "" + var valWeight: Float = -1.0 + var valCount: Int = 0 } // MARK: - 登記全局根常數變數 -fileprivate let urlCurrentFolder = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) +private let urlCurrentFolder = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) -fileprivate let url_CHS_Custom: String = "./components/chs/phrases-custom-chs.txt" -fileprivate let url_CHS_MCBP: String = "./components/chs/phrases-mcbp-chs.txt" -fileprivate let url_CHS_MOE: String = "./components/chs/phrases-moe-chs.txt" -fileprivate let url_CHS_VCHEW: String = "./components/chs/phrases-vchewing-chs.txt" +private let urlCHSforCustom: String = "./components/chs/phrases-custom-chs.txt" +private let urlCHSforMCBP: String = "./components/chs/phrases-mcbp-chs.txt" +private let urlCHSforMOE: String = "./components/chs/phrases-moe-chs.txt" +private let urlCHSforVCHEW: String = "./components/chs/phrases-vchewing-chs.txt" -fileprivate let url_CHT_Custom: String = "./components/cht/phrases-custom-cht.txt" -fileprivate let url_CHT_MCBP: String = "./components/cht/phrases-mcbp-cht.txt" -fileprivate let url_CHT_MOE: String = "./components/cht/phrases-moe-cht.txt" -fileprivate let url_CHT_VCHEW: String = "./components/cht/phrases-vchewing-cht.txt" +private let urlCHTforCustom: String = "./components/cht/phrases-custom-cht.txt" +private let urlCHTforMCBP: String = "./components/cht/phrases-mcbp-cht.txt" +private let urlCHTforMOE: String = "./components/cht/phrases-moe-cht.txt" +private let urlCHTforVCHEW: String = "./components/cht/phrases-vchewing-cht.txt" -fileprivate let urlKanjiCore: String = "./components/common/char-kanji-core.txt" -fileprivate let urlPunctuation: String = "./components/common/data-punctuations.txt" -fileprivate let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt" -fileprivate let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt" +private let urlKanjiCore: String = "./components/common/char-kanji-core.txt" +private let urlPunctuation: String = "./components/common/data-punctuations.txt" +private let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt" +private let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt" -fileprivate let urlOutputCHS: String = "./data-chs.txt" -fileprivate let urlOutputCHT: String = "./data-cht.txt" +private let urlOutputCHS: String = "./data-chs.txt" +private let urlOutputCHT: String = "./data-cht.txt" // MARK: - 載入詞組檔案且輸出數組 func rawDictForPhrases(isCHS: Bool) -> [Entry] { - var arrEntryRAW: [Entry] = [] - var strRAW: String = "" - let urlCustom: String = isCHS ? url_CHS_Custom : url_CHT_Custom - let urlMCBP: String = isCHS ? url_CHS_MCBP : url_CHT_MCBP - let urlMOE: String = isCHS ? url_CHS_MOE : url_CHT_MOE - let urlVCHEW: String = isCHS ? url_CHS_VCHEW : url_CHT_VCHEW - let i18n: String = isCHS ? "簡體中文" : "繁體中文" - // 讀取內容 - do { - strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8) - strRAW += "\n" - strRAW += try String(contentsOfFile: urlMCBP, encoding: .utf8) - strRAW += "\n" - strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8) - strRAW += "\n" - strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8) - } - catch { - NSLog(" - Exception happened when reading raw phrases data.") - return [] - } - // 預處理格式 - strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 - // CJKWhiteSpace (\x{3000}) to ASCII Space - // NonBreakWhiteSpace (\x{A0}) to ASCII Space - // Tab to ASCII Space - // 統整連續空格為一個 ASCII 空格 - strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") - strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // 去除行尾行首空格 - strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 - strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // 以#開頭的行都淨空+去掉所有 WIN32 特有的行 - // 正式整理格式,現在就開始去重複: - let arrData = Array(NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) - for lineData in arrData { - // 第三欄開始是注音 - let arrLineData = lineData.components(separatedBy: " ") - var varLineDataProcessed: String = "" - var count = 0 - for currentCell in arrLineData { - count += 1 - if count < 3 { - varLineDataProcessed += currentCell + "\t" - } else if count < arrLineData.count { - varLineDataProcessed += currentCell + "-" - } else { - varLineDataProcessed += currentCell - } - } - // 然後直接乾脆就轉成 Entry 吧。 - let arrCells : [String] = varLineDataProcessed.components(separatedBy: "\t") - count = 0 // 不需要再定義,因為之前已經有定義過了。 - var phone = "" - var phrase = "" - var occurrence = 0 - for cell in arrCells { - count += 1 - switch count { - case 1: phrase = cell - case 3: phone = cell - case 2: occurrence = Int(cell) ?? 0 - default: break - } - } - if phrase != "" { // 廢掉空數據;之後無須再這樣處理。 - arrEntryRAW += [Entry.init(valPhone: phone, valPhrase: phrase, valWeight: 0.0, valCount: occurrence)] - } - } - NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。") - return arrEntryRAW + var arrEntryRAW: [Entry] = [] + var strRAW: String = "" + let urlCustom: String = isCHS ? urlCHSforCustom : urlCHTforCustom + let urlMCBP: String = isCHS ? urlCHSforMCBP : urlCHTforMCBP + let urlMOE: String = isCHS ? urlCHSforMOE : urlCHTforMOE + let urlVCHEW: String = isCHS ? urlCHSforVCHEW : urlCHTforVCHEW + let i18n: String = isCHS ? "簡體中文" : "繁體中文" + // 讀取內容 + do { + strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8) + strRAW += "\n" + strRAW += try String(contentsOfFile: urlMCBP, encoding: .utf8) + strRAW += "\n" + strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8) + strRAW += "\n" + strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8) + } catch { + NSLog(" - Exception happened when reading raw phrases data.") + return [] + } + // 預處理格式 + strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 + // CJKWhiteSpace (\x{3000}) to ASCII Space + // NonBreakWhiteSpace (\x{A0}) to ASCII Space + // Tab to ASCII Space + // 統整連續空格為一個 ASCII 空格 + strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") + strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // 去除行尾行首空格 + strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 + strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // 以#開頭的行都淨空+去掉所有 WIN32 特有的行 + // 正式整理格式,現在就開始去重複: + let arrData = Array( + NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) + for lineData in arrData { + // 第三欄開始是注音 + let arrLineData = lineData.components(separatedBy: " ") + var varLineDataProcessed: String = "" + var count = 0 + for currentCell in arrLineData { + count += 1 + if count < 3 { + varLineDataProcessed += currentCell + "\t" + } else if count < arrLineData.count { + varLineDataProcessed += currentCell + "-" + } else { + varLineDataProcessed += currentCell + } + } + // 然後直接乾脆就轉成 Entry 吧。 + let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t") + count = 0 // 不需要再定義,因為之前已經有定義過了。 + var phone = "" + var phrase = "" + var occurrence = 0 + for cell in arrCells { + count += 1 + switch count { + case 1: phrase = cell + case 3: phone = cell + case 2: occurrence = Int(cell) ?? 0 + default: break + } + } + if phrase != "" { // 廢掉空數據;之後無須再這樣處理。 + arrEntryRAW += [ + Entry.init( + valPhone: phone, valPhrase: phrase, valWeight: 0.0, + valCount: occurrence) + ] + } + } + NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。") + return arrEntryRAW } // MARK: - 載入單字檔案且輸出數組 func rawDictForKanjis(isCHS: Bool) -> [Entry] { - var arrEntryRAW: [Entry] = [] - var strRAW: String = "" - let i18n: String = isCHS ? "簡體中文" : "繁體中文" - // 讀取內容 - do { - strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8) - } - catch { - NSLog(" - Exception happened when reading raw core kanji data.") - return [] - } - // 預處理格式 - strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 - // CJKWhiteSpace (\x{3000}) to ASCII Space - // NonBreakWhiteSpace (\x{A0}) to ASCII Space - // Tab to ASCII Space - // 統整連續空格為一個 ASCII 空格 - strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") - strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // 去除行尾行首空格 - strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 - strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // 以#開頭的行都淨空+去掉所有 WIN32 特有的行 - // 正式整理格式,現在就開始去重複: - let arrData = Array(NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) - var varLineData: String = "" - for lineData in arrData { - // 簡體中文的話,提取 1,2,4;繁體中文的話,提取 1,3,4。 - let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1).joined(separator: "\t") - let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2).joined(separator: "\t") - varLineData = varLineDataPre + "\t" + varLineDataPost - let arrLineData = varLineData.components(separatedBy: " ") - var varLineDataProcessed: String = "" - var count = 0 - for currentCell in arrLineData { - count += 1 - if count < 3 { - varLineDataProcessed += currentCell + "\t" - } else if count < arrLineData.count { - varLineDataProcessed += currentCell + "-" - } else { - varLineDataProcessed += currentCell - } - } - // 然後直接乾脆就轉成 Entry 吧。 - let arrCells : [String] = varLineDataProcessed.components(separatedBy: "\t") - count = 0 // 不需要再定義,因為之前已經有定義過了。 - var phone = "" - var phrase = "" - var occurrence = 0 - for cell in arrCells { - count += 1 - switch count { - case 1: phrase = cell - case 3: phone = cell - case 2: occurrence = Int(cell) ?? 0 - default: break - } - } - if phrase != "" { // 廢掉空數據;之後無須再這樣處理。 - arrEntryRAW += [Entry.init(valPhone: phone, valPhrase: phrase, valWeight: 0.0, valCount: occurrence)] - } - } - NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。") - return arrEntryRAW + var arrEntryRAW: [Entry] = [] + var strRAW: String = "" + let i18n: String = isCHS ? "簡體中文" : "繁體中文" + // 讀取內容 + do { + strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8) + } catch { + NSLog(" - Exception happened when reading raw core kanji data.") + return [] + } + // 預處理格式 + strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 + // CJKWhiteSpace (\x{3000}) to ASCII Space + // NonBreakWhiteSpace (\x{A0}) to ASCII Space + // Tab to ASCII Space + // 統整連續空格為一個 ASCII 空格 + strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") + strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // 去除行尾行首空格 + strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 + strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // 以#開頭的行都淨空+去掉所有 WIN32 特有的行 + // 正式整理格式,現在就開始去重複: + let arrData = Array( + NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) + var varLineData: String = "" + for lineData in arrData { + // 簡體中文的話,提取 1,2,4;繁體中文的話,提取 1,3,4。 + let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1) + .joined( + separator: "\t") + let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2) + .joined( + separator: "\t") + varLineData = varLineDataPre + "\t" + varLineDataPost + let arrLineData = varLineData.components(separatedBy: " ") + var varLineDataProcessed: String = "" + var count = 0 + for currentCell in arrLineData { + count += 1 + if count < 3 { + varLineDataProcessed += currentCell + "\t" + } else if count < arrLineData.count { + varLineDataProcessed += currentCell + "-" + } else { + varLineDataProcessed += currentCell + } + } + // 然後直接乾脆就轉成 Entry 吧。 + let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t") + count = 0 // 不需要再定義,因為之前已經有定義過了。 + var phone = "" + var phrase = "" + var occurrence = 0 + for cell in arrCells { + count += 1 + switch count { + case 1: phrase = cell + case 3: phone = cell + case 2: occurrence = Int(cell) ?? 0 + default: break + } + } + if phrase != "" { // 廢掉空數據;之後無須再這樣處理。 + arrEntryRAW += [ + Entry.init( + valPhone: phone, valPhrase: phrase, valWeight: 0.0, + valCount: occurrence) + ] + } + } + NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。") + return arrEntryRAW } // MARK: - 載入非漢字檔案且輸出數組 func rawDictForNonKanjis(isCHS: Bool) -> [Entry] { - var arrEntryRAW: [Entry] = [] - var strRAW: String = "" - let i18n: String = isCHS ? "簡體中文" : "繁體中文" - // 讀取內容 - do { - strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8) - strRAW += "\n" - strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8) - } - catch { - NSLog(" - Exception happened when reading raw core kanji data.") - return [] - } - // 預處理格式 - strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 - // CJKWhiteSpace (\x{3000}) to ASCII Space - // NonBreakWhiteSpace (\x{A0}) to ASCII Space - // Tab to ASCII Space - // 統整連續空格為一個 ASCII 空格 - strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") - strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // 去除行尾行首空格 - strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 - strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // 以#開頭的行都淨空+去掉所有 WIN32 特有的行 - // 正式整理格式,現在就開始去重複: - let arrData = Array(NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) - var varLineData: String = "" - for lineData in arrData { - varLineData = lineData - // 先完成某兩步需要分行處理才能完成的格式整理。 - varLineData = varLineData.components(separatedBy: " ").prefix(3).joined(separator: "\t") // 提取前三欄的內容。 - let arrLineData = varLineData.components(separatedBy: " ") - var varLineDataProcessed: String = "" - var count = 0 - for currentCell in arrLineData { - count += 1 - if count < 3 { - varLineDataProcessed += currentCell + "\t" - } else if count < arrLineData.count { - varLineDataProcessed += currentCell + "-" - } else { - varLineDataProcessed += currentCell - } - } - // 然後直接乾脆就轉成 Entry 吧。 - let arrCells : [String] = varLineDataProcessed.components(separatedBy: "\t") - count = 0 // 不需要再定義,因為之前已經有定義過了。 - var phone = "" - var phrase = "" - var occurrence = 0 - for cell in arrCells { - count += 1 - switch count { - case 1: phrase = cell - case 3: phone = cell - case 2: occurrence = Int(cell) ?? 0 - default: break - } - } - if phrase != "" { // 廢掉空數據;之後無須再這樣處理。 - arrEntryRAW += [Entry.init(valPhone: phone, valPhrase: phrase, valWeight: 0.0, valCount: occurrence)] - } - } - NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。") - return arrEntryRAW + var arrEntryRAW: [Entry] = [] + var strRAW: String = "" + let i18n: String = isCHS ? "簡體中文" : "繁體中文" + // 讀取內容 + do { + strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8) + strRAW += "\n" + strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8) + } catch { + NSLog(" - Exception happened when reading raw core kanji data.") + return [] + } + // 預處理格式 + strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 + // CJKWhiteSpace (\x{3000}) to ASCII Space + // NonBreakWhiteSpace (\x{A0}) to ASCII Space + // Tab to ASCII Space + // 統整連續空格為一個 ASCII 空格 + strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") + strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // 去除行尾行首空格 + strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 + strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // 以#開頭的行都淨空+去掉所有 WIN32 特有的行 + // 正式整理格式,現在就開始去重複: + let arrData = Array( + NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) + var varLineData: String = "" + for lineData in arrData { + varLineData = lineData + // 先完成某兩步需要分行處理才能完成的格式整理。 + varLineData = varLineData.components(separatedBy: " ").prefix(3).joined( + separator: "\t") // 提取前三欄的內容。 + let arrLineData = varLineData.components(separatedBy: " ") + var varLineDataProcessed: String = "" + var count = 0 + for currentCell in arrLineData { + count += 1 + if count < 3 { + varLineDataProcessed += currentCell + "\t" + } else if count < arrLineData.count { + varLineDataProcessed += currentCell + "-" + } else { + varLineDataProcessed += currentCell + } + } + // 然後直接乾脆就轉成 Entry 吧。 + let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t") + count = 0 // 不需要再定義,因為之前已經有定義過了。 + var phone = "" + var phrase = "" + var occurrence = 0 + for cell in arrCells { + count += 1 + switch count { + case 1: phrase = cell + case 3: phone = cell + case 2: occurrence = Int(cell) ?? 0 + default: break + } + } + if phrase != "" { // 廢掉空數據;之後無須再這樣處理。 + arrEntryRAW += [ + Entry.init( + valPhone: phone, valPhrase: phrase, valWeight: 0.0, + valCount: occurrence) + ] + } + } + NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。") + return arrEntryRAW } func weightAndSort(_ arrStructUncalculated: [Entry], isCHS: Bool) -> [Entry] { - let i18n: String = isCHS ? "簡體中文" : "繁體中文" - var arrStructCalculated: [Entry] = [] - let fscale: Float = 2.7 - var norm: Float = 0.0 - for entry in arrStructUncalculated { - if entry.valCount >= 0 { - norm += fscale**(Float(entry.valPhrase.count) / 3.0 - 1.0) * Float(entry.valCount) - } - } - // norm 計算完畢,開始將 norm 作為新的固定常數來為每個詞條記錄計算權重。 - // 將新酷音的詞語出現次數數據轉換成小麥引擎可讀的數據形式。 - // 對出現次數小於 1 的詞條,將 0 當成 0.5 來處理、以防止除零。 - for entry in arrStructUncalculated { - var weight: Float = 0 - switch entry.valCount { - case -2: // 拗音假名 - weight = -13 - case -1: // 單個假名 - weight = -13 - case 0: // 墊底低頻漢字與詞語 - weight = log10(fscale**(Float(entry.valPhrase.count) / 3.0 - 1.0) * 0.5 / norm) - default: - weight = log10(fscale**(Float(entry.valPhrase.count) / 3.0 - 1.0) * Float(entry.valCount) / norm) // Credit: MJHsieh. - } - let weightRounded: Float = weight.rounded(toPlaces: 3) // 為了節省生成的檔案體積,僅保留小數點後三位。 - arrStructCalculated += [Entry.init(valPhone: entry.valPhone, valPhrase: entry.valPhrase, valWeight: weightRounded, valCount: entry.valCount)] - } - NSLog(" - \(i18n): 成功計算權重。") - // ========================================== - // 接下來是排序,先按照注音遞減排序一遍、再按照權重遞減排序一遍。 - let arrStructSorted: [Entry] = arrStructCalculated.sorted(by: {(lhs, rhs) -> Bool in return (lhs.valPhone, rhs.valCount) < (rhs.valPhone, lhs.valCount)}) - NSLog(" - \(i18n): 排序整理完畢,準備編譯要寫入的檔案內容。") - return arrStructSorted + let i18n: String = isCHS ? "簡體中文" : "繁體中文" + var arrStructCalculated: [Entry] = [] + let fscale: Float = 2.7 + var norm: Float = 0.0 + for entry in arrStructUncalculated { + if entry.valCount >= 0 { + norm += fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) + * Float(entry.valCount) + } + } + // norm 計算完畢,開始將 norm 作為新的固定常數來為每個詞條記錄計算權重。 + // 將新酷音的詞語出現次數數據轉換成小麥引擎可讀的數據形式。 + // 對出現次數小於 1 的詞條,將 0 當成 0.5 來處理、以防止除零。 + for entry in arrStructUncalculated { + var weight: Float = 0 + switch entry.valCount { + case -2: // 拗音假名 + weight = -13 + case -1: // 單個假名 + weight = -13 + case 0: // 墊底低頻漢字與詞語 + weight = log10( + fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) * 0.5 / norm) + default: + weight = log10( + fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) + * Float(entry.valCount) / norm) // Credit: MJHsieh. + } + let weightRounded: Float = weight.rounded(toPlaces: 3) // 為了節省生成的檔案體積,僅保留小數點後三位。 + arrStructCalculated += [ + Entry.init( + valPhone: entry.valPhone, valPhrase: entry.valPhrase, valWeight: weightRounded, + valCount: entry.valCount) + ] + } + NSLog(" - \(i18n): 成功計算權重。") + // ========================================== + // 接下來是排序,先按照注音遞減排序一遍、再按照權重遞減排序一遍。 + let arrStructSorted: [Entry] = arrStructCalculated.sorted(by: { (lhs, rhs) -> Bool in + return (lhs.valPhone, rhs.valCount) < (rhs.valPhone, lhs.valCount) + }) + NSLog(" - \(i18n): 排序整理完畢,準備編譯要寫入的檔案內容。") + return arrStructSorted } func fileOutput(isCHS: Bool) { - let i18n: String = isCHS ? "簡體中文" : "繁體中文" - let pathOutput = urlCurrentFolder.appendingPathComponent(isCHS ? urlOutputCHS : urlOutputCHT) - var strPrintLine = "" - // 讀取標點內容 - do { - strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8) - } - catch { - NSLog(" - \(i18n): Exception happened when reading raw punctuation data.") - } - NSLog(" - \(i18n): 成功插入標點符號與西文字母數據。") - // 統合辭典內容 - var arrStructUnified: [Entry] = [] - arrStructUnified += rawDictForKanjis(isCHS: isCHS) - arrStructUnified += rawDictForNonKanjis(isCHS: isCHS) - arrStructUnified += rawDictForPhrases(isCHS: isCHS) - // 計算權重且排序 - arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS) - - for entry in arrStructUnified { - strPrintLine += entry.valPhone + " " + entry.valPhrase + " " + String(entry.valWeight) + "\n" - } - NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。") - do { - try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8) - } - catch { - NSLog(" - \(i18n): Error on writing strings to file: \(error)") - } - NSLog(" - \(i18n): 寫入完成。") + let i18n: String = isCHS ? "簡體中文" : "繁體中文" + let pathOutput = urlCurrentFolder.appendingPathComponent( + isCHS ? urlOutputCHS : urlOutputCHT) + var strPrintLine = "" + // 讀取標點內容 + do { + strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8) + } catch { + NSLog(" - \(i18n): Exception happened when reading raw punctuation data.") + } + NSLog(" - \(i18n): 成功插入標點符號與西文字母數據。") + // 統合辭典內容 + var arrStructUnified: [Entry] = [] + arrStructUnified += rawDictForKanjis(isCHS: isCHS) + arrStructUnified += rawDictForNonKanjis(isCHS: isCHS) + arrStructUnified += rawDictForPhrases(isCHS: isCHS) + // 計算權重且排序 + arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS) + + for entry in arrStructUnified { + strPrintLine += + entry.valPhone + " " + entry.valPhrase + " " + String(entry.valWeight) + + "\n" + } + NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。") + do { + try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8) + } catch { + NSLog(" - \(i18n): Error on writing strings to file: \(error)") + } + NSLog(" - \(i18n): 寫入完成。") } // MARK: - 主执行绪 func main() { - NSLog("// 準備編譯繁體中文核心語料檔案。") - fileOutput(isCHS: false) - NSLog("// 準備編譯簡體中文核心語料檔案。") - fileOutput(isCHS: true) + NSLog("// 準備編譯繁體中文核心語料檔案。") + fileOutput(isCHS: false) + NSLog("// 準備編譯簡體中文核心語料檔案。") + fileOutput(isCHS: true) } main() diff --git a/Installer/AppDelegate.swift b/Installer/AppDelegate.swift index b9d212c6..996d9688 100644 --- a/Installer/AppDelegate.swift +++ b/Installer/AppDelegate.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -23,9 +30,11 @@ private let kTargetBin = "vChewing" private let kTargetType = "app" private let kTargetBundle = "vChewing.app" -private let urlDestinationPartial = FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0] +private let urlDestinationPartial = FileManager.default.urls( + for: .inputMethodsDirectory, in: .userDomainMask)[0] private let urlTargetPartial = urlDestinationPartial.appendingPathComponent(kTargetBundle) -private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS/").appendingPathComponent(kTargetBin) +private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS/") + .appendingPathComponent(kTargetBin) private let kDestinationPartial = urlDestinationPartial.path private let kTargetPartialPath = urlTargetPartial.path @@ -35,251 +44,300 @@ private let kTranslocationRemovalTickInterval: TimeInterval = 0.5 private let kTranslocationRemovalDeadline: TimeInterval = 60.0 @NSApplicationMain -@objc (AppDelegate) +@objc(AppDelegate) class AppDelegate: NSWindowController, NSApplicationDelegate { - @IBOutlet weak private var installButton: NSButton! - @IBOutlet weak private var cancelButton: NSButton! - @IBOutlet weak private var progressSheet: NSWindow! - @IBOutlet weak private var progressIndicator: NSProgressIndicator! - @IBOutlet weak private var appVersionLabel: NSTextField! - @IBOutlet weak private var appCopyrightLabel: NSTextField! - @IBOutlet private var appEULAContent: NSTextView! - - private var archiveUtil: ArchiveUtil? - private var installingVersion = "" - private var upgrading = false - private var translocationRemovalStartTime: Date? - private var currentVersionNumber: Int = 0 + @IBOutlet weak private var installButton: NSButton! + @IBOutlet weak private var cancelButton: NSButton! + @IBOutlet weak private var progressSheet: NSWindow! + @IBOutlet weak private var progressIndicator: NSProgressIndicator! + @IBOutlet weak private var appVersionLabel: NSTextField! + @IBOutlet weak private var appCopyrightLabel: NSTextField! + @IBOutlet private var appEULAContent: NSTextView! - func runAlertPanel(title: String, message: String, buttonTitle: String) { - let alert = NSAlert() - alert.alertStyle = .informational - alert.messageText = title - alert.informativeText = message - alert.addButton(withTitle: buttonTitle) - alert.runModal() - } + private var archiveUtil: ArchiveUtil? + private var installingVersion = "" + private var upgrading = false + private var translocationRemovalStartTime: Date? + private var currentVersionNumber: Int = 0 - func applicationDidFinishLaunching(_ notification: Notification) { - guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String, - let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { - return - } - self.installingVersion = installingVersion - self.archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle) - _ = archiveUtil?.validateIfNotarizedArchiveExists() + func runAlertPanel(title: String, message: String, buttonTitle: String) { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = title + alert.informativeText = message + alert.addButton(withTitle: buttonTitle) + alert.runModal() + } - cancelButton.nextKeyView = installButton - installButton.nextKeyView = cancelButton - if let cell = installButton.cell as? NSButtonCell { - window?.defaultButtonCell = cell - } - - if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String { - appCopyrightLabel.stringValue = copyrightLabel - } - if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { - appEULAContent.string = eulaContent - } - appVersionLabel.stringValue = String(format: "%@ Build %@", versionString, installingVersion) + func applicationDidFinishLaunching(_ notification: Notification) { + guard + let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] + as? String, + let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + else { + return + } + self.installingVersion = installingVersion + self.archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle) + _ = archiveUtil?.validateIfNotarizedArchiveExists() - window?.title = String(format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "", versionString, installingVersion) - window?.standardWindowButton(.closeButton)?.isHidden = true - window?.standardWindowButton(.miniaturizeButton)?.isHidden = true - window?.standardWindowButton(.zoomButton)?.isHidden = true + cancelButton.nextKeyView = installButton + installButton.nextKeyView = cancelButton + if let cell = installButton.cell as? NSButtonCell { + window?.defaultButtonCell = cell + } - if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) { - let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath) - let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String - let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String - currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0 - if shortVersion != nil, let currentVersion = currentVersion, currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending { - upgrading = true - } - } + if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] + as? String + { + appCopyrightLabel.stringValue = copyrightLabel + } + if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { + appEULAContent.string = eulaContent + } + appVersionLabel.stringValue = String( + format: "%@ Build %@", versionString, installingVersion) - if upgrading { - installButton.title = NSLocalizedString("Upgrade", comment: "") - } + window?.title = String( + format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "", + versionString, installingVersion) + window?.standardWindowButton(.closeButton)?.isHidden = true + window?.standardWindowButton(.miniaturizeButton)?.isHidden = true + window?.standardWindowButton(.zoomButton)?.isHidden = true - window?.center() - window?.orderFront(self) - NSApp.activate(ignoringOtherApps: true) - } + if FileManager.default.fileExists( + atPath: (kTargetPartialPath as NSString).expandingTildeInPath) + { + let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath) + let shortVersion = + currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String + let currentVersion = + currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String + currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0 + if shortVersion != nil, let currentVersion = currentVersion, + currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending + { + upgrading = true + } + } - @IBAction func agreeAndInstallAction(_ sender: AnyObject) { - cancelButton.isEnabled = false - installButton.isEnabled = false - removeThenInstallInputMethod() - } + if upgrading { + installButton.title = NSLocalizedString("Upgrade", comment: "") + } - @objc func timerTick(_ timer: Timer) { - let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date()) - if elapsed >= kTranslocationRemovalDeadline { - timer.invalidate() - window?.endSheet(progressSheet, returnCode: .cancel) - } else if appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) == false { - progressIndicator.doubleValue = 1.0 - timer.invalidate() - window?.endSheet(progressSheet, returnCode: .continue) - } - } + window?.center() + window?.orderFront(self) + NSApp.activate(ignoringOtherApps: true) + } - func removeThenInstallInputMethod() { - if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) == false { - self.installInputMethod(previousExists: false, previousVersionNotFullyDeactivatedWarning: false) - return - } + @IBAction func agreeAndInstallAction(_ sender: AnyObject) { + cancelButton.isEnabled = false + installButton.isEnabled = false + removeThenInstallInputMethod() + } - let shouldWaitForTranslocationRemoval = appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) && (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false) + @objc func timerTick(_ timer: Timer) { + let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date()) + if elapsed >= kTranslocationRemovalDeadline { + timer.invalidate() + window?.endSheet(progressSheet, returnCode: .cancel) + } else if appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) == false { + progressIndicator.doubleValue = 1.0 + timer.invalidate() + window?.endSheet(progressSheet, returnCode: .continue) + } + } - // 將既存輸入法扔到垃圾桶內 - do { - let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath - let fileManager = FileManager.default - let fileURLString = String(format: "%@/%@", sourceDir, kTargetBundle) - let fileURL = URL(fileURLWithPath: fileURLString) - - // 檢查檔案是否存在 - if fileManager.fileExists(atPath: fileURLString) { - // 塞入垃圾桶 - try fileManager.trashItem(at: fileURL, resultingItemURL: nil) - } else { - NSLog("File does not exist") - } - - } - catch let error as NSError { - NSLog("An error took place: \(error)") - } + func removeThenInstallInputMethod() { + if FileManager.default.fileExists( + atPath: (kTargetPartialPath as NSString).expandingTildeInPath) + == false + { + self.installInputMethod( + previousExists: false, previousVersionNotFullyDeactivatedWarning: false) + return + } - let killTask = Process() - killTask.launchPath = "/usr/bin/killall" - killTask.arguments = ["-9", kTargetBin] - killTask.launch() - killTask.waitUntilExit() + let shouldWaitForTranslocationRemoval = + appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) + && (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false) - if shouldWaitForTranslocationRemoval { - progressIndicator.startAnimation(self) - window?.beginSheet(progressSheet) { returnCode in - DispatchQueue.main.async { - if returnCode == .continue { - self.installInputMethod(previousExists: true, previousVersionNotFullyDeactivatedWarning: false) - } else { - self.installInputMethod(previousExists: true, previousVersionNotFullyDeactivatedWarning: true) - } - } - } + // 將既存輸入法扔到垃圾桶內 + do { + let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath + let fileManager = FileManager.default + let fileURLString = String(format: "%@/%@", sourceDir, kTargetBundle) + let fileURL = URL(fileURLWithPath: fileURLString) - translocationRemovalStartTime = Date() - Timer.scheduledTimer(timeInterval: kTranslocationRemovalTickInterval, target: self, selector: #selector(timerTick(_:)), userInfo: nil, repeats: true) + // 檢查檔案是否存在 + if fileManager.fileExists(atPath: fileURLString) { + // 塞入垃圾桶 + try fileManager.trashItem(at: fileURL, resultingItemURL: nil) + } else { + NSLog("File does not exist") + } + + } catch let error as NSError { + NSLog("An error took place: \(error)") + } + + let killTask = Process() + killTask.launchPath = "/usr/bin/killall" + killTask.arguments = ["-9", kTargetBin] + killTask.launch() + killTask.waitUntilExit() + + if shouldWaitForTranslocationRemoval { + progressIndicator.startAnimation(self) + window?.beginSheet(progressSheet) { returnCode in + DispatchQueue.main.async { + if returnCode == .continue { + self.installInputMethod( + previousExists: true, + previousVersionNotFullyDeactivatedWarning: false) + } else { + self.installInputMethod( + previousExists: true, + previousVersionNotFullyDeactivatedWarning: true) + } + } + } + + translocationRemovalStartTime = Date() + Timer.scheduledTimer( + timeInterval: kTranslocationRemovalTickInterval, target: self, + selector: #selector(timerTick(_:)), userInfo: nil, repeats: true) } else { - self.installInputMethod(previousExists: false, previousVersionNotFullyDeactivatedWarning: false) - } - } + self.installInputMethod( + previousExists: false, previousVersionNotFullyDeactivatedWarning: false) + } + } - func installInputMethod(previousExists: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool) { - guard let targetBundle = archiveUtil?.unzipNotarizedArchive() ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) else { - return - } - let cpTask = Process() - cpTask.launchPath = "/bin/cp" - cpTask.arguments = ["-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath] - cpTask.launch() - cpTask.waitUntilExit() + func installInputMethod( + previousExists: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool + ) { + guard + let targetBundle = archiveUtil?.unzipNotarizedArchive() + ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) + else { + return + } + let cpTask = Process() + cpTask.launchPath = "/bin/cp" + cpTask.arguments = [ + "-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath, + ] + cpTask.launch() + cpTask.waitUntilExit() - if cpTask.terminationStatus != 0 { - runAlertPanel(title: NSLocalizedString("Install Failed", comment: ""), - message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), - buttonTitle: NSLocalizedString("Cancel", comment: "")) - endAppWithDelay() - } + if cpTask.terminationStatus != 0 { + runAlertPanel( + title: NSLocalizedString("Install Failed", comment: ""), + message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), + buttonTitle: NSLocalizedString("Cancel", comment: "")) + endAppWithDelay() + } - guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath), - let imeIdentifier = imeBundle.bundleIdentifier - else { - endAppWithDelay() - return - } + guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath), + let imeIdentifier = imeBundle.bundleIdentifier + else { + endAppWithDelay() + return + } - let imeBundleURL = imeBundle.bundleURL - var inputSource = InputSourceHelper.inputSource(for: imeIdentifier) + let imeBundleURL = imeBundle.bundleURL + var inputSource = InputSourceHelper.inputSource(for: imeIdentifier) - if inputSource == nil { - NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString)."); - let status = InputSourceHelper.registerTnputSource(at: imeBundleURL) - if !status { - let message = String(format: NSLocalizedString("Cannot find input source %@ after registration.", comment: ""), imeIdentifier) - runAlertPanel(title: NSLocalizedString("Fatal Error", comment: ""), message: message, buttonTitle: NSLocalizedString("Abort", comment: "")) - endAppWithDelay() - return - } + if inputSource == nil { + NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).") + let status = InputSourceHelper.registerTnputSource(at: imeBundleURL) + if !status { + let message = String( + format: NSLocalizedString( + "Cannot find input source %@ after registration.", comment: ""), + imeIdentifier) + runAlertPanel( + title: NSLocalizedString("Fatal Error", comment: ""), message: message, + buttonTitle: NSLocalizedString("Abort", comment: "")) + endAppWithDelay() + return + } - inputSource = InputSourceHelper.inputSource(for: imeIdentifier) - if inputSource == nil { - let message = String(format: NSLocalizedString("Cannot find input source %@ after registration.", comment: ""), imeIdentifier) - runAlertPanel(title: NSLocalizedString("Fatal Error", comment: ""), message: message, buttonTitle: NSLocalizedString("Abort", comment: "")) - } - } + inputSource = InputSourceHelper.inputSource(for: imeIdentifier) + if inputSource == nil { + let message = String( + format: NSLocalizedString( + "Cannot find input source %@ after registration.", comment: ""), + imeIdentifier) + runAlertPanel( + title: NSLocalizedString("Fatal Error", comment: ""), message: message, + buttonTitle: NSLocalizedString("Abort", comment: "")) + } + } - var isMacOS12OrAbove = false - if #available(macOS 12.0, *) { - NSLog("macOS 12 or later detected."); - isMacOS12OrAbove = true - } else { - NSLog("Installer runs with the pre-macOS 12 flow."); - } + var isMacOS12OrAbove = false + if #available(macOS 12.0, *) { + NSLog("macOS 12 or later detected.") + isMacOS12OrAbove = true + } else { + 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+, - // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* - // enabled in the user's current set of IMEs (which means the IME does not show up in - // the user's input menu). + // 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* + // enabled in the user's current set of IMEs (which means the IME does not show up in + // the user's input menu). - var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!) - if !mainInputSourceEnabled || isMacOS12OrAbove { - mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!) - if (mainInputSourceEnabled) { - NSLog("Input method enabled: \(imeIdentifier)"); - } else { - NSLog("Failed to enable input method: \(imeIdentifier)"); - } - } - - // Alert Panel - let ntfPostInstall = NSAlert() - if warning { - ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "") - ntfPostInstall.informativeText = NSLocalizedString("vChewing is upgraded, but please log out or reboot for the new version to be fully functional.", comment: "") - ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) - } else { - if !mainInputSourceEnabled && !isMacOS12OrAbove { - ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "") - ntfPostInstall.informativeText = NSLocalizedString("Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", comment: "") - ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: "")) - } else { - ntfPostInstall.messageText = NSLocalizedString("Installation Successful", comment: "") - ntfPostInstall.informativeText = NSLocalizedString("vChewing is ready to use.", comment: "") - ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) - } - } - ntfPostInstall.beginSheetModal(for: window!) { response in - self.endAppWithDelay() - } - } + var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!) + if !mainInputSourceEnabled || isMacOS12OrAbove { + mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!) + if mainInputSourceEnabled { + NSLog("Input method enabled: \(imeIdentifier)") + } else { + NSLog("Failed to enable input method: \(imeIdentifier)") + } + } - func endAppWithDelay() { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { - NSApp.terminate(self) - } - } + // Alert Panel + let ntfPostInstall = NSAlert() + if warning { + ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "") + ntfPostInstall.informativeText = NSLocalizedString( + "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.", + comment: "") + ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) + } else { + if !mainInputSourceEnabled && !isMacOS12OrAbove { + ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "") + ntfPostInstall.informativeText = NSLocalizedString( + "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", + comment: "") + ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: "")) + } else { + ntfPostInstall.messageText = NSLocalizedString( + "Installation Successful", comment: "") + ntfPostInstall.informativeText = NSLocalizedString( + "vChewing is ready to use.", comment: "") + ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) + } + } + ntfPostInstall.beginSheetModal(for: window!) { response in + self.endAppWithDelay() + } + } - @IBAction func cancelAction(_ sender: AnyObject) { - NSApp.terminate(self) - } + func endAppWithDelay() { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { + NSApp.terminate(self) + } + } - func windowWillClose(_ Notification: Notification) { - NSApp.terminate(self) - } + @IBAction func cancelAction(_ sender: AnyObject) { + NSApp.terminate(self) + } + func windowWillClose(_ notification: Notification) { + NSApp.terminate(self) + } } diff --git a/Installer/ArchiveUtil.swift b/Installer/ArchiveUtil.swift index a0d25027..737fd959 100644 --- a/Installer/ArchiveUtil.swift +++ b/Installer/ArchiveUtil.swift @@ -1,117 +1,134 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 struct ArchiveUtil { - var appName: String - var targetAppBundleName: String + var appName: String + var targetAppBundleName: String - init(appName: String, targetAppBundleName: String) { - self.appName = appName - self.targetAppBundleName = targetAppBundleName - } + init(appName: String, targetAppBundleName: String) { + self.appName = appName + self.targetAppBundleName = targetAppBundleName + } - // Returns YES if (1) a zip file under - // Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if - // Resources/$_invalidAppBundleName does not exist. - func validateIfNotarizedArchiveExists() -> Bool { - guard let resourePath = Bundle.main.resourcePath, - let notarizedArchivesPath = notarizedArchivesPath, - let notarizedArchive = notarizedArchive, - let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(atPath: notarizedArchivesPath) - else { - return false - } + // Returns YES if (1) a zip file under + // Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if + // Resources/$_invalidAppBundleName does not exist. + func validateIfNotarizedArchiveExists() -> Bool { + guard let resourePath = Bundle.main.resourcePath, + let notarizedArchivesPath = notarizedArchivesPath, + let notarizedArchive = notarizedArchive, + let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory( + atPath: notarizedArchivesPath) + else { + return false + } - let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName) - let count = notarizedArchivesContent.count - let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive) - let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath) + let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName) + let count = notarizedArchivesContent.count + let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive) + let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath) - if count > 0 { - if count != 1 || !notarizedArchiveExists || devModeAppBundleExists { - let alert = NSAlert() - alert.alertStyle = .informational - alert.messageText = "Internal Error" - alert.informativeText = "devMode installer, expected archive name: \(notarizedArchive), " + - "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)" - alert.addButton(withTitle: "Terminate") - alert.runModal() - NSApp.terminate(nil) - } else { - return true - } - } + if count > 0 { + if count != 1 || !notarizedArchiveExists || devModeAppBundleExists { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = "Internal Error" + alert.informativeText = + "devMode installer, expected archive name: \(notarizedArchive), " + + "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)" + alert.addButton(withTitle: "Terminate") + alert.runModal() + NSApp.terminate(nil) + } else { + return true + } + } - if !devModeAppBundleExists { - let alert = NSAlert() - alert.alertStyle = .informational - alert.messageText = "Internal Error" - alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)" - alert.addButton(withTitle: "Terminate") - alert.runModal() - NSApp.terminate(nil) - } + if !devModeAppBundleExists { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = "Internal Error" + alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)" + alert.addButton(withTitle: "Terminate") + alert.runModal() + NSApp.terminate(nil) + } - return false - } + return false + } - func unzipNotarizedArchive() -> String? { - if !self.validateIfNotarizedArchiveExists() { - return nil - } - guard let notarizedArchive = notarizedArchive, - let resourcePath = Bundle.main.resourcePath else { - return nil - } - let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(UUID().uuidString) - let arguments: [String] = [notarizedArchive, "-d", tempFilePath] - let unzipTask = Process() - unzipTask.launchPath = "/usr/bin/unzip" - unzipTask.currentDirectoryPath = resourcePath - unzipTask.arguments = arguments - unzipTask.launch() - unzipTask.waitUntilExit() + func unzipNotarizedArchive() -> String? { + if !self.validateIfNotarizedArchiveExists() { + return nil + } + guard let notarizedArchive = notarizedArchive, + let resourcePath = Bundle.main.resourcePath + else { + return nil + } + let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent( + UUID().uuidString) + let arguments: [String] = [notarizedArchive, "-d", tempFilePath] + let unzipTask = Process() + unzipTask.launchPath = "/usr/bin/unzip" + unzipTask.currentDirectoryPath = resourcePath + unzipTask.arguments = arguments + unzipTask.launch() + unzipTask.waitUntilExit() - assert(unzipTask.terminationStatus == 0, "Must successfully unzipped") - let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName) - assert(FileManager.default.fileExists(atPath: result), "App bundle must be unzipped at \(result).") - return result - } + assert(unzipTask.terminationStatus == 0, "Must successfully unzipped") + let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName) + assert( + FileManager.default.fileExists(atPath: result), + "App bundle must be unzipped at \(result).") + return result + } - private var notarizedArchivesPath: String? { - guard let resourePath = Bundle.main.resourcePath else { - return nil - } - let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent("NotarizedArchives") - return notarizedArchivesPath - } + private var notarizedArchivesPath: String? { + guard let resourePath = Bundle.main.resourcePath else { + return nil + } + let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent( + "NotarizedArchives") + return notarizedArchivesPath + } - private var notarizedArchive: String? { - guard let notarizedArchivesPath = notarizedArchivesPath, - let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String else { - return nil - } - let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip" - let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent(notarizedArchiveBasename) - return notarizedArchive - } + private var notarizedArchive: String? { + guard let notarizedArchivesPath = notarizedArchivesPath, + let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] + as? String + else { + return nil + } + let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip" + let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent( + notarizedArchiveBasename) + return notarizedArchive + } } diff --git a/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift b/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift index f1f23af8..9d8d209b 100644 --- a/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift +++ b/Source/3rdParty/OpenCCBridge/OpenCCBridge.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -25,28 +32,28 @@ import OpenCC /// 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. public class OpenCCBridge: NSObject { - private static let shared = OpenCCBridge() - private var simplify: ChineseConverter? - private var traditionalize: ChineseConverter? - - private override init() { - try? simplify = ChineseConverter(options: .simplify) - try? traditionalize = ChineseConverter(options: [.traditionalize, .twStandard]) - super.init() - } + private static let shared = OpenCCBridge() + private var simplify: ChineseConverter? + private var traditionalize: ChineseConverter? - /// CrossConvert. - /// - /// - Parameter string: Text in Original Script. - /// - Returns: Text converted to Different Script. - @objc public static func crossConvert(_ string: String) -> String? { - switch ctlInputMethod.currentKeyHandler.inputMode { - case InputMode.imeModeCHS: - return shared.traditionalize?.convert(string) - case InputMode.imeModeCHT: - return shared.simplify?.convert(string) - default: - return string - } - } + private override init() { + try? simplify = ChineseConverter(options: .simplify) + try? traditionalize = ChineseConverter(options: [.traditionalize, .twStandard]) + super.init() + } + + /// CrossConvert. + /// + /// - Parameter string: Text in Original Script. + /// - Returns: Text converted to Different Script. + @objc public static func crossConvert(_ string: String) -> String? { + switch ctlInputMethod.currentKeyHandler.inputMode { + case InputMode.imeModeCHS: + return shared.traditionalize?.convert(string) + case InputMode.imeModeCHT: + return shared.simplify?.convert(string) + default: + return string + } + } } diff --git a/Source/Modules/AppDelegate.swift b/Source/Modules/AppDelegate.swift index 7d69bd11..912c789b 100644 --- a/Source/Modules/AppDelegate.swift +++ b/Source/Modules/AppDelegate.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -29,287 +36,346 @@ private let kNextCheckInterval: TimeInterval = 86400.0 private let kTimeoutInterval: TimeInterval = 60.0 struct VersionUpdateReport { - var siteUrl: URL? - var currentShortVersion: String = "" - var currentVersion: String = "" - var remoteShortVersion: String = "" - var remoteVersion: String = "" - var versionDescription: String = "" + var siteUrl: URL? + var currentShortVersion: String = "" + var currentVersion: String = "" + var remoteShortVersion: String = "" + var remoteVersion: String = "" + var versionDescription: String = "" } enum VersionUpdateApiResult { - case shouldUpdate(report: VersionUpdateReport) - case noNeedToUpdate - case ignored + case shouldUpdate(report: VersionUpdateReport) + case noNeedToUpdate + case ignored } enum VersionUpdateApiError: Error, LocalizedError { - case connectionError(message: String) + case connectionError(message: String) - var errorDescription: String? { - switch self { - case .connectionError(let message): - return String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message) - } - } + var errorDescription: String? { + switch self { + case .connectionError(let message): + return String( + format: NSLocalizedString( + "There may be no internet connection or the server failed to respond.\n\nError message: %@", + comment: ""), message) + } + } } struct VersionUpdateApi { - static func check(forced: Bool, callback: @escaping (Result) -> ()) -> URLSessionTask? { - guard let infoDict = Bundle.main.infoDictionary, - let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, - let updateInfoURL = URL(string: updateInfoURLString) else { - return nil - } + static func check( + forced: Bool, callback: @escaping (Result) -> Void + ) -> URLSessionTask? { + guard let infoDict = Bundle.main.infoDictionary, + let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, + let updateInfoURL = URL(string: updateInfoURLString) + else { + return nil + } - let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval) - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - DispatchQueue.main.async { - forced ? - callback(.failure(VersionUpdateApiError.connectionError(message: error.localizedDescription))) : - callback(.success(.ignored)) - } - return - } + let request = URLRequest( + url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, + timeoutInterval: kTimeoutInterval) + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + DispatchQueue.main.async { + forced + ? callback( + .failure( + VersionUpdateApiError.connectionError( + message: error.localizedDescription))) + : callback(.success(.ignored)) + } + return + } - do { - guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any], - let remoteVersion = plist[kCFBundleVersionKey] as? String, - let infoDict = Bundle.main.infoDictionary - else { - DispatchQueue.main.async { - forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) - } - return - } + do { + guard + let plist = try PropertyListSerialization.propertyList( + from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any], + let remoteVersion = plist[kCFBundleVersionKey] as? String, + let infoDict = Bundle.main.infoDictionary + else { + DispatchQueue.main.async { + forced + ? callback(.success(.noNeedToUpdate)) + : callback(.success(.ignored)) + } + return + } - // TODO: Validate info (e.g. bundle identifier) - // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this + // TODO: Validate info (e.g. bundle identifier) + // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this - let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" - let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil) + let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" + let result = currentVersion.compare( + remoteVersion, options: .numeric, range: nil, locale: nil) - if result != .orderedAscending { - DispatchQueue.main.async { - forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) - } - IME.prtDebugIntel("vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available.") - return - } - IME.prtDebugIntel("vChewingDebug: Update // New version detected, proceeding to the next phase.") - guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, - let siteInfoURL = URL(string: siteInfoURLString) - else { - DispatchQueue.main.async { - forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) - } - IME.prtDebugIntel("vChewingDebug: Update // Failed from retrieving / parsing URL intel.") - return - } - IME.prtDebugIntel("vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.") - var report = VersionUpdateReport(siteUrl: siteInfoURL) - var versionDescription = "" - let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any] - if let versionDescriptions = versionDescriptions { - var locale = "en" - let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"] - let preferredTags = Bundle.preferredLocalizations(from: supportedLocales) - if let first = preferredTags.first { - locale = first - } - versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? "" - if !versionDescription.isEmpty { - versionDescription = "\n\n" + versionDescription - } - } - report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" - report.currentVersion = currentVersion - report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? "" - report.remoteVersion = remoteVersion - report.versionDescription = versionDescription - DispatchQueue.main.async { - callback(.success(.shouldUpdate(report: report))) - } - IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.") - } catch { - DispatchQueue.main.async { - forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) - } - } - } - task.resume() - return task - } + if result != .orderedAscending { + DispatchQueue.main.async { + forced + ? callback(.success(.noNeedToUpdate)) + : callback(.success(.ignored)) + } + IME.prtDebugIntel( + "vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available." + ) + return + } + IME.prtDebugIntel( + "vChewingDebug: Update // New version detected, proceeding to the next phase.") + guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, + let siteInfoURL = URL(string: siteInfoURLString) + else { + DispatchQueue.main.async { + forced + ? callback(.success(.noNeedToUpdate)) + : callback(.success(.ignored)) + } + IME.prtDebugIntel( + "vChewingDebug: Update // Failed from retrieving / parsing URL intel.") + return + } + IME.prtDebugIntel( + "vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.") + var report = VersionUpdateReport(siteUrl: siteInfoURL) + var versionDescription = "" + let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any] + if let versionDescriptions = versionDescriptions { + var locale = "en" + let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"] + let preferredTags = Bundle.preferredLocalizations(from: supportedLocales) + if let first = preferredTags.first { + locale = first + } + versionDescription = + versionDescriptions[locale] as? String ?? versionDescriptions["en"] + as? String ?? "" + if !versionDescription.isEmpty { + versionDescription = "\n\n" + versionDescription + } + } + report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" + report.currentVersion = currentVersion + report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? "" + report.remoteVersion = remoteVersion + report.versionDescription = versionDescription + DispatchQueue.main.async { + callback(.success(.shouldUpdate(report: report))) + } + IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.") + } catch { + DispatchQueue.main.async { + forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) + } + } + } + task.resume() + return task + } } @objc(AppDelegate) -class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate, FSEventStreamHelperDelegate { - func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) { - // 拖 100ms 再重載,畢竟有些有特殊需求的使用者可能會想使用巨型自訂語彙檔案。 - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { - if mgrPrefs.shouldAutoReloadUserDataFiles { - IME.initLangModels(userOnly: true) - } - } - } +class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate, + FSEventStreamHelperDelegate +{ + func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) { + // 拖 100ms 再重載,畢竟有些有特殊需求的使用者可能會想使用巨型自訂語彙檔案。 + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { + if mgrPrefs.shouldAutoReloadUserDataFiles { + 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 weak var window: NSWindow? - private var ctlPrefWindowInstance: ctlPrefWindow? - private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window - private var checkTask: URLSessionTask? - private var updateNextStepURL: URL? - private var fsStreamHelper = FSEventStreamHelper(path: mgrLangModel.dataFolderPath(isDefaultFolder: false), queue: DispatchQueue(label: "vChewing User Phrases")) - private var currentAlertType: String = "" + @IBOutlet weak var window: NSWindow? + private var ctlPrefWindowInstance: ctlPrefWindow? + private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window + private var checkTask: URLSessionTask? + private var updateNextStepURL: URL? + private var fsStreamHelper = FSEventStreamHelper( + path: mgrLangModel.dataFolderPath(isDefaultFolder: false), + queue: DispatchQueue(label: "vChewing User Phrases")) + private var currentAlertType: String = "" - // 補上 dealloc - deinit { - ctlPrefWindowInstance = nil - ctlAboutWindowInstance = nil - checkTask = nil - updateNextStepURL = nil - fsStreamHelper.stop() - fsStreamHelper.delegate = nil - } + // 補上 dealloc + deinit { + ctlPrefWindowInstance = nil + ctlAboutWindowInstance = nil + checkTask = nil + updateNextStepURL = nil + fsStreamHelper.stop() + fsStreamHelper.delegate = nil + } - func applicationDidFinishLaunching(_ notification: Notification) { - IME.initLangModels(userOnly: false) - fsStreamHelper.delegate = self - _ = fsStreamHelper.start() + func applicationDidFinishLaunching(_ notification: Notification) { + IME.initLangModels(userOnly: false) + fsStreamHelper.delegate = self + _ = fsStreamHelper.start() - mgrPrefs.setMissingDefaults() - - // 只要使用者沒有勾選檢查更新、沒有主動做出要檢查更新的操作,就不要檢查更新。 - if (UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) != nil) == true { - checkForUpdate() - } - } + mgrPrefs.setMissingDefaults() - @objc func showPreferences() { - if (ctlPrefWindowInstance == nil) { - 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 - @objc func showAbout() { - if (ctlAboutWindowInstance == nil) { - ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow") - } - ctlAboutWindowInstance?.window?.center() - ctlAboutWindowInstance?.window?.orderFrontRegardless() // 逼著關於視窗往最前方顯示 - ctlAboutWindowInstance?.window?.level = .statusBar - NSApp.setActivationPolicy(.accessory) - } + // 只要使用者沒有勾選檢查更新、沒有主動做出要檢查更新的操作,就不要檢查更新。 + if (UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) != nil) == true { + checkForUpdate() + } + } - @objc(checkForUpdate) - func checkForUpdate() { - checkForUpdate(forced: false) - } + @objc func showPreferences() { + if ctlPrefWindowInstance == nil { + ctlPrefWindowInstance = ctlPrefWindow.init(windowNibName: "frmPrefWindow") + } + ctlPrefWindowInstance?.window?.center() + ctlPrefWindowInstance?.window?.orderFrontRegardless() // 逼著屬性視窗往最前方顯示 + ctlPrefWindowInstance?.window?.level = .statusBar + ctlPrefWindowInstance?.window?.titlebarAppearsTransparent = true + NSApp.setActivationPolicy(.accessory) + } - @objc(checkForUpdateForced:) - func checkForUpdate(forced: Bool) { + // New About Window + @objc func showAbout() { + if ctlAboutWindowInstance == nil { + ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow") + } + ctlAboutWindowInstance?.window?.center() + ctlAboutWindowInstance?.window?.orderFrontRegardless() // 逼著關於視窗往最前方顯示 + ctlAboutWindowInstance?.window?.level = .statusBar + NSApp.setActivationPolicy(.accessory) + } - if checkTask != nil { - // busy - return - } + @objc(checkForUpdate) + func checkForUpdate() { + checkForUpdate(forced: false) + } - // time for update? - if !forced { - if UserDefaults.standard.bool(forKey: kCheckUpdateAutomatically) == false { - return - } - let now = Date() - let date = UserDefaults.standard.object(forKey: kNextUpdateCheckDateKey) as? Date ?? now - if now.compare(date) == .orderedAscending { - return - } - } + @objc(checkForUpdateForced:) + func checkForUpdate(forced: Bool) { - let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date()) - UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey) + if checkTask != nil { + // busy + return + } - checkTask = VersionUpdateApi.check(forced: forced) { [self] result in - defer { - self.checkTask = nil - } - switch result { - case .success(let apiResult): - switch apiResult { - case .shouldUpdate(let report): - self.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)") - self.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)") - self.currentAlertType = "Update" - ctlNonModalAlertWindow.shared.show(title: title, content: content, confirmButtonTitle: buttonTitle, cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) - NSApp.setActivationPolicy(.accessory) - default: - break - } - } - } - } + // time for update? + if !forced { + if UserDefaults.standard.bool(forKey: kCheckUpdateAutomatically) == false { + return + } + let now = Date() + let date = UserDefaults.standard.object(forKey: kNextUpdateCheckDateKey) as? Date ?? now + if now.compare(date) == .orderedAscending { + return + } + } - func selfUninstall() { - self.currentAlertType = "Uninstall" - let content = String(format: NSLocalizedString("This will remove vChewing Input Method from this user account, requiring your confirmation.", comment: "")) - ctlNonModalAlertWindow.shared.show(title: NSLocalizedString("Uninstallation", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""), cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, delegate: self) - NSApp.setActivationPolicy(.accessory) - } + let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date()) + UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey) - func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) { - switch self.currentAlertType { - case "Uninstall": - NSWorkspace.shared.openFile(mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder") - IME.uninstall(isSudo: false, selfKill: true) - case "Update": - if let updateNextStepURL = self.updateNextStepURL { - NSWorkspace.shared.open(updateNextStepURL) - } - self.updateNextStepURL = nil - default: - break - } - } + checkTask = VersionUpdateApi.check(forced: forced) { [self] result in + defer { + self.checkTask = nil + } + switch result { + case .success(let apiResult): + switch apiResult { + case .shouldUpdate(let report): + self.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)") + self.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)") + self.currentAlertType = "Update" + ctlNonModalAlertWindow.shared.show( + title: title, content: content, + confirmButtonTitle: buttonTitle, + cancelButtonTitle: nil, + cancelAsDefault: false, delegate: nil) + NSApp.setActivationPolicy(.accessory) + default: + break + } + } + } + } - func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow) { - switch self.currentAlertType { - case "Update": - self.updateNextStepURL = nil - default: - break - } - } + func selfUninstall() { + self.currentAlertType = "Uninstall" + let content = String( + format: NSLocalizedString( + "This will remove vChewing Input Method from this user account, requiring your confirmation.", + comment: "")) + ctlNonModalAlertWindow.shared.show( + title: NSLocalizedString("Uninstallation", comment: ""), content: content, + confirmButtonTitle: NSLocalizedString("OK", comment: ""), + cancelButtonTitle: NSLocalizedString("Not Now", comment: ""), cancelAsDefault: false, + delegate: self) + NSApp.setActivationPolicy(.accessory) + } - // New About Window - @IBAction func about(_ sender: Any) { - (NSApp.delegate as? AppDelegate)?.showAbout() - NSApplication.shared.activate(ignoringOtherApps: true) - } + func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) { + switch self.currentAlertType { + case "Uninstall": + NSWorkspace.shared.openFile( + mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder") + IME.uninstall(isSudo: false, selfKill: true) + case "Update": + if let updateNextStepURL = self.updateNextStepURL { + NSWorkspace.shared.open(updateNextStepURL) + } + self.updateNextStepURL = nil + default: + break + } + } + + func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow) { + switch self.currentAlertType { + case "Update": + self.updateNextStepURL = nil + default: + break + } + } + + // New About Window + @IBAction func about(_ sender: Any) { + (NSApp.delegate as? AppDelegate)?.showAbout() + NSApplication.shared.activate(ignoringOtherApps: true) + } } diff --git a/Source/Modules/ControllerModules/AppleKeyboardConverter.swift b/Source/Modules/ControllerModules/AppleKeyboardConverter.swift index 87bd1888..1b3c4618 100644 --- a/Source/Modules/ControllerModules/AppleKeyboardConverter.swift +++ b/Source/Modules/ControllerModules/AppleKeyboardConverter.swift @@ -1,329 +1,339 @@ // 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: +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. +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. +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. +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 @objc class AppleKeyboardConverter: NSObject { - @objc class func isDynamicBaseKeyboardLayoutEnabled() -> Bool { - switch mgrPrefs.basisKeyboardLayout { - case "com.apple.keylayout.ZhuyinBopomofo": - return true - case "com.apple.keylayout.ZhuyinEten": - return true - case "org.atelierInmu.vChewing.keyLayouts.vchewingdachen": - return true - case "org.atelierInmu.vChewing.keyLayouts.vchewingmitac": - return true - case "org.atelierInmu.vChewing.keyLayouts.vchewingibm": - return true - case "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou": - return true - case "org.atelierInmu.vChewing.keyLayouts.vchewingeten": - return true - case "org.unknown.keylayout.vChewingDachen": - return true - case "org.unknown.keylayout.vChewingFakeSeigyou": - return true - case "org.unknown.keylayout.vChewingETen": - return true - case "org.unknown.keylayout.vChewingIBM": - return true - case "org.unknown.keylayout.vChewingMiTAC": - return true - default: - return false - } - } - // 處理 Apple 注音鍵盤佈局類型。 - @objc class func cnvApple2ABC(_ charCode: UniChar) -> UniChar { - var charCode = charCode - // 在按鍵資訊被送往 OVMandarin 之前,先轉換為可以被 OVMandarin 正常處理的資訊。 - if self.isDynamicBaseKeyboardLayoutEnabled() { - // 針對不同的 Apple 動態鍵盤佈局糾正大寫英文輸入。 - switch mgrPrefs.basisKeyboardLayout { - case "com.apple.keylayout.ZhuyinBopomofo": do { - if (charCode == 97) {charCode = UniChar(65)} - if (charCode == 98) {charCode = UniChar(66)} - if (charCode == 99) {charCode = UniChar(67)} - if (charCode == 100) {charCode = UniChar(68)} - if (charCode == 101) {charCode = UniChar(69)} - if (charCode == 102) {charCode = UniChar(70)} - if (charCode == 103) {charCode = UniChar(71)} - if (charCode == 104) {charCode = UniChar(72)} - if (charCode == 105) {charCode = UniChar(73)} - if (charCode == 106) {charCode = UniChar(74)} - if (charCode == 107) {charCode = UniChar(75)} - if (charCode == 108) {charCode = UniChar(76)} - if (charCode == 109) {charCode = UniChar(77)} - if (charCode == 110) {charCode = UniChar(78)} - if (charCode == 111) {charCode = UniChar(79)} - if (charCode == 112) {charCode = UniChar(80)} - if (charCode == 113) {charCode = UniChar(81)} - if (charCode == 114) {charCode = UniChar(82)} - if (charCode == 115) {charCode = UniChar(83)} - if (charCode == 116) {charCode = UniChar(84)} - if (charCode == 117) {charCode = UniChar(85)} - if (charCode == 118) {charCode = UniChar(86)} - if (charCode == 119) {charCode = UniChar(87)} - if (charCode == 120) {charCode = UniChar(88)} - if (charCode == 121) {charCode = UniChar(89)} - if (charCode == 122) {charCode = UniChar(90)} - } - case "com.apple.keylayout.ZhuyinEten": do { - if (charCode == 65345) {charCode = UniChar(65)} - if (charCode == 65346) {charCode = UniChar(66)} - if (charCode == 65347) {charCode = UniChar(67)} - if (charCode == 65348) {charCode = UniChar(68)} - if (charCode == 65349) {charCode = UniChar(69)} - if (charCode == 65350) {charCode = UniChar(70)} - if (charCode == 65351) {charCode = UniChar(71)} - if (charCode == 65352) {charCode = UniChar(72)} - if (charCode == 65353) {charCode = UniChar(73)} - if (charCode == 65354) {charCode = UniChar(74)} - if (charCode == 65355) {charCode = UniChar(75)} - if (charCode == 65356) {charCode = UniChar(76)} - if (charCode == 65357) {charCode = UniChar(77)} - if (charCode == 65358) {charCode = UniChar(78)} - if (charCode == 65359) {charCode = UniChar(79)} - if (charCode == 65360) {charCode = UniChar(80)} - if (charCode == 65361) {charCode = UniChar(81)} - if (charCode == 65362) {charCode = UniChar(82)} - if (charCode == 65363) {charCode = UniChar(83)} - if (charCode == 65364) {charCode = UniChar(84)} - if (charCode == 65365) {charCode = UniChar(85)} - if (charCode == 65366) {charCode = UniChar(86)} - if (charCode == 65367) {charCode = UniChar(87)} - if (charCode == 65368) {charCode = UniChar(88)} - if (charCode == 65369) {charCode = UniChar(89)} - if (charCode == 65370) {charCode = UniChar(90)} - } - default: break - } - // 注音鍵群。 - if (charCode == 12573) {charCode = UniChar(44)} - if (charCode == 12582) {charCode = UniChar(45)} - if (charCode == 12577) {charCode = UniChar(46)} - if (charCode == 12581) {charCode = UniChar(47)} - if (charCode == 12578) {charCode = UniChar(48)} - if (charCode == 12549) {charCode = UniChar(49)} - if (charCode == 12553) {charCode = UniChar(50)} - if (charCode == 711) {charCode = UniChar(51)} - if (charCode == 715) {charCode = UniChar(52)} - if (charCode == 12563) {charCode = UniChar(53)} - if (charCode == 714) {charCode = UniChar(54)} - if (charCode == 729) {charCode = UniChar(55)} - if (charCode == 12570) {charCode = UniChar(56)} - if (charCode == 12574) {charCode = UniChar(57)} - if (charCode == 12580) {charCode = UniChar(59)} - if (charCode == 12551) {charCode = UniChar(97)} - if (charCode == 12566) {charCode = UniChar(98)} - if (charCode == 12559) {charCode = UniChar(99)} - if (charCode == 12558) {charCode = UniChar(100)} - if (charCode == 12557) {charCode = UniChar(101)} - if (charCode == 12561) {charCode = UniChar(102)} - if (charCode == 12565) {charCode = UniChar(103)} - if (charCode == 12568) {charCode = UniChar(104)} - if (charCode == 12571) {charCode = UniChar(105)} - if (charCode == 12584) {charCode = UniChar(106)} - if (charCode == 12572) {charCode = UniChar(107)} - if (charCode == 12576) {charCode = UniChar(108)} - if (charCode == 12585) {charCode = UniChar(109)} - if (charCode == 12569) {charCode = UniChar(110)} - if (charCode == 12575) {charCode = UniChar(111)} - if (charCode == 12579) {charCode = UniChar(112)} - if (charCode == 12550) {charCode = UniChar(113)} - if (charCode == 12560) {charCode = UniChar(114)} - if (charCode == 12555) {charCode = UniChar(115)} - if (charCode == 12564) {charCode = UniChar(116)} - if (charCode == 12583) {charCode = UniChar(117)} - if (charCode == 12562) {charCode = UniChar(118)} - if (charCode == 12554) {charCode = UniChar(119)} - if (charCode == 12556) {charCode = UniChar(120)} - if (charCode == 12567) {charCode = UniChar(121)} - if (charCode == 12552) {charCode = UniChar(122)} - // 除了數字鍵區以外的標點符號。 - if (charCode == 12289) {charCode = UniChar(92)} - if (charCode == 12300) {charCode = UniChar(91)} - if (charCode == 12301) {charCode = UniChar(93)} - if (charCode == 12302) {charCode = UniChar(123)} - if (charCode == 12303) {charCode = UniChar(125)} - if (charCode == 65292) {charCode = UniChar(60)} - if (charCode == 12290) {charCode = UniChar(62)} - // 摁了 SHIFT 之後的數字區的符號。 - if (charCode == 65281) {charCode = UniChar(33)} - if (charCode == 65312) {charCode = UniChar(64)} - if (charCode == 65283) {charCode = UniChar(35)} - if (charCode == 65284) {charCode = UniChar(36)} - if (charCode == 65285) {charCode = UniChar(37)} - if (charCode == 65087) {charCode = UniChar(94)} - if (charCode == 65286) {charCode = UniChar(38)} - if (charCode == 65290) {charCode = UniChar(42)} - if (charCode == 65288) {charCode = UniChar(40)} - if (charCode == 65289) {charCode = UniChar(41)} - // 摁了 Alt 的符號。 - if (charCode == 8212) {charCode = UniChar(45)} - // Apple 倚天注音佈局追加符號糾正項目。 - if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { - if (charCode == 65343) {charCode = UniChar(95)} - if (charCode == 65306) {charCode = UniChar(58)} - if (charCode == 65311) {charCode = UniChar(63)} - if (charCode == 65291) {charCode = UniChar(43)} - if (charCode == 65372) {charCode = UniChar(124)} - } - } - return charCode - } + @objc class func isDynamicBaseKeyboardLayoutEnabled() -> Bool { + switch mgrPrefs.basisKeyboardLayout { + case "com.apple.keylayout.ZhuyinBopomofo": + return true + case "com.apple.keylayout.ZhuyinEten": + return true + case "org.atelierInmu.vChewing.keyLayouts.vchewingdachen": + return true + case "org.atelierInmu.vChewing.keyLayouts.vchewingmitac": + return true + case "org.atelierInmu.vChewing.keyLayouts.vchewingibm": + return true + case "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou": + return true + case "org.atelierInmu.vChewing.keyLayouts.vchewingeten": + return true + case "org.unknown.keylayout.vChewingDachen": + return true + case "org.unknown.keylayout.vChewingFakeSeigyou": + return true + case "org.unknown.keylayout.vChewingETen": + return true + case "org.unknown.keylayout.vChewingIBM": + return true + case "org.unknown.keylayout.vChewingMiTAC": + return true + default: + return false + } + } + // 處理 Apple 注音鍵盤佈局類型。 + @objc class func cnvApple2ABC(_ charCode: UniChar) -> UniChar { + var charCode = charCode + // 在按鍵資訊被送往 OVMandarin 之前,先轉換為可以被 OVMandarin 正常處理的資訊。 + if self.isDynamicBaseKeyboardLayoutEnabled() { + // 針對不同的 Apple 動態鍵盤佈局糾正大寫英文輸入。 + switch mgrPrefs.basisKeyboardLayout { + case "com.apple.keylayout.ZhuyinBopomofo": + do { + if charCode == 97 { charCode = UniChar(65) } + if charCode == 98 { charCode = UniChar(66) } + if charCode == 99 { charCode = UniChar(67) } + if charCode == 100 { charCode = UniChar(68) } + if charCode == 101 { charCode = UniChar(69) } + if charCode == 102 { charCode = UniChar(70) } + if charCode == 103 { charCode = UniChar(71) } + if charCode == 104 { charCode = UniChar(72) } + if charCode == 105 { charCode = UniChar(73) } + if charCode == 106 { charCode = UniChar(74) } + if charCode == 107 { charCode = UniChar(75) } + if charCode == 108 { charCode = UniChar(76) } + if charCode == 109 { charCode = UniChar(77) } + if charCode == 110 { charCode = UniChar(78) } + if charCode == 111 { charCode = UniChar(79) } + if charCode == 112 { charCode = UniChar(80) } + if charCode == 113 { charCode = UniChar(81) } + if charCode == 114 { charCode = UniChar(82) } + if charCode == 115 { charCode = UniChar(83) } + if charCode == 116 { charCode = UniChar(84) } + if charCode == 117 { charCode = UniChar(85) } + if charCode == 118 { charCode = UniChar(86) } + if charCode == 119 { charCode = UniChar(87) } + if charCode == 120 { charCode = UniChar(88) } + if charCode == 121 { charCode = UniChar(89) } + if charCode == 122 { charCode = UniChar(90) } + } + case "com.apple.keylayout.ZhuyinEten": + do { + if charCode == 65345 { charCode = UniChar(65) } + if charCode == 65346 { charCode = UniChar(66) } + if charCode == 65347 { charCode = UniChar(67) } + if charCode == 65348 { charCode = UniChar(68) } + if charCode == 65349 { charCode = UniChar(69) } + if charCode == 65350 { charCode = UniChar(70) } + if charCode == 65351 { charCode = UniChar(71) } + if charCode == 65352 { charCode = UniChar(72) } + if charCode == 65353 { charCode = UniChar(73) } + if charCode == 65354 { charCode = UniChar(74) } + if charCode == 65355 { charCode = UniChar(75) } + if charCode == 65356 { charCode = UniChar(76) } + if charCode == 65357 { charCode = UniChar(77) } + if charCode == 65358 { charCode = UniChar(78) } + if charCode == 65359 { charCode = UniChar(79) } + if charCode == 65360 { charCode = UniChar(80) } + if charCode == 65361 { charCode = UniChar(81) } + if charCode == 65362 { charCode = UniChar(82) } + if charCode == 65363 { charCode = UniChar(83) } + if charCode == 65364 { charCode = UniChar(84) } + if charCode == 65365 { charCode = UniChar(85) } + if charCode == 65366 { charCode = UniChar(86) } + if charCode == 65367 { charCode = UniChar(87) } + if charCode == 65368 { charCode = UniChar(88) } + if charCode == 65369 { charCode = UniChar(89) } + if charCode == 65370 { charCode = UniChar(90) } + } + default: break + } + // 注音鍵群。 + if charCode == 12573 { charCode = UniChar(44) } + if charCode == 12582 { charCode = UniChar(45) } + if charCode == 12577 { charCode = UniChar(46) } + if charCode == 12581 { charCode = UniChar(47) } + if charCode == 12578 { charCode = UniChar(48) } + if charCode == 12549 { charCode = UniChar(49) } + if charCode == 12553 { charCode = UniChar(50) } + if charCode == 711 { charCode = UniChar(51) } + if charCode == 715 { charCode = UniChar(52) } + if charCode == 12563 { charCode = UniChar(53) } + if charCode == 714 { charCode = UniChar(54) } + if charCode == 729 { charCode = UniChar(55) } + if charCode == 12570 { charCode = UniChar(56) } + if charCode == 12574 { charCode = UniChar(57) } + if charCode == 12580 { charCode = UniChar(59) } + if charCode == 12551 { charCode = UniChar(97) } + if charCode == 12566 { charCode = UniChar(98) } + if charCode == 12559 { charCode = UniChar(99) } + if charCode == 12558 { charCode = UniChar(100) } + if charCode == 12557 { charCode = UniChar(101) } + if charCode == 12561 { charCode = UniChar(102) } + if charCode == 12565 { charCode = UniChar(103) } + if charCode == 12568 { charCode = UniChar(104) } + if charCode == 12571 { charCode = UniChar(105) } + if charCode == 12584 { charCode = UniChar(106) } + if charCode == 12572 { charCode = UniChar(107) } + if charCode == 12576 { charCode = UniChar(108) } + if charCode == 12585 { charCode = UniChar(109) } + if charCode == 12569 { charCode = UniChar(110) } + if charCode == 12575 { charCode = UniChar(111) } + if charCode == 12579 { charCode = UniChar(112) } + if charCode == 12550 { charCode = UniChar(113) } + if charCode == 12560 { charCode = UniChar(114) } + if charCode == 12555 { charCode = UniChar(115) } + if charCode == 12564 { charCode = UniChar(116) } + if charCode == 12583 { charCode = UniChar(117) } + if charCode == 12562 { charCode = UniChar(118) } + if charCode == 12554 { charCode = UniChar(119) } + if charCode == 12556 { charCode = UniChar(120) } + if charCode == 12567 { charCode = UniChar(121) } + if charCode == 12552 { charCode = UniChar(122) } + // 除了數字鍵區以外的標點符號。 + if charCode == 12289 { charCode = UniChar(92) } + if charCode == 12300 { charCode = UniChar(91) } + if charCode == 12301 { charCode = UniChar(93) } + if charCode == 12302 { charCode = UniChar(123) } + if charCode == 12303 { charCode = UniChar(125) } + if charCode == 65292 { charCode = UniChar(60) } + if charCode == 12290 { charCode = UniChar(62) } + // 摁了 SHIFT 之後的數字區的符號。 + if charCode == 65281 { charCode = UniChar(33) } + if charCode == 65312 { charCode = UniChar(64) } + if charCode == 65283 { charCode = UniChar(35) } + if charCode == 65284 { charCode = UniChar(36) } + if charCode == 65285 { charCode = UniChar(37) } + if charCode == 65087 { charCode = UniChar(94) } + if charCode == 65286 { charCode = UniChar(38) } + if charCode == 65290 { charCode = UniChar(42) } + if charCode == 65288 { charCode = UniChar(40) } + if charCode == 65289 { charCode = UniChar(41) } + // 摁了 Alt 的符號。 + if charCode == 8212 { charCode = UniChar(45) } + // Apple 倚天注音佈局追加符號糾正項目。 + if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { + if charCode == 65343 { charCode = UniChar(95) } + if charCode == 65306 { charCode = UniChar(58) } + if charCode == 65311 { charCode = UniChar(63) } + if charCode == 65291 { charCode = UniChar(43) } + if charCode == 65372 { charCode = UniChar(124) } + } + } + return charCode + } - @objc class func cnvStringApple2ABC(_ strProcessed: String) -> String { - var strProcessed = strProcessed - if self.isDynamicBaseKeyboardLayoutEnabled() { - // 針對不同的 Apple 動態鍵盤佈局糾正大寫英文輸入。 - switch mgrPrefs.basisKeyboardLayout { - case "com.apple.keylayout.ZhuyinBopomofo": do { - if (strProcessed == "a") {strProcessed = "A"} - if (strProcessed == "b") {strProcessed = "B"} - if (strProcessed == "c") {strProcessed = "C"} - if (strProcessed == "d") {strProcessed = "D"} - if (strProcessed == "e") {strProcessed = "E"} - if (strProcessed == "f") {strProcessed = "F"} - if (strProcessed == "g") {strProcessed = "G"} - if (strProcessed == "h") {strProcessed = "H"} - if (strProcessed == "i") {strProcessed = "I"} - if (strProcessed == "j") {strProcessed = "J"} - if (strProcessed == "k") {strProcessed = "K"} - if (strProcessed == "l") {strProcessed = "L"} - if (strProcessed == "m") {strProcessed = "M"} - if (strProcessed == "n") {strProcessed = "N"} - if (strProcessed == "o") {strProcessed = "O"} - if (strProcessed == "p") {strProcessed = "P"} - if (strProcessed == "q") {strProcessed = "Q"} - if (strProcessed == "r") {strProcessed = "R"} - if (strProcessed == "s") {strProcessed = "S"} - if (strProcessed == "t") {strProcessed = "T"} - if (strProcessed == "u") {strProcessed = "U"} - if (strProcessed == "v") {strProcessed = "V"} - if (strProcessed == "w") {strProcessed = "W"} - if (strProcessed == "x") {strProcessed = "X"} - if (strProcessed == "y") {strProcessed = "Y"} - if (strProcessed == "z") {strProcessed = "Z"} - } - case "com.apple.keylayout.ZhuyinEten": do { - if (strProcessed == "a") {strProcessed = "A"} - if (strProcessed == "b") {strProcessed = "B"} - if (strProcessed == "c") {strProcessed = "C"} - if (strProcessed == "d") {strProcessed = "D"} - if (strProcessed == "e") {strProcessed = "E"} - if (strProcessed == "f") {strProcessed = "F"} - if (strProcessed == "g") {strProcessed = "G"} - if (strProcessed == "h") {strProcessed = "H"} - if (strProcessed == "i") {strProcessed = "I"} - if (strProcessed == "j") {strProcessed = "J"} - if (strProcessed == "k") {strProcessed = "K"} - if (strProcessed == "l") {strProcessed = "L"} - if (strProcessed == "m") {strProcessed = "M"} - if (strProcessed == "n") {strProcessed = "N"} - if (strProcessed == "o") {strProcessed = "O"} - if (strProcessed == "p") {strProcessed = "P"} - if (strProcessed == "q") {strProcessed = "Q"} - if (strProcessed == "r") {strProcessed = "R"} - if (strProcessed == "s") {strProcessed = "S"} - if (strProcessed == "t") {strProcessed = "T"} - if (strProcessed == "u") {strProcessed = "U"} - if (strProcessed == "v") {strProcessed = "V"} - if (strProcessed == "w") {strProcessed = "W"} - if (strProcessed == "x") {strProcessed = "X"} - if (strProcessed == "y") {strProcessed = "Y"} - if (strProcessed == "z") {strProcessed = "Z"} - } - default: break - } - // 注音鍵群。 - if (strProcessed == "ㄝ") {strProcessed = ","} - if (strProcessed == "ㄦ") {strProcessed = "-"} - if (strProcessed == "ㄡ") {strProcessed = "."} - if (strProcessed == "ㄥ") {strProcessed = "/"} - if (strProcessed == "ㄢ") {strProcessed = "0"} - if (strProcessed == "ㄅ") {strProcessed = "1"} - if (strProcessed == "ㄉ") {strProcessed = "2"} - if (strProcessed == "ˇ") {strProcessed = "3"} - if (strProcessed == "ˋ") {strProcessed = "4"} - if (strProcessed == "ㄓ") {strProcessed = "5"} - if (strProcessed == "ˊ") {strProcessed = "6"} - if (strProcessed == "˙") {strProcessed = "7"} - if (strProcessed == "ㄚ") {strProcessed = "8"} - if (strProcessed == "ㄞ") {strProcessed = "9"} - if (strProcessed == "ㄤ") {strProcessed = ";"} - if (strProcessed == "ㄇ") {strProcessed = "a"} - if (strProcessed == "ㄖ") {strProcessed = "b"} - if (strProcessed == "ㄏ") {strProcessed = "c"} - if (strProcessed == "ㄎ") {strProcessed = "d"} - if (strProcessed == "ㄍ") {strProcessed = "e"} - if (strProcessed == "ㄑ") {strProcessed = "f"} - if (strProcessed == "ㄕ") {strProcessed = "g"} - if (strProcessed == "ㄘ") {strProcessed = "h"} - if (strProcessed == "ㄛ") {strProcessed = "i"} - if (strProcessed == "ㄨ") {strProcessed = "j"} - if (strProcessed == "ㄜ") {strProcessed = "k"} - if (strProcessed == "ㄠ") {strProcessed = "l"} - if (strProcessed == "ㄩ") {strProcessed = "m"} - if (strProcessed == "ㄙ") {strProcessed = "n"} - if (strProcessed == "ㄟ") {strProcessed = "o"} - if (strProcessed == "ㄣ") {strProcessed = "p"} - if (strProcessed == "ㄆ") {strProcessed = "q"} - if (strProcessed == "ㄐ") {strProcessed = "r"} - if (strProcessed == "ㄋ") {strProcessed = "s"} - if (strProcessed == "ㄔ") {strProcessed = "t"} - if (strProcessed == "ㄧ") {strProcessed = "u"} - if (strProcessed == "ㄒ") {strProcessed = "v"} - if (strProcessed == "ㄊ") {strProcessed = "w"} - if (strProcessed == "ㄌ") {strProcessed = "x"} - if (strProcessed == "ㄗ") {strProcessed = "y"} - 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 = ">"} - // 摁了 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 = ")"} - // 摁了 Alt 的符號。 - if (strProcessed == "—") {strProcessed = "-"} - // Apple 倚天注音佈局追加符號糾正項目。 - if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { - if (strProcessed == "_") {strProcessed = "_"} - if (strProcessed == ":") {strProcessed = ":"} - if (strProcessed == "?") {strProcessed = "?"} - if (strProcessed == "+") {strProcessed = "+"} - if (strProcessed == "|") {strProcessed = "|"} - } - } - return strProcessed - } + @objc class func cnvStringApple2ABC(_ strProcessed: String) -> String { + var strProcessed = strProcessed + if self.isDynamicBaseKeyboardLayoutEnabled() { + // 針對不同的 Apple 動態鍵盤佈局糾正大寫英文輸入。 + switch mgrPrefs.basisKeyboardLayout { + case "com.apple.keylayout.ZhuyinBopomofo": + do { + if strProcessed == "a" { strProcessed = "A" } + if strProcessed == "b" { strProcessed = "B" } + if strProcessed == "c" { strProcessed = "C" } + if strProcessed == "d" { strProcessed = "D" } + if strProcessed == "e" { strProcessed = "E" } + if strProcessed == "f" { strProcessed = "F" } + if strProcessed == "g" { strProcessed = "G" } + if strProcessed == "h" { strProcessed = "H" } + if strProcessed == "i" { strProcessed = "I" } + if strProcessed == "j" { strProcessed = "J" } + if strProcessed == "k" { strProcessed = "K" } + if strProcessed == "l" { strProcessed = "L" } + if strProcessed == "m" { strProcessed = "M" } + if strProcessed == "n" { strProcessed = "N" } + if strProcessed == "o" { strProcessed = "O" } + if strProcessed == "p" { strProcessed = "P" } + if strProcessed == "q" { strProcessed = "Q" } + if strProcessed == "r" { strProcessed = "R" } + if strProcessed == "s" { strProcessed = "S" } + if strProcessed == "t" { strProcessed = "T" } + if strProcessed == "u" { strProcessed = "U" } + if strProcessed == "v" { strProcessed = "V" } + if strProcessed == "w" { strProcessed = "W" } + if strProcessed == "x" { strProcessed = "X" } + if strProcessed == "y" { strProcessed = "Y" } + if strProcessed == "z" { strProcessed = "Z" } + } + case "com.apple.keylayout.ZhuyinEten": + do { + if strProcessed == "a" { strProcessed = "A" } + if strProcessed == "b" { strProcessed = "B" } + if strProcessed == "c" { strProcessed = "C" } + if strProcessed == "d" { strProcessed = "D" } + if strProcessed == "e" { strProcessed = "E" } + if strProcessed == "f" { strProcessed = "F" } + if strProcessed == "g" { strProcessed = "G" } + if strProcessed == "h" { strProcessed = "H" } + if strProcessed == "i" { strProcessed = "I" } + if strProcessed == "j" { strProcessed = "J" } + if strProcessed == "k" { strProcessed = "K" } + if strProcessed == "l" { strProcessed = "L" } + if strProcessed == "m" { strProcessed = "M" } + if strProcessed == "n" { strProcessed = "N" } + if strProcessed == "o" { strProcessed = "O" } + if strProcessed == "p" { strProcessed = "P" } + if strProcessed == "q" { strProcessed = "Q" } + if strProcessed == "r" { strProcessed = "R" } + if strProcessed == "s" { strProcessed = "S" } + if strProcessed == "t" { strProcessed = "T" } + if strProcessed == "u" { strProcessed = "U" } + if strProcessed == "v" { strProcessed = "V" } + if strProcessed == "w" { strProcessed = "W" } + if strProcessed == "x" { strProcessed = "X" } + if strProcessed == "y" { strProcessed = "Y" } + if strProcessed == "z" { strProcessed = "Z" } + } + default: break + } + // 注音鍵群。 + if strProcessed == "ㄝ" { strProcessed = "," } + if strProcessed == "ㄦ" { strProcessed = "-" } + if strProcessed == "ㄡ" { strProcessed = "." } + if strProcessed == "ㄥ" { strProcessed = "/" } + if strProcessed == "ㄢ" { strProcessed = "0" } + if strProcessed == "ㄅ" { strProcessed = "1" } + if strProcessed == "ㄉ" { strProcessed = "2" } + if strProcessed == "ˇ" { strProcessed = "3" } + if strProcessed == "ˋ" { strProcessed = "4" } + if strProcessed == "ㄓ" { strProcessed = "5" } + if strProcessed == "ˊ" { strProcessed = "6" } + if strProcessed == "˙" { strProcessed = "7" } + if strProcessed == "ㄚ" { strProcessed = "8" } + if strProcessed == "ㄞ" { strProcessed = "9" } + if strProcessed == "ㄤ" { strProcessed = ";" } + if strProcessed == "ㄇ" { strProcessed = "a" } + if strProcessed == "ㄖ" { strProcessed = "b" } + if strProcessed == "ㄏ" { strProcessed = "c" } + if strProcessed == "ㄎ" { strProcessed = "d" } + if strProcessed == "ㄍ" { strProcessed = "e" } + if strProcessed == "ㄑ" { strProcessed = "f" } + if strProcessed == "ㄕ" { strProcessed = "g" } + if strProcessed == "ㄘ" { strProcessed = "h" } + if strProcessed == "ㄛ" { strProcessed = "i" } + if strProcessed == "ㄨ" { strProcessed = "j" } + if strProcessed == "ㄜ" { strProcessed = "k" } + if strProcessed == "ㄠ" { strProcessed = "l" } + if strProcessed == "ㄩ" { strProcessed = "m" } + if strProcessed == "ㄙ" { strProcessed = "n" } + if strProcessed == "ㄟ" { strProcessed = "o" } + if strProcessed == "ㄣ" { strProcessed = "p" } + if strProcessed == "ㄆ" { strProcessed = "q" } + if strProcessed == "ㄐ" { strProcessed = "r" } + if strProcessed == "ㄋ" { strProcessed = "s" } + if strProcessed == "ㄔ" { strProcessed = "t" } + if strProcessed == "ㄧ" { strProcessed = "u" } + if strProcessed == "ㄒ" { strProcessed = "v" } + if strProcessed == "ㄊ" { strProcessed = "w" } + if strProcessed == "ㄌ" { strProcessed = "x" } + if strProcessed == "ㄗ" { strProcessed = "y" } + 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 = ">" } + // 摁了 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 = ")" } + // 摁了 Alt 的符號。 + if strProcessed == "—" { strProcessed = "-" } + // Apple 倚天注音佈局追加符號糾正項目。 + if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { + if strProcessed == "_" { strProcessed = "_" } + if strProcessed == ":" { strProcessed = ":" } + if strProcessed == "?" { strProcessed = "?" } + if strProcessed == "+" { strProcessed = "+" } + if strProcessed == "|" { strProcessed = "|" } + } + } + return strProcessed + } } diff --git a/Source/Modules/ControllerModules/InputState.swift b/Source/Modules/ControllerModules/InputState.swift index a2ea1f99..2d159521 100644 --- a/Source/Modules/ControllerModules/InputState.swift +++ b/Source/Modules/ControllerModules/InputState.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -52,359 +59,424 @@ import Cocoa /// one among the candidates. class InputState: NSObject { - /// Represents that the input controller is deactivated. - @objc (InputStateDeactivated) - class Deactivated: InputState { - override var description: String { - "" - } - } + /// Represents that the input controller is deactivated. + @objc(InputStateDeactivated) + class Deactivated: InputState { + override var description: String { + "" + } + } - // MARK: - + // MARK: - - /// Represents that the composing buffer is empty. - @objc (InputStateEmpty) - class Empty: InputState { - @objc var composingBuffer: String { - "" - } + /// Represents that the composing buffer is empty. + @objc(InputStateEmpty) + class Empty: InputState { + @objc var composingBuffer: String { + "" + } - override var description: String { - "" - } - } + override var description: String { + "" + } + } - // MARK: - + // MARK: - - /// Represents that the composing buffer is empty. - @objc (InputStateEmptyIgnoringPreviousState) - class EmptyIgnoringPreviousState: InputState { - @objc var composingBuffer: String { - "" - } - override var description: String { - "" - } - } + /// Represents that the composing buffer is empty. + @objc(InputStateEmptyIgnoringPreviousState) + class EmptyIgnoringPreviousState: InputState { + @objc var composingBuffer: String { + "" + } + override var description: String { + "" + } + } - // MARK: - + // MARK: - - /// Represents that the input controller is committing text into client app. - @objc (InputStateCommitting) - class Committing: InputState { - @objc private(set) var poppedText: String = "" + /// Represents that the input controller is committing text into client app. + @objc(InputStateCommitting) + class Committing: InputState { + @objc private(set) var poppedText: String = "" - @objc convenience init(poppedText: String) { - self.init() - self.poppedText = poppedText - } + @objc convenience init(poppedText: String) { + self.init() + self.poppedText = poppedText + } - override var description: String { - "" - } - } + override var description: String { + "" + } + } - // MARK: - + // MARK: - - /// Represents that the composing buffer is not empty. - @objc (InputStateNotEmpty) - class NotEmpty: InputState { - @objc private(set) var composingBuffer: String - @objc private(set) var cursorIndex: UInt + /// Represents that the composing buffer is not empty. + @objc(InputStateNotEmpty) + class NotEmpty: InputState { + @objc private(set) var composingBuffer: String + @objc private(set) var cursorIndex: UInt - @objc init(composingBuffer: String, cursorIndex: UInt) { - self.composingBuffer = composingBuffer - self.cursorIndex = cursorIndex - } + @objc init(composingBuffer: String, cursorIndex: UInt) { + self.composingBuffer = composingBuffer + self.cursorIndex = cursorIndex + } - override var description: String { - "" - } - } + override var description: String { + "" + } + } - // MARK: - + // MARK: - - /// Represents that the user is inputting text. - @objc (InputStateInputting) - class Inputting: NotEmpty { - @objc var poppedText: String = "" - @objc var tooltip: String = "" + /// Represents that the user is inputting text. + @objc(InputStateInputting) + class Inputting: NotEmpty { + @objc var poppedText: String = "" + @objc var tooltip: String = "" - @objc override init(composingBuffer: String, cursorIndex: UInt) { - super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) - } + @objc override init(composingBuffer: String, cursorIndex: UInt) { + super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + } - @objc var attributedString: NSAttributedString { - let attributedSting = NSAttributedString(string: composingBuffer, attributes: [ - .underlineStyle: NSUnderlineStyle.single.rawValue, - .markedClauseSegment: 0 - ]) - return attributedSting - } + @objc var attributedString: NSAttributedString { + let attributedSting = NSAttributedString( + string: composingBuffer, + attributes: [ + .underlineStyle: NSUnderlineStyle.single.rawValue, + .markedClauseSegment: 0, + ]) + return attributedSting + } - override var description: String { - ", poppedText:\(poppedText)>" - } - } + override var description: String { + ", poppedText:\(poppedText)>" + } + } - // MARK: - + // MARK: - - private let kMinMarkRangeLength = 2 - private let kMaxMarkRangeLength = mgrPrefs.maxCandidateLength + private let kMinMarkRangeLength = 2 + private let kMaxMarkRangeLength = mgrPrefs.maxCandidateLength - /// Represents that the user is marking a range in the composing buffer. - @objc (InputStateMarking) - class Marking: NotEmpty { + /// Represents that the user is marking a range in the composing buffer. + @objc(InputStateMarking) + class Marking: NotEmpty { - @objc private(set) var markerIndex: UInt - @objc private(set) var markedRange: NSRange - @objc private var deleteTargetExists = false - @objc var tooltip: String { + @objc private(set) var markerIndex: UInt + @objc private(set) var markedRange: NSRange + @objc private var deleteTargetExists = false + @objc var tooltip: String { - if composingBuffer.count != readings.count { - TooltipController.backgroundColor = NSColor(red: 0.55, green: 0.00, blue: 0.00, alpha: 1.00) - TooltipController.textColor = NSColor.white - return NSLocalizedString("⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: "") - } + if composingBuffer.count != readings.count { + TooltipController.backgroundColor = NSColor( + red: 0.55, green: 0.00, blue: 0.00, alpha: 1.00) + TooltipController.textColor = NSColor.white + return NSLocalizedString( + "⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: "") + } - if mgrPrefs.phraseReplacementEnabled { - TooltipController.backgroundColor = NSColor.purple - TooltipController.textColor = NSColor.white - return NSLocalizedString("⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: "") - } - if markedRange.length == 0 { - return "" - } + if mgrPrefs.phraseReplacementEnabled { + TooltipController.backgroundColor = NSColor.purple + TooltipController.textColor = NSColor.white + return NSLocalizedString( + "⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: "" + ) + } + if markedRange.length == 0 { + return "" + } - let text = (composingBuffer as NSString).substring(with: markedRange) - if markedRange.length < kMinMarkRangeLength { - TooltipController.backgroundColor = NSColor(red: 0.18, green: 0.18, blue: 0.18, alpha: 1.00) - TooltipController.textColor = NSColor(red: 0.86, green: 0.86, blue: 0.86, alpha: 1.00) - return String(format: NSLocalizedString("\"%@\" length must ≥ 2 for a user phrase.", comment: ""), text) - } else if (markedRange.length > kMaxMarkRangeLength) { - TooltipController.backgroundColor = NSColor(red: 0.26, green: 0.16, blue: 0.00, alpha: 1.00) - TooltipController.textColor = NSColor(red: 1.00, green: 0.60, blue: 0.00, alpha: 1.00) - return String(format: NSLocalizedString("\"%@\" length should ≤ %d for a user phrase.", comment: ""), text, kMaxMarkRangeLength) - } + let text = (composingBuffer as NSString).substring(with: markedRange) + if markedRange.length < kMinMarkRangeLength { + TooltipController.backgroundColor = NSColor( + red: 0.18, green: 0.18, blue: 0.18, alpha: 1.00) + TooltipController.textColor = NSColor( + red: 0.86, green: 0.86, blue: 0.86, alpha: 1.00) + return String( + format: NSLocalizedString( + "\"%@\" length must ≥ 2 for a user phrase.", comment: ""), text) + } else if markedRange.length > kMaxMarkRangeLength { + TooltipController.backgroundColor = NSColor( + red: 0.26, green: 0.16, blue: 0.00, alpha: 1.00) + TooltipController.textColor = NSColor( + red: 1.00, green: 0.60, blue: 0.00, alpha: 1.00) + return String( + format: NSLocalizedString( + "\"%@\" length should ≤ %d for a user phrase.", comment: ""), + text, kMaxMarkRangeLength) + } - let (exactBegin, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location) - let (exactEnd, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location + markedRange.length) - let selectedReadings = readings[exactBegin.." - } + override var description: String { + "" + } - @objc func convertToInputting() -> Inputting { - let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) - state.tooltip = tooltipForInputting - return state - } + @objc func convertToInputting() -> Inputting { + let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex) + state.tooltip = tooltipForInputting + return state + } - @objc var validToWrite: Bool { - /// vChewing allows users to input a string whose length differs - /// from the amount of Bopomofo readings. In this case, the range - /// in the composing buffer and the readings could not match, so - /// we disable the function to write user phrases in this case. - if composingBuffer.count != readings.count { - return false - } - if markedRange.length < kMinMarkRangeLength { - return false - } - if markedRange.length > kMaxMarkRangeLength { - return false - } - if ctlInputMethod.areWeDeleting && !deleteTargetExists { - return false - } - return markedRange.length >= kMinMarkRangeLength && markedRange.length <= kMaxMarkRangeLength - } + @objc var validToWrite: Bool { + /// vChewing allows users to input a string whose length differs + /// from the amount of Bopomofo readings. In this case, the range + /// in the composing buffer and the readings could not match, so + /// we disable the function to write user phrases in this case. + if composingBuffer.count != readings.count { + return false + } + if markedRange.length < kMinMarkRangeLength { + return false + } + if markedRange.length > kMaxMarkRangeLength { + return false + } + if ctlInputMethod.areWeDeleting && !deleteTargetExists { + return false + } + return markedRange.length >= kMinMarkRangeLength + && markedRange.length <= kMaxMarkRangeLength + } - @objc var chkIfUserPhraseExists: Bool { - let text = (composingBuffer as NSString).substring(with: markedRange) - let (exactBegin, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location) - let (exactEnd, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location + markedRange.length) - let selectedReadings = readings[exactBegin.." - } - } + override var description: String { + "" + } + } - // MARK: - + // MARK: - - /// Represents that the user is choosing in a candidates list - /// in the associated phrases mode. - @objc (InputStateAssociatedPhrases) - class AssociatedPhrases: InputState { - @objc private(set) var candidates: [String] = [] - @objc private(set) var useVerticalMode: Bool = false - @objc init(candidates: [String], useVerticalMode: Bool) { - self.candidates = candidates - self.useVerticalMode = useVerticalMode - super.init() - } + /// Represents that the user is choosing in a candidates list + /// in the associated phrases mode. + @objc(InputStateAssociatedPhrases) + class AssociatedPhrases: InputState { + @objc private(set) var candidates: [String] = [] + @objc private(set) var useVerticalMode: Bool = false + @objc init(candidates: [String], useVerticalMode: Bool) { + self.candidates = candidates + self.useVerticalMode = useVerticalMode + super.init() + } - override var description: String { - "" - } - } + override var description: String { + "" + } + } - @objc (InputStateSymbolTable) - class SymbolTable: ChoosingCandidate { - @objc var node: SymbolNode + @objc(InputStateSymbolTable) + class SymbolTable: ChoosingCandidate { + @objc var node: SymbolNode - @objc init(node: SymbolNode, useVerticalMode: Bool) { - self.node = node - let candidates = node.children?.map { $0.title } ?? [String]() - super.init(composingBuffer: "", cursorIndex: 0, candidates: candidates, useVerticalMode: useVerticalMode) - } + @objc init(node: SymbolNode, useVerticalMode: Bool) { + self.node = node + let candidates = node.children?.map { $0.title } ?? [String]() + super.init( + composingBuffer: "", cursorIndex: 0, candidates: candidates, + useVerticalMode: useVerticalMode) + } - override var description: String { - "" - } - } + override var description: String { + "" + } + } } @objc class SymbolNode: NSObject { - @objc var title: String - @objc var children: [SymbolNode]? + @objc var title: String + @objc var children: [SymbolNode]? - @objc init(_ title: String, _ children: [SymbolNode]? = nil) { - self.title = title - self.children = children - super.init() - } + @objc init(_ title: String, _ children: [SymbolNode]? = nil) { + self.title = title + self.children = children + super.init() + } - @objc init(_ title: String, symbols: String) { - self.title = title - self.children = Array(symbols).map { SymbolNode(String($0), nil) } - super.init() - } + @objc init(_ title: String, symbols: String) { + self.title = title + self.children = Array(symbols).map { SymbolNode(String($0), nil) } + super.init() + } - @objc static let catCommonSymbols = String(format: NSLocalizedString("catCommonSymbols", comment: "")) - @objc static let catHoriBrackets = String(format: NSLocalizedString("catHoriBrackets", comment: "")) - @objc static let catVertBrackets = String(format: NSLocalizedString("catVertBrackets", comment: "")) - @objc static let catGreekLetters = String(format: NSLocalizedString("catGreekLetters", comment: "")) - @objc static let catMathSymbols = String(format: NSLocalizedString("catMathSymbols", comment: "")) - @objc static let catCurrencyUnits = String(format: NSLocalizedString("catCurrencyUnits", comment: "")) - @objc static let catSpecialSymbols = String(format: NSLocalizedString("catSpecialSymbols", comment: "")) - @objc static let catUnicodeSymbols = String(format: NSLocalizedString("catUnicodeSymbols", comment: "")) - @objc static let catCircledKanjis = String(format: NSLocalizedString("catCircledKanjis", comment: "")) - @objc static let catCircledKataKana = String(format: NSLocalizedString("catCircledKataKana", comment: "")) - @objc static let catBracketKanjis = String(format: NSLocalizedString("catBracketKanjis", comment: "")) - @objc static let catSingleTableLines = String(format: NSLocalizedString("catSingleTableLines", comment: "")) - @objc static let catDoubleTableLines = String(format: NSLocalizedString("catDoubleTableLines", comment: "")) - @objc static let catFillingBlocks = String(format: NSLocalizedString("catFillingBlocks", comment: "")) - @objc static let catLineSegments = String(format: NSLocalizedString("catLineSegments", comment: "")) - - @objc static let root: SymbolNode = SymbolNode("/", [ - SymbolNode("`"), - SymbolNode(catCommonSymbols, symbols:",、。.?!;:‧‥﹐﹒˙·‘’“”〝〞‵′〃~$%@&#*"), - SymbolNode(catHoriBrackets, symbols:"()「」〔〕{}〈〉『』《》【】﹙﹚﹝﹞﹛﹜"), - SymbolNode(catVertBrackets, symbols:"︵︶﹁﹂︹︺︷︸︿﹀﹃﹄︽︾︻︼"), - SymbolNode(catGreekLetters, symbols:"αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"), - SymbolNode(catMathSymbols, symbols:"+-×÷=≠≒∞±√<>﹤﹥≦≧∩∪ˇ⊥∠∟⊿㏒㏑∫∮∵∴╳﹢"), - SymbolNode(catCurrencyUnits, symbols:"$€¥¢£₽₨₩฿₺₮₱₭₴₦৲৳૱௹﷼₹₲₪₡₫៛₵₢₸₤₳₥₠₣₰₧₯₶₷"), - SymbolNode(catSpecialSymbols, symbols:"↑↓←→↖↗↙↘↺⇧⇩⇦⇨⇄⇆⇅⇵↻◎○●⊕⊙※△▲☆★◇◆□■▽▼§¥〒¢£♀♂↯"), - SymbolNode(catUnicodeSymbols, symbols:"♨☀☁☂☃♠♥♣♦♩♪♫♬☺☻"), - SymbolNode(catCircledKanjis, symbols:"㊟㊞㊚㊛㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗︎㊘㊙︎㊜㊝㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰🈚︎🈯︎"), - SymbolNode(catCircledKataKana, symbols:"㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋾"), - SymbolNode(catBracketKanjis, symbols:"㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃"), - SymbolNode(catSingleTableLines, symbols:"├─┼┴┬┤┌┐╞═╪╡│▕└┘╭╮╰╯"), - SymbolNode(catDoubleTableLines, symbols:"╔╦╗╠═╬╣╓╥╖╒╤╕║╚╩╝╟╫╢╙╨╜╞╪╡╘╧╛"), - SymbolNode(catFillingBlocks, symbols:"_ˍ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉◢◣◥◤"), - SymbolNode(catLineSegments, symbols:"﹣﹦≡|∣∥–︱—︳╴¯ ̄﹉﹊﹍﹎﹋﹌﹏︴∕﹨╱╲/\"), - ]) + @objc static let catCommonSymbols = String( + format: NSLocalizedString("catCommonSymbols", comment: "")) + @objc static let catHoriBrackets = String( + format: NSLocalizedString("catHoriBrackets", comment: "")) + @objc static let catVertBrackets = String( + format: NSLocalizedString("catVertBrackets", comment: "")) + @objc static let catGreekLetters = String( + format: NSLocalizedString("catGreekLetters", comment: "")) + @objc static let catMathSymbols = String( + format: NSLocalizedString("catMathSymbols", comment: "")) + @objc static let catCurrencyUnits = String( + format: NSLocalizedString("catCurrencyUnits", comment: "")) + @objc static let catSpecialSymbols = String( + format: NSLocalizedString("catSpecialSymbols", comment: "")) + @objc static let catUnicodeSymbols = String( + format: NSLocalizedString("catUnicodeSymbols", comment: "")) + @objc static let catCircledKanjis = String( + format: NSLocalizedString("catCircledKanjis", comment: "")) + @objc static let catCircledKataKana = String( + format: NSLocalizedString("catCircledKataKana", comment: "")) + @objc static let catBracketKanjis = String( + format: NSLocalizedString("catBracketKanjis", comment: "")) + @objc static let catSingleTableLines = String( + format: NSLocalizedString("catSingleTableLines", comment: "")) + @objc static let catDoubleTableLines = String( + format: NSLocalizedString("catDoubleTableLines", comment: "")) + @objc static let catFillingBlocks = String( + format: NSLocalizedString("catFillingBlocks", comment: "")) + @objc static let catLineSegments = String( + format: NSLocalizedString("catLineSegments", comment: "")) + + @objc static let root: SymbolNode = SymbolNode( + "/", + [ + SymbolNode("`"), + SymbolNode(catCommonSymbols, symbols: ",、。.?!;:‧‥﹐﹒˙·‘’“”〝〞‵′〃~$%@&#*"), + SymbolNode(catHoriBrackets, symbols: "()「」〔〕{}〈〉『』《》【】﹙﹚﹝﹞﹛﹜"), + SymbolNode(catVertBrackets, symbols: "︵︶﹁﹂︹︺︷︸︿﹀﹃﹄︽︾︻︼"), + SymbolNode( + catGreekLetters, symbols: "αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"), + SymbolNode(catMathSymbols, symbols: "+-×÷=≠≒∞±√<>﹤﹥≦≧∩∪ˇ⊥∠∟⊿㏒㏑∫∮∵∴╳﹢"), + SymbolNode(catCurrencyUnits, symbols: "$€¥¢£₽₨₩฿₺₮₱₭₴₦৲৳૱௹﷼₹₲₪₡₫៛₵₢₸₤₳₥₠₣₰₧₯₶₷"), + SymbolNode(catSpecialSymbols, symbols: "↑↓←→↖↗↙↘↺⇧⇩⇦⇨⇄⇆⇅⇵↻◎○●⊕⊙※△▲☆★◇◆□■▽▼§¥〒¢£♀♂↯"), + SymbolNode(catUnicodeSymbols, symbols: "♨☀☁☂☃♠♥♣♦♩♪♫♬☺☻"), + SymbolNode(catCircledKanjis, symbols: "㊟㊞㊚㊛㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗︎㊘㊙︎㊜㊝㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰🈚︎🈯︎"), + SymbolNode( + catCircledKataKana, symbols: "㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋾"), + SymbolNode(catBracketKanjis, symbols: "㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃"), + SymbolNode(catSingleTableLines, symbols: "├─┼┴┬┤┌┐╞═╪╡│▕└┘╭╮╰╯"), + SymbolNode(catDoubleTableLines, symbols: "╔╦╗╠═╬╣╓╥╖╒╤╕║╚╩╝╟╫╢╙╨╜╞╪╡╘╧╛"), + SymbolNode(catFillingBlocks, symbols: "_ˍ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉◢◣◥◤"), + SymbolNode(catLineSegments, symbols: "﹣﹦≡|∣∥–︱—︳╴¯ ̄﹉﹊﹍﹎﹋﹌﹏︴∕﹨╱╲/\"), + ]) } diff --git a/Source/Modules/ControllerModules/KeyParser.swift b/Source/Modules/ControllerModules/KeyParser.swift index 36054d5e..1060b474 100644 --- a/Source/Modules/ControllerModules/KeyParser.swift +++ b/Source/Modules/ControllerModules/KeyParser.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -22,254 +29,264 @@ import Cocoa // 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 @objc enum KeyCode: UInt16 { - case none = 0 - case space = 49 - case backSpace = 51 - case esc = 53 - case tab = 48 - case enterLF = 76 - case enterCR = 36 - case up = 126 - case down = 125 - case left = 123 - case right = 124 - case pageUp = 116 - case pageDown = 121 - case home = 115 - case end = 119 - case delete = 117 - case leftShift = 56 - case rightShift = 60 - case capsLock = 57 - case symbolMenuPhysicalKey = 50 + case none = 0 + case space = 49 + case backSpace = 51 + case esc = 53 + case tab = 48 + case enterLF = 76 + case enterCR = 36 + case up = 126 + case down = 125 + case left = 123 + case right = 124 + case pageUp = 116 + case pageDown = 121 + case home = 115 + case end = 119 + case delete = 117 + case leftShift = 56 + case rightShift = 60 + case capsLock = 57 + case symbolMenuPhysicalKey = 50 } // CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html -enum CharCode: UInt/*16*/ { - case yajuusenpai = 1145141919810893 - // - CharCode is not reliable at all. KeyCode is the most accurate. KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts but only focuses on which physical key is pressed. +enum CharCode: UInt /*16*/ { + case yajuusenpai = 1_145_141_919_810_893 + // - CharCode is not reliable at all. KeyCode is the most accurate. KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts but only focuses on which physical key is pressed. } class keyParser: NSObject { - @objc private (set) var useVerticalMode: Bool - @objc private (set) var inputText: String? - @objc private (set) var inputTextIgnoringModifiers: String? - @objc private (set) var charCode: UInt16 - @objc private (set) var keyCode: UInt16 - private var isFlagChanged: Bool - private var flags: NSEvent.ModifierFlags - private var cursorForwardKey: KeyCode - private var cursorBackwardKey: KeyCode - private var extraChooseCandidateKey: KeyCode - private var extraChooseCandidateKeyReverse: KeyCode - private var absorbedArrowKey: KeyCode - private var verticalModeOnlyChooseCandidateKey: KeyCode - @objc private (set) var emacsKey: vChewingEmacsKey + @objc private(set) var useVerticalMode: Bool + @objc private(set) var inputText: String? + @objc private(set) var inputTextIgnoringModifiers: String? + @objc private(set) var charCode: UInt16 + @objc private(set) var keyCode: UInt16 + private var isFlagChanged: Bool + private var flags: NSEvent.ModifierFlags + private var cursorForwardKey: KeyCode + private var cursorBackwardKey: KeyCode + private var extraChooseCandidateKey: KeyCode + private var extraChooseCandidateKeyReverse: KeyCode + private var absorbedArrowKey: KeyCode + private var verticalModeOnlyChooseCandidateKey: KeyCode + @objc private(set) var emacsKey: vChewingEmacsKey - @objc init(inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil) { - let inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") - let inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(inputTextIgnoringModifiers ?? inputText) - self.inputText = inputText - self.inputTextIgnoringModifiers = inputTextIgnoringModifiers - self.keyCode = keyCode - self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) - self.flags = flags - self.isFlagChanged = false - useVerticalMode = isVerticalMode - emacsKey = EmacsKeyHelper.detect(charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags) - cursorForwardKey = useVerticalMode ? .down : .right - cursorBackwardKey = useVerticalMode ? .up : .left - extraChooseCandidateKey = useVerticalMode ? .left : .down - extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up - absorbedArrowKey = useVerticalMode ? .right : .up - verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none - super.init() - } + @objc init( + inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, + isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil + ) { + let inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") + let inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC( + inputTextIgnoringModifiers ?? inputText) + self.inputText = inputText + self.inputTextIgnoringModifiers = inputTextIgnoringModifiers + self.keyCode = keyCode + self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) + self.flags = flags + self.isFlagChanged = false + useVerticalMode = isVerticalMode + emacsKey = EmacsKeyHelper.detect( + charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags) + cursorForwardKey = useVerticalMode ? .down : .right + cursorBackwardKey = useVerticalMode ? .up : .left + extraChooseCandidateKey = useVerticalMode ? .left : .down + extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up + absorbedArrowKey = useVerticalMode ? .right : .up + verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none + super.init() + } - @objc init(event: NSEvent, isVerticalMode: Bool) { - inputText = AppleKeyboardConverter.cnvStringApple2ABC(event.characters ?? "") - inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(event.charactersIgnoringModifiers ?? "") - keyCode = event.keyCode - flags = event.modifierFlags - isFlagChanged = (event.type == .flagsChanged) ? true : false - useVerticalMode = isVerticalMode - let charCode: UInt16 = { - guard let inputText = event.characters, inputText.count > 0 else { - return 0 - } - let first = inputText[inputText.startIndex].utf16.first! - return first - }() - self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) - emacsKey = EmacsKeyHelper.detect(charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: event.modifierFlags) - cursorForwardKey = useVerticalMode ? .down : .right - cursorBackwardKey = useVerticalMode ? .up : .left - extraChooseCandidateKey = useVerticalMode ? .left : .down - extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up - absorbedArrowKey = useVerticalMode ? .right : .up - verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none - super.init() - } + @objc init(event: NSEvent, isVerticalMode: Bool) { + inputText = AppleKeyboardConverter.cnvStringApple2ABC(event.characters ?? "") + inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC( + event.charactersIgnoringModifiers ?? "") + keyCode = event.keyCode + flags = event.modifierFlags + isFlagChanged = (event.type == .flagsChanged) ? true : false + useVerticalMode = isVerticalMode + let charCode: UInt16 = { + guard let inputText = event.characters, inputText.count > 0 else { + return 0 + } + let first = inputText[inputText.startIndex].utf16.first! + return first + }() + self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) + emacsKey = EmacsKeyHelper.detect( + charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: event.modifierFlags) + cursorForwardKey = useVerticalMode ? .down : .right + cursorBackwardKey = useVerticalMode ? .up : .left + extraChooseCandidateKey = useVerticalMode ? .left : .down + extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up + absorbedArrowKey = useVerticalMode ? .right : .up + verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none + super.init() + } - override var description: String { - charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) - inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") - inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(inputTextIgnoringModifiers ?? "") - 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)>" - } + override var description: String { + charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) + inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") + inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC( + inputTextIgnoringModifiers ?? "") + 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)>" + } - @objc var isShiftHold: Bool { - flags.contains([.shift]) - } + @objc var isShiftHold: Bool { + flags.contains([.shift]) + } - @objc var isCommandHold: Bool { - flags.contains([.command]) - } + @objc var isCommandHold: Bool { + flags.contains([.command]) + } - @objc var isControlHold: Bool { - flags.contains([.control]) - } + @objc var isControlHold: Bool { + flags.contains([.control]) + } - @objc var isControlHotKey: Bool { - flags.contains([.control]) && inputText?.first?.isLetter ?? false - } + @objc var isControlHotKey: Bool { + flags.contains([.control]) && inputText?.first?.isLetter ?? false + } - @objc var isOptionHotKey: Bool { - flags.contains([.option]) && inputText?.first?.isLetter ?? false - } + @objc var isOptionHotKey: Bool { + flags.contains([.option]) && inputText?.first?.isLetter ?? false + } - @objc var isOptionHold: Bool { - flags.contains([.option]) - } + @objc var isOptionHold: Bool { + flags.contains([.option]) + } - @objc var isCapsLockOn: Bool { - flags.contains([.capsLock]) - } + @objc var isCapsLockOn: Bool { + flags.contains([.capsLock]) + } - @objc var isNumericPad: Bool { - flags.contains([.numericPad]) - } + @objc var isNumericPad: Bool { + flags.contains([.numericPad]) + } - @objc var isFunctionKeyHold: Bool { - flags.contains([.function]) - } + @objc var isFunctionKeyHold: Bool { + flags.contains([.function]) + } - @objc var isReservedKey: Bool { - guard let code = KeyCode(rawValue: keyCode) else { - return false - } - return code.rawValue != KeyCode.none.rawValue - } + @objc var isReservedKey: Bool { + guard let code = KeyCode(rawValue: keyCode) else { + return false + } + return code.rawValue != KeyCode.none.rawValue + } - @objc var isTab: Bool { - KeyCode(rawValue: keyCode) == KeyCode.tab - } + @objc var isTab: Bool { + KeyCode(rawValue: keyCode) == KeyCode.tab + } - @objc var isEnter: Bool { - (KeyCode(rawValue: keyCode) == KeyCode.enterCR) || (KeyCode(rawValue: keyCode) == KeyCode.enterLF) - } + @objc var isEnter: Bool { + (KeyCode(rawValue: keyCode) == KeyCode.enterCR) + || (KeyCode(rawValue: keyCode) == KeyCode.enterLF) + } - @objc var isUp: Bool { - KeyCode(rawValue: keyCode) == KeyCode.up - } + @objc var isUp: Bool { + KeyCode(rawValue: keyCode) == KeyCode.up + } - @objc var isDown: Bool { - KeyCode(rawValue: keyCode) == KeyCode.down - } + @objc var isDown: Bool { + KeyCode(rawValue: keyCode) == KeyCode.down + } - @objc var isLeft: Bool { - KeyCode(rawValue: keyCode) == KeyCode.left - } + @objc var isLeft: Bool { + KeyCode(rawValue: keyCode) == KeyCode.left + } - @objc var isRight: Bool { - KeyCode(rawValue: keyCode) == KeyCode.right - } + @objc var isRight: Bool { + KeyCode(rawValue: keyCode) == KeyCode.right + } - @objc var isPageUp: Bool { - KeyCode(rawValue: keyCode) == KeyCode.pageUp - } + @objc var isPageUp: Bool { + KeyCode(rawValue: keyCode) == KeyCode.pageUp + } - @objc var isPageDown: Bool { - KeyCode(rawValue: keyCode) == KeyCode.pageDown - } + @objc var isPageDown: Bool { + KeyCode(rawValue: keyCode) == KeyCode.pageDown + } - @objc var isSpace: Bool { - KeyCode(rawValue: keyCode) == KeyCode.space - } + @objc var isSpace: Bool { + KeyCode(rawValue: keyCode) == KeyCode.space + } - @objc var isBackSpace: Bool { - KeyCode(rawValue: keyCode) == KeyCode.backSpace - } + @objc var isBackSpace: Bool { + KeyCode(rawValue: keyCode) == KeyCode.backSpace + } - @objc var isESC: Bool { - KeyCode(rawValue: keyCode) == KeyCode.esc - } + @objc var isESC: Bool { + KeyCode(rawValue: keyCode) == KeyCode.esc + } - @objc var isHome: Bool { - KeyCode(rawValue: keyCode) == KeyCode.home - } + @objc var isHome: Bool { + KeyCode(rawValue: keyCode) == KeyCode.home + } - @objc var isEnd: Bool { - KeyCode(rawValue: keyCode) == KeyCode.end - } + @objc var isEnd: Bool { + KeyCode(rawValue: keyCode) == KeyCode.end + } - @objc var isDelete: Bool { - KeyCode(rawValue: keyCode) == KeyCode.delete - } + @objc var isDelete: Bool { + KeyCode(rawValue: keyCode) == KeyCode.delete + } - @objc var isCursorBackward: Bool { - KeyCode(rawValue: keyCode) == cursorBackwardKey - } + @objc var isCursorBackward: Bool { + KeyCode(rawValue: keyCode) == cursorBackwardKey + } - @objc var isCursorForward: Bool { - KeyCode(rawValue: keyCode) == cursorForwardKey - } + @objc var isCursorForward: Bool { + KeyCode(rawValue: keyCode) == cursorForwardKey + } - @objc var isAbsorbedArrowKey: Bool { - KeyCode(rawValue: keyCode) == absorbedArrowKey - } + @objc var isAbsorbedArrowKey: Bool { + KeyCode(rawValue: keyCode) == absorbedArrowKey + } - @objc var isExtraChooseCandidateKey: Bool { - KeyCode(rawValue: keyCode) == extraChooseCandidateKey - } + @objc var isExtraChooseCandidateKey: Bool { + KeyCode(rawValue: keyCode) == extraChooseCandidateKey + } - @objc var isExtraChooseCandidateKeyReverse: Bool { - KeyCode(rawValue: keyCode) == extraChooseCandidateKeyReverse - } + @objc var isExtraChooseCandidateKeyReverse: Bool { + KeyCode(rawValue: keyCode) == extraChooseCandidateKeyReverse + } - @objc var isVerticalModeOnlyChooseCandidateKey: Bool { - KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey - } + @objc var isVerticalModeOnlyChooseCandidateKey: Bool { + KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey + } - @objc var isUpperCaseASCIILetterKey: Bool { - // 這裡必須加上「flags == .shift」,否則會出現某些情況下輸入法「誤判當前鍵入的非 Shift 字符為大寫」的問題。 - self.charCode >= 65 && self.charCode <= 90 && flags == .shift - } + @objc var isUpperCaseASCIILetterKey: Bool { + // 這裡必須加上「flags == .shift」,否則會出現某些情況下輸入法「誤判當前鍵入的非 Shift 字符為大寫」的問題。 + self.charCode >= 65 && self.charCode <= 90 && flags == .shift + } - @objc var isSymbolMenuPhysicalKey: Bool { - // 這裡必須用 KeyCode,這樣才不會受隨 macOS 版本更動的 Apple 動態注音鍵盤排列內容的影響。 - // 只是必須得與 ![input isShift] 搭配使用才可以(也就是僅判定 Shift 沒被摁下的情形)。 - KeyCode(rawValue: keyCode) == KeyCode.symbolMenuPhysicalKey - } + @objc var isSymbolMenuPhysicalKey: Bool { + // 這裡必須用 KeyCode,這樣才不會受隨 macOS 版本更動的 Apple 動態注音鍵盤排列內容的影響。 + // 只是必須得與 ![input isShift] 搭配使用才可以(也就是僅判定 Shift 沒被摁下的情形)。 + KeyCode(rawValue: keyCode) == KeyCode.symbolMenuPhysicalKey + } } @objc enum vChewingEmacsKey: UInt16 { - case none = 0 - case forward = 6 // F - case backward = 2 // B - case home = 1 // A - case end = 5 // E - case delete = 4 // D - case nextPage = 22 // V + case none = 0 + case forward = 6 // F + case backward = 2 // B + case home = 1 // A + case end = 5 // E + case delete = 4 // D + case nextPage = 22 // V } class EmacsKeyHelper: NSObject { - @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { - let charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) - if flags.contains(.control) { - return vChewingEmacsKey(rawValue: charCode) ?? .none - } - return .none; - } + @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { + let charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) + if flags.contains(.control) { + return vChewingEmacsKey(rawValue: charCode) ?? .none + } + return .none + } } diff --git a/Source/Modules/ControllerModules/NSStringUtils.swift b/Source/Modules/ControllerModules/NSStringUtils.swift index 852d7422..20394f81 100644 --- a/Source/Modules/ControllerModules/NSStringUtils.swift +++ b/Source/Modules/ControllerModules/NSStringUtils.swift @@ -1,69 +1,76 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 extension NSString { +extension NSString { - /// 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 - /// the length of an NSString is only the sum of the UTF-16 code points. It - /// causes that the NSString and Swift string representation of the same - /// string have different lengths once the string contains such Emoji. The - /// method helps to find the index in a Swift string by passing the index - /// in an NSString. - func characterIndex(from utf16Index:Int) -> (Int, String) { - let string = (self as String) - var length = 0 - for (i, character) in string.enumerated() { - length += character.utf16.count - if length > utf16Index { - return (i, string) - } - } - return (string.count, 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 + /// the length of an NSString is only the sum of the UTF-16 code points. It + /// causes that the NSString and Swift string representation of the same + /// string have different lengths once the string contains such Emoji. The + /// method helps to find the index in a Swift string by passing the index + /// in an NSString. + public func characterIndex(from utf16Index: Int) -> (Int, String) { + let string = (self as String) + var length = 0 + for (i, character) in string.enumerated() { + length += character.utf16.count + if length > utf16Index { + return (i, string) + } + } + return (string.count, string) + } - @objc func nextUtf16Position(for index: Int) -> Int { - var (fixedIndex, string) = characterIndex(from: index) - if fixedIndex < string.count { - fixedIndex += 1 - } - return string[.. Int { + var (fixedIndex, string) = characterIndex(from: index) + if fixedIndex < string.count { + fixedIndex += 1 + } + return string[.. Int { - var (fixedIndex, string) = characterIndex(from: index) - if fixedIndex > 0 { - fixedIndex -= 1 - } - return string[.. Int { + var (fixedIndex, string) = characterIndex(from: index) + if fixedIndex > 0 { + fixedIndex -= 1 + } + return string[.. [NSString] { - Array(self as String).map { - NSString(string: String($0)) - } - } + @objc public func split() -> [NSString] { + Array(self as String).map { + NSString(string: String($0)) + } + } } diff --git a/Source/Modules/ControllerModules/vChewingKanjiConverter.swift b/Source/Modules/ControllerModules/vChewingKanjiConverter.swift index d1bb12f0..70572011 100644 --- a/Source/Modules/ControllerModules/vChewingKanjiConverter.swift +++ b/Source/Modules/ControllerModules/vChewingKanjiConverter.swift @@ -1,769 +1,775 @@ // 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: +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. +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. +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. +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 -private extension String { - mutating func selfReplace(_ strOf: String, _ strWith: String = "") { - self = self.replacingOccurrences(of: strOf, with: strWith) - } +extension String { + fileprivate mutating func selfReplace(_ strOf: String, _ strWith: String = "") { + self = self.replacingOccurrences(of: strOf, with: strWith) + } } @objc class vChewingKanjiConverter: NSObject { - @objc class func cnvTradToKangXi(_ strObj: String) -> String { - var strObj = strObj - strObj.selfReplace("偽", "僞") - strObj.selfReplace("啟", "啓") - strObj.selfReplace("吃", "喫") - strObj.selfReplace("嫻", "嫺") - strObj.selfReplace("媯", "嬀") - strObj.selfReplace("峰", "峯") - strObj.selfReplace("么", "幺") - strObj.selfReplace("抬", "擡") - strObj.selfReplace("稜", "棱") - strObj.selfReplace("簷", "檐") - strObj.selfReplace("汙", "污") - strObj.selfReplace("洩", "泄") - strObj.selfReplace("溈", "潙") - strObj.selfReplace("潀", "潨") - strObj.selfReplace("為", "爲") - strObj.selfReplace("床", "牀") - strObj.selfReplace("痺", "痹") - strObj.selfReplace("痴", "癡") - strObj.selfReplace("皂", "皁") - strObj.selfReplace("著", "着") - strObj.selfReplace("睪", "睾") - strObj.selfReplace("秘", "祕") - strObj.selfReplace("灶", "竈") - strObj.selfReplace("粽", "糉") - strObj.selfReplace("韁", "繮") - strObj.selfReplace("才", "纔") - strObj.selfReplace("群", "羣") - strObj.selfReplace("唇", "脣") - strObj.selfReplace("參", "蔘") - strObj.selfReplace("蒍", "蔿") - strObj.selfReplace("眾", "衆") - strObj.selfReplace("裡", "裏") - strObj.selfReplace("核", "覈") - strObj.selfReplace("踴", "踊") - strObj.selfReplace("缽", "鉢") - strObj.selfReplace("針", "鍼") - strObj.selfReplace("鯰", "鮎") - strObj.selfReplace("麵", "麪") - strObj.selfReplace("顎", "齶") - strObj.selfReplace("口喫", "口吃") - strObj.selfReplace("合着", "合著") - strObj.selfReplace("名着", "名著") - strObj.selfReplace("巨着", "巨著") - strObj.selfReplace("鉅着", "鉅著") - strObj.selfReplace("昭着", "昭著") - strObj.selfReplace("所着", "所著") - strObj.selfReplace("遺着", "遺著") - strObj.selfReplace("顯着", "顯著") - strObj.selfReplace("土着", "土著") - strObj.selfReplace("着作", "著作") - strObj.selfReplace("着名", "著名") - strObj.selfReplace("着式", "著式") - strObj.selfReplace("着志", "著志") - strObj.selfReplace("着於", "著於") - strObj.selfReplace("着書", "著書") - strObj.selfReplace("着白", "著白") - strObj.selfReplace("着稱", "著稱") - strObj.selfReplace("着者", "著者") - strObj.selfReplace("着述", "著述") - strObj.selfReplace("着錄", "著錄") - strObj.selfReplace("蹇喫", "蹇吃") - strObj.selfReplace("大着", "大著") - strObj.selfReplace("刊着", "刊著") - strObj.selfReplace("玄着", "玄著") - strObj.selfReplace("白着", "白著") - strObj.selfReplace("住着", "住著") - strObj.selfReplace("刻着", "刻著") - strObj.selfReplace("卓着", "卓著") - strObj.selfReplace("拙着", "拙著") - strObj.selfReplace("查着", "查著") - strObj.selfReplace("炳着", "炳著") - strObj.selfReplace("原着", "原著") - strObj.selfReplace("專着", "專著") - strObj.selfReplace("焯着", "焯著") - strObj.selfReplace("着論", "著論") - strObj.selfReplace("着績", "著績") - strObj.selfReplace("較着", "較著") - strObj.selfReplace("彰着", "彰著") - strObj.selfReplace("撰着", "撰著") - strObj.selfReplace("編着", "編著") - strObj.selfReplace("論着", "論著") - strObj.selfReplace("雜着", "雜著") - strObj.selfReplace("譯着", "譯著") - strObj.selfReplace("地覈", "地核") - strObj.selfReplace("多覈", "多核") - strObj.selfReplace("氘覈", "氘核") - strObj.selfReplace("杏覈", "杏核") - strObj.selfReplace("非覈", "非核") - strObj.selfReplace("覈三", "核三") - strObj.selfReplace("覈下", "核下") - strObj.selfReplace("覈災", "核災") - strObj.selfReplace("覈武", "核武") - strObj.selfReplace("覈狀", "核狀") - strObj.selfReplace("覈桃", "核桃") - strObj.selfReplace("覈彈", "核彈") - strObj.selfReplace("覈戰", "核戰") - strObj.selfReplace("覈糖", "核糖") - strObj.selfReplace("覈醣", "核醣") - strObj.selfReplace("晶覈", "晶核") - strObj.selfReplace("熱覈", "熱核") - strObj.selfReplace("反覈", "反核") - strObj.selfReplace("卵覈", "卵核") - strObj.selfReplace("果覈", "果核") - strObj.selfReplace("剋覈", "剋核") - strObj.selfReplace("覈力", "核力") - strObj.selfReplace("覈子", "核子") - strObj.selfReplace("覈仁", "核仁") - strObj.selfReplace("覈心", "核心") - strObj.selfReplace("覈四", "核四") - strObj.selfReplace("覈果", "核果") - strObj.selfReplace("覈型", "核型") - strObj.selfReplace("覈苷", "核苷") - strObj.selfReplace("覈能", "核能") - strObj.selfReplace("覈傘", "核傘") - strObj.selfReplace("覈發", "核發") - strObj.selfReplace("覈電", "核電") - strObj.selfReplace("覈塵", "核塵") - strObj.selfReplace("覈酸", "核酸") - strObj.selfReplace("覈膜", "核膜") - strObj.selfReplace("覈爆", "核爆") - strObj.selfReplace("痔覈", "痔核") - strObj.selfReplace("陰覈", "陰核") - strObj.selfReplace("殽覈", "殽核") - strObj.selfReplace("結覈", "結核") - strObj.selfReplace("菌覈", "菌核") - strObj.selfReplace("煤覈", "煤核") - strObj.selfReplace("着涎茶", "著涎茶") - strObj.selfReplace("喫口令", "吃口令") - strObj.selfReplace("鄧艾喫", "鄧艾吃") - strObj.selfReplace("杏仁覈", "杏仁核") - strObj.selfReplace("覈一廠", "核一廠") - strObj.selfReplace("覈二廠", "核二廠") - strObj.selfReplace("覈三廠", "核三廠") - strObj.selfReplace("覈融合", "核融合") - strObj.selfReplace("覈四廠", "核四廠") - strObj.selfReplace("覈生化", "核生化") - strObj.selfReplace("覈災變", "核災變") - strObj.selfReplace("覈動力", "核動力") - strObj.selfReplace("覈試爆", "核試爆") - strObj.selfReplace("杏覈兒", "杏核兒") - strObj.selfReplace("原子覈", "原子核") - strObj.selfReplace("覈分裂", "核分裂") - strObj.selfReplace("覈化學", "核化學") - strObj.selfReplace("覈反應", "核反應") - strObj.selfReplace("覈半徑", "核半徑") - strObj.selfReplace("覈污染", "核污染") - strObj.selfReplace("覈武器", "核武器") - strObj.selfReplace("覈苷酸", "核苷酸") - strObj.selfReplace("覈蛋白", "核蛋白") - strObj.selfReplace("覈黃疸", "核黃疸") - strObj.selfReplace("覈黃素", "核黃素") - strObj.selfReplace("覈裝置", "核裝置") - strObj.selfReplace("覈電廠", "核電廠") - strObj.selfReplace("覈廢料", "核廢料") - strObj.selfReplace("覈彈頭", "核彈頭") - strObj.selfReplace("覈潛艇", "核潛艇") - strObj.selfReplace("覈燃料", "核燃料") - strObj.selfReplace("桃覈雕", "桃核雕") - strObj.selfReplace("細胞覈", "細胞核") - strObj.selfReplace("棗覈臉", "棗核臉") - strObj.selfReplace("以微知着", "以微知著") - strObj.selfReplace("見微知着", "見微知著") - strObj.selfReplace("恩威並着", "恩威並著") - strObj.selfReplace("視微知着", "視微知著") - strObj.selfReplace("睹微知着", "睹微知著") - strObj.selfReplace("遐邇着聞", "遐邇著聞") - strObj.selfReplace("積微成着", "積微成著") - strObj.selfReplace("地下覈試", "地下核試") - strObj.selfReplace("地下覈爆", "地下核爆") - strObj.selfReplace("非覈武區", "非核武區") - strObj.selfReplace("覈反應器", "核反應器") - strObj.selfReplace("覈物理學", "核物理學") - strObj.selfReplace("覈能發電", "核能發電") - strObj.selfReplace("覈能電廠", "核能電廠") - strObj.selfReplace("覈能廢料", "核能廢料") - strObj.selfReplace("覈能潛艇", "核能潛艇") - strObj.selfReplace("覈磁共振", "核磁共振") - strObj.selfReplace("熱覈反應", "熱核反應") - strObj.selfReplace("賣李鑽覈", "賣李鑽核") - strObj.selfReplace("雙覈都市", "雙核都市") - strObj.selfReplace("罵人不吐覈", "罵人不吐核") - return strObj - } + @objc class func cnvTradToKangXi(_ strObj: String) -> String { + var strObj = strObj + strObj.selfReplace("偽", "僞") + strObj.selfReplace("啟", "啓") + strObj.selfReplace("吃", "喫") + strObj.selfReplace("嫻", "嫺") + strObj.selfReplace("媯", "嬀") + strObj.selfReplace("峰", "峯") + strObj.selfReplace("么", "幺") + strObj.selfReplace("抬", "擡") + strObj.selfReplace("稜", "棱") + strObj.selfReplace("簷", "檐") + strObj.selfReplace("汙", "污") + strObj.selfReplace("洩", "泄") + strObj.selfReplace("溈", "潙") + strObj.selfReplace("潀", "潨") + strObj.selfReplace("為", "爲") + strObj.selfReplace("床", "牀") + strObj.selfReplace("痺", "痹") + strObj.selfReplace("痴", "癡") + strObj.selfReplace("皂", "皁") + strObj.selfReplace("著", "着") + strObj.selfReplace("睪", "睾") + strObj.selfReplace("秘", "祕") + strObj.selfReplace("灶", "竈") + strObj.selfReplace("粽", "糉") + strObj.selfReplace("韁", "繮") + strObj.selfReplace("才", "纔") + strObj.selfReplace("群", "羣") + strObj.selfReplace("唇", "脣") + strObj.selfReplace("參", "蔘") + strObj.selfReplace("蒍", "蔿") + strObj.selfReplace("眾", "衆") + strObj.selfReplace("裡", "裏") + strObj.selfReplace("核", "覈") + strObj.selfReplace("踴", "踊") + strObj.selfReplace("缽", "鉢") + strObj.selfReplace("針", "鍼") + strObj.selfReplace("鯰", "鮎") + strObj.selfReplace("麵", "麪") + strObj.selfReplace("顎", "齶") + strObj.selfReplace("口喫", "口吃") + strObj.selfReplace("合着", "合著") + strObj.selfReplace("名着", "名著") + strObj.selfReplace("巨着", "巨著") + strObj.selfReplace("鉅着", "鉅著") + strObj.selfReplace("昭着", "昭著") + strObj.selfReplace("所着", "所著") + strObj.selfReplace("遺着", "遺著") + strObj.selfReplace("顯着", "顯著") + strObj.selfReplace("土着", "土著") + strObj.selfReplace("着作", "著作") + strObj.selfReplace("着名", "著名") + strObj.selfReplace("着式", "著式") + strObj.selfReplace("着志", "著志") + strObj.selfReplace("着於", "著於") + strObj.selfReplace("着書", "著書") + strObj.selfReplace("着白", "著白") + strObj.selfReplace("着稱", "著稱") + strObj.selfReplace("着者", "著者") + strObj.selfReplace("着述", "著述") + strObj.selfReplace("着錄", "著錄") + strObj.selfReplace("蹇喫", "蹇吃") + strObj.selfReplace("大着", "大著") + strObj.selfReplace("刊着", "刊著") + strObj.selfReplace("玄着", "玄著") + strObj.selfReplace("白着", "白著") + strObj.selfReplace("住着", "住著") + strObj.selfReplace("刻着", "刻著") + strObj.selfReplace("卓着", "卓著") + strObj.selfReplace("拙着", "拙著") + strObj.selfReplace("查着", "查著") + strObj.selfReplace("炳着", "炳著") + strObj.selfReplace("原着", "原著") + strObj.selfReplace("專着", "專著") + strObj.selfReplace("焯着", "焯著") + strObj.selfReplace("着論", "著論") + strObj.selfReplace("着績", "著績") + strObj.selfReplace("較着", "較著") + strObj.selfReplace("彰着", "彰著") + strObj.selfReplace("撰着", "撰著") + strObj.selfReplace("編着", "編著") + strObj.selfReplace("論着", "論著") + strObj.selfReplace("雜着", "雜著") + strObj.selfReplace("譯着", "譯著") + strObj.selfReplace("地覈", "地核") + strObj.selfReplace("多覈", "多核") + strObj.selfReplace("氘覈", "氘核") + strObj.selfReplace("杏覈", "杏核") + strObj.selfReplace("非覈", "非核") + strObj.selfReplace("覈三", "核三") + strObj.selfReplace("覈下", "核下") + strObj.selfReplace("覈災", "核災") + strObj.selfReplace("覈武", "核武") + strObj.selfReplace("覈狀", "核狀") + strObj.selfReplace("覈桃", "核桃") + strObj.selfReplace("覈彈", "核彈") + strObj.selfReplace("覈戰", "核戰") + strObj.selfReplace("覈糖", "核糖") + strObj.selfReplace("覈醣", "核醣") + strObj.selfReplace("晶覈", "晶核") + strObj.selfReplace("熱覈", "熱核") + strObj.selfReplace("反覈", "反核") + strObj.selfReplace("卵覈", "卵核") + strObj.selfReplace("果覈", "果核") + strObj.selfReplace("剋覈", "剋核") + strObj.selfReplace("覈力", "核力") + strObj.selfReplace("覈子", "核子") + strObj.selfReplace("覈仁", "核仁") + strObj.selfReplace("覈心", "核心") + strObj.selfReplace("覈四", "核四") + strObj.selfReplace("覈果", "核果") + strObj.selfReplace("覈型", "核型") + strObj.selfReplace("覈苷", "核苷") + strObj.selfReplace("覈能", "核能") + strObj.selfReplace("覈傘", "核傘") + strObj.selfReplace("覈發", "核發") + strObj.selfReplace("覈電", "核電") + strObj.selfReplace("覈塵", "核塵") + strObj.selfReplace("覈酸", "核酸") + strObj.selfReplace("覈膜", "核膜") + strObj.selfReplace("覈爆", "核爆") + strObj.selfReplace("痔覈", "痔核") + strObj.selfReplace("陰覈", "陰核") + strObj.selfReplace("殽覈", "殽核") + strObj.selfReplace("結覈", "結核") + strObj.selfReplace("菌覈", "菌核") + strObj.selfReplace("煤覈", "煤核") + strObj.selfReplace("着涎茶", "著涎茶") + strObj.selfReplace("喫口令", "吃口令") + strObj.selfReplace("鄧艾喫", "鄧艾吃") + strObj.selfReplace("杏仁覈", "杏仁核") + strObj.selfReplace("覈一廠", "核一廠") + strObj.selfReplace("覈二廠", "核二廠") + strObj.selfReplace("覈三廠", "核三廠") + strObj.selfReplace("覈融合", "核融合") + strObj.selfReplace("覈四廠", "核四廠") + strObj.selfReplace("覈生化", "核生化") + strObj.selfReplace("覈災變", "核災變") + strObj.selfReplace("覈動力", "核動力") + strObj.selfReplace("覈試爆", "核試爆") + strObj.selfReplace("杏覈兒", "杏核兒") + strObj.selfReplace("原子覈", "原子核") + strObj.selfReplace("覈分裂", "核分裂") + strObj.selfReplace("覈化學", "核化學") + strObj.selfReplace("覈反應", "核反應") + strObj.selfReplace("覈半徑", "核半徑") + strObj.selfReplace("覈污染", "核污染") + strObj.selfReplace("覈武器", "核武器") + strObj.selfReplace("覈苷酸", "核苷酸") + strObj.selfReplace("覈蛋白", "核蛋白") + strObj.selfReplace("覈黃疸", "核黃疸") + strObj.selfReplace("覈黃素", "核黃素") + strObj.selfReplace("覈裝置", "核裝置") + strObj.selfReplace("覈電廠", "核電廠") + strObj.selfReplace("覈廢料", "核廢料") + strObj.selfReplace("覈彈頭", "核彈頭") + strObj.selfReplace("覈潛艇", "核潛艇") + strObj.selfReplace("覈燃料", "核燃料") + strObj.selfReplace("桃覈雕", "桃核雕") + strObj.selfReplace("細胞覈", "細胞核") + strObj.selfReplace("棗覈臉", "棗核臉") + strObj.selfReplace("以微知着", "以微知著") + strObj.selfReplace("見微知着", "見微知著") + strObj.selfReplace("恩威並着", "恩威並著") + strObj.selfReplace("視微知着", "視微知著") + strObj.selfReplace("睹微知着", "睹微知著") + strObj.selfReplace("遐邇着聞", "遐邇著聞") + strObj.selfReplace("積微成着", "積微成著") + strObj.selfReplace("地下覈試", "地下核試") + strObj.selfReplace("地下覈爆", "地下核爆") + strObj.selfReplace("非覈武區", "非核武區") + strObj.selfReplace("覈反應器", "核反應器") + strObj.selfReplace("覈物理學", "核物理學") + strObj.selfReplace("覈能發電", "核能發電") + strObj.selfReplace("覈能電廠", "核能電廠") + strObj.selfReplace("覈能廢料", "核能廢料") + strObj.selfReplace("覈能潛艇", "核能潛艇") + strObj.selfReplace("覈磁共振", "核磁共振") + strObj.selfReplace("熱覈反應", "熱核反應") + strObj.selfReplace("賣李鑽覈", "賣李鑽核") + strObj.selfReplace("雙覈都市", "雙核都市") + strObj.selfReplace("罵人不吐覈", "罵人不吐核") + return strObj + } - @objc class func cnvTradToJIS(_ strObj: String) -> String { - // 該轉換是由康熙繁體轉換至日語當用漢字的,所以需要先跑一遍康熙轉換。 - var strObj = cnvTradToKangXi(strObj) - strObj.selfReplace("兩", "両") - strObj.selfReplace("輛", "両") - strObj.selfReplace("辨", "弁") - strObj.selfReplace("辯", "弁") - strObj.selfReplace("瓣", "弁") - strObj.selfReplace("辦", "弁") - strObj.selfReplace("禦", "御") - strObj.selfReplace("缺", "欠") - strObj.selfReplace("絲", "糸") - strObj.selfReplace("藝", "芸") - strObj.selfReplace("濱", "浜") - strObj.selfReplace("乘", "乗") - strObj.selfReplace("亂", "乱") - strObj.selfReplace("亙", "亘") - strObj.selfReplace("亞", "亜") - strObj.selfReplace("佛", "仏") - strObj.selfReplace("來", "来") - strObj.selfReplace("假", "仮") - strObj.selfReplace("傳", "伝") - strObj.selfReplace("僞", "偽") - strObj.selfReplace("價", "価") - strObj.selfReplace("儉", "倹") - strObj.selfReplace("兒", "児") - strObj.selfReplace("內", "内") - strObj.selfReplace("剎", "刹") - strObj.selfReplace("剩", "剰") - strObj.selfReplace("劍", "剣") - strObj.selfReplace("剱", "剣") - strObj.selfReplace("劎", "剣") - strObj.selfReplace("劒", "剣") - strObj.selfReplace("劔", "剣") - strObj.selfReplace("劑", "剤") - strObj.selfReplace("勞", "労") - strObj.selfReplace("勳", "勲") - strObj.selfReplace("勵", "励") - strObj.selfReplace("勸", "勧") - strObj.selfReplace("勻", "匀") - strObj.selfReplace("區", "区") - strObj.selfReplace("卷", "巻") - strObj.selfReplace("卻", "却") - strObj.selfReplace("參", "参") - strObj.selfReplace("吳", "呉") - strObj.selfReplace("咒", "呪") - strObj.selfReplace("啞", "唖") - strObj.selfReplace("單", "単") - strObj.selfReplace("噓", "嘘") - strObj.selfReplace("嚙", "噛") - strObj.selfReplace("嚴", "厳") - strObj.selfReplace("囑", "嘱") - strObj.selfReplace("圈", "圏") - strObj.selfReplace("國", "国") - strObj.selfReplace("圍", "囲") - strObj.selfReplace("圓", "円") - strObj.selfReplace("圖", "図") - strObj.selfReplace("團", "団") - strObj.selfReplace("增", "増") - strObj.selfReplace("墮", "堕") - strObj.selfReplace("壓", "圧") - strObj.selfReplace("壘", "塁") - strObj.selfReplace("壞", "壊") - strObj.selfReplace("壤", "壌") - strObj.selfReplace("壯", "壮") - strObj.selfReplace("壹", "壱") - strObj.selfReplace("壽", "寿") - strObj.selfReplace("奧", "奥") - strObj.selfReplace("奬", "奨") - strObj.selfReplace("妝", "粧") - strObj.selfReplace("孃", "嬢") - strObj.selfReplace("學", "学") - strObj.selfReplace("寢", "寝") - strObj.selfReplace("實", "実") - strObj.selfReplace("寫", "写") - strObj.selfReplace("寬", "寛") - strObj.selfReplace("寶", "宝") - strObj.selfReplace("將", "将") - strObj.selfReplace("專", "専") - strObj.selfReplace("對", "対") - strObj.selfReplace("屆", "届") - strObj.selfReplace("屬", "属") - strObj.selfReplace("峯", "峰") - strObj.selfReplace("峽", "峡") - strObj.selfReplace("嶽", "岳") - strObj.selfReplace("巖", "巌") - strObj.selfReplace("巢", "巣") - strObj.selfReplace("帶", "帯") - strObj.selfReplace("廁", "厠") - strObj.selfReplace("廢", "廃") - strObj.selfReplace("廣", "広") - strObj.selfReplace("廳", "庁") - strObj.selfReplace("彈", "弾") - strObj.selfReplace("彌", "弥") - strObj.selfReplace("彎", "弯") - strObj.selfReplace("彥", "彦") - strObj.selfReplace("徑", "径") - strObj.selfReplace("從", "従") - strObj.selfReplace("徵", "徴") - strObj.selfReplace("德", "徳") - strObj.selfReplace("恆", "恒") - strObj.selfReplace("悅", "悦") - strObj.selfReplace("惠", "恵") - strObj.selfReplace("惡", "悪") - strObj.selfReplace("惱", "悩") - strObj.selfReplace("慘", "惨") - strObj.selfReplace("應", "応") - strObj.selfReplace("懷", "懐") - strObj.selfReplace("戀", "恋") - strObj.selfReplace("戰", "戦") - strObj.selfReplace("戲", "戯") - strObj.selfReplace("戶", "戸") - strObj.selfReplace("戾", "戻") - strObj.selfReplace("拂", "払") - strObj.selfReplace("拔", "抜") - strObj.selfReplace("拜", "拝") - strObj.selfReplace("挾", "挟") - strObj.selfReplace("插", "挿") - strObj.selfReplace("揭", "掲") - strObj.selfReplace("搔", "掻") - strObj.selfReplace("搖", "揺") - strObj.selfReplace("搜", "捜") - strObj.selfReplace("摑", "掴") - strObj.selfReplace("擇", "択") - strObj.selfReplace("擊", "撃") - strObj.selfReplace("擔", "担") - strObj.selfReplace("據", "拠") - strObj.selfReplace("擴", "拡") - strObj.selfReplace("攝", "摂") - strObj.selfReplace("攪", "撹") - strObj.selfReplace("收", "収") - strObj.selfReplace("效", "効") - strObj.selfReplace("敕", "勅") - strObj.selfReplace("敘", "叙") - strObj.selfReplace("數", "数") - strObj.selfReplace("斷", "断") - strObj.selfReplace("晉", "晋") - strObj.selfReplace("晚", "晩") - strObj.selfReplace("晝", "昼") - strObj.selfReplace("暨", "曁") - strObj.selfReplace("曆", "暦") - strObj.selfReplace("曉", "暁") - strObj.selfReplace("曾", "曽") - strObj.selfReplace("會", "会") - strObj.selfReplace("枡", "桝") - strObj.selfReplace("查", "査") - strObj.selfReplace("條", "条") - strObj.selfReplace("棧", "桟") - strObj.selfReplace("棱", "稜") - strObj.selfReplace("榆", "楡") - strObj.selfReplace("榮", "栄") - strObj.selfReplace("樂", "楽") - strObj.selfReplace("樓", "楼") - strObj.selfReplace("樞", "枢") - strObj.selfReplace("樣", "様") - strObj.selfReplace("橫", "横") - strObj.selfReplace("檢", "検") - strObj.selfReplace("櫻", "桜") - strObj.selfReplace("權", "権") - strObj.selfReplace("歐", "欧") - strObj.selfReplace("歡", "歓") - strObj.selfReplace("步", "歩") - strObj.selfReplace("歲", "歳") - strObj.selfReplace("歷", "歴") - strObj.selfReplace("歸", "帰") - strObj.selfReplace("殘", "残") - strObj.selfReplace("殼", "殻") - strObj.selfReplace("毆", "殴") - strObj.selfReplace("每", "毎") - strObj.selfReplace("氣", "気") - strObj.selfReplace("污", "汚") - strObj.selfReplace("沒", "没") - strObj.selfReplace("涉", "渉") - strObj.selfReplace("淚", "涙") - strObj.selfReplace("淨", "浄") - strObj.selfReplace("淺", "浅") - strObj.selfReplace("渴", "渇") - strObj.selfReplace("溌", "潑") - strObj.selfReplace("溪", "渓") - strObj.selfReplace("溫", "温") - strObj.selfReplace("溼", "湿") - strObj.selfReplace("滯", "滞") - strObj.selfReplace("滿", "満") - strObj.selfReplace("潛", "潜") - strObj.selfReplace("澀", "渋") - strObj.selfReplace("澤", "沢") - strObj.selfReplace("濟", "済") - strObj.selfReplace("濤", "涛") - strObj.selfReplace("濾", "沪") - strObj.selfReplace("瀧", "滝") - strObj.selfReplace("瀨", "瀬") - strObj.selfReplace("灣", "湾") - strObj.selfReplace("焰", "焔") - strObj.selfReplace("燈", "灯") - strObj.selfReplace("燒", "焼") - strObj.selfReplace("營", "営") - strObj.selfReplace("爐", "炉") - strObj.selfReplace("爭", "争") - strObj.selfReplace("爲", "為") - strObj.selfReplace("牀", "床") - strObj.selfReplace("犧", "犠") - strObj.selfReplace("狀", "状") - strObj.selfReplace("狹", "狭") - strObj.selfReplace("獨", "独") - strObj.selfReplace("獵", "猟") - strObj.selfReplace("獸", "獣") - strObj.selfReplace("獻", "献") - strObj.selfReplace("產", "産") - strObj.selfReplace("畫", "画") - strObj.selfReplace("當", "当") - strObj.selfReplace("疊", "畳") - strObj.selfReplace("疎", "疏") - strObj.selfReplace("痹", "痺") - strObj.selfReplace("瘦", "痩") - strObj.selfReplace("癡", "痴") - strObj.selfReplace("發", "発") - strObj.selfReplace("皋", "皐") - strObj.selfReplace("盜", "盗") - strObj.selfReplace("盡", "尽") - strObj.selfReplace("碎", "砕") - strObj.selfReplace("祕", "秘") - strObj.selfReplace("祿", "禄") - strObj.selfReplace("禪", "禅") - strObj.selfReplace("禮", "礼") - strObj.selfReplace("禱", "祷") - strObj.selfReplace("稅", "税") - strObj.selfReplace("稱", "称") - strObj.selfReplace("稻", "稲") - strObj.selfReplace("穎", "頴") - strObj.selfReplace("穗", "穂") - strObj.selfReplace("穩", "穏") - strObj.selfReplace("穰", "穣") - strObj.selfReplace("竃", "竈") - strObj.selfReplace("竊", "窃") - strObj.selfReplace("粹", "粋") - strObj.selfReplace("糉", "粽") - strObj.selfReplace("絕", "絶") - strObj.selfReplace("經", "経") - strObj.selfReplace("綠", "緑") - strObj.selfReplace("緖", "緒") - strObj.selfReplace("緣", "縁") - strObj.selfReplace("縣", "県") - strObj.selfReplace("縱", "縦") - strObj.selfReplace("總", "総") - strObj.selfReplace("繋", "繫") - strObj.selfReplace("繡", "繍") - strObj.selfReplace("繩", "縄") - strObj.selfReplace("繪", "絵") - strObj.selfReplace("繼", "継") - strObj.selfReplace("續", "続") - strObj.selfReplace("纔", "才") - strObj.selfReplace("纖", "繊") - strObj.selfReplace("罐", "缶") - strObj.selfReplace("羣", "群") - strObj.selfReplace("聯", "連") - strObj.selfReplace("聰", "聡") - strObj.selfReplace("聲", "声") - strObj.selfReplace("聽", "聴") - strObj.selfReplace("肅", "粛") - strObj.selfReplace("脣", "唇") - strObj.selfReplace("脫", "脱") - strObj.selfReplace("腦", "脳") - strObj.selfReplace("腳", "脚") - strObj.selfReplace("膽", "胆") - strObj.selfReplace("臟", "臓") - strObj.selfReplace("臺", "台") - strObj.selfReplace("與", "与") - strObj.selfReplace("舉", "挙") - strObj.selfReplace("舊", "旧") - strObj.selfReplace("舍", "舎") - strObj.selfReplace("荔", "茘") - strObj.selfReplace("莊", "荘") - strObj.selfReplace("莖", "茎") - strObj.selfReplace("菸", "煙") - strObj.selfReplace("萊", "莱") - strObj.selfReplace("萬", "万") - strObj.selfReplace("蔣", "蒋") - strObj.selfReplace("蔥", "葱") - strObj.selfReplace("薰", "薫") - strObj.selfReplace("藏", "蔵") - strObj.selfReplace("藥", "薬") - strObj.selfReplace("蘆", "芦") - strObj.selfReplace("處", "処") - strObj.selfReplace("虛", "虚") - strObj.selfReplace("號", "号") - strObj.selfReplace("螢", "蛍") - strObj.selfReplace("蟲", "虫") - strObj.selfReplace("蠟", "蝋") - strObj.selfReplace("蠶", "蚕") - strObj.selfReplace("蠻", "蛮") - strObj.selfReplace("裝", "装") - strObj.selfReplace("覺", "覚") - strObj.selfReplace("覽", "覧") - strObj.selfReplace("觀", "観") - strObj.selfReplace("觸", "触") - strObj.selfReplace("說", "説") - strObj.selfReplace("謠", "謡") - strObj.selfReplace("證", "証") - strObj.selfReplace("譯", "訳") - strObj.selfReplace("譽", "誉") - strObj.selfReplace("讀", "読") - strObj.selfReplace("變", "変") - strObj.selfReplace("讓", "譲") - strObj.selfReplace("豐", "豊") - strObj.selfReplace("豫", "予") - strObj.selfReplace("貓", "猫") - strObj.selfReplace("貳", "弐") - strObj.selfReplace("賣", "売") - strObj.selfReplace("賴", "頼") - strObj.selfReplace("贊", "賛") - strObj.selfReplace("贗", "贋") - strObj.selfReplace("踐", "践") - strObj.selfReplace("輕", "軽") - strObj.selfReplace("輛", "輌") - strObj.selfReplace("轉", "転") - strObj.selfReplace("辭", "辞") - strObj.selfReplace("遞", "逓") - strObj.selfReplace("遥", "遙") - strObj.selfReplace("遲", "遅") - strObj.selfReplace("邊", "辺") - strObj.selfReplace("鄉", "郷") - strObj.selfReplace("酢", "醋") - strObj.selfReplace("醉", "酔") - strObj.selfReplace("醗", "醱") - strObj.selfReplace("醫", "医") - strObj.selfReplace("醬", "醤") - strObj.selfReplace("釀", "醸") - strObj.selfReplace("釋", "釈") - strObj.selfReplace("鋪", "舗") - strObj.selfReplace("錄", "録") - strObj.selfReplace("錢", "銭") - strObj.selfReplace("鍊", "錬") - strObj.selfReplace("鐵", "鉄") - strObj.selfReplace("鑄", "鋳") - strObj.selfReplace("鑛", "鉱") - strObj.selfReplace("閱", "閲") - strObj.selfReplace("關", "関") - strObj.selfReplace("陷", "陥") - strObj.selfReplace("隨", "随") - strObj.selfReplace("險", "険") - strObj.selfReplace("隱", "隠") - strObj.selfReplace("雙", "双") - strObj.selfReplace("雜", "雑") - strObj.selfReplace("雞", "鶏") - strObj.selfReplace("霸", "覇") - strObj.selfReplace("靈", "霊") - strObj.selfReplace("靜", "静") - strObj.selfReplace("顏", "顔") - strObj.selfReplace("顯", "顕") - strObj.selfReplace("餘", "余") - strObj.selfReplace("騷", "騒") - strObj.selfReplace("驅", "駆") - strObj.selfReplace("驗", "験") - strObj.selfReplace("驛", "駅") - strObj.selfReplace("髓", "髄") - strObj.selfReplace("體", "体") - strObj.selfReplace("髮", "髪") - strObj.selfReplace("鬥", "闘") - strObj.selfReplace("鱉", "鼈") - strObj.selfReplace("鷗", "鴎") - strObj.selfReplace("鹼", "鹸") - strObj.selfReplace("鹽", "塩") - strObj.selfReplace("麥", "麦") - strObj.selfReplace("麪", "麺") - strObj.selfReplace("麴", "麹") - strObj.selfReplace("黃", "黄") - strObj.selfReplace("黑", "黒") - strObj.selfReplace("默", "黙") - strObj.selfReplace("點", "点") - strObj.selfReplace("黨", "党") - strObj.selfReplace("齊", "斉") - strObj.selfReplace("齋", "斎") - strObj.selfReplace("齒", "歯") - strObj.selfReplace("齡", "齢") - strObj.selfReplace("龍", "竜") - strObj.selfReplace("龜", "亀") - strObj.selfReplace("叮嚀", "丁寧") - strObj.selfReplace("鄭重", "丁重") - strObj.selfReplace("輿論", "世論") - strObj.selfReplace("唖鈴", "亜鈴") - strObj.selfReplace("交叉", "交差") - strObj.selfReplace("饗宴", "供宴") - strObj.selfReplace("駿馬", "俊馬") - strObj.selfReplace("堡塁", "保塁") - strObj.selfReplace("扁平", "偏平") - strObj.selfReplace("碇泊", "停泊") - strObj.selfReplace("優駿", "優俊") - strObj.selfReplace("尖兵", "先兵") - strObj.selfReplace("尖鋭", "先鋭") - strObj.selfReplace("共軛", "共役") - strObj.selfReplace("饒舌", "冗舌") - strObj.selfReplace("兇器", "凶器") - strObj.selfReplace("鑿岩", "削岩") - strObj.selfReplace("庖丁", "包丁") - strObj.selfReplace("繃帯", "包帯") - strObj.selfReplace("区劃", "区画") - strObj.selfReplace("儼然", "厳然") - strObj.selfReplace("友誼", "友宜") - strObj.selfReplace("叛乱", "反乱") - strObj.selfReplace("蒐集", "収集") - strObj.selfReplace("抒情", "叙情") - strObj.selfReplace("擡頭", "台頭") - strObj.selfReplace("合弁", "合弁") - strObj.selfReplace("歎願", "嘆願") - strObj.selfReplace("廻転", "回転") - strObj.selfReplace("回游", "回遊") - strObj.selfReplace("捧持", "奉持") - strObj.selfReplace("萎縮", "委縮") - strObj.selfReplace("輾転", "展転") - strObj.selfReplace("稀少", "希少") - strObj.selfReplace("眩惑", "幻惑") - strObj.selfReplace("広汎", "広範") - strObj.selfReplace("曠野", "広野") - strObj.selfReplace("廃墟", "廃虚") - strObj.selfReplace("弁当", "弁当") - strObj.selfReplace("弁膜", "弁膜") - strObj.selfReplace("弁護", "弁護") - strObj.selfReplace("辮髪", "弁髪") - strObj.selfReplace("絃歌", "弦歌") - strObj.selfReplace("恩誼", "恩義") - strObj.selfReplace("意嚮", "意向") - strObj.selfReplace("臆断", "憶断") - strObj.selfReplace("臆病", "憶病") - strObj.selfReplace("戦歿", "戦没") - strObj.selfReplace("煽情", "扇情") - strObj.selfReplace("手帖", "手帳") - strObj.selfReplace("伎倆", "技量") - strObj.selfReplace("抜萃", "抜粋") - strObj.selfReplace("披瀝", "披歴") - strObj.selfReplace("牴触", "抵触") - strObj.selfReplace("抽籤", "抽選") - strObj.selfReplace("勾引", "拘引") - strObj.selfReplace("醵出", "拠出") - strObj.selfReplace("醵金", "拠金") - strObj.selfReplace("掘鑿", "掘削") - strObj.selfReplace("扣除", "控除") - strObj.selfReplace("掩護", "援護") - strObj.selfReplace("抛棄", "放棄") - strObj.selfReplace("撒水", "散水") - strObj.selfReplace("敬虔", "敬謙") - strObj.selfReplace("敷衍", "敷延") - strObj.selfReplace("断乎", "断固") - strObj.selfReplace("簇生", "族生") - strObj.selfReplace("陞叙", "昇叙") - strObj.selfReplace("煖房", "暖房") - strObj.selfReplace("暗誦", "暗唱") - strObj.selfReplace("闇夜", "暗夜") - strObj.selfReplace("曝露", "暴露") - strObj.selfReplace("涸渇", "枯渇") - strObj.selfReplace("恰好", "格好") - strObj.selfReplace("恰幅", "格幅") - strObj.selfReplace("毀損", "棄損") - strObj.selfReplace("摸索", "模索") - strObj.selfReplace("欠欠", "欠缺") - strObj.selfReplace("屍体", "死体") - strObj.selfReplace("臀部", "殿部") - strObj.selfReplace("拇指", "母指") - strObj.selfReplace("気魄", "気迫") - strObj.selfReplace("訣別", "決別") - strObj.selfReplace("決潰", "決壊") - strObj.selfReplace("沈澱", "沈殿") - strObj.selfReplace("波瀾", "波乱") - strObj.selfReplace("註釈", "注釈") - strObj.selfReplace("洗滌", "洗濯") - strObj.selfReplace("活潑", "活発") - strObj.selfReplace("滲透", "浸透") - strObj.selfReplace("浸蝕", "浸食") - strObj.selfReplace("銷却", "消却") - strObj.selfReplace("渾然", "混然") - strObj.selfReplace("弯曲", "湾曲") - strObj.selfReplace("熔接", "溶接") - strObj.selfReplace("漁撈", "漁労") - strObj.selfReplace("飄然", "漂然") - strObj.selfReplace("激昂", "激高") - strObj.selfReplace("火焔", "火炎") - strObj.selfReplace("焦躁", "焦燥") - strObj.selfReplace("斑点", "班点") - strObj.selfReplace("溜飲", "留飲") - strObj.selfReplace("掠奪", "略奪") - strObj.selfReplace("疏通", "疎通") - strObj.selfReplace("醱酵", "発酵") - strObj.selfReplace("白堊", "白亜") - strObj.selfReplace("相剋", "相克") - strObj.selfReplace("智慧", "知恵") - strObj.selfReplace("破毀", "破棄") - strObj.selfReplace("確乎", "確固") - strObj.selfReplace("禁錮", "禁固") - strObj.selfReplace("符牒", "符丁") - strObj.selfReplace("扮装", "粉装") - strObj.selfReplace("紫斑", "紫班") - strObj.selfReplace("終熄", "終息") - strObj.selfReplace("綜合", "総合") - strObj.selfReplace("編輯", "編集") - strObj.selfReplace("義捐", "義援") - strObj.selfReplace("肝腎", "肝心") - strObj.selfReplace("悖徳", "背徳") - strObj.selfReplace("脈搏", "脈拍") - strObj.selfReplace("膨脹", "膨張") - strObj.selfReplace("芳醇", "芳純") - strObj.selfReplace("叡智", "英知") - strObj.selfReplace("蒸溜", "蒸留") - strObj.selfReplace("燻蒸", "薫蒸") - strObj.selfReplace("燻製", "薫製") - strObj.selfReplace("衣裳", "衣装") - strObj.selfReplace("衰頽", "衰退") - strObj.selfReplace("悠然", "裕然") - strObj.selfReplace("輔佐", "補佐") - strObj.selfReplace("訓誡", "訓戒") - strObj.selfReplace("試煉", "試練") - strObj.selfReplace("詭弁", "詭弁") - strObj.selfReplace("媾和", "講和") - strObj.selfReplace("象嵌", "象眼") - strObj.selfReplace("貫禄", "貫録") - strObj.selfReplace("買弁", "買弁") - strObj.selfReplace("讚辞", "賛辞") - strObj.selfReplace("蹈襲", "踏襲") - strObj.selfReplace("車両", "車両") - strObj.selfReplace("顛倒", "転倒") - strObj.selfReplace("輪廓", "輪郭") - strObj.selfReplace("褪色", "退色") - strObj.selfReplace("杜絶", "途絶") - strObj.selfReplace("連繫", "連係") - strObj.selfReplace("連合", "連合") - strObj.selfReplace("銓衡", "選考") - strObj.selfReplace("醋酸", "酢酸") - strObj.selfReplace("野鄙", "野卑") - strObj.selfReplace("礦石", "鉱石") - strObj.selfReplace("間歇", "間欠") - strObj.selfReplace("函数", "関数") - strObj.selfReplace("防御", "防御") - strObj.selfReplace("嶮岨", "険阻") - strObj.selfReplace("牆壁", "障壁") - strObj.selfReplace("障礙", "障害") - strObj.selfReplace("湮滅", "隠滅") - strObj.selfReplace("聚落", "集落") - strObj.selfReplace("雇傭", "雇用") - strObj.selfReplace("諷喩", "風諭") - strObj.selfReplace("蜚語", "飛語") - strObj.selfReplace("香奠", "香典") - strObj.selfReplace("骨骼", "骨格") - strObj.selfReplace("亢進", "高進") - strObj.selfReplace("鳥瞰", "鳥観") - strObj.selfReplace("一攫", "一獲") - strObj.selfReplace("肩胛", "肩甲") - strObj.selfReplace("箇条", "個条") - strObj.selfReplace("啓動", "起動") - strObj.selfReplace("三叉路", "三差路") - strObj.selfReplace("嬉遊曲", "喜遊曲") - strObj.selfReplace("建蔽率", "建坪率") - strObj.selfReplace("慰藉料", "慰謝料") - strObj.selfReplace("橋頭堡", "橋頭保") - strObj.selfReplace("油槽船", "油送船") - strObj.selfReplace("耕耘機", "耕運機") - return strObj - } + @objc class func cnvTradToJIS(_ strObj: String) -> String { + // 該轉換是由康熙繁體轉換至日語當用漢字的,所以需要先跑一遍康熙轉換。 + var strObj = cnvTradToKangXi(strObj) + strObj.selfReplace("兩", "両") + strObj.selfReplace("輛", "両") + strObj.selfReplace("辨", "弁") + strObj.selfReplace("辯", "弁") + strObj.selfReplace("瓣", "弁") + strObj.selfReplace("辦", "弁") + strObj.selfReplace("禦", "御") + strObj.selfReplace("缺", "欠") + strObj.selfReplace("絲", "糸") + strObj.selfReplace("藝", "芸") + strObj.selfReplace("濱", "浜") + strObj.selfReplace("乘", "乗") + strObj.selfReplace("亂", "乱") + strObj.selfReplace("亙", "亘") + strObj.selfReplace("亞", "亜") + strObj.selfReplace("佛", "仏") + strObj.selfReplace("來", "来") + strObj.selfReplace("假", "仮") + strObj.selfReplace("傳", "伝") + strObj.selfReplace("僞", "偽") + strObj.selfReplace("價", "価") + strObj.selfReplace("儉", "倹") + strObj.selfReplace("兒", "児") + strObj.selfReplace("內", "内") + strObj.selfReplace("剎", "刹") + strObj.selfReplace("剩", "剰") + strObj.selfReplace("劍", "剣") + strObj.selfReplace("剱", "剣") + strObj.selfReplace("劎", "剣") + strObj.selfReplace("劒", "剣") + strObj.selfReplace("劔", "剣") + strObj.selfReplace("劑", "剤") + strObj.selfReplace("勞", "労") + strObj.selfReplace("勳", "勲") + strObj.selfReplace("勵", "励") + strObj.selfReplace("勸", "勧") + strObj.selfReplace("勻", "匀") + strObj.selfReplace("區", "区") + strObj.selfReplace("卷", "巻") + strObj.selfReplace("卻", "却") + strObj.selfReplace("參", "参") + strObj.selfReplace("吳", "呉") + strObj.selfReplace("咒", "呪") + strObj.selfReplace("啞", "唖") + strObj.selfReplace("單", "単") + strObj.selfReplace("噓", "嘘") + strObj.selfReplace("嚙", "噛") + strObj.selfReplace("嚴", "厳") + strObj.selfReplace("囑", "嘱") + strObj.selfReplace("圈", "圏") + strObj.selfReplace("國", "国") + strObj.selfReplace("圍", "囲") + strObj.selfReplace("圓", "円") + strObj.selfReplace("圖", "図") + strObj.selfReplace("團", "団") + strObj.selfReplace("增", "増") + strObj.selfReplace("墮", "堕") + strObj.selfReplace("壓", "圧") + strObj.selfReplace("壘", "塁") + strObj.selfReplace("壞", "壊") + strObj.selfReplace("壤", "壌") + strObj.selfReplace("壯", "壮") + strObj.selfReplace("壹", "壱") + strObj.selfReplace("壽", "寿") + strObj.selfReplace("奧", "奥") + strObj.selfReplace("奬", "奨") + strObj.selfReplace("妝", "粧") + strObj.selfReplace("孃", "嬢") + strObj.selfReplace("學", "学") + strObj.selfReplace("寢", "寝") + strObj.selfReplace("實", "実") + strObj.selfReplace("寫", "写") + strObj.selfReplace("寬", "寛") + strObj.selfReplace("寶", "宝") + strObj.selfReplace("將", "将") + strObj.selfReplace("專", "専") + strObj.selfReplace("對", "対") + strObj.selfReplace("屆", "届") + strObj.selfReplace("屬", "属") + strObj.selfReplace("峯", "峰") + strObj.selfReplace("峽", "峡") + strObj.selfReplace("嶽", "岳") + strObj.selfReplace("巖", "巌") + strObj.selfReplace("巢", "巣") + strObj.selfReplace("帶", "帯") + strObj.selfReplace("廁", "厠") + strObj.selfReplace("廢", "廃") + strObj.selfReplace("廣", "広") + strObj.selfReplace("廳", "庁") + strObj.selfReplace("彈", "弾") + strObj.selfReplace("彌", "弥") + strObj.selfReplace("彎", "弯") + strObj.selfReplace("彥", "彦") + strObj.selfReplace("徑", "径") + strObj.selfReplace("從", "従") + strObj.selfReplace("徵", "徴") + strObj.selfReplace("德", "徳") + strObj.selfReplace("恆", "恒") + strObj.selfReplace("悅", "悦") + strObj.selfReplace("惠", "恵") + strObj.selfReplace("惡", "悪") + strObj.selfReplace("惱", "悩") + strObj.selfReplace("慘", "惨") + strObj.selfReplace("應", "応") + strObj.selfReplace("懷", "懐") + strObj.selfReplace("戀", "恋") + strObj.selfReplace("戰", "戦") + strObj.selfReplace("戲", "戯") + strObj.selfReplace("戶", "戸") + strObj.selfReplace("戾", "戻") + strObj.selfReplace("拂", "払") + strObj.selfReplace("拔", "抜") + strObj.selfReplace("拜", "拝") + strObj.selfReplace("挾", "挟") + strObj.selfReplace("插", "挿") + strObj.selfReplace("揭", "掲") + strObj.selfReplace("搔", "掻") + strObj.selfReplace("搖", "揺") + strObj.selfReplace("搜", "捜") + strObj.selfReplace("摑", "掴") + strObj.selfReplace("擇", "択") + strObj.selfReplace("擊", "撃") + strObj.selfReplace("擔", "担") + strObj.selfReplace("據", "拠") + strObj.selfReplace("擴", "拡") + strObj.selfReplace("攝", "摂") + strObj.selfReplace("攪", "撹") + strObj.selfReplace("收", "収") + strObj.selfReplace("效", "効") + strObj.selfReplace("敕", "勅") + strObj.selfReplace("敘", "叙") + strObj.selfReplace("數", "数") + strObj.selfReplace("斷", "断") + strObj.selfReplace("晉", "晋") + strObj.selfReplace("晚", "晩") + strObj.selfReplace("晝", "昼") + strObj.selfReplace("暨", "曁") + strObj.selfReplace("曆", "暦") + strObj.selfReplace("曉", "暁") + strObj.selfReplace("曾", "曽") + strObj.selfReplace("會", "会") + strObj.selfReplace("枡", "桝") + strObj.selfReplace("查", "査") + strObj.selfReplace("條", "条") + strObj.selfReplace("棧", "桟") + strObj.selfReplace("棱", "稜") + strObj.selfReplace("榆", "楡") + strObj.selfReplace("榮", "栄") + strObj.selfReplace("樂", "楽") + strObj.selfReplace("樓", "楼") + strObj.selfReplace("樞", "枢") + strObj.selfReplace("樣", "様") + strObj.selfReplace("橫", "横") + strObj.selfReplace("檢", "検") + strObj.selfReplace("櫻", "桜") + strObj.selfReplace("權", "権") + strObj.selfReplace("歐", "欧") + strObj.selfReplace("歡", "歓") + strObj.selfReplace("步", "歩") + strObj.selfReplace("歲", "歳") + strObj.selfReplace("歷", "歴") + strObj.selfReplace("歸", "帰") + strObj.selfReplace("殘", "残") + strObj.selfReplace("殼", "殻") + strObj.selfReplace("毆", "殴") + strObj.selfReplace("每", "毎") + strObj.selfReplace("氣", "気") + strObj.selfReplace("污", "汚") + strObj.selfReplace("沒", "没") + strObj.selfReplace("涉", "渉") + strObj.selfReplace("淚", "涙") + strObj.selfReplace("淨", "浄") + strObj.selfReplace("淺", "浅") + strObj.selfReplace("渴", "渇") + strObj.selfReplace("溌", "潑") + strObj.selfReplace("溪", "渓") + strObj.selfReplace("溫", "温") + strObj.selfReplace("溼", "湿") + strObj.selfReplace("滯", "滞") + strObj.selfReplace("滿", "満") + strObj.selfReplace("潛", "潜") + strObj.selfReplace("澀", "渋") + strObj.selfReplace("澤", "沢") + strObj.selfReplace("濟", "済") + strObj.selfReplace("濤", "涛") + strObj.selfReplace("濾", "沪") + strObj.selfReplace("瀧", "滝") + strObj.selfReplace("瀨", "瀬") + strObj.selfReplace("灣", "湾") + strObj.selfReplace("焰", "焔") + strObj.selfReplace("燈", "灯") + strObj.selfReplace("燒", "焼") + strObj.selfReplace("營", "営") + strObj.selfReplace("爐", "炉") + strObj.selfReplace("爭", "争") + strObj.selfReplace("爲", "為") + strObj.selfReplace("牀", "床") + strObj.selfReplace("犧", "犠") + strObj.selfReplace("狀", "状") + strObj.selfReplace("狹", "狭") + strObj.selfReplace("獨", "独") + strObj.selfReplace("獵", "猟") + strObj.selfReplace("獸", "獣") + strObj.selfReplace("獻", "献") + strObj.selfReplace("產", "産") + strObj.selfReplace("畫", "画") + strObj.selfReplace("當", "当") + strObj.selfReplace("疊", "畳") + strObj.selfReplace("疎", "疏") + strObj.selfReplace("痹", "痺") + strObj.selfReplace("瘦", "痩") + strObj.selfReplace("癡", "痴") + strObj.selfReplace("發", "発") + strObj.selfReplace("皋", "皐") + strObj.selfReplace("盜", "盗") + strObj.selfReplace("盡", "尽") + strObj.selfReplace("碎", "砕") + strObj.selfReplace("祕", "秘") + strObj.selfReplace("祿", "禄") + strObj.selfReplace("禪", "禅") + strObj.selfReplace("禮", "礼") + strObj.selfReplace("禱", "祷") + strObj.selfReplace("稅", "税") + strObj.selfReplace("稱", "称") + strObj.selfReplace("稻", "稲") + strObj.selfReplace("穎", "頴") + strObj.selfReplace("穗", "穂") + strObj.selfReplace("穩", "穏") + strObj.selfReplace("穰", "穣") + strObj.selfReplace("竃", "竈") + strObj.selfReplace("竊", "窃") + strObj.selfReplace("粹", "粋") + strObj.selfReplace("糉", "粽") + strObj.selfReplace("絕", "絶") + strObj.selfReplace("經", "経") + strObj.selfReplace("綠", "緑") + strObj.selfReplace("緖", "緒") + strObj.selfReplace("緣", "縁") + strObj.selfReplace("縣", "県") + strObj.selfReplace("縱", "縦") + strObj.selfReplace("總", "総") + strObj.selfReplace("繋", "繫") + strObj.selfReplace("繡", "繍") + strObj.selfReplace("繩", "縄") + strObj.selfReplace("繪", "絵") + strObj.selfReplace("繼", "継") + strObj.selfReplace("續", "続") + strObj.selfReplace("纔", "才") + strObj.selfReplace("纖", "繊") + strObj.selfReplace("罐", "缶") + strObj.selfReplace("羣", "群") + strObj.selfReplace("聯", "連") + strObj.selfReplace("聰", "聡") + strObj.selfReplace("聲", "声") + strObj.selfReplace("聽", "聴") + strObj.selfReplace("肅", "粛") + strObj.selfReplace("脣", "唇") + strObj.selfReplace("脫", "脱") + strObj.selfReplace("腦", "脳") + strObj.selfReplace("腳", "脚") + strObj.selfReplace("膽", "胆") + strObj.selfReplace("臟", "臓") + strObj.selfReplace("臺", "台") + strObj.selfReplace("與", "与") + strObj.selfReplace("舉", "挙") + strObj.selfReplace("舊", "旧") + strObj.selfReplace("舍", "舎") + strObj.selfReplace("荔", "茘") + strObj.selfReplace("莊", "荘") + strObj.selfReplace("莖", "茎") + strObj.selfReplace("菸", "煙") + strObj.selfReplace("萊", "莱") + strObj.selfReplace("萬", "万") + strObj.selfReplace("蔣", "蒋") + strObj.selfReplace("蔥", "葱") + strObj.selfReplace("薰", "薫") + strObj.selfReplace("藏", "蔵") + strObj.selfReplace("藥", "薬") + strObj.selfReplace("蘆", "芦") + strObj.selfReplace("處", "処") + strObj.selfReplace("虛", "虚") + strObj.selfReplace("號", "号") + strObj.selfReplace("螢", "蛍") + strObj.selfReplace("蟲", "虫") + strObj.selfReplace("蠟", "蝋") + strObj.selfReplace("蠶", "蚕") + strObj.selfReplace("蠻", "蛮") + strObj.selfReplace("裝", "装") + strObj.selfReplace("覺", "覚") + strObj.selfReplace("覽", "覧") + strObj.selfReplace("觀", "観") + strObj.selfReplace("觸", "触") + strObj.selfReplace("說", "説") + strObj.selfReplace("謠", "謡") + strObj.selfReplace("證", "証") + strObj.selfReplace("譯", "訳") + strObj.selfReplace("譽", "誉") + strObj.selfReplace("讀", "読") + strObj.selfReplace("變", "変") + strObj.selfReplace("讓", "譲") + strObj.selfReplace("豐", "豊") + strObj.selfReplace("豫", "予") + strObj.selfReplace("貓", "猫") + strObj.selfReplace("貳", "弐") + strObj.selfReplace("賣", "売") + strObj.selfReplace("賴", "頼") + strObj.selfReplace("贊", "賛") + strObj.selfReplace("贗", "贋") + strObj.selfReplace("踐", "践") + strObj.selfReplace("輕", "軽") + strObj.selfReplace("輛", "輌") + strObj.selfReplace("轉", "転") + strObj.selfReplace("辭", "辞") + strObj.selfReplace("遞", "逓") + strObj.selfReplace("遥", "遙") + strObj.selfReplace("遲", "遅") + strObj.selfReplace("邊", "辺") + strObj.selfReplace("鄉", "郷") + strObj.selfReplace("酢", "醋") + strObj.selfReplace("醉", "酔") + strObj.selfReplace("醗", "醱") + strObj.selfReplace("醫", "医") + strObj.selfReplace("醬", "醤") + strObj.selfReplace("釀", "醸") + strObj.selfReplace("釋", "釈") + strObj.selfReplace("鋪", "舗") + strObj.selfReplace("錄", "録") + strObj.selfReplace("錢", "銭") + strObj.selfReplace("鍊", "錬") + strObj.selfReplace("鐵", "鉄") + strObj.selfReplace("鑄", "鋳") + strObj.selfReplace("鑛", "鉱") + strObj.selfReplace("閱", "閲") + strObj.selfReplace("關", "関") + strObj.selfReplace("陷", "陥") + strObj.selfReplace("隨", "随") + strObj.selfReplace("險", "険") + strObj.selfReplace("隱", "隠") + strObj.selfReplace("雙", "双") + strObj.selfReplace("雜", "雑") + strObj.selfReplace("雞", "鶏") + strObj.selfReplace("霸", "覇") + strObj.selfReplace("靈", "霊") + strObj.selfReplace("靜", "静") + strObj.selfReplace("顏", "顔") + strObj.selfReplace("顯", "顕") + strObj.selfReplace("餘", "余") + strObj.selfReplace("騷", "騒") + strObj.selfReplace("驅", "駆") + strObj.selfReplace("驗", "験") + strObj.selfReplace("驛", "駅") + strObj.selfReplace("髓", "髄") + strObj.selfReplace("體", "体") + strObj.selfReplace("髮", "髪") + strObj.selfReplace("鬥", "闘") + strObj.selfReplace("鱉", "鼈") + strObj.selfReplace("鷗", "鴎") + strObj.selfReplace("鹼", "鹸") + strObj.selfReplace("鹽", "塩") + strObj.selfReplace("麥", "麦") + strObj.selfReplace("麪", "麺") + strObj.selfReplace("麴", "麹") + strObj.selfReplace("黃", "黄") + strObj.selfReplace("黑", "黒") + strObj.selfReplace("默", "黙") + strObj.selfReplace("點", "点") + strObj.selfReplace("黨", "党") + strObj.selfReplace("齊", "斉") + strObj.selfReplace("齋", "斎") + strObj.selfReplace("齒", "歯") + strObj.selfReplace("齡", "齢") + strObj.selfReplace("龍", "竜") + strObj.selfReplace("龜", "亀") + strObj.selfReplace("叮嚀", "丁寧") + strObj.selfReplace("鄭重", "丁重") + strObj.selfReplace("輿論", "世論") + strObj.selfReplace("唖鈴", "亜鈴") + strObj.selfReplace("交叉", "交差") + strObj.selfReplace("饗宴", "供宴") + strObj.selfReplace("駿馬", "俊馬") + strObj.selfReplace("堡塁", "保塁") + strObj.selfReplace("扁平", "偏平") + strObj.selfReplace("碇泊", "停泊") + strObj.selfReplace("優駿", "優俊") + strObj.selfReplace("尖兵", "先兵") + strObj.selfReplace("尖鋭", "先鋭") + strObj.selfReplace("共軛", "共役") + strObj.selfReplace("饒舌", "冗舌") + strObj.selfReplace("兇器", "凶器") + strObj.selfReplace("鑿岩", "削岩") + strObj.selfReplace("庖丁", "包丁") + strObj.selfReplace("繃帯", "包帯") + strObj.selfReplace("区劃", "区画") + strObj.selfReplace("儼然", "厳然") + strObj.selfReplace("友誼", "友宜") + strObj.selfReplace("叛乱", "反乱") + strObj.selfReplace("蒐集", "収集") + strObj.selfReplace("抒情", "叙情") + strObj.selfReplace("擡頭", "台頭") + strObj.selfReplace("合弁", "合弁") + strObj.selfReplace("歎願", "嘆願") + strObj.selfReplace("廻転", "回転") + strObj.selfReplace("回游", "回遊") + strObj.selfReplace("捧持", "奉持") + strObj.selfReplace("萎縮", "委縮") + strObj.selfReplace("輾転", "展転") + strObj.selfReplace("稀少", "希少") + strObj.selfReplace("眩惑", "幻惑") + strObj.selfReplace("広汎", "広範") + strObj.selfReplace("曠野", "広野") + strObj.selfReplace("廃墟", "廃虚") + strObj.selfReplace("弁当", "弁当") + strObj.selfReplace("弁膜", "弁膜") + strObj.selfReplace("弁護", "弁護") + strObj.selfReplace("辮髪", "弁髪") + strObj.selfReplace("絃歌", "弦歌") + strObj.selfReplace("恩誼", "恩義") + strObj.selfReplace("意嚮", "意向") + strObj.selfReplace("臆断", "憶断") + strObj.selfReplace("臆病", "憶病") + strObj.selfReplace("戦歿", "戦没") + strObj.selfReplace("煽情", "扇情") + strObj.selfReplace("手帖", "手帳") + strObj.selfReplace("伎倆", "技量") + strObj.selfReplace("抜萃", "抜粋") + strObj.selfReplace("披瀝", "披歴") + strObj.selfReplace("牴触", "抵触") + strObj.selfReplace("抽籤", "抽選") + strObj.selfReplace("勾引", "拘引") + strObj.selfReplace("醵出", "拠出") + strObj.selfReplace("醵金", "拠金") + strObj.selfReplace("掘鑿", "掘削") + strObj.selfReplace("扣除", "控除") + strObj.selfReplace("掩護", "援護") + strObj.selfReplace("抛棄", "放棄") + strObj.selfReplace("撒水", "散水") + strObj.selfReplace("敬虔", "敬謙") + strObj.selfReplace("敷衍", "敷延") + strObj.selfReplace("断乎", "断固") + strObj.selfReplace("簇生", "族生") + strObj.selfReplace("陞叙", "昇叙") + strObj.selfReplace("煖房", "暖房") + strObj.selfReplace("暗誦", "暗唱") + strObj.selfReplace("闇夜", "暗夜") + strObj.selfReplace("曝露", "暴露") + strObj.selfReplace("涸渇", "枯渇") + strObj.selfReplace("恰好", "格好") + strObj.selfReplace("恰幅", "格幅") + strObj.selfReplace("毀損", "棄損") + strObj.selfReplace("摸索", "模索") + strObj.selfReplace("欠欠", "欠缺") + strObj.selfReplace("屍体", "死体") + strObj.selfReplace("臀部", "殿部") + strObj.selfReplace("拇指", "母指") + strObj.selfReplace("気魄", "気迫") + strObj.selfReplace("訣別", "決別") + strObj.selfReplace("決潰", "決壊") + strObj.selfReplace("沈澱", "沈殿") + strObj.selfReplace("波瀾", "波乱") + strObj.selfReplace("註釈", "注釈") + strObj.selfReplace("洗滌", "洗濯") + strObj.selfReplace("活潑", "活発") + strObj.selfReplace("滲透", "浸透") + strObj.selfReplace("浸蝕", "浸食") + strObj.selfReplace("銷却", "消却") + strObj.selfReplace("渾然", "混然") + strObj.selfReplace("弯曲", "湾曲") + strObj.selfReplace("熔接", "溶接") + strObj.selfReplace("漁撈", "漁労") + strObj.selfReplace("飄然", "漂然") + strObj.selfReplace("激昂", "激高") + strObj.selfReplace("火焔", "火炎") + strObj.selfReplace("焦躁", "焦燥") + strObj.selfReplace("斑点", "班点") + strObj.selfReplace("溜飲", "留飲") + strObj.selfReplace("掠奪", "略奪") + strObj.selfReplace("疏通", "疎通") + strObj.selfReplace("醱酵", "発酵") + strObj.selfReplace("白堊", "白亜") + strObj.selfReplace("相剋", "相克") + strObj.selfReplace("智慧", "知恵") + strObj.selfReplace("破毀", "破棄") + strObj.selfReplace("確乎", "確固") + strObj.selfReplace("禁錮", "禁固") + strObj.selfReplace("符牒", "符丁") + strObj.selfReplace("扮装", "粉装") + strObj.selfReplace("紫斑", "紫班") + strObj.selfReplace("終熄", "終息") + strObj.selfReplace("綜合", "総合") + strObj.selfReplace("編輯", "編集") + strObj.selfReplace("義捐", "義援") + strObj.selfReplace("肝腎", "肝心") + strObj.selfReplace("悖徳", "背徳") + strObj.selfReplace("脈搏", "脈拍") + strObj.selfReplace("膨脹", "膨張") + strObj.selfReplace("芳醇", "芳純") + strObj.selfReplace("叡智", "英知") + strObj.selfReplace("蒸溜", "蒸留") + strObj.selfReplace("燻蒸", "薫蒸") + strObj.selfReplace("燻製", "薫製") + strObj.selfReplace("衣裳", "衣装") + strObj.selfReplace("衰頽", "衰退") + strObj.selfReplace("悠然", "裕然") + strObj.selfReplace("輔佐", "補佐") + strObj.selfReplace("訓誡", "訓戒") + strObj.selfReplace("試煉", "試練") + strObj.selfReplace("詭弁", "詭弁") + strObj.selfReplace("媾和", "講和") + strObj.selfReplace("象嵌", "象眼") + strObj.selfReplace("貫禄", "貫録") + strObj.selfReplace("買弁", "買弁") + strObj.selfReplace("讚辞", "賛辞") + strObj.selfReplace("蹈襲", "踏襲") + strObj.selfReplace("車両", "車両") + strObj.selfReplace("顛倒", "転倒") + strObj.selfReplace("輪廓", "輪郭") + strObj.selfReplace("褪色", "退色") + strObj.selfReplace("杜絶", "途絶") + strObj.selfReplace("連繫", "連係") + strObj.selfReplace("連合", "連合") + strObj.selfReplace("銓衡", "選考") + strObj.selfReplace("醋酸", "酢酸") + strObj.selfReplace("野鄙", "野卑") + strObj.selfReplace("礦石", "鉱石") + strObj.selfReplace("間歇", "間欠") + strObj.selfReplace("函数", "関数") + strObj.selfReplace("防御", "防御") + strObj.selfReplace("嶮岨", "険阻") + strObj.selfReplace("牆壁", "障壁") + strObj.selfReplace("障礙", "障害") + strObj.selfReplace("湮滅", "隠滅") + strObj.selfReplace("聚落", "集落") + strObj.selfReplace("雇傭", "雇用") + strObj.selfReplace("諷喩", "風諭") + strObj.selfReplace("蜚語", "飛語") + strObj.selfReplace("香奠", "香典") + strObj.selfReplace("骨骼", "骨格") + strObj.selfReplace("亢進", "高進") + strObj.selfReplace("鳥瞰", "鳥観") + strObj.selfReplace("一攫", "一獲") + strObj.selfReplace("肩胛", "肩甲") + strObj.selfReplace("箇条", "個条") + strObj.selfReplace("啓動", "起動") + strObj.selfReplace("三叉路", "三差路") + strObj.selfReplace("嬉遊曲", "喜遊曲") + strObj.selfReplace("建蔽率", "建坪率") + strObj.selfReplace("慰藉料", "慰謝料") + strObj.selfReplace("橋頭堡", "橋頭保") + strObj.selfReplace("油槽船", "油送船") + strObj.selfReplace("耕耘機", "耕運機") + return strObj + } } diff --git a/Source/Modules/FileHandlers/FSEventStreamHelper.swift b/Source/Modules/FileHandlers/FSEventStreamHelper.swift index ca7dc052..59948360 100644 --- a/Source/Modules/FileHandlers/FSEventStreamHelper.swift +++ b/Source/Modules/FileHandlers/FSEventStreamHelper.swift @@ -1,92 +1,104 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 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 { - var path: String - var flags: FSEventStreamEventFlags - var id: FSEventStreamEventId - } + public struct Event { + var path: String + var flags: FSEventStreamEventFlags + var id: FSEventStreamEventId + } - public let path: String - public let dispatchQueue: DispatchQueue - public weak var delegate: FSEventStreamHelperDelegate? + public let path: String + public let dispatchQueue: DispatchQueue + public weak var delegate: FSEventStreamHelperDelegate? - @objc public init(path: String, queue: DispatchQueue) { - self.path = path - self.dispatchQueue = queue - } + @objc public init(path: String, queue: DispatchQueue) { + self.path = path + self.dispatchQueue = queue + } - private var stream: FSEventStreamRef? = nil + private var stream: FSEventStreamRef? = nil - public func start() -> Bool { - if stream != nil { - return false - } - var context = FSEventStreamContext() - context.info = Unmanaged.passUnretained(self).toOpaque() - guard let stream = FSEventStreamCreate(nil, { - (stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in - let helper = Unmanaged.fromOpaque(clientCallBackInfo!).takeUnretainedValue() - let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer.self) - let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount) - let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount) - let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount) - let events = (0.. Bool { + if stream != nil { + return false + } + var context = FSEventStreamContext() + context.info = Unmanaged.passUnretained(self).toOpaque() + guard + let stream = FSEventStreamCreate( + nil, + { + (stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in + let helper = Unmanaged.fromOpaque(clientCallBackInfo!) + .takeUnretainedValue() + let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer.self) + let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount) + let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount) + let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount) + let events = (0.. Bool { - if #available(macOS 10.15, *) { - let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription.lowercased() - if appearanceDescription.contains("dark") { - return true - } - } else if #available(macOS 10.14, *) { - if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle") as? String { - if appleInterfaceStyle.lowercased().contains("dark") { - return true - } - } - } - return false - } + // MARK: - System Dark Mode Status Detector. + @objc static func isDarkMode() -> Bool { + if #available(macOS 10.15, *) { + let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription + .lowercased() + if appearanceDescription.contains("dark") { + return true + } + } else if #available(macOS 10.14, *) { + if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle") + as? String + { + if appleInterfaceStyle.lowercased().contains("dark") { + return true + } + } + } + return false + } - // MARK: - Trash a file if it exists. - @discardableResult static func trashTargetIfExists(_ path: String) -> Bool { - do { - if FileManager.default.fileExists(atPath: path) { - // 塞入垃圾桶 - try FileManager.default.trashItem(at: URL(fileURLWithPath: path), resultingItemURL: nil) - } else { - NSLog("Item doesn't exist: \(path)") - } - } catch let error as NSError { - NSLog("Failed from removing this object: \(path) || Error: \(error)") - return false - } - return true - } - // MARK: - Uninstalling the input method. - @discardableResult static func uninstall(isSudo: Bool = false, selfKill: Bool = true) -> Int32 { - // 輸入法自毀處理。這裡不用「Bundle.main.bundleURL」是為了方便使用者以 sudo 身分來移除被錯誤安裝到系統目錄內的輸入法。 - guard let bundleID = Bundle.main.bundleIdentifier else { - NSLog("Failed to ensure the bundle identifier.") - return -1 - } + // MARK: - Trash a file if it exists. + @discardableResult static func trashTargetIfExists(_ path: String) -> Bool { + do { + if FileManager.default.fileExists(atPath: path) { + // 塞入垃圾桶 + try FileManager.default.trashItem( + at: URL(fileURLWithPath: path), resultingItemURL: nil) + } else { + NSLog("Item doesn't exist: \(path)") + } + } catch let error as NSError { + NSLog("Failed from removing this object: \(path) || Error: \(error)") + return false + } + return true + } - let kTargetBin = "vChewing" - let kTargetBundle = "/vChewing.app" - let pathLibrary = isSudo ? "/Library" : FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].path - let pathIMELibrary = isSudo ? "/Library/Input Methods" : FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0].path - let pathUnitKeyboardLayouts = "/Keyboard Layouts" - let arrKeyLayoutFiles = ["/vChewing ETen.keylayout", "/vChewingKeyLayout.bundle", "/vChewing MiTAC.keylayout", "/vChewing IBM.keylayout", "/vChewing FakeSeigyou.keylayout", "/vChewing Dachen.keylayout"] + // MARK: - Uninstall the input method. + @discardableResult static func uninstall(isSudo: Bool = false, selfKill: Bool = true) -> Int32 { + // 輸入法自毀處理。這裡不用「Bundle.main.bundleURL」是為了方便使用者以 sudo 身分來移除被錯誤安裝到系統目錄內的輸入法。 + guard let bundleID = Bundle.main.bundleIdentifier else { + NSLog("Failed to ensure the bundle identifier.") + return -1 + } - // 先移除各種鍵盤佈局。 - for objPath in arrKeyLayoutFiles { - let objFullPath = pathLibrary + pathUnitKeyboardLayouts + objPath - if !IME.trashTargetIfExists(objFullPath) { return -1 } - } - if CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "--all" && CommandLine.arguments[1] == "uninstall" { - // 再處理是否需要移除放在預設使用者資料夾內的檔案的情況。 - // 如果使用者有在輸入法偏好設定內將該目錄改到別的地方(而不是用 symbol link)的話,則不處理。 - // 目前暫時無法應對 symbol link 的情況。 - IME.trashTargetIfExists(mgrLangModel.dataFolderPath(isDefaultFolder: true)) - IME.trashTargetIfExists(pathLibrary + "/Preferences/" + bundleID + ".plist") // 之後移除 App 偏好設定 - } - if !IME.trashTargetIfExists(pathIMELibrary + kTargetBundle) { return -1 } // 最後移除 App 自身 - // 幹掉殘留在記憶體內的執行緒。 - if selfKill { - let killTask = Process() - killTask.launchPath = "/usr/bin/killall" - killTask.arguments = ["-9", kTargetBin] - killTask.launch() - killTask.waitUntilExit() - } - return 0 - } + let kTargetBin = "vChewing" + let kTargetBundle = "/vChewing.app" + let pathLibrary = + isSudo + ? "/Library" + : FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].path + let pathIMELibrary = + isSudo + ? "/Library/Input Methods" + : FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0].path + let pathUnitKeyboardLayouts = "/Keyboard Layouts" + let arrKeyLayoutFiles = [ + "/vChewing ETen.keylayout", "/vChewingKeyLayout.bundle", "/vChewing MiTAC.keylayout", + "/vChewing IBM.keylayout", "/vChewing FakeSeigyou.keylayout", + "/vChewing Dachen.keylayout", + ] - // MARK: - Registering the input method. - @discardableResult static func registerInputMethod() -> Int32 { - guard let bundleID = Bundle.main.bundleIdentifier else { - return -1 - } - let bundleUrl = Bundle.main.bundleURL - var maybeInputSource = InputSourceHelper.inputSource(for: bundleID) + // 先移除各種鍵盤佈局。 + for objPath in arrKeyLayoutFiles { + let objFullPath = pathLibrary + pathUnitKeyboardLayouts + objPath + if !IME.trashTargetIfExists(objFullPath) { return -1 } + } + if CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "--all" + && CommandLine.arguments[1] == "uninstall" + { + // 再處理是否需要移除放在預設使用者資料夾內的檔案的情況。 + // 如果使用者有在輸入法偏好設定內將該目錄改到別的地方(而不是用 symbol link)的話,則不處理。 + // 目前暫時無法應對 symbol link 的情況。 + IME.trashTargetIfExists(mgrLangModel.dataFolderPath(isDefaultFolder: true)) + IME.trashTargetIfExists(pathLibrary + "/Preferences/" + bundleID + ".plist") // 之後移除 App 偏好設定 + } + if !IME.trashTargetIfExists(pathIMELibrary + kTargetBundle) { return -1 } // 最後移除 App 自身 + // 幹掉殘留在記憶體內的執行緒。 + if selfKill { + let killTask = Process() + killTask.launchPath = "/usr/bin/killall" + killTask.arguments = ["-9", kTargetBin] + killTask.launch() + killTask.waitUntilExit() + } + return 0 + } - if maybeInputSource == nil { - NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)"); - // then register - let status = InputSourceHelper.registerTnputSource(at: bundleUrl) + // MARK: - Registering the input method. + @discardableResult static func registerInputMethod() -> Int32 { + guard let bundleID = Bundle.main.bundleIdentifier else { + return -1 + } + let bundleUrl = Bundle.main.bundleURL + var maybeInputSource = InputSourceHelper.inputSource(for: bundleID) - if !status { - NSLog("Fatal error: Cannot register input source \(bundleID) at \(bundleUrl.absoluteString).") - return -1 - } + if maybeInputSource == nil { + NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)") + // then register + let status = InputSourceHelper.registerTnputSource(at: bundleUrl) - maybeInputSource = InputSourceHelper.inputSource(for: bundleID) - } + if !status { + NSLog( + "Fatal error: Cannot register input source \(bundleID) at \(bundleUrl.absoluteString)." + ) + return -1 + } - guard let inputSource = maybeInputSource else { - NSLog("Fatal error: Cannot find input source \(bundleID) after registration.") - return -1 - } + maybeInputSource = InputSourceHelper.inputSource(for: bundleID) + } - if !InputSourceHelper.inputSourceEnabled(for: inputSource) { - NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).") - let status = InputSourceHelper.enable(inputSource: inputSource) - if !status { - NSLog("Fatal error: Cannot enable input source \(bundleID).") - return -1 - } - if !InputSourceHelper.inputSourceEnabled(for: inputSource) { - NSLog("Fatal error: Cannot enable input source \(bundleID).") - return -1 - } - } + guard let inputSource = maybeInputSource else { + NSLog("Fatal error: Cannot find input source \(bundleID) after registration.") + return -1 + } - if CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "--all" { - let enabled = InputSourceHelper.enableAllInputMode(for: bundleID) - NSLog(enabled ? "All input sources enabled for \(bundleID)" : "Cannot enable all input sources for \(bundleID), but this is ignored") - } - return 0 - } + if !InputSourceHelper.inputSourceEnabled(for: inputSource) { + NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).") + let status = InputSourceHelper.enable(inputSource: inputSource) + if !status { + NSLog("Fatal error: Cannot enable input source \(bundleID).") + return -1 + } + if !InputSourceHelper.inputSourceEnabled(for: inputSource) { + NSLog("Fatal error: Cannot enable input source \(bundleID).") + return -1 + } + } + if CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "--all" { + let enabled = InputSourceHelper.enableAllInputMode(for: bundleID) + NSLog( + enabled + ? "All input sources enabled for \(bundleID)" + : "Cannot enable all input sources for \(bundleID), but this is ignored") + } + return 0 + } } diff --git a/Source/Modules/IMEModules/InputSourceHelper.swift b/Source/Modules/IMEModules/InputSourceHelper.swift index 37429f8a..f045d4cf 100644 --- a/Source/Modules/IMEModules/InputSourceHelper.swift +++ b/Source/Modules/IMEModules/InputSourceHelper.swift @@ -1,127 +1,140 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 import Carbon +import Cocoa public class InputSourceHelper: NSObject { - @available(*, unavailable) - public override init() { - super.init() - } + @available(*, unavailable) + public override init() { + super.init() + } - public static func allInstalledInputSources() -> [TISInputSource] { - TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] - } + public static func allInstalledInputSources() -> [TISInputSource] { + TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] + } - @objc(inputSourceForProperty:stringValue:) - public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? { - let stringID = CFStringGetTypeID() - for source in allInstalledInputSources() { - if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) { - let property = Unmanaged.fromOpaque(propertyPtr).takeUnretainedValue() - let typeID = CFGetTypeID(property) - if typeID != stringID { - continue - } - if stringValue == property as? String { - return source - } - } - } - return nil - } + @objc(inputSourceForProperty:stringValue:) + public static func inputSource(for propertyKey: CFString, stringValue: String) + -> TISInputSource? + { + let stringID = CFStringGetTypeID() + for source in allInstalledInputSources() { + if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) { + let property = Unmanaged.fromOpaque(propertyPtr).takeUnretainedValue() + let typeID = CFGetTypeID(property) + if typeID != stringID { + continue + } + if stringValue == property as? String { + return source + } + } + } + return nil + } - @objc(inputSourceForInputSourceID:) - public static func inputSource(for sourceID: String) -> TISInputSource? { - inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) - } + @objc(inputSourceForInputSourceID:) + public static func inputSource(for sourceID: String) -> TISInputSource? { + inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) + } - @objc(inputSourceEnabled:) - public static func inputSourceEnabled(for source: TISInputSource) -> Bool { - if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { - let value = Unmanaged.fromOpaque(valuePts).takeUnretainedValue() - return value == kCFBooleanTrue - } - return false - } + @objc(inputSourceEnabled:) + public static func inputSourceEnabled(for source: TISInputSource) -> Bool { + if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { + let value = Unmanaged.fromOpaque(valuePts).takeUnretainedValue() + return value == kCFBooleanTrue + } + return false + } - @objc(enableInputSource:) - public static func enable(inputSource: TISInputSource) -> Bool { - let status = TISEnableInputSource(inputSource) - return status == noErr - } + @objc(enableInputSource:) + public static func enable(inputSource: TISInputSource) -> Bool { + let status = TISEnableInputSource(inputSource) + return status == noErr + } - @objc(enableAllInputModesForInputSourceBundleID:) - public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { - var enabled = false - for source in allInstalledInputSources() { - guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), - let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { - continue - } - let bundleID = Unmanaged.fromOpaque(bundleIDPtr).takeUnretainedValue() - if String(bundleID) == inputSourceBundleD { - let modeEnabled = self.enable(inputSource: source) - if !modeEnabled { - return false - } - enabled = true - } - } + @objc(enableAllInputModesForInputSourceBundleID:) + public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { + var enabled = false + for source in allInstalledInputSources() { + guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), + let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID) + else { + continue + } + let bundleID = Unmanaged.fromOpaque(bundleIDPtr).takeUnretainedValue() + if String(bundleID) == inputSourceBundleD { + let modeEnabled = self.enable(inputSource: source) + if !modeEnabled { + return false + } + enabled = true + } + } - return enabled - } + return enabled + } - @objc(enableInputMode:forInputSourceBundleID:) - public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { - for source in allInstalledInputSources() { - guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), - let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { - continue - } - let inputsSourceBundleID = Unmanaged.fromOpaque(bundleIDPtr).takeUnretainedValue() - let inputsSourceModeID = Unmanaged.fromOpaque(modePtr).takeUnretainedValue() - if modeID == String(inputsSourceModeID) && bundleID == String(inputsSourceBundleID) { - let enabled = enable(inputSource: source) - print("Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)") - return enabled - } + @objc(enableInputMode:forInputSourceBundleID:) + public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { + for source in allInstalledInputSources() { + guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), + let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) + else { + continue + } + let inputsSourceBundleID = Unmanaged.fromOpaque(bundleIDPtr) + .takeUnretainedValue() + let inputsSourceModeID = Unmanaged.fromOpaque(modePtr).takeUnretainedValue() + if modeID == String(inputsSourceModeID) && bundleID == String(inputsSourceBundleID) { + let enabled = enable(inputSource: source) + print( + "Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)" + ) + return enabled + } - } - print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)") - return false + } + print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)") + return false - } + } - @objc(disableInputSource:) - public static func disable(inputSource: TISInputSource) -> Bool { - let status = TISDisableInputSource(inputSource) - return status == noErr - } + @objc(disableInputSource:) + public static func disable(inputSource: TISInputSource) -> Bool { + let status = TISDisableInputSource(inputSource) + return status == noErr + } - @objc(registerInputSource:) - public static func registerTnputSource(at url: URL) -> Bool { - let status = TISRegisterInputSource(url as CFURL) - return status == noErr - } + @objc(registerInputSource:) + public static func registerTnputSource(at url: URL) -> Bool { + let status = TISRegisterInputSource(url as CFURL) + return status == noErr + } } - diff --git a/Source/Modules/IMEModules/ctlInputMethod.swift b/Source/Modules/IMEModules/ctlInputMethod.swift index 9500a800..4bfc6675 100644 --- a/Source/Modules/IMEModules/ctlInputMethod.swift +++ b/Source/Modules/IMEModules/ctlInputMethod.swift @@ -1,344 +1,444 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 import InputMethodKit -private extension Bool { - var state: NSControl.StateValue { - self ? .on : .off - } +extension Bool { + fileprivate var state: NSControl.StateValue { + self ? .on : .off + } } private let kMinKeyLabelSize: CGFloat = 10 private var gCurrentCandidateController: CandidateController? -private extension CandidateController { - static let horizontal = HorizontalCandidateController() - static let vertical = VerticalCandidateController() +extension CandidateController { + fileprivate static let horizontal = HorizontalCandidateController() + fileprivate static let vertical = VerticalCandidateController() } @objc(ctlInputMethod) class ctlInputMethod: IMKInputController { - @objc static let kIMEModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS"; - @objc static let kIMEModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT"; - @objc static let kIMEModeNULL = "org.atelierInmu.inputmethod.vChewing.IMENULL"; - - @objc static var areWeDeleting = false; - - private static let tooltipController = TooltipController() - - // MARK: - - - private var currentCandidateClient: Any? - - private var keyHandler: KeyHandler = KeyHandler() - private var state: InputState = InputState.Empty() - - // 想讓 keyHandler 能夠被外界調查狀態與參數的話,就得對 keyHandler 做常態處理。 - // 這樣 InputState 可以藉由這個 ctlInputMethod 了解到當前的輸入模式是簡體中文還是繁體中文。 - // 然而,要是直接對 keyHandler 做常態處理的話,反而會導致 keyParser 無法協同處理。 - // 所以才需要「currentKeyHandler」這個假 keyHandler。 - // 這個「currentKeyHandler」僅用來讓其他模組知道當前的輸入模式是什麼模式,除此之外別無屌用。 - static var currentKeyHandler: KeyHandler = KeyHandler() - @objc static var currentInputMode = "" - - // MARK: - Keyboard Layout Specifier - - @objc func setKeyLayout() { - let client = client().self as IMKTextInput - client.overrideKeyboard(withKeyboardNamed: mgrPrefs.basisKeyboardLayout) - } - - // MARK: - IMKInputController methods - - override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { - super.init(server: server, delegate: delegate, client: inputClient) - keyHandler.delegate = self - } - - override func menu() -> NSMenu! { - let optionKeyPressed = NSEvent.modifierFlags.contains(.option) - - let menu = NSMenu(title: "Input Method Menu") - - let useSCPCTypingModeItem = menu.addItem(withTitle: NSLocalizedString("Per-Char Select Mode", comment: ""), action: #selector(toggleSCPCTypingMode(_:)), keyEquivalent: "P") - useSCPCTypingModeItem.keyEquivalentModifierMask = [.command, .control] - useSCPCTypingModeItem.state = mgrPrefs.useSCPCTypingMode.state - - let useCNS11643SupportItem = menu.addItem(withTitle: NSLocalizedString("CNS11643 Mode", comment: ""), action: #selector(toggleCNS11643Enabled(_:)), keyEquivalent: "L") - useCNS11643SupportItem.keyEquivalentModifierMask = [.command, .control] - useCNS11643SupportItem.state = mgrPrefs.cns11643Enabled.state - - if keyHandler.inputMode == InputMode.imeModeCHT { - let chineseConversionItem = menu.addItem(withTitle: NSLocalizedString("Force KangXi Writing", comment: ""), action: #selector(toggleChineseConverter(_:)), keyEquivalent: "K") - chineseConversionItem.keyEquivalentModifierMask = [.command, .control] - chineseConversionItem.state = mgrPrefs.chineseConversionEnabled.state - - let shiftJISConversionItem = menu.addItem(withTitle: NSLocalizedString("JIS Shinjitai Output", comment: ""), action: #selector(toggleShiftJISShinjitaiOutput(_:)), keyEquivalent: "J") - shiftJISConversionItem.keyEquivalentModifierMask = [.command, .control] - shiftJISConversionItem.state = mgrPrefs.shiftJISShinjitaiOutputEnabled.state - } - - let halfWidthPunctuationItem = menu.addItem(withTitle: NSLocalizedString("Half-Width Punctuation Mode", comment: ""), action: #selector(toggleHalfWidthPunctuation(_:)), keyEquivalent: "H") - halfWidthPunctuationItem.keyEquivalentModifierMask = [.command, .control] - halfWidthPunctuationItem.state = mgrPrefs.halfWidthPunctuationEnabled.state - - let userAssociatedPhrasesItem = menu.addItem(withTitle: NSLocalizedString("Per-Char Associated Phrases", comment: ""), action: #selector(toggleAssociatedPhrasesEnabled(_:)), keyEquivalent: "O") - userAssociatedPhrasesItem.keyEquivalentModifierMask = [.command, .control] - userAssociatedPhrasesItem.state = mgrPrefs.associatedPhrasesEnabled.state - - if optionKeyPressed { - let phaseReplacementItem = menu.addItem(withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""), action: #selector(togglePhraseReplacement(_:)), keyEquivalent: "") - phaseReplacementItem.state = mgrPrefs.phraseReplacementEnabled.state - - let toggleSymbolInputItem = menu.addItem(withTitle: NSLocalizedString("Symbol & Emoji Input", comment: ""), action: #selector(toggleSymbolEnabled(_:)), keyEquivalent: "") - toggleSymbolInputItem.state = mgrPrefs.symbolInputEnabled.state - } - - menu.addItem(NSMenuItem.separator()) // --------------------- - - menu.addItem(withTitle: NSLocalizedString("Open User Data Folder", comment: ""), action: #selector(openUserDataFolder(_:)), keyEquivalent: "") - menu.addItem(withTitle: NSLocalizedString("Edit User Phrases…", comment: ""), action: #selector(openUserPhrases(_:)), keyEquivalent: "") - - if optionKeyPressed { - menu.addItem(withTitle: NSLocalizedString("Edit Excluded Phrases…", comment: ""), action: #selector(openExcludedPhrases(_:)), keyEquivalent: "") - menu.addItem(withTitle: NSLocalizedString("Edit Phrase Replacement Table…", comment: ""), action: #selector(openPhraseReplacement(_:)), keyEquivalent: "") - menu.addItem(withTitle: NSLocalizedString("Edit Associated Phrases…", comment: ""), action: #selector(openAssociatedPhrases(_:)), keyEquivalent: "") - menu.addItem(withTitle: NSLocalizedString("Edit User Symbol & Emoji Data…", comment: ""), action: #selector(openUserSymbols(_:)), keyEquivalent: "") - } - - if (optionKeyPressed || !mgrPrefs.shouldAutoReloadUserDataFiles) { - menu.addItem(withTitle: NSLocalizedString("Reload User Phrases", comment: ""), action: #selector(reloadUserPhrases(_:)), keyEquivalent: "") - } - - menu.addItem(NSMenuItem.separator()) // --------------------- - - menu.addItem(withTitle: NSLocalizedString("vChewing Preferences…", comment: ""), action: #selector(showPreferences(_:)), keyEquivalent: "") - if !optionKeyPressed { - menu.addItem(withTitle: NSLocalizedString("Check for Updates…", comment: ""), action: #selector(checkForUpdate(_:)), keyEquivalent: "") - } - menu.addItem(withTitle: NSLocalizedString("Reboot vChewing…", comment: ""), action: #selector(selfTerminate(_:)), keyEquivalent: "") - menu.addItem(withTitle: NSLocalizedString("About vChewing…", comment: ""), action: #selector(showAbout(_:)), keyEquivalent: "") - if optionKeyPressed { - menu.addItem(withTitle: NSLocalizedString("Uninstall vChewing…", comment: ""), action: #selector(selfUninstall(_:)), keyEquivalent: "") - } - - // NSMenu 會阻止任何 modified key 相關的訊號傳回輸入法,所以咱們在此重設鍵盤佈局 - setKeyLayout() - return menu - } - - // MARK: - IMKStateSetting protocol methods - - override func activateServer(_ client: Any!) { - UserDefaults.standard.synchronize() - - // Override the keyboard layout to the basic one. - setKeyLayout() - // reset the state - currentCandidateClient = nil - - keyHandler.clear() - keyHandler.syncWithPreferences() - self.handle(state: .Empty(), client: client) - (NSApp.delegate as? AppDelegate)?.checkForUpdate() - } - - override func deactivateServer(_ client: Any!) { - keyHandler.clear() - self.handle(state: .Empty(), client: client) - self.handle(state: .Deactivated(), client: client) - } - - override func setValue(_ value: Any!, forTag tag: Int, client: Any!) { - var newInputMode = InputMode(rawValue: value as? String ?? InputMode.imeModeNULL.rawValue) - switch newInputMode { - case InputMode.imeModeCHS: - newInputMode = InputMode.imeModeCHS - case InputMode.imeModeCHT: - newInputMode = InputMode.imeModeCHT - default: - newInputMode = InputMode.imeModeNULL - } - mgrLangModel.loadDataModel(newInputMode) - - // Remember to override the keyboard layout again -- treat this as an activate event. - setKeyLayout() - - if keyHandler.inputMode != newInputMode { - UserDefaults.standard.synchronize() - keyHandler.clear() - keyHandler.inputMode = newInputMode - self.handle(state: .Empty(), client: client) - } - - // 讓外界知道目前的簡繁體輸入模式。 - ctlInputMethod.currentKeyHandler.inputMode = keyHandler.inputMode - } - - // MARK: - IMKServerInput protocol methods - - override func recognizedEvents(_ sender: Any!) -> Int { - let events: NSEvent.EventTypeMask = [.keyDown, .flagsChanged] - return Int(events.rawValue) - } - - override func handle(_ event: NSEvent!, client: Any!) -> Bool { - - // 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。 - // 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。 - // 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false, - // 否則,每次處理這種判斷時都會觸發 NSInternalInconsistencyException。 - if event.type == .flagsChanged { - return false - } - - // 準備修飾鍵,用來判定是否需要利用就地新增語彙時的 Enter 鍵來砍詞。 - ctlInputMethod.areWeDeleting = event.modifierFlags.contains([.shift, .command]) - - var textFrame = NSRect.zero - let attributes: [AnyHashable: Any]? = (client as? IMKTextInput)?.attributes(forCharacterIndex: 0, lineHeightRectangle: &textFrame) - let useVerticalMode = (attributes?["IMKTextOrientation"] as? NSNumber)?.intValue == 0 || false - - if (client as? IMKTextInput)?.bundleIdentifier() == "org.atelierInmu.vChewing.vChewingPhraseEditor" { - ctlInputMethod.areWeUsingOurOwnPhraseEditor = true - } else { - ctlInputMethod.areWeUsingOurOwnPhraseEditor = false - } - - let input = keyParser(event: event, isVerticalMode: useVerticalMode) - - let result = keyHandler.handle(input: input, state: state) { newState in - self.handle(state: newState, client: client) - } errorCallback: { - clsSFX.beep() - } - return result - } - - // MARK: - Menu Items - - @objc override func showPreferences(_ sender: Any?) { - (NSApp.delegate as? AppDelegate)?.showPreferences() - NSApp.activate(ignoringOtherApps: true) - } - - @objc func toggleSCPCTypingMode(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("Per-Char Select Mode", comment: ""), "\n", mgrPrefs.toggleSCPCTypingModeEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func toggleChineseConverter(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("Force KangXi Writing", comment: ""), "\n", mgrPrefs.toggleChineseConversionEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func toggleShiftJISShinjitaiOutput(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("JIS Shinjitai Output", comment: ""), "\n", mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func toggleHalfWidthPunctuation(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("Half-Width Punctuation Mode", comment: ""), "\n", mgrPrefs.toggleHalfWidthPunctuationEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func toggleCNS11643Enabled(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("CNS11643 Mode", comment: ""), "\n", mgrPrefs.toggleCNS11643Enabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func toggleSymbolEnabled(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("Symbol & Emoji Input", comment: ""), "\n", mgrPrefs.toggleSymbolInputEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func toggleAssociatedPhrasesEnabled(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("Per-Char Associated Phrases", comment: ""), "\n", mgrPrefs.toggleAssociatedPhrasesEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func togglePhraseReplacement(_ sender: Any?) { - NotifierController.notify(message: String(format: "%@%@%@", NSLocalizedString("Use Phrase Replacement", comment: ""), "\n", mgrPrefs.togglePhraseReplacementEnabled() ? NSLocalizedString("NotificationSwitchON", comment: "") : NSLocalizedString("NotificationSwitchOFF", comment: ""))) - } - - @objc func selfUninstall(_ sender: Any?) { - (NSApp.delegate as? AppDelegate)?.selfUninstall() - } - - @objc func selfTerminate(_ sender: Any?) { - NSApp.terminate(nil) - } - - @objc func checkForUpdate(_ sender: Any?) { - (NSApp.delegate as? AppDelegate)?.checkForUpdate(forced: true) - } - - private func open(userFileAt path: String) { - func checkIfUserFilesExist() -> Bool { - if !mgrLangModel.checkIfUserLanguageModelFilesExist() { - let content = String(format: NSLocalizedString("Please check the permission at \"%@\".", comment: ""), mgrLangModel.dataFolderPath(isDefaultFolder: false)) - ctlNonModalAlertWindow.shared.show(title: NSLocalizedString("Unable to create the user phrase file.", comment: ""), content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""), cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) - NSApp.setActivationPolicy(.accessory) - return false - } - return true - } - - if !checkIfUserFilesExist() { - return - } - NSWorkspace.shared.openFile(path, withApplication: "vChewingPhraseEditor") - } - - @objc func openUserPhrases(_ sender: Any?) { - open(userFileAt: mgrLangModel.userPhrasesDataPath(keyHandler.inputMode)) - } - - @objc func openUserDataFolder(_ sender: Any?) { - if !mgrLangModel.checkIfUserDataFolderExists() { - return - } - NSWorkspace.shared.openFile(mgrLangModel.dataFolderPath(isDefaultFolder: false), withApplication: "Finder") - } - - @objc func openExcludedPhrases(_ sender: Any?) { - open(userFileAt: mgrLangModel.excludedPhrasesDataPath(keyHandler.inputMode)) - } - - @objc func openUserSymbols(_ sender: Any?) { - open(userFileAt: mgrLangModel.userSymbolDataPath(keyHandler.inputMode)) - } - - @objc func openPhraseReplacement(_ sender: Any?) { - open(userFileAt: mgrLangModel.phraseReplacementDataPath(keyHandler.inputMode)) - } - - @objc func openAssociatedPhrases(_ sender: Any?) { - open(userFileAt: mgrLangModel.userAssociatedPhrasesDataPath(keyHandler.inputMode)) - } - - @objc func reloadUserPhrases(_ sender: Any?) { - mgrLangModel.loadUserPhrases() - mgrLangModel.loadUserPhraseReplacement() - } - - @objc func showAbout(_ sender: Any?) { - (NSApp.delegate as? AppDelegate)?.showAbout() - NSApp.activate(ignoringOtherApps: true) - } + @objc static let kIMEModeCHS = "org.atelierInmu.inputmethod.vChewing.IMECHS" + @objc static let kIMEModeCHT = "org.atelierInmu.inputmethod.vChewing.IMECHT" + @objc static let kIMEModeNULL = "org.atelierInmu.inputmethod.vChewing.IMENULL" + + @objc static var areWeDeleting = false + + private static let tooltipController = TooltipController() + + // MARK: - + + private var currentCandidateClient: Any? + + private var keyHandler: KeyHandler = KeyHandler() + private var state: InputState = InputState.Empty() + + // 想讓 keyHandler 能夠被外界調查狀態與參數的話,就得對 keyHandler 做常態處理。 + // 這樣 InputState 可以藉由這個 ctlInputMethod 了解到當前的輸入模式是簡體中文還是繁體中文。 + // 然而,要是直接對 keyHandler 做常態處理的話,反而會導致 keyParser 無法協同處理。 + // 所以才需要「currentKeyHandler」這個假 keyHandler。 + // 這個「currentKeyHandler」僅用來讓其他模組知道當前的輸入模式是什麼模式,除此之外別無屌用。 + static var currentKeyHandler: KeyHandler = KeyHandler() + @objc static var currentInputMode = "" + + // MARK: - Keyboard Layout Specifier + + @objc func setKeyLayout() { + let client = client().self as IMKTextInput + client.overrideKeyboard(withKeyboardNamed: mgrPrefs.basisKeyboardLayout) + } + + // MARK: - IMKInputController methods + + override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { + super.init(server: server, delegate: delegate, client: inputClient) + keyHandler.delegate = self + } + + override func menu() -> NSMenu! { + let optionKeyPressed = NSEvent.modifierFlags.contains(.option) + + let menu = NSMenu(title: "Input Method Menu") + + let useSCPCTypingModeItem = menu.addItem( + withTitle: NSLocalizedString("Per-Char Select Mode", comment: ""), + action: #selector(toggleSCPCTypingMode(_:)), keyEquivalent: "P") + useSCPCTypingModeItem.keyEquivalentModifierMask = [.command, .control] + useSCPCTypingModeItem.state = mgrPrefs.useSCPCTypingMode.state + + let useCNS11643SupportItem = menu.addItem( + withTitle: NSLocalizedString("CNS11643 Mode", comment: ""), + action: #selector(toggleCNS11643Enabled(_:)), keyEquivalent: "L") + useCNS11643SupportItem.keyEquivalentModifierMask = [.command, .control] + useCNS11643SupportItem.state = mgrPrefs.cns11643Enabled.state + + if keyHandler.inputMode == InputMode.imeModeCHT { + let chineseConversionItem = menu.addItem( + withTitle: NSLocalizedString("Force KangXi Writing", comment: ""), + action: #selector(toggleChineseConverter(_:)), keyEquivalent: "K") + chineseConversionItem.keyEquivalentModifierMask = [.command, .control] + chineseConversionItem.state = mgrPrefs.chineseConversionEnabled.state + + let shiftJISConversionItem = menu.addItem( + withTitle: NSLocalizedString("JIS Shinjitai Output", comment: ""), + action: #selector(toggleShiftJISShinjitaiOutput(_:)), keyEquivalent: "J") + shiftJISConversionItem.keyEquivalentModifierMask = [.command, .control] + shiftJISConversionItem.state = mgrPrefs.shiftJISShinjitaiOutputEnabled.state + } + + let halfWidthPunctuationItem = menu.addItem( + withTitle: NSLocalizedString("Half-Width Punctuation Mode", comment: ""), + action: #selector(toggleHalfWidthPunctuation(_:)), keyEquivalent: "H") + halfWidthPunctuationItem.keyEquivalentModifierMask = [.command, .control] + halfWidthPunctuationItem.state = mgrPrefs.halfWidthPunctuationEnabled.state + + let userAssociatedPhrasesItem = menu.addItem( + withTitle: NSLocalizedString("Per-Char Associated Phrases", comment: ""), + action: #selector(toggleAssociatedPhrasesEnabled(_:)), keyEquivalent: "O") + userAssociatedPhrasesItem.keyEquivalentModifierMask = [.command, .control] + userAssociatedPhrasesItem.state = mgrPrefs.associatedPhrasesEnabled.state + + if optionKeyPressed { + let phaseReplacementItem = menu.addItem( + withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""), + action: #selector(togglePhraseReplacement(_:)), keyEquivalent: "") + phaseReplacementItem.state = mgrPrefs.phraseReplacementEnabled.state + + let toggleSymbolInputItem = menu.addItem( + withTitle: NSLocalizedString("Symbol & Emoji Input", comment: ""), + action: #selector(toggleSymbolEnabled(_:)), keyEquivalent: "") + toggleSymbolInputItem.state = mgrPrefs.symbolInputEnabled.state + } + + menu.addItem(NSMenuItem.separator()) // --------------------- + + menu.addItem( + withTitle: NSLocalizedString("Open User Data Folder", comment: ""), + action: #selector(openUserDataFolder(_:)), keyEquivalent: "") + menu.addItem( + withTitle: NSLocalizedString("Edit User Phrases…", comment: ""), + action: #selector(openUserPhrases(_:)), keyEquivalent: "") + + if optionKeyPressed { + menu.addItem( + withTitle: NSLocalizedString("Edit Excluded Phrases…", comment: ""), + action: #selector(openExcludedPhrases(_:)), keyEquivalent: "") + menu.addItem( + withTitle: NSLocalizedString("Edit Phrase Replacement Table…", comment: ""), + action: #selector(openPhraseReplacement(_:)), keyEquivalent: "") + menu.addItem( + withTitle: NSLocalizedString("Edit Associated Phrases…", comment: ""), + action: #selector(openAssociatedPhrases(_:)), keyEquivalent: "") + menu.addItem( + withTitle: NSLocalizedString("Edit User Symbol & Emoji Data…", comment: ""), + action: #selector(openUserSymbols(_:)), keyEquivalent: "") + } + + if optionKeyPressed || !mgrPrefs.shouldAutoReloadUserDataFiles { + menu.addItem( + withTitle: NSLocalizedString("Reload User Phrases", comment: ""), + action: #selector(reloadUserPhrases(_:)), keyEquivalent: "") + } + + menu.addItem(NSMenuItem.separator()) // --------------------- + + menu.addItem( + withTitle: NSLocalizedString("vChewing Preferences…", comment: ""), + action: #selector(showPreferences(_:)), keyEquivalent: "") + if !optionKeyPressed { + menu.addItem( + withTitle: NSLocalizedString("Check for Updates…", comment: ""), + action: #selector(checkForUpdate(_:)), keyEquivalent: "") + } + menu.addItem( + withTitle: NSLocalizedString("Reboot vChewing…", comment: ""), + action: #selector(selfTerminate(_:)), keyEquivalent: "") + menu.addItem( + withTitle: NSLocalizedString("About vChewing…", comment: ""), + action: #selector(showAbout(_:)), keyEquivalent: "") + if optionKeyPressed { + menu.addItem( + withTitle: NSLocalizedString("Uninstall vChewing…", comment: ""), + action: #selector(selfUninstall(_:)), keyEquivalent: "") + } + + // NSMenu 會阻止任何 modified key 相關的訊號傳回輸入法,所以咱們在此重設鍵盤佈局 + setKeyLayout() + return menu + } + + // MARK: - IMKStateSetting protocol methods + + override func activateServer(_ client: Any!) { + UserDefaults.standard.synchronize() + + // Override the keyboard layout to the basic one. + setKeyLayout() + // reset the state + currentCandidateClient = nil + + keyHandler.clear() + keyHandler.syncWithPreferences() + self.handle(state: .Empty(), client: client) + (NSApp.delegate as? AppDelegate)?.checkForUpdate() + } + + override func deactivateServer(_ client: Any!) { + keyHandler.clear() + self.handle(state: .Empty(), client: client) + self.handle(state: .Deactivated(), client: client) + } + + override func setValue(_ value: Any!, forTag tag: Int, client: Any!) { + var newInputMode = InputMode(rawValue: value as? String ?? InputMode.imeModeNULL.rawValue) + switch newInputMode { + case InputMode.imeModeCHS: + newInputMode = InputMode.imeModeCHS + case InputMode.imeModeCHT: + newInputMode = InputMode.imeModeCHT + default: + newInputMode = InputMode.imeModeNULL + } + mgrLangModel.loadDataModel(newInputMode) + + // Remember to override the keyboard layout again -- treat this as an activate event. + setKeyLayout() + + if keyHandler.inputMode != newInputMode { + UserDefaults.standard.synchronize() + keyHandler.clear() + keyHandler.inputMode = newInputMode + self.handle(state: .Empty(), client: client) + } + + // 讓外界知道目前的簡繁體輸入模式。 + ctlInputMethod.currentKeyHandler.inputMode = keyHandler.inputMode + } + + // MARK: - IMKServerInput protocol methods + + override func recognizedEvents(_ sender: Any!) -> Int { + let events: NSEvent.EventTypeMask = [.keyDown, .flagsChanged] + return Int(events.rawValue) + } + + override func handle(_ event: NSEvent!, client: Any!) -> Bool { + + // 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。 + // 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。 + // 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false, + // 否則,每次處理這種判斷時都會觸發 NSInternalInconsistencyException。 + if event.type == .flagsChanged { + return false + } + + // 準備修飾鍵,用來判定是否需要利用就地新增語彙時的 Enter 鍵來砍詞。 + ctlInputMethod.areWeDeleting = event.modifierFlags.contains([.shift, .command]) + + var textFrame = NSRect.zero + let attributes: [AnyHashable: Any]? = (client as? IMKTextInput)?.attributes( + forCharacterIndex: 0, lineHeightRectangle: &textFrame) + let useVerticalMode = + (attributes?["IMKTextOrientation"] as? NSNumber)?.intValue == 0 || false + + if (client as? IMKTextInput)?.bundleIdentifier() + == "org.atelierInmu.vChewing.vChewingPhraseEditor" + { + ctlInputMethod.areWeUsingOurOwnPhraseEditor = true + } else { + ctlInputMethod.areWeUsingOurOwnPhraseEditor = false + } + + let input = keyParser(event: event, isVerticalMode: useVerticalMode) + + let result = keyHandler.handle(input: input, state: state) { newState in + self.handle(state: newState, client: client) + } errorCallback: { + clsSFX.beep() + } + return result + } + + // MARK: - Menu Items + + @objc override func showPreferences(_ sender: Any?) { + (NSApp.delegate as? AppDelegate)?.showPreferences() + NSApp.activate(ignoringOtherApps: true) + } + + @objc func toggleSCPCTypingMode(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("Per-Char Select Mode", comment: ""), "\n", + mgrPrefs.toggleSCPCTypingModeEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func toggleChineseConverter(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("Force KangXi Writing", comment: ""), "\n", + mgrPrefs.toggleChineseConversionEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func toggleShiftJISShinjitaiOutput(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("JIS Shinjitai Output", comment: ""), "\n", + mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func toggleHalfWidthPunctuation(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("Half-Width Punctuation Mode", comment: ""), + "\n", + mgrPrefs.toggleHalfWidthPunctuationEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func toggleCNS11643Enabled(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("CNS11643 Mode", comment: ""), "\n", + mgrPrefs.toggleCNS11643Enabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func toggleSymbolEnabled(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("Symbol & Emoji Input", comment: ""), "\n", + mgrPrefs.toggleSymbolInputEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func toggleAssociatedPhrasesEnabled(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("Per-Char Associated Phrases", comment: ""), + "\n", + mgrPrefs.toggleAssociatedPhrasesEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func togglePhraseReplacement(_ sender: Any?) { + NotifierController.notify( + message: String( + format: "%@%@%@", NSLocalizedString("Use Phrase Replacement", comment: ""), "\n", + mgrPrefs.togglePhraseReplacementEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: ""))) + } + + @objc func selfUninstall(_ sender: Any?) { + (NSApp.delegate as? AppDelegate)?.selfUninstall() + } + + @objc func selfTerminate(_ sender: Any?) { + NSApp.terminate(nil) + } + + @objc func checkForUpdate(_ sender: Any?) { + (NSApp.delegate as? AppDelegate)?.checkForUpdate(forced: true) + } + + private func open(userFileAt path: String) { + func checkIfUserFilesExist() -> Bool { + if !mgrLangModel.checkIfUserLanguageModelFilesExist() { + let content = String( + format: NSLocalizedString( + "Please check the permission at \"%@\".", comment: ""), + mgrLangModel.dataFolderPath(isDefaultFolder: false)) + ctlNonModalAlertWindow.shared.show( + title: NSLocalizedString("Unable to create the user phrase file.", comment: ""), + content: content, confirmButtonTitle: NSLocalizedString("OK", comment: ""), + cancelButtonTitle: nil, cancelAsDefault: false, delegate: nil) + NSApp.setActivationPolicy(.accessory) + return false + } + return true + } + + if !checkIfUserFilesExist() { + return + } + NSWorkspace.shared.openFile(path, withApplication: "vChewingPhraseEditor") + } + + @objc func openUserPhrases(_ sender: Any?) { + open(userFileAt: mgrLangModel.userPhrasesDataPath(keyHandler.inputMode)) + } + + @objc func openUserDataFolder(_ sender: Any?) { + if !mgrLangModel.checkIfUserDataFolderExists() { + return + } + NSWorkspace.shared.openFile( + mgrLangModel.dataFolderPath(isDefaultFolder: false), withApplication: "Finder") + } + + @objc func openExcludedPhrases(_ sender: Any?) { + open(userFileAt: mgrLangModel.excludedPhrasesDataPath(keyHandler.inputMode)) + } + + @objc func openUserSymbols(_ sender: Any?) { + open(userFileAt: mgrLangModel.userSymbolDataPath(keyHandler.inputMode)) + } + + @objc func openPhraseReplacement(_ sender: Any?) { + open(userFileAt: mgrLangModel.phraseReplacementDataPath(keyHandler.inputMode)) + } + + @objc func openAssociatedPhrases(_ sender: Any?) { + open(userFileAt: mgrLangModel.userAssociatedPhrasesDataPath(keyHandler.inputMode)) + } + + @objc func reloadUserPhrases(_ sender: Any?) { + mgrLangModel.loadUserPhrases() + mgrLangModel.loadUserPhraseReplacement() + } + + @objc func showAbout(_ sender: Any?) { + (NSApp.delegate as? AppDelegate)?.showAbout() + NSApp.activate(ignoringOtherApps: true) + } } @@ -346,391 +446,454 @@ class ctlInputMethod: IMKInputController { extension ctlInputMethod { - private func handle(state newState: InputState, client: Any?) { - let previous = state - state = newState + private func handle(state newState: InputState, client: Any?) { + let previous = state + state = newState - if let newState = newState as? InputState.Deactivated { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Empty { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.EmptyIgnoringPreviousState { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Committing { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Inputting { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.Marking { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.ChoosingCandidate { - handle(state: newState, previous: previous, client: client) - } else if let newState = newState as? InputState.AssociatedPhrases { - handle(state: newState, previous: previous, client: client) - } - } + if let newState = newState as? InputState.Deactivated { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.Empty { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.EmptyIgnoringPreviousState { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.Committing { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.Inputting { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.Marking { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.ChoosingCandidate { + handle(state: newState, previous: previous, client: client) + } else if let newState = newState as? InputState.AssociatedPhrases { + handle(state: newState, previous: previous, client: client) + } + } - private func commit(text: String, client: Any!) { + private func commit(text: String, client: Any!) { - func kanjiConversionIfRequired(_ text: String) -> String { - if keyHandler.inputMode == InputMode.imeModeCHT { - if !mgrPrefs.chineseConversionEnabled && mgrPrefs.shiftJISShinjitaiOutputEnabled { - return vChewingKanjiConverter.cnvTradToJIS(text) - } - if mgrPrefs.chineseConversionEnabled && !mgrPrefs.shiftJISShinjitaiOutputEnabled { - return vChewingKanjiConverter.cnvTradToKangXi(text) - } - // 本來這兩個開關不該同時開啟的,但萬一被開啟了的話就這樣處理: - if mgrPrefs.chineseConversionEnabled && mgrPrefs.shiftJISShinjitaiOutputEnabled { - return vChewingKanjiConverter.cnvTradToJIS(text) - } - // if (!mgrPrefs.chineseConversionEnabled && !mgrPrefs.shiftJISShinjitaiOutputEnabled) || (keyHandler.inputMode != InputMode.imeModeCHT); - return text - } - return text - } + func kanjiConversionIfRequired(_ text: String) -> String { + if keyHandler.inputMode == InputMode.imeModeCHT { + if !mgrPrefs.chineseConversionEnabled && mgrPrefs.shiftJISShinjitaiOutputEnabled { + return vChewingKanjiConverter.cnvTradToJIS(text) + } + if mgrPrefs.chineseConversionEnabled && !mgrPrefs.shiftJISShinjitaiOutputEnabled { + return vChewingKanjiConverter.cnvTradToKangXi(text) + } + // 本來這兩個開關不該同時開啟的,但萬一被開啟了的話就這樣處理: + if mgrPrefs.chineseConversionEnabled && mgrPrefs.shiftJISShinjitaiOutputEnabled { + return vChewingKanjiConverter.cnvTradToJIS(text) + } + // if (!mgrPrefs.chineseConversionEnabled && !mgrPrefs.shiftJISShinjitaiOutputEnabled) || (keyHandler.inputMode != InputMode.imeModeCHT); + return text + } + return text + } - let buffer = kanjiConversionIfRequired(text) - if buffer.isEmpty { - return - } - (client as? IMKTextInput)?.insertText(buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) - } + let buffer = kanjiConversionIfRequired(text) + if buffer.isEmpty { + return + } + (client as? IMKTextInput)?.insertText( + buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) + } - private func handle(state: InputState.Deactivated, previous: InputState, client: Any?) { - currentCandidateClient = nil + private func handle(state: InputState.Deactivated, previous: InputState, client: Any?) { + currentCandidateClient = nil - gCurrentCandidateController?.delegate = nil - gCurrentCandidateController?.visible = false - hideTooltip() + gCurrentCandidateController?.delegate = nil + gCurrentCandidateController?.visible = false + hideTooltip() - if let previous = previous as? InputState.NotEmpty { - commit(text: previous.composingBuffer, client: client) - } - (client as? IMKTextInput)?.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - } + if let previous = previous as? InputState.NotEmpty { + commit(text: previous.composingBuffer, client: client) + } + (client as? IMKTextInput)?.setMarkedText( + "", selectionRange: NSMakeRange(0, 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + } - private func handle(state: InputState.Empty, previous: InputState, client: Any?) { - gCurrentCandidateController?.visible = false - hideTooltip() + private func handle(state: InputState.Empty, previous: InputState, client: Any?) { + gCurrentCandidateController?.visible = false + hideTooltip() - guard let client = client as? IMKTextInput else { - return - } + guard let client = client as? IMKTextInput else { + return + } - if let previous = previous as? InputState.NotEmpty { - commit(text: previous.composingBuffer, client: client) - } - client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - } + if let previous = previous as? InputState.NotEmpty { + commit(text: previous.composingBuffer, client: client) + } + client.setMarkedText( + "", selectionRange: NSMakeRange(0, 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + } - private func handle(state: InputState.EmptyIgnoringPreviousState, previous: InputState, client: Any!) { - gCurrentCandidateController?.visible = false - hideTooltip() + private func handle( + state: InputState.EmptyIgnoringPreviousState, previous: InputState, client: Any! + ) { + gCurrentCandidateController?.visible = false + hideTooltip() - guard let client = client as? IMKTextInput else { - return - } + guard let client = client as? IMKTextInput else { + return + } - client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - } + client.setMarkedText( + "", selectionRange: NSMakeRange(0, 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + } - private func handle(state: InputState.Committing, previous: InputState, client: Any?) { - gCurrentCandidateController?.visible = false - hideTooltip() + private func handle(state: InputState.Committing, previous: InputState, client: Any?) { + gCurrentCandidateController?.visible = false + hideTooltip() - guard let client = client as? IMKTextInput else { - return - } + guard let client = client as? IMKTextInput else { + return + } - let poppedText = state.poppedText - if !poppedText.isEmpty { - commit(text: poppedText, client: client) - } - client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - } + let poppedText = state.poppedText + if !poppedText.isEmpty { + commit(text: poppedText, client: client) + } + client.setMarkedText( + "", selectionRange: NSMakeRange(0, 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + } - private func handle(state: InputState.Inputting, previous: InputState, client: Any?) { - gCurrentCandidateController?.visible = false - hideTooltip() + private func handle(state: InputState.Inputting, previous: InputState, client: Any?) { + gCurrentCandidateController?.visible = false + hideTooltip() - guard let client = client as? IMKTextInput else { - return - } + guard let client = client as? IMKTextInput else { + return + } - let poppedText = state.poppedText - if !poppedText.isEmpty { - commit(text: poppedText, client: client) - } + let poppedText = state.poppedText + if !poppedText.isEmpty { + commit(text: poppedText, client: client) + } - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText(state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - if !state.tooltip.isEmpty { - show(tooltip: state.tooltip, composingBuffer: state.composingBuffer, cursorIndex: state.cursorIndex, client: client) - } - } + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put this composing buffer + client.setMarkedText( + state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + if !state.tooltip.isEmpty { + show( + tooltip: state.tooltip, composingBuffer: state.composingBuffer, + cursorIndex: state.cursorIndex, client: client) + } + } - private func handle(state: InputState.Marking, previous: InputState, client: Any?) { - gCurrentCandidateController?.visible = false - guard let client = client as? IMKTextInput else { - hideTooltip() - return - } + private func handle(state: InputState.Marking, previous: InputState, client: Any?) { + gCurrentCandidateController?.visible = false + guard let client = client as? IMKTextInput else { + hideTooltip() + return + } - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText(state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put this composing buffer + client.setMarkedText( + state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - if state.tooltip.isEmpty { - hideTooltip() - } else { - show(tooltip: state.tooltip, composingBuffer: state.composingBuffer, cursorIndex: state.markerIndex, client: client) - } - } + if state.tooltip.isEmpty { + hideTooltip() + } else { + show( + tooltip: state.tooltip, composingBuffer: state.composingBuffer, + cursorIndex: state.markerIndex, client: client) + } + } - private func handle(state: InputState.ChoosingCandidate, previous: InputState, client: Any?) { - hideTooltip() - guard let client = client as? IMKTextInput else { - gCurrentCandidateController?.visible = false - return - } + private func handle(state: InputState.ChoosingCandidate, previous: InputState, client: Any?) { + hideTooltip() + guard let client = client as? IMKTextInput else { + gCurrentCandidateController?.visible = false + return + } - // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, - // i.e. the client app needs to take care of where to put this composing buffer - client.setMarkedText(state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - show(candidateWindowWith: state, client: client) - } + // the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound, + // i.e. the client app needs to take care of where to put this composing buffer + client.setMarkedText( + state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + show(candidateWindowWith: state, client: client) + } - private func handle(state: InputState.AssociatedPhrases, previous: InputState, client: Any?) { - hideTooltip() - guard let client = client as? IMKTextInput else { - gCurrentCandidateController?.visible = false - return - } - client.setMarkedText("", selectionRange: NSMakeRange(0, 0), replacementRange: NSMakeRange(NSNotFound, NSNotFound)) - show(candidateWindowWith: state, client: client) - } + private func handle(state: InputState.AssociatedPhrases, previous: InputState, client: Any?) { + hideTooltip() + guard let client = client as? IMKTextInput else { + gCurrentCandidateController?.visible = false + return + } + client.setMarkedText( + "", selectionRange: NSMakeRange(0, 0), + replacementRange: NSMakeRange(NSNotFound, NSNotFound)) + show(candidateWindowWith: state, client: client) + } } // MARK: - extension ctlInputMethod { - private func show(candidateWindowWith state: InputState, client: Any!) { - let useVerticalMode: Bool = { - var useVerticalMode = false - var candidates: [String] = [] - if let state = state as? InputState.ChoosingCandidate { - useVerticalMode = state.useVerticalMode - candidates = state.candidates - } else if let state = state as? InputState.AssociatedPhrases { - useVerticalMode = state.useVerticalMode - candidates = state.candidates - } - if useVerticalMode == true { - return true - } - candidates.sort { - return $0.count > $1.count - } - // If there is a candidate which is too long, we use the vertical - // candidate list window automatically. - if candidates.first?.count ?? 0 > 8 { - // return true // 禁用這一項。威注音回頭會換候選窗格。 - } - return false - }() - - gCurrentCandidateController?.delegate = nil + private func show(candidateWindowWith state: InputState, client: Any!) { + let useVerticalMode: Bool = { + var useVerticalMode = false + var candidates: [String] = [] + if let state = state as? InputState.ChoosingCandidate { + useVerticalMode = state.useVerticalMode + candidates = state.candidates + } else if let state = state as? InputState.AssociatedPhrases { + useVerticalMode = state.useVerticalMode + candidates = state.candidates + } + if useVerticalMode == true { + return true + } + candidates.sort { + return $0.count > $1.count + } + // If there is a candidate which is too long, we use the vertical + // candidate list window automatically. + if candidates.first?.count ?? 0 > 8 { + // return true // 禁用這一項。威注音回頭會換候選窗格。 + } + return false + }() - if useVerticalMode { - gCurrentCandidateController = .vertical - } else if mgrPrefs.useHorizontalCandidateList { - gCurrentCandidateController = .horizontal - } else { - gCurrentCandidateController = .vertical - } + gCurrentCandidateController?.delegate = nil - // set the attributes for the candidate panel (which uses NSAttributedString) - let textSize = mgrPrefs.candidateListTextSize - let keyLabelSize = max(textSize / 2, kMinKeyLabelSize) + if useVerticalMode { + gCurrentCandidateController = .vertical + } else if mgrPrefs.useHorizontalCandidateList { + gCurrentCandidateController = .horizontal + } else { + gCurrentCandidateController = .vertical + } - func labelFont(name: String?, size: CGFloat) -> NSFont { - if let name = name { - return NSFont(name: name, size: size) ?? NSFont.systemFont(ofSize: size) - } - return NSFont.systemFont(ofSize: size) - } + // set the attributes for the candidate panel (which uses NSAttributedString) + let textSize = mgrPrefs.candidateListTextSize + let keyLabelSize = max(textSize / 2, kMinKeyLabelSize) - func candidateFont(name: String?, size: CGFloat) -> NSFont { - let currentMUIFont = (keyHandler.inputMode == InputMode.imeModeCHS) ? "Sarasa Term Slab SC" : "Sarasa Term Slab TC" - var finalReturnFont = NSFont(name: currentMUIFont, size: size) ?? NSFont.systemFont(ofSize: size) - // 對更紗黑體的依賴到 macOS 11 Big Sur 為止。macOS 12 Monterey 開始則依賴系統內建的函數使用蘋方來處理。 - if #available(macOS 12.0, *) {finalReturnFont = NSFont.systemFont(ofSize: size)} - if let name = name { - return NSFont(name: name, size: size) ?? finalReturnFont - } - return finalReturnFont - } + func labelFont(name: String?, size: CGFloat) -> NSFont { + if let name = name { + return NSFont(name: name, size: size) ?? NSFont.systemFont(ofSize: size) + } + return NSFont.systemFont(ofSize: size) + } - gCurrentCandidateController?.keyLabelFont = labelFont(name: mgrPrefs.candidateKeyLabelFontName, size: keyLabelSize) - gCurrentCandidateController?.candidateFont = candidateFont(name: mgrPrefs.candidateTextFontName, size: textSize) + func candidateFont(name: String?, size: CGFloat) -> NSFont { + let currentMUIFont = + (keyHandler.inputMode == InputMode.imeModeCHS) + ? "Sarasa Term Slab SC" : "Sarasa Term Slab TC" + var finalReturnFont = + NSFont(name: currentMUIFont, size: size) ?? NSFont.systemFont(ofSize: size) + // 對更紗黑體的依賴到 macOS 11 Big Sur 為止。macOS 12 Monterey 開始則依賴系統內建的函數使用蘋方來處理。 + if #available(macOS 12.0, *) { finalReturnFont = NSFont.systemFont(ofSize: size) } + if let name = name { + return NSFont(name: name, size: size) ?? finalReturnFont + } + return finalReturnFont + } - let candidateKeys = mgrPrefs.candidateKeys - let keyLabels = candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys) - let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : "" - gCurrentCandidateController?.keyLabels = keyLabels.map { - CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix) - } + gCurrentCandidateController?.keyLabelFont = labelFont( + name: mgrPrefs.candidateKeyLabelFontName, size: keyLabelSize) + gCurrentCandidateController?.candidateFont = candidateFont( + name: mgrPrefs.candidateTextFontName, size: textSize) - gCurrentCandidateController?.delegate = self - gCurrentCandidateController?.reloadData() - currentCandidateClient = client + let candidateKeys = mgrPrefs.candidateKeys + let keyLabels = + candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys) + let keyLabelSuffix = state is InputState.AssociatedPhrases ? "^" : "" + gCurrentCandidateController?.keyLabels = keyLabels.map { + CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix) + } - gCurrentCandidateController?.visible = true + gCurrentCandidateController?.delegate = self + gCurrentCandidateController?.reloadData() + currentCandidateClient = client - var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0) - var cursor: Int = 0 + gCurrentCandidateController?.visible = true - if let state = state as? InputState.ChoosingCandidate { - cursor = Int(state.cursorIndex) - if cursor == state.composingBuffer.count && cursor != 0 { - cursor -= 1 - } - } + var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0) + var cursor: Int = 0 - while lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor >= 0 { - (client as? IMKTextInput)?.attributes(forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect) - cursor -= 1 - } + if let state = state as? InputState.ChoosingCandidate { + cursor = Int(state.cursorIndex) + if cursor == state.composingBuffer.count && cursor != 0 { + cursor -= 1 + } + } - if useVerticalMode { - gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0) - } else { - gCurrentCandidateController?.set(windowTopLeftPoint: NSMakePoint(lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0), bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0) - } - } + while lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor >= 0 { + (client as? IMKTextInput)?.attributes( + forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect) + cursor -= 1 + } - private func show(tooltip: String, composingBuffer: String, cursorIndex: UInt, client: Any!) { - var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0) - var cursor: Int = Int(cursorIndex) - if cursor == composingBuffer.count && cursor != 0 { - cursor -= 1 - } - while lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor >= 0 { - (client as? IMKTextInput)?.attributes(forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect) - cursor -= 1 - } - ctlInputMethod.tooltipController.show(tooltip: tooltip, at: lineHeightRect.origin) - } + if useVerticalMode { + gCurrentCandidateController?.set( + windowTopLeftPoint: NSMakePoint( + lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, + lineHeightRect.origin.y - 4.0), + bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0) + } else { + gCurrentCandidateController?.set( + windowTopLeftPoint: NSMakePoint( + lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0), + bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0) + } + } - private func hideTooltip() { - ctlInputMethod.tooltipController.hide() - } + private func show(tooltip: String, composingBuffer: String, cursorIndex: UInt, client: Any!) { + var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0) + var cursor: Int = Int(cursorIndex) + if cursor == composingBuffer.count && cursor != 0 { + cursor -= 1 + } + while lineHeightRect.origin.x == 0 && lineHeightRect.origin.y == 0 && cursor >= 0 { + (client as? IMKTextInput)?.attributes( + forCharacterIndex: cursor, lineHeightRectangle: &lineHeightRect) + cursor -= 1 + } + ctlInputMethod.tooltipController.show(tooltip: tooltip, at: lineHeightRect.origin) + } + + private func hideTooltip() { + ctlInputMethod.tooltipController.hide() + } } // MARK: - 開關判定當前應用究竟是? @objc extension ctlInputMethod { - @objc static var areWeUsingOurOwnPhraseEditor: Bool = false + @objc static var areWeUsingOurOwnPhraseEditor: Bool = false } // MARK: - extension ctlInputMethod: KeyHandlerDelegate { - func candidateController(for keyHandler: KeyHandler) -> Any { - gCurrentCandidateController ?? .vertical - } + func candidateController(for keyHandler: KeyHandler) -> Any { + gCurrentCandidateController ?? .vertical + } - func keyHandler(_ keyHandler: KeyHandler, didSelectCandidateAt index: Int, candidateController controller: Any) { - if let controller = controller as? CandidateController { - self.candidateController(controller, didSelectCandidateAtIndex: UInt(index)) - } - } + func keyHandler( + _ keyHandler: KeyHandler, didSelectCandidateAt index: Int, + candidateController controller: Any + ) { + if let controller = controller as? CandidateController { + self.candidateController(controller, didSelectCandidateAtIndex: UInt(index)) + } + } - func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputState) -> Bool { - guard let state = state as? InputState.Marking else { - return false - } - if !state.validToWrite { - return false - } - let InputModeReversed: InputMode = (keyHandler.inputMode == InputMode.imeModeCHT) ? InputMode.imeModeCHS : InputMode.imeModeCHT - mgrLangModel.writeUserPhrase(state.userPhrase, inputMode: keyHandler.inputMode, areWeDuplicating: state.chkIfUserPhraseExists, areWeDeleting: ctlInputMethod.areWeDeleting) - mgrLangModel.writeUserPhrase(state.userPhraseConverted, inputMode: InputModeReversed, areWeDuplicating: false, areWeDeleting: ctlInputMethod.areWeDeleting) - return true - } + func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputState) + -> Bool + { + guard let state = state as? InputState.Marking else { + return false + } + if !state.validToWrite { + return false + } + let refInputModeReversed: InputMode = + (keyHandler.inputMode == InputMode.imeModeCHT) + ? InputMode.imeModeCHS : InputMode.imeModeCHT + mgrLangModel.writeUserPhrase( + state.userPhrase, inputMode: keyHandler.inputMode, + areWeDuplicating: state.chkIfUserPhraseExists, + areWeDeleting: ctlInputMethod.areWeDeleting) + mgrLangModel.writeUserPhrase( + state.userPhraseConverted, inputMode: refInputModeReversed, + areWeDuplicating: false, + areWeDeleting: ctlInputMethod.areWeDeleting) + return true + } } // MARK: - extension ctlInputMethod: CandidateControllerDelegate { - func candidateCountForController(_ controller: CandidateController) -> UInt { - if let state = state as? InputState.ChoosingCandidate { - return UInt(state.candidates.count) - } else if let state = state as? InputState.AssociatedPhrases { - return UInt(state.candidates.count) - } - return 0 - } + func candidateCountForController(_ controller: CandidateController) -> UInt { + if let state = state as? InputState.ChoosingCandidate { + return UInt(state.candidates.count) + } else if let state = state as? InputState.AssociatedPhrases { + return UInt(state.candidates.count) + } + return 0 + } - func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String { - if let state = state as? InputState.ChoosingCandidate { - return state.candidates[Int(index)] - } else if let state = state as? InputState.AssociatedPhrases { - return state.candidates[Int(index)] - } - return "" - } + func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) + -> String + { + if let state = state as? InputState.ChoosingCandidate { + return state.candidates[Int(index)] + } else if let state = state as? InputState.AssociatedPhrases { + return state.candidates[Int(index)] + } + return "" + } - func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) { - let client = currentCandidateClient + func candidateController( + _ controller: CandidateController, didSelectCandidateAtIndex index: UInt + ) { + let client = currentCandidateClient - if let state = state as? InputState.SymbolTable, - let node = state.node.children?[Int(index)] { - if let children = node.children, !children.isEmpty { - self.handle(state: .SymbolTable(node: node, useVerticalMode: state.useVerticalMode), client: currentCandidateClient) - } else { - self.handle(state: .Committing(poppedText: node.title), client: client) - self.handle(state: .Empty(), client: client) - } - return - } + if let state = state as? InputState.SymbolTable, + let node = state.node.children?[Int(index)] + { + if let children = node.children, !children.isEmpty { + self.handle( + state: .SymbolTable(node: node, useVerticalMode: state.useVerticalMode), + client: currentCandidateClient) + } else { + self.handle(state: .Committing(poppedText: node.title), client: client) + self.handle(state: .Empty(), client: client) + } + return + } - if let state = state as? InputState.ChoosingCandidate { - let selectedValue = state.candidates[Int(index)] - keyHandler.fixNode(value: selectedValue) + if let state = state as? InputState.ChoosingCandidate { + let selectedValue = state.candidates[Int(index)] + keyHandler.fixNode(value: selectedValue) - guard let inputting = keyHandler.buildInputtingState() as? InputState.Inputting else { - return - } + guard let inputting = keyHandler.buildInputtingState() as? InputState.Inputting else { + return + } - if mgrPrefs.useSCPCTypingMode { - keyHandler.clear() - let composingBuffer = inputting.composingBuffer - handle(state: .Committing(poppedText: composingBuffer), client: client) - if mgrPrefs.associatedPhrasesEnabled, - let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: composingBuffer, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases { - self.handle(state: associatePhrases, client: client) - } else { - handle(state: .Empty(), client: client) - } - } else { - handle(state: inputting, client: client) - } - return - } + if mgrPrefs.useSCPCTypingMode { + keyHandler.clear() + let composingBuffer = inputting.composingBuffer + handle(state: .Committing(poppedText: composingBuffer), client: client) + if mgrPrefs.associatedPhrasesEnabled, + let associatePhrases = keyHandler.buildAssociatePhraseState( + withKey: composingBuffer, useVerticalMode: state.useVerticalMode) + as? InputState.AssociatedPhrases + { + self.handle(state: associatePhrases, client: client) + } else { + handle(state: .Empty(), client: client) + } + } else { + handle(state: inputting, client: client) + } + return + } - if let state = state as? InputState.AssociatedPhrases { - let selectedValue = state.candidates[Int(index)] - handle(state: .Committing(poppedText: selectedValue), client: currentCandidateClient) - if mgrPrefs.associatedPhrasesEnabled, - let associatePhrases = keyHandler.buildAssociatePhraseState(withKey: selectedValue, useVerticalMode: state.useVerticalMode) as? InputState.AssociatedPhrases { - self.handle(state: associatePhrases, client: client) - } else { - handle(state: .Empty(), client: client) - } - } - } + if let state = state as? InputState.AssociatedPhrases { + let selectedValue = state.candidates[Int(index)] + handle(state: .Committing(poppedText: selectedValue), client: currentCandidateClient) + if mgrPrefs.associatedPhrasesEnabled, + let associatePhrases = keyHandler.buildAssociatePhraseState( + withKey: selectedValue, useVerticalMode: state.useVerticalMode) + as? InputState.AssociatedPhrases + { + self.handle(state: associatePhrases, client: client) + } else { + handle(state: .Empty(), client: client) + } + } + } } - diff --git a/Source/Modules/IMEModules/mgrPrefs.swift b/Source/Modules/IMEModules/mgrPrefs.swift index 96b817bf..1ab95969 100644 --- a/Source/Modules/IMEModules/mgrPrefs.swift +++ b/Source/Modules/IMEModules/mgrPrefs.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -72,464 +79,492 @@ private let kDefaultKeys = "123456789" @propertyWrapper struct UserDefault { - let key: String - let defaultValue: Value - var container: UserDefaults = .standard + let key: String + let defaultValue: Value + var container: UserDefaults = .standard - var wrappedValue: Value { - get { - container.object(forKey: key) as? Value ?? defaultValue - } - set { - container.set(newValue, forKey: key) - } - } + var wrappedValue: Value { + get { + container.object(forKey: key) as? Value ?? defaultValue + } + set { + container.set(newValue, forKey: key) + } + } } @propertyWrapper struct CandidateListTextSize { - let key: String - let defaultValue: CGFloat = kDefaultCandidateListTextSize - lazy var container: UserDefault = { - UserDefault(key: key, defaultValue: defaultValue) - }() + let key: String + let defaultValue: CGFloat = kDefaultCandidateListTextSize + lazy var container: UserDefault = { + UserDefault(key: key, defaultValue: defaultValue) + }() - var wrappedValue: CGFloat { - mutating get { - var value = container.wrappedValue - if value < kMinCandidateListTextSize { - value = kMinCandidateListTextSize - } else if value > kMaxCandidateListTextSize { - value = kMaxCandidateListTextSize - } - return value - } - set { - var value = newValue - if value < kMinCandidateListTextSize { - value = kMinCandidateListTextSize - } else if value > kMaxCandidateListTextSize { - value = kMaxCandidateListTextSize - } - container.wrappedValue = value - } - } + var wrappedValue: CGFloat { + mutating get { + var value = container.wrappedValue + if value < kMinCandidateListTextSize { + value = kMinCandidateListTextSize + } else if value > kMaxCandidateListTextSize { + value = kMaxCandidateListTextSize + } + return value + } + set { + var value = newValue + if value < kMinCandidateListTextSize { + value = kMinCandidateListTextSize + } else if value > kMaxCandidateListTextSize { + value = kMaxCandidateListTextSize + } + container.wrappedValue = value + } + } } @propertyWrapper struct ComposingBufferSize { - let key: String - let defaultValue: Int = kDefaultComposingBufferSize - lazy var container: UserDefault = { - UserDefault(key: key, defaultValue: defaultValue) - }() + let key: String + let defaultValue: Int = kDefaultComposingBufferSize + lazy var container: UserDefault = { + UserDefault(key: key, defaultValue: defaultValue) + }() - var wrappedValue: Int { - mutating get { - let currentValue = container.wrappedValue - if currentValue < kMinComposingBufferSize { - return kMinComposingBufferSize - } else if currentValue > kMaxComposingBufferSize { - return kMaxComposingBufferSize - } - return currentValue - } - set { - var value = newValue - if value < kMinComposingBufferSize { - value = kMinComposingBufferSize - } else if value > kMaxComposingBufferSize { - value = kMaxComposingBufferSize - } - container.wrappedValue = value - } - } + var wrappedValue: Int { + mutating get { + let currentValue = container.wrappedValue + if currentValue < kMinComposingBufferSize { + return kMinComposingBufferSize + } else if currentValue > kMaxComposingBufferSize { + return kMaxComposingBufferSize + } + return currentValue + } + set { + var value = newValue + if value < kMinComposingBufferSize { + value = kMinComposingBufferSize + } else if value > kMaxComposingBufferSize { + value = kMaxComposingBufferSize + } + container.wrappedValue = value + } + } } // MARK: - @objc enum KeyboardLayout: Int { - case standard = 0 - case eten = 1 - case hsu = 2 - case eten26 = 3 - case IBM = 4 - case MiTAC = 5 - case FakeSeigyou = 6 - case hanyuPinyin = 10 + case standard = 0 + case eten = 1 + case hsu = 2 + case eten26 = 3 + case ibm = 4 + case mitac = 5 + case fakeSeigyou = 6 + case hanyuPinyin = 10 - var name: String { - switch (self) { - case .standard: - return "Standard" - case .eten: - return "ETen" - case .hsu: - return "Hsu" - case .eten26: - return "ETen26" - case .IBM: - return "IBM" - case .MiTAC: - return "MiTAC" - case .FakeSeigyou: - return "FakeSeigyou" - case .hanyuPinyin: - return "HanyuPinyin" - } - } + var name: String { + switch self { + case .standard: + return "Standard" + case .eten: + return "ETen" + case .hsu: + return "Hsu" + case .eten26: + return "ETen26" + case .ibm: + return "IBM" + case .mitac: + return "MiTAC" + case .fakeSeigyou: + return "FakeSeigyou" + case .hanyuPinyin: + return "HanyuPinyin" + } + } } // MARK: - @objc public class mgrPrefs: NSObject { - static var allKeys:[String] { - [kIsDebugModeEnabled, - kUserDataFolderSpecified, - kKeyboardLayoutPreference, - kBasisKeyboardLayoutPreference, - kShowPageButtonsInCandidateWindow, - kCandidateListTextSize, - kAppleLanguagesPreferences, - kShouldAutoReloadUserDataFiles, - kSelectPhraseAfterCursorAsCandidatePreference, - kUseHorizontalCandidateListPreference, - kComposingBufferSizePreference, - kChooseCandidateUsingSpace, - kCNS11643Enabled, - kSymbolInputEnabled, - kChineseConversionEnabled, - kShiftJISShinjitaiOutputEnabled, - kHalfWidthPunctuationEnabled, - kSpecifyTabKeyBehavior, - kSpecifySpaceKeyBehavior, - kEscToCleanInputBuffer, - kCandidateTextFontName, - kCandidateKeyLabelFontName, - kCandidateKeys, - kMoveCursorAfterSelectingCandidate, - kPhraseReplacementEnabled, - kUseSCPCTypingMode, - kMaxCandidateLength, - kShouldNotFartInLieuOfBeep, - kAssociatedPhrasesEnabled] - } - - @objc public static func setMissingDefaults () { - // 既然 Preferences Module 的預設屬性不自動寫入 plist、而且還是 private,那這邊就先寫入了。 - - // 首次啟用輸入法時不要啟用偵錯模式。 - if UserDefaults.standard.object(forKey: kIsDebugModeEnabled) == nil { - UserDefaults.standard.set(mgrPrefs.isDebugModeEnabled, forKey: kIsDebugModeEnabled) - } - - // 首次啟用輸入法時設定不要自動更新,免得在某些要隔絕外部網路連線的保密機構內觸犯資安規則。 - if UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) == nil { - UserDefaults.standard.set(false, forKey: kCheckUpdateAutomatically) - } - - // 預設顯示選字窗翻頁按鈕 - if UserDefaults.standard.object(forKey: kShowPageButtonsInCandidateWindow) == nil { - UserDefaults.standard.set(mgrPrefs.showPageButtonsInCandidateWindow, forKey: kShowPageButtonsInCandidateWindow) - } - - // 預設啟用繪文字與符號輸入 - if UserDefaults.standard.object(forKey: kSymbolInputEnabled) == nil { - UserDefaults.standard.set(mgrPrefs.symbolInputEnabled, forKey: kSymbolInputEnabled) - } - - // 預設選字窗字詞文字尺寸,設成 18 剛剛好 - if UserDefaults.standard.object(forKey: kCandidateListTextSize) == nil { - UserDefaults.standard.set(mgrPrefs.candidateListTextSize, forKey: kCandidateListTextSize) - } - - // 預設摁空格鍵來選字,所以設成 true - if UserDefaults.standard.object(forKey: kChooseCandidateUsingSpace) == nil { - UserDefaults.standard.set(mgrPrefs.chooseCandidateUsingSpace, forKey: kChooseCandidateUsingSpace) - } - - // 自動檢測使用者自訂語彙數據的變動並載入。 - if UserDefaults.standard.object(forKey: kShouldAutoReloadUserDataFiles) == nil { - UserDefaults.standard.set(mgrPrefs.shouldAutoReloadUserDataFiles, forKey: kShouldAutoReloadUserDataFiles) - } - - // 預設情況下讓 Tab 鍵在選字窗內切換候選字、而不是用來換頁。 - if UserDefaults.standard.object(forKey: kSpecifyTabKeyBehavior) == nil { - UserDefaults.standard.set(mgrPrefs.specifyTabKeyBehavior, forKey: kSpecifyTabKeyBehavior) - } - - // 預設情況下讓 Space 鍵在選字窗內切換候選字、而不是用來換頁。 - if UserDefaults.standard.object(forKey: kSpecifySpaceKeyBehavior) == nil { - UserDefaults.standard.set(mgrPrefs.specifySpaceKeyBehavior, forKey: kSpecifySpaceKeyBehavior) - } - - // 預設禁用逐字選字模式(就是每個字都要選的那種),所以設成 false - if UserDefaults.standard.object(forKey: kUseSCPCTypingMode) == nil { - UserDefaults.standard.set(mgrPrefs.useSCPCTypingMode, forKey: kUseSCPCTypingMode) - } - - // 預設禁用逐字選字模式時的聯想詞功能,所以設成 false - if UserDefaults.standard.object(forKey: kAssociatedPhrasesEnabled) == nil { - UserDefaults.standard.set(mgrPrefs.associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled) - } - - // 預設漢音風格選字,所以要設成 0 - if UserDefaults.standard.object(forKey: kSelectPhraseAfterCursorAsCandidatePreference) == nil { - UserDefaults.standard.set(mgrPrefs.selectPhraseAfterCursorAsCandidate, forKey: kSelectPhraseAfterCursorAsCandidatePreference) - } - - // 預設在選字後自動移動游標 - if UserDefaults.standard.object(forKey: kMoveCursorAfterSelectingCandidate) == nil { - UserDefaults.standard.set(mgrPrefs.moveCursorAfterSelectingCandidate, forKey: kMoveCursorAfterSelectingCandidate) - } - - // 預設橫向選字窗,不爽請自行改成縱向選字窗 - if UserDefaults.standard.object(forKey: kUseHorizontalCandidateListPreference) == nil { - UserDefaults.standard.set(mgrPrefs.useHorizontalCandidateList, forKey: kUseHorizontalCandidateListPreference) - } - - // 預設停用全字庫支援 - if UserDefaults.standard.object(forKey: kCNS11643Enabled) == nil { - UserDefaults.standard.set(mgrPrefs.cns11643Enabled, forKey: kCNS11643Enabled) - } - - // 預設停用繁體轉康熙模組 - if UserDefaults.standard.object(forKey: kChineseConversionEnabled) == nil { - UserDefaults.standard.set(mgrPrefs.chineseConversionEnabled, forKey: kChineseConversionEnabled) - } - - // 預設停用繁體轉 JIS 當用新字體模組 - if UserDefaults.standard.object(forKey: kShiftJISShinjitaiOutputEnabled) == nil { - UserDefaults.standard.set(mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) - } - - // 預設停用自訂語彙置換 - if UserDefaults.standard.object(forKey: kPhraseReplacementEnabled) == nil { - UserDefaults.standard.set(mgrPrefs.phraseReplacementEnabled, forKey: kPhraseReplacementEnabled) - } - - // 預設沒事不要在那裡放屁 - if UserDefaults.standard.object(forKey: kShouldNotFartInLieuOfBeep) == nil { - UserDefaults.standard.set(mgrPrefs.shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep) - } - - UserDefaults.standard.synchronize() - } - - @UserDefault(key: kIsDebugModeEnabled, defaultValue: false) - @objc static var isDebugModeEnabled: Bool - - @UserDefault(key: kUserDataFolderSpecified, defaultValue: "") - @objc static var userDataFolderSpecified: String - - @objc static func ifSpecifiedUserDataPathExistsInPlist() -> Bool { - UserDefaults.standard.object(forKey: kUserDataFolderSpecified) != nil - } - - @UserDefault(key: kAppleLanguagesPreferences, defaultValue: []) - @objc static var appleLanguages: Array - - @UserDefault(key: kKeyboardLayoutPreference, defaultValue: 0) - @objc static var keyboardLayout: Int - - @objc static var keyboardLayoutName: String { - (KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name - } - - @UserDefault(key: kBasisKeyboardLayoutPreference, defaultValue: "com.apple.keylayout.ZhuyinBopomofo") - @objc static var basisKeyboardLayout: String - - @UserDefault(key: kShowPageButtonsInCandidateWindow, defaultValue: true) - @objc static var showPageButtonsInCandidateWindow: Bool - - @CandidateListTextSize(key: kCandidateListTextSize) - @objc static var candidateListTextSize: CGFloat - - @UserDefault(key: kShouldAutoReloadUserDataFiles, defaultValue: true) - @objc static var shouldAutoReloadUserDataFiles: Bool - - @UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreference, defaultValue: false) - @objc static var selectPhraseAfterCursorAsCandidate: Bool - - @UserDefault(key: kMoveCursorAfterSelectingCandidate, defaultValue: false) - @objc static var moveCursorAfterSelectingCandidate: Bool - - @UserDefault(key: kUseHorizontalCandidateListPreference, defaultValue: true) - @objc static var useHorizontalCandidateList: Bool - - @ComposingBufferSize(key: kComposingBufferSizePreference) - @objc static var composingBufferSize: Int - - @UserDefault(key: kChooseCandidateUsingSpace, defaultValue: true) - @objc static var chooseCandidateUsingSpace: Bool - - @UserDefault(key: kUseSCPCTypingMode, defaultValue: false) - @objc static var useSCPCTypingMode: Bool - - @objc static func toggleSCPCTypingModeEnabled() -> Bool { - useSCPCTypingMode = !useSCPCTypingMode - UserDefaults.standard.set(useSCPCTypingMode, forKey: kUseSCPCTypingMode) - return useSCPCTypingMode - } - - @UserDefault(key: kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2) - @objc static var maxCandidateLength: Int - - @UserDefault(key: kShouldNotFartInLieuOfBeep, defaultValue: true) - @objc static var shouldNotFartInLieuOfBeep: Bool - - @objc static func toggleShouldNotFartInLieuOfBeep() -> Bool { - shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep - UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep) - return shouldNotFartInLieuOfBeep - } - - @UserDefault(key: kCNS11643Enabled, defaultValue: false) - @objc static var cns11643Enabled: Bool - - @objc static func toggleCNS11643Enabled() -> Bool { - cns11643Enabled = !cns11643Enabled - mgrLangModel.setCNSEnabled(cns11643Enabled) // 很重要 - UserDefaults.standard.set(cns11643Enabled, forKey: kCNS11643Enabled) - return cns11643Enabled - } - - @UserDefault(key: kSymbolInputEnabled, defaultValue: true) - @objc static var symbolInputEnabled: Bool - - @objc static func toggleSymbolInputEnabled() -> Bool { - symbolInputEnabled = !symbolInputEnabled - mgrLangModel.setSymbolEnabled(symbolInputEnabled) // 很重要 - UserDefaults.standard.set(symbolInputEnabled, forKey: kSymbolInputEnabled) - return symbolInputEnabled - } - - @UserDefault(key: kChineseConversionEnabled, defaultValue: false) - @objc static var chineseConversionEnabled: Bool - - @objc @discardableResult static func toggleChineseConversionEnabled() -> Bool { - chineseConversionEnabled = !chineseConversionEnabled - // 康熙轉換與 JIS 轉換不能同時開啟,否則會出現某些奇奇怪怪的情況 - if chineseConversionEnabled && shiftJISShinjitaiOutputEnabled { - self.toggleShiftJISShinjitaiOutputEnabled() - UserDefaults.standard.set(shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) - } - UserDefaults.standard.set(chineseConversionEnabled, forKey: kChineseConversionEnabled) - return chineseConversionEnabled - } - - @UserDefault(key: kShiftJISShinjitaiOutputEnabled, defaultValue: false) - @objc static var shiftJISShinjitaiOutputEnabled: Bool - - @objc @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool { - shiftJISShinjitaiOutputEnabled = !shiftJISShinjitaiOutputEnabled - // 康熙轉換與 JIS 轉換不能同時開啟,否則會出現某些奇奇怪怪的情況 - if shiftJISShinjitaiOutputEnabled && chineseConversionEnabled {self.toggleChineseConversionEnabled()} - UserDefaults.standard.set(shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) - return shiftJISShinjitaiOutputEnabled - } - - @UserDefault(key: kHalfWidthPunctuationEnabled, defaultValue: false) - @objc static var halfWidthPunctuationEnabled: Bool - - @objc static func toggleHalfWidthPunctuationEnabled() -> Bool { - halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled - return halfWidthPunctuationEnabled - } - - @UserDefault(key: kEscToCleanInputBuffer, defaultValue: true) - @objc static var escToCleanInputBuffer: Bool - - - @UserDefault(key: kSpecifyTabKeyBehavior, defaultValue: false) - @objc static var specifyTabKeyBehavior: Bool - - @UserDefault(key: kSpecifySpaceKeyBehavior, defaultValue: false) - @objc static var specifySpaceKeyBehavior: Bool - - // MARK: - Optional settings - @UserDefault(key: kCandidateTextFontName, defaultValue: nil) - @objc static var candidateTextFontName: String? - - @UserDefault(key: kCandidateKeyLabelFontName, defaultValue: nil) - @objc static var candidateKeyLabelFontName: String? - - @UserDefault(key: kCandidateKeys, defaultValue: kDefaultKeys) - @objc static var candidateKeys: String - - @objc static var defaultCandidateKeys: String { - kDefaultKeys - } - @objc static var suggestedCandidateKeys: [String] { - [kDefaultKeys, "234567890", "QWERTYUIO", "QWERTASDF", "ASDFGHJKL", "ASDFZXCVB"] - } - - @objc static func validate(candidateKeys: String) throws { - let trimmed = candidateKeys.trimmingCharacters(in: .whitespacesAndNewlines) - if trimmed.isEmpty { - throw CandidateKeyError.empty - } - if !trimmed.canBeConverted(to: .ascii) { - throw CandidateKeyError.invalidCharacters - } - if trimmed.contains(" ") { - throw CandidateKeyError.containSpace - } - if trimmed.count < 4 { - throw CandidateKeyError.tooShort - } - if trimmed.count > 15 { - throw CandidateKeyError.tooLong - } - let set = Set(Array(trimmed)) - if set.count != trimmed.count { - throw CandidateKeyError.duplicatedCharacters - } - } - - enum CandidateKeyError: Error, LocalizedError { - case empty - case invalidCharacters - case containSpace - case duplicatedCharacters - case tooShort - case tooLong - - var errorDescription: String? { - switch self { - case .empty: - return NSLocalizedString("Candidates keys cannot be empty.", comment: "") - case .invalidCharacters: - return NSLocalizedString("Candidate keys can only contain ASCII characters like alphanumericals.", comment: "") - case .containSpace: - return NSLocalizedString("Candidate keys cannot contain space.", comment: "") - case .duplicatedCharacters: - return NSLocalizedString("There should not be duplicated keys.", comment: "") - case .tooShort: - return NSLocalizedString("Please specify at least 4 candidate keys.", comment: "") - case .tooLong: - return NSLocalizedString("Maximum 15 candidate keys allowed.", comment: "") - } - } - - } - - @UserDefault(key: kPhraseReplacementEnabled, defaultValue: false) - @objc static var phraseReplacementEnabled: Bool - - @objc static func togglePhraseReplacementEnabled() -> Bool { - phraseReplacementEnabled = !phraseReplacementEnabled - mgrLangModel.setPhraseReplacementEnabled(phraseReplacementEnabled) - UserDefaults.standard.set(phraseReplacementEnabled, forKey: kPhraseReplacementEnabled) - return phraseReplacementEnabled - } - - @UserDefault(key: kAssociatedPhrasesEnabled, defaultValue: false) - @objc static var associatedPhrasesEnabled: Bool - - @objc static func toggleAssociatedPhrasesEnabled() -> Bool { - associatedPhrasesEnabled = !associatedPhrasesEnabled - UserDefaults.standard.set(associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled) - return associatedPhrasesEnabled - } + static var allKeys: [String] { + [ + kIsDebugModeEnabled, + kUserDataFolderSpecified, + kKeyboardLayoutPreference, + kBasisKeyboardLayoutPreference, + kShowPageButtonsInCandidateWindow, + kCandidateListTextSize, + kAppleLanguagesPreferences, + kShouldAutoReloadUserDataFiles, + kSelectPhraseAfterCursorAsCandidatePreference, + kUseHorizontalCandidateListPreference, + kComposingBufferSizePreference, + kChooseCandidateUsingSpace, + kCNS11643Enabled, + kSymbolInputEnabled, + kChineseConversionEnabled, + kShiftJISShinjitaiOutputEnabled, + kHalfWidthPunctuationEnabled, + kSpecifyTabKeyBehavior, + kSpecifySpaceKeyBehavior, + kEscToCleanInputBuffer, + kCandidateTextFontName, + kCandidateKeyLabelFontName, + kCandidateKeys, + kMoveCursorAfterSelectingCandidate, + kPhraseReplacementEnabled, + kUseSCPCTypingMode, + kMaxCandidateLength, + kShouldNotFartInLieuOfBeep, + kAssociatedPhrasesEnabled, + ] + } + + @objc public static func setMissingDefaults() { + // 既然 Preferences Module 的預設屬性不自動寫入 plist、而且還是 private,那這邊就先寫入了。 + + // 首次啟用輸入法時不要啟用偵錯模式。 + if UserDefaults.standard.object(forKey: kIsDebugModeEnabled) == nil { + UserDefaults.standard.set(mgrPrefs.isDebugModeEnabled, forKey: kIsDebugModeEnabled) + } + + // 首次啟用輸入法時設定不要自動更新,免得在某些要隔絕外部網路連線的保密機構內觸犯資安規則。 + if UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) == nil { + UserDefaults.standard.set(false, forKey: kCheckUpdateAutomatically) + } + + // 預設顯示選字窗翻頁按鈕 + if UserDefaults.standard.object(forKey: kShowPageButtonsInCandidateWindow) == nil { + UserDefaults.standard.set( + mgrPrefs.showPageButtonsInCandidateWindow, forKey: kShowPageButtonsInCandidateWindow + ) + } + + // 預設啟用繪文字與符號輸入 + if UserDefaults.standard.object(forKey: kSymbolInputEnabled) == nil { + UserDefaults.standard.set(mgrPrefs.symbolInputEnabled, forKey: kSymbolInputEnabled) + } + + // 預設選字窗字詞文字尺寸,設成 18 剛剛好 + if UserDefaults.standard.object(forKey: kCandidateListTextSize) == nil { + UserDefaults.standard.set( + mgrPrefs.candidateListTextSize, forKey: kCandidateListTextSize) + } + + // 預設摁空格鍵來選字,所以設成 true + if UserDefaults.standard.object(forKey: kChooseCandidateUsingSpace) == nil { + UserDefaults.standard.set( + mgrPrefs.chooseCandidateUsingSpace, forKey: kChooseCandidateUsingSpace) + } + + // 自動檢測使用者自訂語彙數據的變動並載入。 + if UserDefaults.standard.object(forKey: kShouldAutoReloadUserDataFiles) == nil { + UserDefaults.standard.set( + mgrPrefs.shouldAutoReloadUserDataFiles, forKey: kShouldAutoReloadUserDataFiles) + } + + // 預設情況下讓 Tab 鍵在選字窗內切換候選字、而不是用來換頁。 + if UserDefaults.standard.object(forKey: kSpecifyTabKeyBehavior) == nil { + UserDefaults.standard.set( + mgrPrefs.specifyTabKeyBehavior, forKey: kSpecifyTabKeyBehavior) + } + + // 預設情況下讓 Space 鍵在選字窗內切換候選字、而不是用來換頁。 + if UserDefaults.standard.object(forKey: kSpecifySpaceKeyBehavior) == nil { + UserDefaults.standard.set( + mgrPrefs.specifySpaceKeyBehavior, forKey: kSpecifySpaceKeyBehavior) + } + + // 預設禁用逐字選字模式(就是每個字都要選的那種),所以設成 false + if UserDefaults.standard.object(forKey: kUseSCPCTypingMode) == nil { + UserDefaults.standard.set(mgrPrefs.useSCPCTypingMode, forKey: kUseSCPCTypingMode) + } + + // 預設禁用逐字選字模式時的聯想詞功能,所以設成 false + if UserDefaults.standard.object(forKey: kAssociatedPhrasesEnabled) == nil { + UserDefaults.standard.set( + mgrPrefs.associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled) + } + + // 預設漢音風格選字,所以要設成 0 + if UserDefaults.standard.object(forKey: kSelectPhraseAfterCursorAsCandidatePreference) + == nil + { + UserDefaults.standard.set( + mgrPrefs.selectPhraseAfterCursorAsCandidate, + forKey: kSelectPhraseAfterCursorAsCandidatePreference) + } + + // 預設在選字後自動移動游標 + if UserDefaults.standard.object(forKey: kMoveCursorAfterSelectingCandidate) == nil { + UserDefaults.standard.set( + mgrPrefs.moveCursorAfterSelectingCandidate, + forKey: kMoveCursorAfterSelectingCandidate) + } + + // 預設橫向選字窗,不爽請自行改成縱向選字窗 + if UserDefaults.standard.object(forKey: kUseHorizontalCandidateListPreference) == nil { + UserDefaults.standard.set( + mgrPrefs.useHorizontalCandidateList, forKey: kUseHorizontalCandidateListPreference) + } + + // 預設停用全字庫支援 + if UserDefaults.standard.object(forKey: kCNS11643Enabled) == nil { + UserDefaults.standard.set(mgrPrefs.cns11643Enabled, forKey: kCNS11643Enabled) + } + + // 預設停用繁體轉康熙模組 + if UserDefaults.standard.object(forKey: kChineseConversionEnabled) == nil { + UserDefaults.standard.set( + mgrPrefs.chineseConversionEnabled, forKey: kChineseConversionEnabled) + } + + // 預設停用繁體轉 JIS 當用新字體模組 + if UserDefaults.standard.object(forKey: kShiftJISShinjitaiOutputEnabled) == nil { + UserDefaults.standard.set( + mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) + } + + // 預設停用自訂語彙置換 + if UserDefaults.standard.object(forKey: kPhraseReplacementEnabled) == nil { + UserDefaults.standard.set( + mgrPrefs.phraseReplacementEnabled, forKey: kPhraseReplacementEnabled) + } + + // 預設沒事不要在那裡放屁 + if UserDefaults.standard.object(forKey: kShouldNotFartInLieuOfBeep) == nil { + UserDefaults.standard.set( + mgrPrefs.shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep) + } + + UserDefaults.standard.synchronize() + } + + @UserDefault(key: kIsDebugModeEnabled, defaultValue: false) + @objc static var isDebugModeEnabled: Bool + + @UserDefault(key: kUserDataFolderSpecified, defaultValue: "") + @objc static var userDataFolderSpecified: String + + @objc static func ifSpecifiedUserDataPathExistsInPlist() -> Bool { + UserDefaults.standard.object(forKey: kUserDataFolderSpecified) != nil + } + + @UserDefault(key: kAppleLanguagesPreferences, defaultValue: []) + @objc static var appleLanguages: [String] + + @UserDefault(key: kKeyboardLayoutPreference, defaultValue: 0) + @objc static var keyboardLayout: Int + + @objc static var keyboardLayoutName: String { + (KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name + } + + @UserDefault( + key: kBasisKeyboardLayoutPreference, defaultValue: "com.apple.keylayout.ZhuyinBopomofo") + @objc static var basisKeyboardLayout: String + + @UserDefault(key: kShowPageButtonsInCandidateWindow, defaultValue: true) + @objc static var showPageButtonsInCandidateWindow: Bool + + @CandidateListTextSize(key: kCandidateListTextSize) + @objc static var candidateListTextSize: CGFloat + + @UserDefault(key: kShouldAutoReloadUserDataFiles, defaultValue: true) + @objc static var shouldAutoReloadUserDataFiles: Bool + + @UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreference, defaultValue: false) + @objc static var selectPhraseAfterCursorAsCandidate: Bool + + @UserDefault(key: kMoveCursorAfterSelectingCandidate, defaultValue: false) + @objc static var moveCursorAfterSelectingCandidate: Bool + + @UserDefault(key: kUseHorizontalCandidateListPreference, defaultValue: true) + @objc static var useHorizontalCandidateList: Bool + + @ComposingBufferSize(key: kComposingBufferSizePreference) + @objc static var composingBufferSize: Int + + @UserDefault(key: kChooseCandidateUsingSpace, defaultValue: true) + @objc static var chooseCandidateUsingSpace: Bool + + @UserDefault(key: kUseSCPCTypingMode, defaultValue: false) + @objc static var useSCPCTypingMode: Bool + + @objc static func toggleSCPCTypingModeEnabled() -> Bool { + useSCPCTypingMode = !useSCPCTypingMode + UserDefaults.standard.set(useSCPCTypingMode, forKey: kUseSCPCTypingMode) + return useSCPCTypingMode + } + + @UserDefault(key: kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2) + @objc static var maxCandidateLength: Int + + @UserDefault(key: kShouldNotFartInLieuOfBeep, defaultValue: true) + @objc static var shouldNotFartInLieuOfBeep: Bool + + @objc static func toggleShouldNotFartInLieuOfBeep() -> Bool { + shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep + UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep) + return shouldNotFartInLieuOfBeep + } + + @UserDefault(key: kCNS11643Enabled, defaultValue: false) + @objc static var cns11643Enabled: Bool + + @objc static func toggleCNS11643Enabled() -> Bool { + cns11643Enabled = !cns11643Enabled + mgrLangModel.setCNSEnabled(cns11643Enabled) // 很重要 + UserDefaults.standard.set(cns11643Enabled, forKey: kCNS11643Enabled) + return cns11643Enabled + } + + @UserDefault(key: kSymbolInputEnabled, defaultValue: true) + @objc static var symbolInputEnabled: Bool + + @objc static func toggleSymbolInputEnabled() -> Bool { + symbolInputEnabled = !symbolInputEnabled + mgrLangModel.setSymbolEnabled(symbolInputEnabled) // 很重要 + UserDefaults.standard.set(symbolInputEnabled, forKey: kSymbolInputEnabled) + return symbolInputEnabled + } + + @UserDefault(key: kChineseConversionEnabled, defaultValue: false) + @objc static var chineseConversionEnabled: Bool + + @objc @discardableResult static func toggleChineseConversionEnabled() -> Bool { + chineseConversionEnabled = !chineseConversionEnabled + // 康熙轉換與 JIS 轉換不能同時開啟,否則會出現某些奇奇怪怪的情況 + if chineseConversionEnabled && shiftJISShinjitaiOutputEnabled { + self.toggleShiftJISShinjitaiOutputEnabled() + UserDefaults.standard.set( + shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) + } + UserDefaults.standard.set(chineseConversionEnabled, forKey: kChineseConversionEnabled) + return chineseConversionEnabled + } + + @UserDefault(key: kShiftJISShinjitaiOutputEnabled, defaultValue: false) + @objc static var shiftJISShinjitaiOutputEnabled: Bool + + @objc @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool { + shiftJISShinjitaiOutputEnabled = !shiftJISShinjitaiOutputEnabled + // 康熙轉換與 JIS 轉換不能同時開啟,否則會出現某些奇奇怪怪的情況 + if shiftJISShinjitaiOutputEnabled && chineseConversionEnabled { + self.toggleChineseConversionEnabled() + } + UserDefaults.standard.set( + shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) + return shiftJISShinjitaiOutputEnabled + } + + @UserDefault(key: kHalfWidthPunctuationEnabled, defaultValue: false) + @objc static var halfWidthPunctuationEnabled: Bool + + @objc static func toggleHalfWidthPunctuationEnabled() -> Bool { + halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled + return halfWidthPunctuationEnabled + } + + @UserDefault(key: kEscToCleanInputBuffer, defaultValue: true) + @objc static var escToCleanInputBuffer: Bool + + @UserDefault(key: kSpecifyTabKeyBehavior, defaultValue: false) + @objc static var specifyTabKeyBehavior: Bool + + @UserDefault(key: kSpecifySpaceKeyBehavior, defaultValue: false) + @objc static var specifySpaceKeyBehavior: Bool + + // MARK: - Optional settings + @UserDefault(key: kCandidateTextFontName, defaultValue: nil) + @objc static var candidateTextFontName: String? + + @UserDefault(key: kCandidateKeyLabelFontName, defaultValue: nil) + @objc static var candidateKeyLabelFontName: String? + + @UserDefault(key: kCandidateKeys, defaultValue: kDefaultKeys) + @objc static var candidateKeys: String + + @objc static var defaultCandidateKeys: String { + kDefaultKeys + } + @objc static var suggestedCandidateKeys: [String] { + [kDefaultKeys, "234567890", "QWERTYUIO", "QWERTASDF", "ASDFGHJKL", "ASDFZXCVB"] + } + + @objc static func validate(candidateKeys: String) throws { + let trimmed = candidateKeys.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + throw CandidateKeyError.empty + } + if !trimmed.canBeConverted(to: .ascii) { + throw CandidateKeyError.invalidCharacters + } + if trimmed.contains(" ") { + throw CandidateKeyError.containSpace + } + if trimmed.count < 4 { + throw CandidateKeyError.tooShort + } + if trimmed.count > 15 { + throw CandidateKeyError.tooLong + } + let set = Set(Array(trimmed)) + if set.count != trimmed.count { + throw CandidateKeyError.duplicatedCharacters + } + } + + enum CandidateKeyError: Error, LocalizedError { + case empty + case invalidCharacters + case containSpace + case duplicatedCharacters + case tooShort + case tooLong + + var errorDescription: String? { + switch self { + case .empty: + return NSLocalizedString("Candidates keys cannot be empty.", comment: "") + case .invalidCharacters: + return NSLocalizedString( + "Candidate keys can only contain ASCII characters like alphanumericals.", + comment: "") + case .containSpace: + return NSLocalizedString("Candidate keys cannot contain space.", comment: "") + case .duplicatedCharacters: + return NSLocalizedString("There should not be duplicated keys.", comment: "") + case .tooShort: + return NSLocalizedString( + "Please specify at least 4 candidate keys.", comment: "") + case .tooLong: + return NSLocalizedString("Maximum 15 candidate keys allowed.", comment: "") + } + } + + } + + @UserDefault(key: kPhraseReplacementEnabled, defaultValue: false) + @objc static var phraseReplacementEnabled: Bool + + @objc static func togglePhraseReplacementEnabled() -> Bool { + phraseReplacementEnabled = !phraseReplacementEnabled + mgrLangModel.setPhraseReplacementEnabled(phraseReplacementEnabled) + UserDefaults.standard.set(phraseReplacementEnabled, forKey: kPhraseReplacementEnabled) + return phraseReplacementEnabled + } + + @UserDefault(key: kAssociatedPhrasesEnabled, defaultValue: false) + @objc static var associatedPhrasesEnabled: Bool + + @objc static func toggleAssociatedPhrasesEnabled() -> Bool { + associatedPhrasesEnabled = !associatedPhrasesEnabled + UserDefaults.standard.set(associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled) + return associatedPhrasesEnabled + } } diff --git a/Source/Modules/LangModelRelated/mgrLangModel.mm b/Source/Modules/LangModelRelated/mgrLangModel.mm index 9d5e411d..3ca207ba 100644 --- a/Source/Modules/LangModelRelated/mgrLangModel.mm +++ b/Source/Modules/LangModelRelated/mgrLangModel.mm @@ -235,7 +235,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing [currentMarkedPhrase appendString:userPhrase]; if (areWeDuplicating && !areWeDeleting) { // Do not use ASCII characters to comment here. - // Otherwise, it will be scrambled by HYPY2BPMF module shipped in the vChewing Phrase Editor. + // Otherwise, it will be scrambled by cnvHYPYtoBPMF module shipped in the vChewing Phrase Editor. [currentMarkedPhrase appendString:@"\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"]; } [currentMarkedPhrase appendString:@"\n"]; diff --git a/Source/Modules/SFX/clsSFX.swift b/Source/Modules/SFX/clsSFX.swift index 5f4de74f..6b2f2d1e 100644 --- a/Source/Modules/SFX/clsSFX.swift +++ b/Source/Modules/SFX/clsSFX.swift @@ -1,59 +1,65 @@ // Copyright (c) 2022 and onwards Isaac Xen (MIT License). // All possible vChewing-specific modifications are (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: +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. +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. +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. +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 @objc public class clsSFX: NSObject, NSSoundDelegate { - private static let shared = clsSFX() - private override init(){ - super.init() - } - private var currentBeep: NSSound? - private func beep() { - // Stop existing beep - if let beep = currentBeep { - if beep.isPlaying { - beep.stop() - } - } - // Create a new beep sound if possible - var sndBeep:String - if mgrPrefs.shouldNotFartInLieuOfBeep == false { - sndBeep = "Fart" - } else { - sndBeep = "Beep" - } - guard - let beep = NSSound(named:sndBeep) - else { - NSSound.beep() - return - } - beep.delegate = self - beep.volume = 0.4 - beep.play() - currentBeep = beep - } - @objc public func sound(_ sound: NSSound, didFinishPlaying flag: Bool) { - currentBeep = nil - } - @objc static func beep() { - shared.beep() - } + private static let shared = clsSFX() + private override init() { + super.init() + } + private var currentBeep: NSSound? + private func beep() { + // Stop existing beep + if let beep = currentBeep { + if beep.isPlaying { + beep.stop() + } + } + // Create a new beep sound if possible + var sndBeep: String + if mgrPrefs.shouldNotFartInLieuOfBeep == false { + sndBeep = "Fart" + } else { + sndBeep = "Beep" + } + guard + let beep = NSSound(named: sndBeep) + else { + NSSound.beep() + return + } + beep.delegate = self + beep.volume = 0.4 + beep.play() + currentBeep = beep + } + @objc public func sound(_ sound: NSSound, didFinishPlaying flag: Bool) { + currentBeep = nil + } + @objc static func beep() { + shared.beep() + } } diff --git a/Source/Modules/main.swift b/Source/Modules/main.swift index 9996268f..185e29bf 100644 --- a/Source/Modules/main.swift +++ b/Source/Modules/main.swift @@ -1,20 +1,27 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @@ -23,30 +30,32 @@ import InputMethodKit let kConnectionName = "vChewing_1_Connection" if CommandLine.arguments.count > 1 { - if CommandLine.arguments[1] == "install" { - let exitCode = IME.registerInputMethod() - exit(exitCode) - } - if CommandLine.arguments[1] == "uninstall" { - let exitCode = IME.uninstall(isSudo: IME.isSudoMode) - exit(exitCode) - } + if CommandLine.arguments[1] == "install" { + let exitCode = IME.registerInputMethod() + exit(exitCode) + } + if CommandLine.arguments[1] == "uninstall" { + let exitCode = IME.uninstall(isSudo: IME.isSudoMode) + exit(exitCode) + } } guard let mainNibName = Bundle.main.infoDictionary?["NSMainNibFile"] as? String else { - NSLog("Fatal error: NSMainNibFile key not defined in Info.plist."); - exit(-1) + NSLog("Fatal error: NSMainNibFile key not defined in Info.plist.") + exit(-1) } let loaded = Bundle.main.loadNibNamed(mainNibName, owner: NSApp, topLevelObjects: nil) if !loaded { - NSLog("Fatal error: Cannot load \(mainNibName).") - exit(-1) + NSLog("Fatal error: Cannot load \(mainNibName).") + exit(-1) } -guard let bundleID = Bundle.main.bundleIdentifier, let server = IMKServer(name: kConnectionName, bundleIdentifier: bundleID) else { - NSLog("Fatal error: Cannot initialize input method server with connection \(kConnectionName).") - exit(-1) +guard let bundleID = Bundle.main.bundleIdentifier, + let server = IMKServer(name: kConnectionName, bundleIdentifier: bundleID) +else { + NSLog("Fatal error: Cannot initialize input method server with connection \(kConnectionName).") + exit(-1) } NSApp.run() diff --git a/Source/UI/CandidateUI/CandidateController.swift b/Source/UI/CandidateUI/CandidateController.swift index eaabad44..2ef1ce09 100644 --- a/Source/UI/CandidateUI/CandidateController.swift +++ b/Source/UI/CandidateUI/CandidateController.swift @@ -1,165 +1,176 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @objc(VTCandidateKeyLabel) public class CandidateKeyLabel: NSObject { - @objc public private(set) var key: String - @objc public private(set) var displayedText: String + @objc public private(set) var key: String + @objc public private(set) var displayedText: String - public init(key: String, displayedText: String) { - self.key = key - self.displayedText = displayedText - super.init() - } + public init(key: String, displayedText: String) { + self.key = key + self.displayedText = displayedText + super.init() + } } @objc(VTCandidateControllerDelegate) public protocol CandidateControllerDelegate: AnyObject { - func candidateCountForController(_ controller: CandidateController) -> UInt - func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String - func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) + func candidateCountForController(_ controller: CandidateController) -> UInt + func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) + -> String + func candidateController( + _ controller: CandidateController, didSelectCandidateAtIndex index: UInt) } @objc(VTCandidateController) public class CandidateController: NSWindowController { - @objc public weak var delegate: CandidateControllerDelegate? { - didSet { - reloadData() - } - } - @objc public var selectedCandidateIndex: UInt = UInt.max - @objc public var visible: Bool = false { - didSet { - NSObject.cancelPreviousPerformRequests(withTarget: self) - if visible { - window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0) - } else { - window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0) - } - } - } - @objc public var windowTopLeftPoint: NSPoint { - get { - guard let frameRect = window?.frame else { - return NSPoint.zero - } - return NSPoint(x: frameRect.minX, y: frameRect.maxY) - } - set { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { - self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0) - } - } - } + @objc public weak var delegate: CandidateControllerDelegate? { + didSet { + reloadData() + } + } + @objc public var selectedCandidateIndex: UInt = UInt.max + @objc public var visible: Bool = false { + didSet { + NSObject.cancelPreviousPerformRequests(withTarget: self) + if visible { + window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0) + } else { + window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0) + } + } + } + @objc public var windowTopLeftPoint: NSPoint { + get { + guard let frameRect = window?.frame else { + return NSPoint.zero + } + return NSPoint(x: frameRect.minX, y: frameRect.maxY) + } + set { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { + self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0) + } + } + } - @objc public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map { - CandidateKeyLabel(key: $0, displayedText: $0) - } - @objc public var keyLabelFont: NSFont = NSFont.monospacedDigitSystemFont(ofSize: 14, weight: .medium) - @objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18) - @objc public var tooltip: String = "" + @objc public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + .map { + CandidateKeyLabel(key: $0, displayedText: $0) + } + @objc public var keyLabelFont: NSFont = NSFont.monospacedDigitSystemFont( + ofSize: 14, weight: .medium) + @objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18) + @objc public var tooltip: String = "" - @objc public func reloadData() { - } + @objc public func reloadData() { + } - @objc public func showNextPage() -> Bool { - false - } + @objc public func showNextPage() -> Bool { + false + } - @objc public func showPreviousPage() -> Bool { - false - } + @objc public func showPreviousPage() -> Bool { + false + } - @objc public func highlightNextCandidate() -> Bool { - false - } + @objc public func highlightNextCandidate() -> Bool { + false + } - @objc public func highlightPreviousCandidate() -> Bool { - false - } + @objc public func highlightPreviousCandidate() -> Bool { + false + } - @objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { - UInt.max - } + @objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + UInt.max + } - /// Sets the location of the candidate window. - /// - /// Please note that the method has side effects that modifies - /// `windowTopLeftPoint` to make the candidate window to stay in at least - /// in a screen. - /// - /// - Parameters: - /// - windowTopLeftPoint: The given location. - /// - height: The height that helps the window not to be out of the bottom - /// of a screen. - @objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:) - public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { - self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height) - } - } + /// Sets the location of the candidate window. + /// + /// Please note that the method has side effects that modifies + /// `windowTopLeftPoint` to make the candidate window to stay in at least + /// in a screen. + /// + /// - Parameters: + /// - windowTopLeftPoint: The given location. + /// - height: The height that helps the window not to be out of the bottom + /// of a screen. + @objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:) + public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { + self.doSet( + windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height) + } + } - func doSet(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { - var adjustedPoint = windowTopLeftPoint - var adjustedHeight = height + func doSet(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { + var adjustedPoint = windowTopLeftPoint + var adjustedHeight = height - var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero - for screen in NSScreen.screens { - let frame = screen.visibleFrame - if windowTopLeftPoint.x >= frame.minX && - windowTopLeftPoint.x <= frame.maxX && - windowTopLeftPoint.y >= frame.minY && - windowTopLeftPoint.y <= frame.maxY { - screenFrame = frame - break - } - } + var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero + for screen in NSScreen.screens { + let frame = screen.visibleFrame + if windowTopLeftPoint.x >= frame.minX && windowTopLeftPoint.x <= frame.maxX + && windowTopLeftPoint.y >= frame.minY && windowTopLeftPoint.y <= frame.maxY + { + screenFrame = frame + break + } + } - if adjustedHeight > screenFrame.size.height / 2.0 { - adjustedHeight = 0.0 - } + if adjustedHeight > screenFrame.size.height / 2.0 { + adjustedHeight = 0.0 + } - let windowSize = window?.frame.size ?? NSSize.zero + let windowSize = window?.frame.size ?? NSSize.zero - // bottom beneath the screen? - if adjustedPoint.y - windowSize.height < screenFrame.minY { - adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height - } + // bottom beneath the screen? + if adjustedPoint.y - windowSize.height < screenFrame.minY { + adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height + } - // top over the screen? - if adjustedPoint.y >= screenFrame.maxY { - adjustedPoint.y = screenFrame.maxY - 1.0 - } + // top over the screen? + if adjustedPoint.y >= screenFrame.maxY { + adjustedPoint.y = screenFrame.maxY - 1.0 + } - // right - if adjustedPoint.x + windowSize.width >= screenFrame.maxX { - adjustedPoint.x = screenFrame.maxX - windowSize.width - } + // right + if adjustedPoint.x + windowSize.width >= screenFrame.maxX { + adjustedPoint.x = screenFrame.maxX - windowSize.width + } - // left - if adjustedPoint.x < screenFrame.minX { - adjustedPoint.x = screenFrame.minX - } + // left + if adjustedPoint.x < screenFrame.minX { + adjustedPoint.x = screenFrame.minX + } - window?.setFrameTopLeftPoint(adjustedPoint) - } + window?.setFrameTopLeftPoint(adjustedPoint) + } } diff --git a/Source/UI/CandidateUI/HorizontalCandidateController.swift b/Source/UI/CandidateUI/HorizontalCandidateController.swift index bdcb39d5..d6f2be0a 100644 --- a/Source/UI/CandidateUI/HorizontalCandidateController.swift +++ b/Source/UI/CandidateUI/HorizontalCandidateController.swift @@ -1,411 +1,462 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 -fileprivate class HorizontalCandidateView: NSView { - var highlightedIndex: UInt = 0 - var action: Selector? - weak var target: AnyObject? +private class HorizontalCandidateView: NSView { + var highlightedIndex: UInt = 0 + var action: Selector? + weak var target: AnyObject? - private var keyLabels: [String] = [] - private var displayedCandidates: [String] = [] - private var dispCandidatesWithLabels: [String] = [] - private var keyLabelHeight: CGFloat = 0 - private var keyLabelWidth: CGFloat = 0 - private var candidateTextHeight: CGFloat = 0 - private var cellPadding: CGFloat = 0 - private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] - private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:] - private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] - private var elementWidths: [CGFloat] = [] - private var trackingHighlightedIndex: UInt = UInt.max + private var keyLabels: [String] = [] + private var displayedCandidates: [String] = [] + private var dispCandidatesWithLabels: [String] = [] + private var keyLabelHeight: CGFloat = 0 + private var keyLabelWidth: CGFloat = 0 + private var candidateTextHeight: CGFloat = 0 + private var cellPadding: CGFloat = 0 + private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] + private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:] + private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] + private var elementWidths: [CGFloat] = [] + private var trackingHighlightedIndex: UInt = UInt.max - override var isFlipped: Bool { - true - } + override var isFlipped: Bool { + true + } - var sizeForView: NSSize { - var result = NSSize.zero + var sizeForView: NSSize { + var result = NSSize.zero - if !elementWidths.isEmpty { - result.width = elementWidths.reduce(0, +) - result.width += CGFloat(elementWidths.count) - result.height = candidateTextHeight + cellPadding - } - return result - } + if !elementWidths.isEmpty { + result.width = elementWidths.reduce(0, +) + result.width += CGFloat(elementWidths.count) + result.height = candidateTextHeight + cellPadding + } + return result + } - @objc(setKeyLabels:displayedCandidates:) - func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { - let count = min(labels.count, candidates.count) - keyLabels = Array(labels[0.. UInt? { - let location = convert(event.locationInWindow, to: nil) - if !NSPointInRect(location, self.bounds) { - return nil - } - var accuWidth: CGFloat = 0.0 - for index in 0..= accuWidth && location.x <= accuWidth + currentWidth { - return UInt(index) - } - accuWidth += currentWidth + 1.0 - } - return nil + var activeCandidateIndexAttr = keyLabelAttrDict + var activeCandidateAttr = candidateAttrDict + if index == highlightedIndex { + let colorBlendAmount: CGFloat = IME.isDarkMode() ? 0.25 : 0 + // The background color of the highlightened candidate + switch ctlInputMethod.currentKeyHandler.inputMode { + case InputMode.imeModeCHS: + NSColor.systemRed.blended( + withFraction: colorBlendAmount, + of: NSColor.controlBackgroundColor)! + .setFill() + case InputMode.imeModeCHT: + NSColor.systemBlue.blended( + withFraction: colorBlendAmount, + of: NSColor.controlBackgroundColor)! + .setFill() + default: + NSColor.alternateSelectedControlColor.setFill() + } + // Highlightened index text color + activeCandidateIndexAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor + .withAlphaComponent(0.84) + // Highlightened phrase text color + activeCandidateAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor + } else { + NSColor.controlBackgroundColor.setFill() + } + switch ctlInputMethod.currentKeyHandler.inputMode { + case InputMode.imeModeCHS: + if #available(macOS 12.0, *) { + activeCandidateAttr[.languageIdentifier] = "zh-Hans" as AnyObject + } + case InputMode.imeModeCHT: + if #available(macOS 12.0, *) { + activeCandidateAttr[.languageIdentifier] = "zh-Hant" as AnyObject + } + default: + break + } + NSBezierPath.fill(rctCandidateArea) + (keyLabels[index] as NSString).draw( + in: rctLabel, withAttributes: activeCandidateIndexAttr) + (displayedCandidates[index] as NSString).draw( + in: rctCandidatePhrase, withAttributes: activeCandidateAttr) + accuWidth += currentWidth + 1.0 + } + } - } + private func findHitIndex(event: NSEvent) -> UInt? { + let location = convert(event.locationInWindow, to: nil) + if !NSPointInRect(location, self.bounds) { + return nil + } + var accuWidth: CGFloat = 0.0 + for index in 0..= accuWidth && location.x <= accuWidth + currentWidth { + return UInt(index) + } + accuWidth += currentWidth + 1.0 + } + return nil - override func mouseDown(with event: NSEvent) { - guard let newIndex = findHitIndex(event: event) else { - return - } - var triggerAction = false - if newIndex == highlightedIndex { - triggerAction = true - } else { - highlightedIndex = trackingHighlightedIndex - } + } - trackingHighlightedIndex = 0 - self.setNeedsDisplay(self.bounds) - if triggerAction { - if let target = target as? NSObject, let action = action { - target.perform(action, with: self) - } - } - } + override func mouseUp(with event: NSEvent) { + trackingHighlightedIndex = highlightedIndex + guard let newIndex = findHitIndex(event: event) else { + return + } + highlightedIndex = newIndex + self.setNeedsDisplay(self.bounds) + } + + override func mouseDown(with event: NSEvent) { + guard let newIndex = findHitIndex(event: event) else { + return + } + var triggerAction = false + if newIndex == highlightedIndex { + triggerAction = true + } else { + highlightedIndex = trackingHighlightedIndex + } + + trackingHighlightedIndex = 0 + self.setNeedsDisplay(self.bounds) + if triggerAction { + if let target = target as? NSObject, let action = action { + target.perform(action, with: self) + } + } + } } @objc(VTHorizontalCandidateController) public class HorizontalCandidateController: CandidateController { - private var candidateView: HorizontalCandidateView - private var prevPageButton: NSButton - private var nextPageButton: NSButton - private var currentPage: UInt = 0 + private var candidateView: HorizontalCandidateView + private var prevPageButton: NSButton + private var nextPageButton: NSButton + private var currentPage: UInt = 0 - public init() { - var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) - let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] - let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) - panel.hasShadow = true - panel.isOpaque = false - panel.backgroundColor = NSColor.clear - - contentRect.origin = NSPoint.zero - candidateView = HorizontalCandidateView(frame: contentRect) + public init() { + var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) + let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] + let panel = NSPanel( + contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.hasShadow = true + panel.isOpaque = false + panel.backgroundColor = NSColor.clear - candidateView.wantsLayer = true - candidateView.layer?.borderColor = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor - candidateView.layer?.borderWidth = 1.0 - if #available(macOS 10.13, *) { - candidateView.layer?.cornerRadius = 6.0 - } + contentRect.origin = NSPoint.zero + candidateView = HorizontalCandidateView(frame: contentRect) - panel.contentView?.addSubview(candidateView) - - contentRect.size = NSSize(width: 20.0, height: 10.0) // Reduce the button width - let buttonAttribute: [NSAttributedString.Key : Any] = [.font : NSFont.systemFont(ofSize: 9.0)] + candidateView.wantsLayer = true + candidateView.layer?.borderColor = + NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor + candidateView.layer?.borderWidth = 1.0 + if #available(macOS 10.13, *) { + candidateView.layer?.cornerRadius = 6.0 + } - nextPageButton = NSButton(frame: contentRect) - NSColor.controlBackgroundColor.setFill() - NSBezierPath.fill(nextPageButton.bounds) - nextPageButton.wantsLayer = true - nextPageButton.layer?.masksToBounds = true - nextPageButton.layer?.borderColor = NSColor.clear.cgColor - nextPageButton.layer?.borderWidth = 0.0 - nextPageButton.setButtonType(.momentaryLight) - nextPageButton.bezelStyle = .disclosure - nextPageButton.userInterfaceLayoutDirection = .leftToRight - nextPageButton.attributedTitle = NSMutableAttributedString(string: " ", attributes: buttonAttribute) // Next Page Arrow - prevPageButton = NSButton(frame: contentRect) - NSColor.controlBackgroundColor.setFill() - NSBezierPath.fill(prevPageButton.bounds) - prevPageButton.wantsLayer = true - prevPageButton.layer?.masksToBounds = true - prevPageButton.layer?.borderColor = NSColor.clear.cgColor - prevPageButton.layer?.borderWidth = 0.0 - prevPageButton.setButtonType(.momentaryLight) - prevPageButton.bezelStyle = .disclosure - prevPageButton.userInterfaceLayoutDirection = .rightToLeft - prevPageButton.attributedTitle = NSMutableAttributedString(string: " ", attributes: buttonAttribute) // Previous Page Arrow - panel.contentView?.addSubview(nextPageButton) - panel.contentView?.addSubview(prevPageButton) + panel.contentView?.addSubview(candidateView) - super.init(window: panel) + contentRect.size = NSSize(width: 20.0, height: 10.0) // Reduce the button width + let buttonAttribute: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: 9.0)] - candidateView.target = self - candidateView.action = #selector(candidateViewMouseDidClick(_:)) + nextPageButton = NSButton(frame: contentRect) + NSColor.controlBackgroundColor.setFill() + NSBezierPath.fill(nextPageButton.bounds) + nextPageButton.wantsLayer = true + nextPageButton.layer?.masksToBounds = true + nextPageButton.layer?.borderColor = NSColor.clear.cgColor + nextPageButton.layer?.borderWidth = 0.0 + nextPageButton.setButtonType(.momentaryLight) + nextPageButton.bezelStyle = .disclosure + nextPageButton.userInterfaceLayoutDirection = .leftToRight + nextPageButton.attributedTitle = NSMutableAttributedString( + string: " ", attributes: buttonAttribute) // Next Page Arrow + prevPageButton = NSButton(frame: contentRect) + NSColor.controlBackgroundColor.setFill() + NSBezierPath.fill(prevPageButton.bounds) + prevPageButton.wantsLayer = true + prevPageButton.layer?.masksToBounds = true + prevPageButton.layer?.borderColor = NSColor.clear.cgColor + prevPageButton.layer?.borderWidth = 0.0 + prevPageButton.setButtonType(.momentaryLight) + prevPageButton.bezelStyle = .disclosure + prevPageButton.userInterfaceLayoutDirection = .rightToLeft + prevPageButton.attributedTitle = NSMutableAttributedString( + string: " ", attributes: buttonAttribute) // Previous Page Arrow + panel.contentView?.addSubview(nextPageButton) + panel.contentView?.addSubview(prevPageButton) - nextPageButton.target = self - nextPageButton.action = #selector(pageButtonAction(_:)) + super.init(window: panel) - prevPageButton.target = self - prevPageButton.action = #selector(pageButtonAction(_:)) - } + candidateView.target = self + candidateView.action = #selector(candidateViewMouseDidClick(_:)) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + nextPageButton.target = self + nextPageButton.action = #selector(pageButtonAction(_:)) - public override func reloadData() { - candidateView.highlightedIndex = 0 - currentPage = 0 - layoutCandidateView() - } + prevPageButton.target = self + prevPageButton.action = #selector(pageButtonAction(_:)) + } - public override func showNextPage() -> Bool { - guard delegate != nil else {return false} - if pageCount == 1 {return highlightNextCandidate()} - currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1 - candidateView.highlightedIndex = 0 - layoutCandidateView() - return true - } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - public override func showPreviousPage() -> Bool { - guard delegate != nil else {return false} - if pageCount == 1 {return highlightPreviousCandidate()} - currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1 - candidateView.highlightedIndex = 0 - layoutCandidateView() - return true - } + public override func reloadData() { + candidateView.highlightedIndex = 0 + currentPage = 0 + layoutCandidateView() + } - public override func highlightNextCandidate() -> Bool { - guard let delegate = delegate else {return false} - selectedCandidateIndex = (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self)) ? 0 : selectedCandidateIndex + 1 - return true - } + public override func showNextPage() -> Bool { + guard delegate != nil else { return false } + if pageCount == 1 { return highlightNextCandidate() } + currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1 + candidateView.highlightedIndex = 0 + layoutCandidateView() + return true + } - public override func highlightPreviousCandidate() -> Bool { - guard let delegate = delegate else {return false} - selectedCandidateIndex = (selectedCandidateIndex == 0) ? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1 - return true - } + public override func showPreviousPage() -> Bool { + guard delegate != nil else { return false } + if pageCount == 1 { return highlightPreviousCandidate() } + currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1 + candidateView.highlightedIndex = 0 + layoutCandidateView() + return true + } - public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { - guard let delegate = delegate else { - return UInt.max - } + public override func highlightNextCandidate() -> Bool { + guard let delegate = delegate else { return false } + selectedCandidateIndex = + (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self)) + ? 0 : selectedCandidateIndex + 1 + return true + } - let result = currentPage * UInt(keyLabels.count) + index - return result < delegate.candidateCountForController(self) ? result : UInt.max - } + public override func highlightPreviousCandidate() -> Bool { + guard let delegate = delegate else { return false } + selectedCandidateIndex = + (selectedCandidateIndex == 0) + ? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1 + return true + } - public override var selectedCandidateIndex: UInt { - get { - currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex - } - set { - guard let delegate = delegate else { - return - } - let keyLabelCount = UInt(keyLabels.count) - if newValue < delegate.candidateCountForController(self) { - currentPage = newValue / keyLabelCount - candidateView.highlightedIndex = newValue % keyLabelCount - layoutCandidateView() - } - } - } + public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + guard let delegate = delegate else { + return UInt.max + } + + let result = currentPage * UInt(keyLabels.count) + index + return result < delegate.candidateCountForController(self) ? result : UInt.max + } + + public override var selectedCandidateIndex: UInt { + get { + currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex + } + set { + guard let delegate = delegate else { + return + } + let keyLabelCount = UInt(keyLabels.count) + if newValue < delegate.candidateCountForController(self) { + currentPage = newValue / keyLabelCount + candidateView.highlightedIndex = newValue % keyLabelCount + layoutCandidateView() + } + } + } } extension HorizontalCandidateController { - private var pageCount: UInt { - guard let delegate = delegate else { - return 0 - } - let totalCount = delegate.candidateCountForController(self) - let keyLabelCount = UInt(keyLabels.count) - return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) - } + private var pageCount: UInt { + guard let delegate = delegate else { + return 0 + } + let totalCount = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) + return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) + } - private func layoutCandidateView() { - guard let delegate = delegate else { - return - } + private func layoutCandidateView() { + guard let delegate = delegate else { + return + } - candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) - var candidates = [String]() - let count = delegate.candidateCountForController(self) - let keyLabelCount = UInt(keyLabels.count) + candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) + var candidates = [String]() + let count = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) - let begin = currentPage * keyLabelCount - for index in begin.. 1 && mgrPrefs.showPageButtonsInCandidateWindow { - var buttonRect = nextPageButton.frame - let spacing:CGFloat = 0.0 + if pageCount > 1 && mgrPrefs.showPageButtonsInCandidateWindow { + var buttonRect = nextPageButton.frame + let spacing: CGFloat = 0.0 - buttonRect.size.height = floor(newSize.height / 2) + buttonRect.size.height = floor(newSize.height / 2) - let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0 - buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY) - nextPageButton.frame = buttonRect + let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0 + buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY) + nextPageButton.frame = buttonRect - buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing) - prevPageButton.frame = buttonRect + buttonRect.origin = NSPoint( + x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing) + prevPageButton.frame = buttonRect - newSize.width += 20 - nextPageButton.isHidden = false - prevPageButton.isHidden = false - } else { - nextPageButton.isHidden = true - prevPageButton.isHidden = true - } + newSize.width += 20 + nextPageButton.isHidden = false + prevPageButton.isHidden = false + } else { + nextPageButton.isHidden = true + prevPageButton.isHidden = true + } - frameRect = window?.frame ?? NSRect.zero + frameRect = window?.frame ?? NSRect.zero - let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height) - frameRect.size = newSize - frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) - self.window?.setFrame(frameRect, display: false) - candidateView.setNeedsDisplay(candidateView.bounds) - } + let topLeftPoint = NSMakePoint( + frameRect.origin.x, frameRect.origin.y + frameRect.size.height) + frameRect.size = newSize + frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) + self.window?.setFrame(frameRect, display: false) + candidateView.setNeedsDisplay(candidateView.bounds) + } - @objc fileprivate func pageButtonAction(_ sender: Any) { - guard let sender = sender as? NSButton else { - return - } - if sender == nextPageButton { - _ = showNextPage() - } else if sender == prevPageButton { - _ = showPreviousPage() - } - } + @objc fileprivate func pageButtonAction(_ sender: Any) { + guard let sender = sender as? NSButton else { + return + } + if sender == nextPageButton { + _ = showNextPage() + } else if sender == prevPageButton { + _ = showPreviousPage() + } + } - @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { - delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex) - } + @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { + delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex) + } } diff --git a/Source/UI/CandidateUI/VerticalCandidateController.swift b/Source/UI/CandidateUI/VerticalCandidateController.swift index 7f3acb8d..3c571c90 100644 --- a/Source/UI/CandidateUI/VerticalCandidateController.swift +++ b/Source/UI/CandidateUI/VerticalCandidateController.swift @@ -1,417 +1,467 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 -fileprivate class VerticalCandidateView: NSView { - var highlightedIndex: UInt = 0 - var action: Selector? - weak var target: AnyObject? +private class VerticalCandidateView: NSView { + var highlightedIndex: UInt = 0 + var action: Selector? + weak var target: AnyObject? - private var keyLabels: [String] = [] - private var displayedCandidates: [String] = [] - private var dispCandidatesWithLabels: [String] = [] - private var keyLabelHeight: CGFloat = 0 - private var keyLabelWidth: CGFloat = 0 - private var candidateTextHeight: CGFloat = 0 - private var cellPadding: CGFloat = 0 - private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] - private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:] - private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] - private var windowWidth: CGFloat = 0 - private var elementWidths: [CGFloat] = [] - private var elementHeights: [CGFloat] = [] - private var trackingHighlightedIndex: UInt = UInt.max + private var keyLabels: [String] = [] + private var displayedCandidates: [String] = [] + private var dispCandidatesWithLabels: [String] = [] + private var keyLabelHeight: CGFloat = 0 + private var keyLabelWidth: CGFloat = 0 + private var candidateTextHeight: CGFloat = 0 + private var cellPadding: CGFloat = 0 + private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] + private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:] + private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] + private var windowWidth: CGFloat = 0 + private var elementWidths: [CGFloat] = [] + private var elementHeights: [CGFloat] = [] + private var trackingHighlightedIndex: UInt = UInt.max - override var isFlipped: Bool { - true - } + override var isFlipped: Bool { + true + } - var sizeForView: NSSize { - var result = NSSize.zero + var sizeForView: NSSize { + var result = NSSize.zero - if !elementWidths.isEmpty { - result.width = windowWidth - result.height = elementHeights.reduce(0, +) - } - return result - } + if !elementWidths.isEmpty { + result.width = windowWidth + result.height = elementHeights.reduce(0, +) + } + return result + } - @objc(setKeyLabels:displayedCandidates:) - func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { - let count = min(labels.count, candidates.count) - keyLabels = Array(labels[0.. UInt? { - let location = convert(event.locationInWindow, to: nil) - if !NSPointInRect(location, self.bounds) { - return nil - } - var accuHeight: CGFloat = 0.0 - for index in 0..= accuHeight && location.y <= accuHeight + currentHeight { - return UInt(index) - } - accuHeight += currentHeight - } - return nil + var activeCandidateIndexAttr = keyLabelAttrDict + var activeCandidateAttr = candidateAttrDict + if index == highlightedIndex { + let colorBlendAmount: CGFloat = IME.isDarkMode() ? 0.25 : 0 + // The background color of the highlightened candidate + switch ctlInputMethod.currentKeyHandler.inputMode { + case InputMode.imeModeCHS: + NSColor.systemRed.blended( + withFraction: colorBlendAmount, + of: NSColor.controlBackgroundColor)! + .setFill() + case InputMode.imeModeCHT: + NSColor.systemBlue.blended( + withFraction: colorBlendAmount, + of: NSColor.controlBackgroundColor)! + .setFill() + default: + NSColor.alternateSelectedControlColor.setFill() + } + // Highlightened index text color + activeCandidateIndexAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor + .withAlphaComponent(0.84) + // Highlightened phrase text color + activeCandidateAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor + } else { + NSColor.controlBackgroundColor.setFill() + } + switch ctlInputMethod.currentKeyHandler.inputMode { + case InputMode.imeModeCHS: + if #available(macOS 12.0, *) { + activeCandidateAttr[.languageIdentifier] = "zh-Hans" as AnyObject + } + case InputMode.imeModeCHT: + if #available(macOS 12.0, *) { + activeCandidateAttr[.languageIdentifier] = "zh-Hant" as AnyObject + } + default: + break + } + NSBezierPath.fill(rctCandidateArea) + (keyLabels[index] as NSString).draw( + in: rctLabel, withAttributes: activeCandidateIndexAttr) + (displayedCandidates[index] as NSString).draw( + in: rctCandidatePhrase, withAttributes: activeCandidateAttr) + accuHeight += currentHeight + } + } - } + private func findHitIndex(event: NSEvent) -> UInt? { + let location = convert(event.locationInWindow, to: nil) + if !NSPointInRect(location, self.bounds) { + return nil + } + var accuHeight: CGFloat = 0.0 + for index in 0..= accuHeight && location.y <= accuHeight + currentHeight { + return UInt(index) + } + accuHeight += currentHeight + } + return nil - override func mouseDown(with event: NSEvent) { - guard let newIndex = findHitIndex(event: event) else { - return - } - var triggerAction = false - if newIndex == highlightedIndex { - triggerAction = true - } else { - highlightedIndex = trackingHighlightedIndex - } + } - trackingHighlightedIndex = 0 - self.setNeedsDisplay(self.bounds) - if triggerAction { - if let target = target as? NSObject, let action = action { - target.perform(action, with: self) - } - } - } + override func mouseUp(with event: NSEvent) { + trackingHighlightedIndex = highlightedIndex + guard let newIndex = findHitIndex(event: event) else { + return + } + highlightedIndex = newIndex + self.setNeedsDisplay(self.bounds) + } + + override func mouseDown(with event: NSEvent) { + guard let newIndex = findHitIndex(event: event) else { + return + } + var triggerAction = false + if newIndex == highlightedIndex { + triggerAction = true + } else { + highlightedIndex = trackingHighlightedIndex + } + + trackingHighlightedIndex = 0 + self.setNeedsDisplay(self.bounds) + if triggerAction { + if let target = target as? NSObject, let action = action { + target.perform(action, with: self) + } + } + } } @objc(VTVerticalCandidateController) public class VerticalCandidateController: CandidateController { - private var candidateView: VerticalCandidateView - private var prevPageButton: NSButton - private var nextPageButton: NSButton - private var currentPage: UInt = 0 + private var candidateView: VerticalCandidateView + private var prevPageButton: NSButton + private var nextPageButton: NSButton + private var currentPage: UInt = 0 - public init() { - var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) - let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] - let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) - panel.hasShadow = true - panel.isOpaque = false - panel.backgroundColor = NSColor.clear - - contentRect.origin = NSPoint.zero - candidateView = VerticalCandidateView(frame: contentRect) + public init() { + var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) + let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] + let panel = NSPanel( + contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.hasShadow = true + panel.isOpaque = false + panel.backgroundColor = NSColor.clear - candidateView.wantsLayer = true - candidateView.layer?.borderColor = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor - candidateView.layer?.borderWidth = 1.0 - if #available(macOS 10.13, *) { - candidateView.layer?.cornerRadius = 6.0 - } + contentRect.origin = NSPoint.zero + candidateView = VerticalCandidateView(frame: contentRect) - panel.contentView?.addSubview(candidateView) + candidateView.wantsLayer = true + candidateView.layer?.borderColor = + NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor + candidateView.layer?.borderWidth = 1.0 + if #available(macOS 10.13, *) { + candidateView.layer?.cornerRadius = 6.0 + } - contentRect.size = NSSize(width: 20.0, height: 10.0) // Reduce the button width - let buttonAttribute: [NSAttributedString.Key : Any] = [.font : NSFont.systemFont(ofSize: 9.0)] + panel.contentView?.addSubview(candidateView) - nextPageButton = NSButton(frame: contentRect) - NSColor.controlBackgroundColor.setFill() - NSBezierPath.fill(nextPageButton.bounds) - nextPageButton.wantsLayer = true - nextPageButton.layer?.masksToBounds = true - nextPageButton.layer?.borderColor = NSColor.clear.cgColor - nextPageButton.layer?.borderWidth = 0.0 - nextPageButton.setButtonType(.momentaryLight) - nextPageButton.bezelStyle = .disclosure - nextPageButton.userInterfaceLayoutDirection = .leftToRight - nextPageButton.attributedTitle = NSMutableAttributedString(string: " ", attributes: buttonAttribute) // Next Page Arrow - prevPageButton = NSButton(frame: contentRect) - NSColor.controlBackgroundColor.setFill() - NSBezierPath.fill(prevPageButton.bounds) - prevPageButton.wantsLayer = true - prevPageButton.layer?.masksToBounds = true - prevPageButton.layer?.borderColor = NSColor.clear.cgColor - prevPageButton.layer?.borderWidth = 0.0 - prevPageButton.setButtonType(.momentaryLight) - prevPageButton.bezelStyle = .disclosure - prevPageButton.userInterfaceLayoutDirection = .rightToLeft - prevPageButton.attributedTitle = NSMutableAttributedString(string: " ", attributes: buttonAttribute) // Previous Page Arrow - panel.contentView?.addSubview(nextPageButton) - panel.contentView?.addSubview(prevPageButton) + contentRect.size = NSSize(width: 20.0, height: 10.0) // Reduce the button width + let buttonAttribute: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: 9.0)] - super.init(window: panel) + nextPageButton = NSButton(frame: contentRect) + NSColor.controlBackgroundColor.setFill() + NSBezierPath.fill(nextPageButton.bounds) + nextPageButton.wantsLayer = true + nextPageButton.layer?.masksToBounds = true + nextPageButton.layer?.borderColor = NSColor.clear.cgColor + nextPageButton.layer?.borderWidth = 0.0 + nextPageButton.setButtonType(.momentaryLight) + nextPageButton.bezelStyle = .disclosure + nextPageButton.userInterfaceLayoutDirection = .leftToRight + nextPageButton.attributedTitle = NSMutableAttributedString( + string: " ", attributes: buttonAttribute) // Next Page Arrow + prevPageButton = NSButton(frame: contentRect) + NSColor.controlBackgroundColor.setFill() + NSBezierPath.fill(prevPageButton.bounds) + prevPageButton.wantsLayer = true + prevPageButton.layer?.masksToBounds = true + prevPageButton.layer?.borderColor = NSColor.clear.cgColor + prevPageButton.layer?.borderWidth = 0.0 + prevPageButton.setButtonType(.momentaryLight) + prevPageButton.bezelStyle = .disclosure + prevPageButton.userInterfaceLayoutDirection = .rightToLeft + prevPageButton.attributedTitle = NSMutableAttributedString( + string: " ", attributes: buttonAttribute) // Previous Page Arrow + panel.contentView?.addSubview(nextPageButton) + panel.contentView?.addSubview(prevPageButton) - candidateView.target = self - candidateView.action = #selector(candidateViewMouseDidClick(_:)) + super.init(window: panel) - nextPageButton.target = self - nextPageButton.action = #selector(pageButtonAction(_:)) + candidateView.target = self + candidateView.action = #selector(candidateViewMouseDidClick(_:)) - prevPageButton.target = self - prevPageButton.action = #selector(pageButtonAction(_:)) - } + nextPageButton.target = self + nextPageButton.action = #selector(pageButtonAction(_:)) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + prevPageButton.target = self + prevPageButton.action = #selector(pageButtonAction(_:)) + } - public override func reloadData() { - candidateView.highlightedIndex = 0 - currentPage = 0 - layoutCandidateView() - } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - public override func showNextPage() -> Bool { - guard delegate != nil else {return false} - if pageCount == 1 {return highlightNextCandidate()} - currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1 - candidateView.highlightedIndex = 0 - layoutCandidateView() - return true - } + public override func reloadData() { + candidateView.highlightedIndex = 0 + currentPage = 0 + layoutCandidateView() + } - public override func showPreviousPage() -> Bool { - guard delegate != nil else {return false} - if pageCount == 1 {return highlightPreviousCandidate()} - currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1 - candidateView.highlightedIndex = 0 - layoutCandidateView() - return true - } + public override func showNextPage() -> Bool { + guard delegate != nil else { return false } + if pageCount == 1 { return highlightNextCandidate() } + currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1 + candidateView.highlightedIndex = 0 + layoutCandidateView() + return true + } - public override func highlightNextCandidate() -> Bool { - guard let delegate = delegate else {return false} - selectedCandidateIndex = (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self)) ? 0 : selectedCandidateIndex + 1 - return true - } + public override func showPreviousPage() -> Bool { + guard delegate != nil else { return false } + if pageCount == 1 { return highlightPreviousCandidate() } + currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1 + candidateView.highlightedIndex = 0 + layoutCandidateView() + return true + } - public override func highlightPreviousCandidate() -> Bool { - guard let delegate = delegate else {return false} - selectedCandidateIndex = (selectedCandidateIndex == 0) ? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1 - return true - } + public override func highlightNextCandidate() -> Bool { + guard let delegate = delegate else { return false } + selectedCandidateIndex = + (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self)) + ? 0 : selectedCandidateIndex + 1 + return true + } - public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { - guard let delegate = delegate else { - return UInt.max - } + public override func highlightPreviousCandidate() -> Bool { + guard let delegate = delegate else { return false } + selectedCandidateIndex = + (selectedCandidateIndex == 0) + ? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1 + return true + } - let result = currentPage * UInt(keyLabels.count) + index - return result < delegate.candidateCountForController(self) ? result : UInt.max - } + public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + guard let delegate = delegate else { + return UInt.max + } - public override var selectedCandidateIndex: UInt { - get { - currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex - } - set { - guard let delegate = delegate else { - return - } - let keyLabelCount = UInt(keyLabels.count) - if newValue < delegate.candidateCountForController(self) { - currentPage = newValue / keyLabelCount - candidateView.highlightedIndex = newValue % keyLabelCount - layoutCandidateView() - } - } - } + let result = currentPage * UInt(keyLabels.count) + index + return result < delegate.candidateCountForController(self) ? result : UInt.max + } + + public override var selectedCandidateIndex: UInt { + get { + currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex + } + set { + guard let delegate = delegate else { + return + } + let keyLabelCount = UInt(keyLabels.count) + if newValue < delegate.candidateCountForController(self) { + currentPage = newValue / keyLabelCount + candidateView.highlightedIndex = newValue % keyLabelCount + layoutCandidateView() + } + } + } } extension VerticalCandidateController { - private var pageCount: UInt { - guard let delegate = delegate else { - return 0 - } - let totalCount = delegate.candidateCountForController(self) - let keyLabelCount = UInt(keyLabels.count) - return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) - } + private var pageCount: UInt { + guard let delegate = delegate else { + return 0 + } + let totalCount = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) + return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) + } - private func layoutCandidateView() { - guard let delegate = delegate else { - return - } + private func layoutCandidateView() { + guard let delegate = delegate else { + return + } - candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) - var candidates = [String]() - let count = delegate.candidateCountForController(self) - let keyLabelCount = UInt(keyLabels.count) + candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) + var candidates = [String]() + let count = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) - let begin = currentPage * keyLabelCount - for index in begin.. 1 && mgrPrefs.showPageButtonsInCandidateWindow { - var buttonRect = nextPageButton.frame - let spacing:CGFloat = 0.0 + if pageCount > 1 && mgrPrefs.showPageButtonsInCandidateWindow { + var buttonRect = nextPageButton.frame + let spacing: CGFloat = 0.0 - // buttonRect.size.height = floor(candidateTextHeight + cellPadding / 2) + // buttonRect.size.height = floor(candidateTextHeight + cellPadding / 2) - let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) // / 2.0 - buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY) - nextPageButton.frame = buttonRect + let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) // / 2.0 + buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY) + nextPageButton.frame = buttonRect - buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing) - prevPageButton.frame = buttonRect + buttonRect.origin = NSPoint( + x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing) + prevPageButton.frame = buttonRect - newSize.width += 20 - nextPageButton.isHidden = false - prevPageButton.isHidden = false - } else { - nextPageButton.isHidden = true - prevPageButton.isHidden = true - } + newSize.width += 20 + nextPageButton.isHidden = false + prevPageButton.isHidden = false + } else { + nextPageButton.isHidden = true + prevPageButton.isHidden = true + } - frameRect = window?.frame ?? NSRect.zero + frameRect = window?.frame ?? NSRect.zero - let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height) - frameRect.size = newSize - frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) - self.window?.setFrame(frameRect, display: false) - candidateView.setNeedsDisplay(candidateView.bounds) - } + let topLeftPoint = NSMakePoint( + frameRect.origin.x, frameRect.origin.y + frameRect.size.height) + frameRect.size = newSize + frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) + self.window?.setFrame(frameRect, display: false) + candidateView.setNeedsDisplay(candidateView.bounds) + } - @objc fileprivate func pageButtonAction(_ sender: Any) { - guard let sender = sender as? NSButton else { - return - } - if sender == nextPageButton { - _ = showNextPage() - } else if sender == prevPageButton { - _ = showPreviousPage() - } - } + @objc fileprivate func pageButtonAction(_ sender: Any) { + guard let sender = sender as? NSButton else { + return + } + if sender == nextPageButton { + _ = showNextPage() + } else if sender == prevPageButton { + _ = showPreviousPage() + } + } - @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { - delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex) - } + @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { + delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex) + } } diff --git a/Source/UI/NotifierUI/NotifierController.swift b/Source/UI/NotifierUI/NotifierController.swift index d4f83084..54bcbfff 100644 --- a/Source/UI/NotifierUI/NotifierController.swift +++ b/Source/UI/NotifierUI/NotifierController.swift @@ -1,203 +1,216 @@ // Copyright (c) 2021 and onwards Weizhong Yang (MIT License). // All possible vChewing-specific modifications are (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: +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. +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. +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. +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 private protocol NotifierWindowDelegate: AnyObject { - func windowDidBecomeClicked(_ window: NotifierWindow) + func windowDidBecomeClicked(_ window: NotifierWindow) } private class NotifierWindow: NSWindow { - weak var clickDelegate: NotifierWindowDelegate? + weak var clickDelegate: NotifierWindowDelegate? - override func mouseDown(with event: NSEvent) { - clickDelegate?.windowDidBecomeClicked(self) - } + override func mouseDown(with event: NSEvent) { + clickDelegate?.windowDidBecomeClicked(self) + } } private let kWindowWidth: CGFloat = 213.0 private let kWindowHeight: CGFloat = 60.0 public class NotifierController: NSWindowController, NotifierWindowDelegate { - private var messageTextField: NSTextField + private var messageTextField: NSTextField - private var message: String = "" { - didSet { - let paraStyle = NSMutableParagraphStyle() - paraStyle.setParagraphStyle(NSParagraphStyle.default) - paraStyle.alignment = .center - let attr: [NSAttributedString.Key: AnyObject] = [ - .foregroundColor: foregroundColor, - .font: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)), - .paragraphStyle: paraStyle - ] - let attrString = NSAttributedString(string: message, attributes: attr) - messageTextField.attributedStringValue = attrString - let width = window?.frame.width ?? kWindowWidth - let rect = attrString.boundingRect(with: NSSize(width: width, height: 1600), options: .usesLineFragmentOrigin) - let height = rect.height - let x = messageTextField.frame.origin.x - let y = ((window?.frame.height ?? kWindowHeight) - height) / 2 - let newFrame = NSRect(x: x, y: y, width: width, height: height) - messageTextField.frame = newFrame - } - } - private var shouldStay: Bool = false - private var backgroundColor: NSColor = .textBackgroundColor { - didSet { - self.window?.backgroundColor = backgroundColor - } - } - private var foregroundColor: NSColor = .controlTextColor { - didSet { - self.messageTextField.textColor = foregroundColor - } - } - private var waitTimer: Timer? - private var fadeTimer: Timer? + private var message: String = "" { + didSet { + let paraStyle = NSMutableParagraphStyle() + paraStyle.setParagraphStyle(NSParagraphStyle.default) + paraStyle.alignment = .center + let attr: [NSAttributedString.Key: AnyObject] = [ + .foregroundColor: foregroundColor, + .font: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)), + .paragraphStyle: paraStyle, + ] + let attrString = NSAttributedString(string: message, attributes: attr) + messageTextField.attributedStringValue = attrString + let width = window?.frame.width ?? kWindowWidth + let rect = attrString.boundingRect( + with: NSSize(width: width, height: 1600), options: .usesLineFragmentOrigin) + let height = rect.height + let x = messageTextField.frame.origin.x + let y = ((window?.frame.height ?? kWindowHeight) - height) / 2 + let newFrame = NSRect(x: x, y: y, width: width, height: height) + messageTextField.frame = newFrame + } + } + private var shouldStay: Bool = false + private var backgroundColor: NSColor = .textBackgroundColor { + didSet { + self.window?.backgroundColor = backgroundColor + } + } + private var foregroundColor: NSColor = .controlTextColor { + didSet { + self.messageTextField.textColor = foregroundColor + } + } + private var waitTimer: Timer? + private var fadeTimer: Timer? - private static var instanceCount = 0 - private static var lastLocation = NSPoint.zero + private static var instanceCount = 0 + private static var lastLocation = NSPoint.zero - @objc public static func notify(message: String, stay: Bool = false) { - let controller = NotifierController() - controller.message = message - controller.shouldStay = stay - controller.show() - } + @objc public static func notify(message: String, stay: Bool = false) { + let controller = NotifierController() + controller.message = message + controller.shouldStay = stay + controller.show() + } - private static func increaseInstanceCount() { - instanceCount += 1 - } + private static func increaseInstanceCount() { + instanceCount += 1 + } - private static func decreaseInstanceCount() { - instanceCount -= 1 - if instanceCount < 0 { - instanceCount = 0 - } - } + private static func decreaseInstanceCount() { + instanceCount -= 1 + if instanceCount < 0 { + instanceCount = 0 + } + } - private init() { - let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero - let contentRect = NSRect(x: 0, y: 0, width: kWindowWidth, height: kWindowHeight) - var windowRect = contentRect - windowRect.origin.x = screenRect.maxX - windowRect.width - 10 - windowRect.origin.y = screenRect.maxY - windowRect.height - 10 - let styleMask: NSWindow.StyleMask = [.fullSizeContentView, .titled] + private init() { + let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero + let contentRect = NSRect(x: 0, y: 0, width: kWindowWidth, height: kWindowHeight) + var windowRect = contentRect + windowRect.origin.x = screenRect.maxX - windowRect.width - 10 + windowRect.origin.y = screenRect.maxY - windowRect.height - 10 + let styleMask: NSWindow.StyleMask = [.fullSizeContentView, .titled] - let transparentVisualEffect = NSVisualEffectView() - transparentVisualEffect.blendingMode = .behindWindow - transparentVisualEffect.state = .active + let transparentVisualEffect = NSVisualEffectView() + transparentVisualEffect.blendingMode = .behindWindow + transparentVisualEffect.state = .active - let panel = NotifierWindow(contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false) - panel.contentView = transparentVisualEffect - panel.isMovableByWindowBackground = true - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel)) - panel.hasShadow = true - panel.backgroundColor = backgroundColor - panel.title = "" - panel.titlebarAppearsTransparent = true - panel.titleVisibility = .hidden - panel.showsToolbarButton = false - panel.standardWindowButton(NSWindow.ButtonType.fullScreenButton)?.isHidden = true - panel.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true - panel.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true - panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true + let panel = NotifierWindow( + contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.contentView = transparentVisualEffect + panel.isMovableByWindowBackground = true + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel)) + panel.hasShadow = true + panel.backgroundColor = backgroundColor + panel.title = "" + panel.titlebarAppearsTransparent = true + panel.titleVisibility = .hidden + panel.showsToolbarButton = false + panel.standardWindowButton(NSWindow.ButtonType.fullScreenButton)?.isHidden = true + panel.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true + panel.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true + panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true - messageTextField = NSTextField() - messageTextField.frame = contentRect - messageTextField.isEditable = false - messageTextField.isSelectable = false - messageTextField.isBezeled = false - messageTextField.textColor = foregroundColor - messageTextField.drawsBackground = false - messageTextField.font = .boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)) - panel.contentView?.addSubview(messageTextField) + messageTextField = NSTextField() + messageTextField.frame = contentRect + messageTextField.isEditable = false + messageTextField.isSelectable = false + messageTextField.isBezeled = false + messageTextField.textColor = foregroundColor + messageTextField.drawsBackground = false + messageTextField.font = .boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)) + panel.contentView?.addSubview(messageTextField) - super.init(window: panel) + super.init(window: panel) - panel.clickDelegate = self - } + panel.clickDelegate = self + } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - private func show() { - func setStartLocation() { - if NotifierController.instanceCount == 0 { - return - } - let lastLocation = NotifierController.lastLocation - let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero - var windowRect = self.window?.frame ?? NSRect.zero - windowRect.origin.x = lastLocation.x - windowRect.origin.y = lastLocation.y - 10 - windowRect.height + private func show() { + func setStartLocation() { + if NotifierController.instanceCount == 0 { + return + } + let lastLocation = NotifierController.lastLocation + let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero + var windowRect = self.window?.frame ?? NSRect.zero + windowRect.origin.x = lastLocation.x + windowRect.origin.y = lastLocation.y - 10 - windowRect.height - if windowRect.origin.y < screenRect.minY { - return - } + if windowRect.origin.y < screenRect.minY { + return + } - self.window?.setFrame(windowRect, display: true) - } + self.window?.setFrame(windowRect, display: true) + } - func moveIn() { - let afterRect = self.window?.frame ?? NSRect.zero - NotifierController.lastLocation = afterRect.origin - var beforeRect = afterRect - beforeRect.origin.y += 10 - window?.setFrame(beforeRect, display: true) - window?.orderFront(self) - window?.setFrame(afterRect, display: true, animate: true) - } + func moveIn() { + let afterRect = self.window?.frame ?? NSRect.zero + NotifierController.lastLocation = afterRect.origin + var beforeRect = afterRect + beforeRect.origin.y += 10 + window?.setFrame(beforeRect, display: true) + window?.orderFront(self) + window?.setFrame(afterRect, display: true, animate: true) + } - setStartLocation() - moveIn() - NotifierController.increaseInstanceCount() - waitTimer = Timer.scheduledTimer(timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut), userInfo: nil, repeats: false) - } + setStartLocation() + moveIn() + NotifierController.increaseInstanceCount() + waitTimer = Timer.scheduledTimer( + timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut), + userInfo: nil, + repeats: false) + } - @objc private func doFadeOut(_ timer: Timer) { - let opacity = self.window?.alphaValue ?? 0 - if opacity <= 0 { - self.close() - } else { - self.window?.alphaValue = opacity - 0.2 - } - } + @objc private func doFadeOut(_ timer: Timer) { + let opacity = self.window?.alphaValue ?? 0 + if opacity <= 0 { + self.close() + } else { + self.window?.alphaValue = opacity - 0.2 + } + } - @objc private func fadeOut() { - waitTimer?.invalidate() - waitTimer = nil - NotifierController.decreaseInstanceCount() - fadeTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(doFadeOut(_:)), userInfo: nil, repeats: true) - } + @objc private func fadeOut() { + waitTimer?.invalidate() + waitTimer = nil + NotifierController.decreaseInstanceCount() + fadeTimer = Timer.scheduledTimer( + timeInterval: 0.01, target: self, selector: #selector(doFadeOut(_:)), userInfo: nil, + repeats: true) + } - public override func close() { - waitTimer?.invalidate() - waitTimer = nil - fadeTimer?.invalidate() - fadeTimer = nil - super.close() - } + public override func close() { + waitTimer?.invalidate() + waitTimer = nil + fadeTimer?.invalidate() + fadeTimer = nil + super.close() + } - fileprivate func windowDidBecomeClicked(_ window: NotifierWindow) { - self.fadeOut() - } + fileprivate func windowDidBecomeClicked(_ window: NotifierWindow) { + self.fadeOut() + } } diff --git a/Source/UI/TooltipUI/TooltipController.swift b/Source/UI/TooltipUI/TooltipController.swift index ecb41aff..7a2f0c81 100644 --- a/Source/UI/TooltipUI/TooltipController.swift +++ b/Source/UI/TooltipUI/TooltipController.swift @@ -1,122 +1,129 @@ // Copyright (c) 2021 and onwards Weizhong Yang (MIT License). // All possible vChewing-specific modifications are (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: +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. +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. +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. +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 class TooltipController: NSWindowController { - static var backgroundColor = NSColor.windowBackgroundColor - static var textColor = NSColor.windowBackgroundColor - private var messageTextField: NSTextField - private var tooltip: String = "" { - didSet { - messageTextField.stringValue = tooltip - adjustSize() - } - } + static var backgroundColor = NSColor.windowBackgroundColor + static var textColor = NSColor.windowBackgroundColor + private var messageTextField: NSTextField + private var tooltip: String = "" { + didSet { + messageTextField.stringValue = tooltip + adjustSize() + } + } - public init() { - let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0) - let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] - let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) - panel.hasShadow = true + public init() { + let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0) + let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] + let panel = NSPanel( + contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.hasShadow = true - messageTextField = NSTextField() - messageTextField.isEditable = false - messageTextField.isSelectable = false - messageTextField.isBezeled = false - messageTextField.textColor = TooltipController.textColor - messageTextField.drawsBackground = true - messageTextField.backgroundColor = TooltipController.backgroundColor - messageTextField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) - panel.contentView?.addSubview(messageTextField) + messageTextField = NSTextField() + messageTextField.isEditable = false + messageTextField.isSelectable = false + messageTextField.isBezeled = false + messageTextField.textColor = TooltipController.textColor + messageTextField.drawsBackground = true + messageTextField.backgroundColor = TooltipController.backgroundColor + messageTextField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) + panel.contentView?.addSubview(messageTextField) - super.init(window: panel) - } + super.init(window: panel) + } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - @objc(showTooltip:atPoint:) - public func show(tooltip: String, at point: NSPoint) { - messageTextField.textColor = TooltipController.textColor - messageTextField.backgroundColor = TooltipController.backgroundColor - self.tooltip = tooltip - window?.orderFront(nil) - set(windowLocation: point) - } + @objc(showTooltip:atPoint:) + public func show(tooltip: String, at point: NSPoint) { + messageTextField.textColor = TooltipController.textColor + messageTextField.backgroundColor = TooltipController.backgroundColor + self.tooltip = tooltip + window?.orderFront(nil) + set(windowLocation: point) + } - @objc - public func hide() { - window?.orderOut(nil) - } + @objc + public func hide() { + window?.orderOut(nil) + } - private func set(windowLocation windowTopLeftPoint: NSPoint) { + private func set(windowLocation windowTopLeftPoint: NSPoint) { - var adjustedPoint = windowTopLeftPoint - adjustedPoint.y -= 5 + var adjustedPoint = windowTopLeftPoint + adjustedPoint.y -= 5 - var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero - for screen in NSScreen.screens { - let frame = screen.visibleFrame - if windowTopLeftPoint.x >= frame.minX && - windowTopLeftPoint.x <= frame.maxX && - windowTopLeftPoint.y >= frame.minY && - windowTopLeftPoint.y <= frame.maxY { - screenFrame = frame - break - } - } + var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero + for screen in NSScreen.screens { + let frame = screen.visibleFrame + if windowTopLeftPoint.x >= frame.minX && windowTopLeftPoint.x <= frame.maxX + && windowTopLeftPoint.y >= frame.minY && windowTopLeftPoint.y <= frame.maxY + { + screenFrame = frame + break + } + } - let windowSize = window?.frame.size ?? NSSize.zero + let windowSize = window?.frame.size ?? NSSize.zero - // bottom beneath the screen? - if adjustedPoint.y - windowSize.height < screenFrame.minY { - adjustedPoint.y = screenFrame.minY + windowSize.height - } + // bottom beneath the screen? + if adjustedPoint.y - windowSize.height < screenFrame.minY { + adjustedPoint.y = screenFrame.minY + windowSize.height + } - // top over the screen? - if adjustedPoint.y >= screenFrame.maxY { - adjustedPoint.y = screenFrame.maxY - 1.0 - } + // top over the screen? + if adjustedPoint.y >= screenFrame.maxY { + adjustedPoint.y = screenFrame.maxY - 1.0 + } - // right - if adjustedPoint.x + windowSize.width >= screenFrame.maxX { - adjustedPoint.x = screenFrame.maxX - windowSize.width - } + // right + if adjustedPoint.x + windowSize.width >= screenFrame.maxX { + adjustedPoint.x = screenFrame.maxX - windowSize.width + } - // left - if adjustedPoint.x < screenFrame.minX { - adjustedPoint.x = screenFrame.minX - } + // left + if adjustedPoint.x < screenFrame.minX { + adjustedPoint.x = screenFrame.minX + } - window?.setFrameTopLeftPoint(adjustedPoint) + window?.setFrameTopLeftPoint(adjustedPoint) - } + } - private func adjustSize() { - let attrString = messageTextField.attributedStringValue; - var rect = attrString.boundingRect(with: NSSize(width: 1600.0, height: 1600.0), options: .usesLineFragmentOrigin) - rect.size.width += 10 - messageTextField.frame = rect - window?.setFrame(rect, display: true) - } + private func adjustSize() { + let attrString = messageTextField.attributedStringValue + var rect = attrString.boundingRect( + with: NSSize(width: 1600.0, height: 1600.0), options: .usesLineFragmentOrigin) + rect.size.width += 10 + messageTextField.frame = rect + window?.setFrame(rect, display: true) + } } diff --git a/Source/WindowControllers/ctlAboutWindow.swift b/Source/WindowControllers/ctlAboutWindow.swift index c144e201..e38945b5 100644 --- a/Source/WindowControllers/ctlAboutWindow.swift +++ b/Source/WindowControllers/ctlAboutWindow.swift @@ -1,51 +1,64 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @objc(AboutWindow) class ctlAboutWindow: NSWindowController { - @IBOutlet weak var appVersionLabel: NSTextField! - @IBOutlet weak var appCopyrightLabel: NSTextField! - @IBOutlet var appEULAContent: NSTextView! + @IBOutlet weak var appVersionLabel: NSTextField! + @IBOutlet weak var appCopyrightLabel: NSTextField! + @IBOutlet var appEULAContent: NSTextView! - override func windowDidLoad() { - super.windowDidLoad() - - window?.standardWindowButton(.closeButton)?.isHidden = true - window?.standardWindowButton(.miniaturizeButton)?.isHidden = true - window?.standardWindowButton(.zoomButton)?.isHidden = true - guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String, - let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { - return - } - if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String { - appCopyrightLabel.stringValue = copyrightLabel - } - if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { - appEULAContent.string = eulaContent - } - appVersionLabel.stringValue = String(format: "%@ Build %@", versionString, installingVersion) - } + override func windowDidLoad() { + super.windowDidLoad() - @IBAction func btnWiki(_ sender: NSButton) { - if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") { - NSWorkspace.shared.open(url) - } - } + window?.standardWindowButton(.closeButton)?.isHidden = true + window?.standardWindowButton(.miniaturizeButton)?.isHidden = true + window?.standardWindowButton(.zoomButton)?.isHidden = true + guard + let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] + as? String, + let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + else { + return + } + if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] + as? String + { + appCopyrightLabel.stringValue = copyrightLabel + } + if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { + appEULAContent.string = eulaContent + } + appVersionLabel.stringValue = String( + format: "%@ Build %@", versionString, installingVersion) + } + + @IBAction func btnWiki(_ sender: NSButton) { + if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") { + NSWorkspace.shared.open(url) + } + } } diff --git a/Source/WindowControllers/ctlNonModalAlertWindow.swift b/Source/WindowControllers/ctlNonModalAlertWindow.swift index a9ed42a3..ec38f9a1 100644 --- a/Source/WindowControllers/ctlNonModalAlertWindow.swift +++ b/Source/WindowControllers/ctlNonModalAlertWindow.swift @@ -1,118 +1,130 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @objc protocol ctlNonModalAlertWindowDelegate: AnyObject { - func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) - func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow) + func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) + func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow) } class ctlNonModalAlertWindow: NSWindowController { - @objc(sharedInstance) - static let shared = ctlNonModalAlertWindow(windowNibName: "frmNonModalAlertWindow") + @objc(sharedInstance) + static let shared = ctlNonModalAlertWindow(windowNibName: "frmNonModalAlertWindow") - @IBOutlet weak var titleTextField: NSTextField! - @IBOutlet weak var contentTextField: NSTextField! - @IBOutlet weak var confirmButton: NSButton! - @IBOutlet weak var cancelButton: NSButton! - weak var delegate: ctlNonModalAlertWindowDelegate? + @IBOutlet weak var titleTextField: NSTextField! + @IBOutlet weak var contentTextField: NSTextField! + @IBOutlet weak var confirmButton: NSButton! + @IBOutlet weak var cancelButton: NSButton! + weak var delegate: ctlNonModalAlertWindowDelegate? - @objc func show(title: String, content: String, confirmButtonTitle: String, cancelButtonTitle: String?, cancelAsDefault: Bool, delegate: ctlNonModalAlertWindowDelegate?) { - if window?.isVisible == true { - self.delegate?.ctlNonModalAlertWindowDidCancel(self) - } + @objc func show( + title: String, content: String, confirmButtonTitle: String, cancelButtonTitle: String?, + cancelAsDefault: Bool, delegate: ctlNonModalAlertWindowDelegate? + ) { + if window?.isVisible == true { + self.delegate?.ctlNonModalAlertWindowDidCancel(self) + } - self.delegate = delegate + self.delegate = delegate - var oldFrame = confirmButton.frame - confirmButton.title = confirmButtonTitle - confirmButton.sizeToFit() + var oldFrame = confirmButton.frame + confirmButton.title = confirmButtonTitle + confirmButton.sizeToFit() - var newFrame = confirmButton.frame - newFrame.size.width = max(90, newFrame.size.width + 10) - newFrame.origin.x += oldFrame.size.width - newFrame.size.width - confirmButton.frame = newFrame + var newFrame = confirmButton.frame + newFrame.size.width = max(90, newFrame.size.width + 10) + newFrame.origin.x += oldFrame.size.width - newFrame.size.width + confirmButton.frame = newFrame - if let cancelButtonTitle = cancelButtonTitle { - cancelButton.title = cancelButtonTitle - cancelButton.sizeToFit() - var adjustFrame = cancelButton.frame - adjustFrame.size.width = max(90, adjustFrame.size.width + 10) - adjustFrame.origin.x = newFrame.origin.x - adjustFrame.size.width - confirmButton.frame = adjustFrame - cancelButton.isHidden = false - } else { - cancelButton.isHidden = true - } + if let cancelButtonTitle = cancelButtonTitle { + cancelButton.title = cancelButtonTitle + cancelButton.sizeToFit() + var adjustFrame = cancelButton.frame + adjustFrame.size.width = max(90, adjustFrame.size.width + 10) + adjustFrame.origin.x = newFrame.origin.x - adjustFrame.size.width + confirmButton.frame = adjustFrame + cancelButton.isHidden = false + } else { + cancelButton.isHidden = true + } - cancelButton.nextKeyView = confirmButton - confirmButton.nextKeyView = cancelButton + cancelButton.nextKeyView = confirmButton + confirmButton.nextKeyView = cancelButton - if cancelButtonTitle != nil { - if cancelAsDefault { - window?.defaultButtonCell = cancelButton.cell as? NSButtonCell - } else { - cancelButton.keyEquivalent = " " - window?.defaultButtonCell = confirmButton.cell as? NSButtonCell - } - } else { - window?.defaultButtonCell = confirmButton.cell as? NSButtonCell - } + if cancelButtonTitle != nil { + if cancelAsDefault { + window?.defaultButtonCell = cancelButton.cell as? NSButtonCell + } else { + cancelButton.keyEquivalent = " " + window?.defaultButtonCell = confirmButton.cell as? NSButtonCell + } + } else { + window?.defaultButtonCell = confirmButton.cell as? NSButtonCell + } - titleTextField.stringValue = title + titleTextField.stringValue = title - oldFrame = contentTextField.frame - contentTextField.stringValue = content + oldFrame = contentTextField.frame + contentTextField.stringValue = content - var infiniteHeightFrame = oldFrame - infiniteHeightFrame.size.width -= 4.0 - infiniteHeightFrame.size.height = 10240 - newFrame = (content as NSString).boundingRect(with: infiniteHeightFrame.size, options: [.usesLineFragmentOrigin], attributes: [.font: contentTextField.font!]) - newFrame.size.width = max(newFrame.size.width, oldFrame.size.width) - newFrame.size.height += 4.0 - newFrame.origin = oldFrame.origin - newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height) - contentTextField.frame = newFrame + var infiniteHeightFrame = oldFrame + infiniteHeightFrame.size.width -= 4.0 + infiniteHeightFrame.size.height = 10240 + newFrame = (content as NSString).boundingRect( + with: infiniteHeightFrame.size, options: [.usesLineFragmentOrigin], + attributes: [.font: contentTextField.font!]) + newFrame.size.width = max(newFrame.size.width, oldFrame.size.width) + newFrame.size.height += 4.0 + newFrame.origin = oldFrame.origin + newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height) + contentTextField.frame = newFrame - var windowFrame = window?.frame ?? NSRect.zero - windowFrame.size.height += (newFrame.size.height - oldFrame.size.height) - window?.level = NSWindow.Level(Int(CGShieldingWindowLevel()) + 1) - window?.setFrame(windowFrame, display: true) - window?.center() - window?.makeKeyAndOrderFront(self) - NSApp.activate(ignoringOtherApps: true) - } + var windowFrame = window?.frame ?? NSRect.zero + windowFrame.size.height += (newFrame.size.height - oldFrame.size.height) + window?.level = NSWindow.Level(Int(CGShieldingWindowLevel()) + 1) + window?.setFrame(windowFrame, display: true) + window?.center() + window?.makeKeyAndOrderFront(self) + NSApp.activate(ignoringOtherApps: true) + } - @IBAction func confirmButtonAction(_ sender: Any) { - delegate?.ctlNonModalAlertWindowDidConfirm(self) - window?.orderOut(self) - } + @IBAction func confirmButtonAction(_ sender: Any) { + delegate?.ctlNonModalAlertWindowDidConfirm(self) + window?.orderOut(self) + } - @IBAction func cancelButtonAction(_ sender: Any) { - cancel(sender) - } + @IBAction func cancelButtonAction(_ sender: Any) { + cancel(sender) + } - func cancel(_ sender: Any) { - delegate?.ctlNonModalAlertWindowDidCancel(self) - delegate = nil - window?.orderOut(self) - } + func cancel(_ sender: Any) { + delegate?.ctlNonModalAlertWindowDidCancel(self) + delegate = nil + window?.orderOut(self) + } } diff --git a/Source/WindowControllers/ctlPrefWindow.swift b/Source/WindowControllers/ctlPrefWindow.swift index 26d271e7..23f86590 100644 --- a/Source/WindowControllers/ctlPrefWindow.swift +++ b/Source/WindowControllers/ctlPrefWindow.swift @@ -1,279 +1,301 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 import Carbon +import Cocoa // Extend the RangeReplaceableCollection to allow it clean duplicated characters. extension RangeReplaceableCollection where Element: Hashable { - var charDeDuplicate: Self { - var set = Set() - return filter{ set.insert($0).inserted } - } + var charDeDuplicate: Self { + var set = Set() + return filter { set.insert($0).inserted } + } } // Please note that the class should be exposed using the same class name // in Objective-C in order to let IMK to see the same class name as // the "InputMethodServerPreferencesWindowControllerClass" in Info.plist. @objc(ctlPrefWindow) class ctlPrefWindow: NSWindowController { - @IBOutlet weak var fontSizePopUpButton: NSPopUpButton! - @IBOutlet weak var uiLanguageButton: NSPopUpButton! - @IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton! - @IBOutlet weak var selectionKeyComboBox: NSComboBox! - @IBOutlet weak var chkTrad2KangXi: NSButton! - @IBOutlet weak var chkTrad2JISShinjitai: NSButton! - @IBOutlet weak var lblCurrentlySpecifiedUserDataFolder: NSTextFieldCell! - - var currentLanguageSelectItem: NSMenuItem? = nil + @IBOutlet weak var fontSizePopUpButton: NSPopUpButton! + @IBOutlet weak var uiLanguageButton: NSPopUpButton! + @IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton! + @IBOutlet weak var selectionKeyComboBox: NSComboBox! + @IBOutlet weak var chkTrad2KangXi: NSButton! + @IBOutlet weak var chkTrad2JISShinjitai: NSButton! + @IBOutlet weak var lblCurrentlySpecifiedUserDataFolder: NSTextFieldCell! - override func windowDidLoad() { - super.windowDidLoad() + var currentLanguageSelectItem: NSMenuItem? = nil - lblCurrentlySpecifiedUserDataFolder.placeholderString = mgrLangModel.dataFolderPath(isDefaultFolder: true) + override func windowDidLoad() { + super.windowDidLoad() - let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"] - var autoMUISelectItem: NSMenuItem? = nil - var chosenLanguageItem: NSMenuItem? = nil - uiLanguageButton.menu?.removeAllItems() - - let appleLanguages = mgrPrefs.appleLanguages - for language in languages { - let menuItem = NSMenuItem() - menuItem.title = NSLocalizedString(language, comment: "") - menuItem.representedObject = language - - if language == "auto" { - autoMUISelectItem = menuItem - } - - if !appleLanguages.isEmpty { - if appleLanguages[0] == language { - chosenLanguageItem = menuItem - } - } - uiLanguageButton.menu?.addItem(menuItem) - } - - currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem - uiLanguageButton.select(currentLanguageSelectItem) + lblCurrentlySpecifiedUserDataFolder.placeholderString = mgrLangModel.dataFolderPath( + isDefaultFolder: true) - let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] - var usKeyboardLayoutItem: NSMenuItem? = nil - var chosenBaseKeyboardLayoutItem: NSMenuItem? = nil + let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"] + var autoMUISelectItem: NSMenuItem? = nil + var chosenLanguageItem: NSMenuItem? = nil + uiLanguageButton.menu?.removeAllItems() - basisKeyboardLayoutButton.menu?.removeAllItems() + let appleLanguages = mgrPrefs.appleLanguages + for language in languages { + let menuItem = NSMenuItem() + menuItem.title = NSLocalizedString(language, comment: "") + menuItem.representedObject = language - let menuItem_AppleZhuyinBopomofo = NSMenuItem() - menuItem_AppleZhuyinBopomofo.title = String(format: NSLocalizedString("Apple Zhuyin Bopomofo", comment: "")) - menuItem_AppleZhuyinBopomofo.representedObject = String("com.apple.keylayout.ZhuyinBopomofo") - basisKeyboardLayoutButton.menu?.addItem(menuItem_AppleZhuyinBopomofo) + if language == "auto" { + autoMUISelectItem = menuItem + } - let menuItem_AppleZhuyinEten = NSMenuItem() - menuItem_AppleZhuyinEten.title = String(format: NSLocalizedString("Apple Zhuyin Eten", comment: "")) - menuItem_AppleZhuyinEten.representedObject = String("com.apple.keylayout.ZhuyinEten") - basisKeyboardLayoutButton.menu?.addItem(menuItem_AppleZhuyinEten) + if !appleLanguages.isEmpty { + if appleLanguages[0] == language { + chosenLanguageItem = menuItem + } + } + uiLanguageButton.menu?.addItem(menuItem) + } - let basisKeyboardLayoutID = mgrPrefs.basisKeyboardLayout + currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem + uiLanguageButton.select(currentLanguageSelectItem) - for source in list { - if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { - let category = Unmanaged.fromOpaque(categoryPtr).takeUnretainedValue() - if category != kTISCategoryKeyboardInputSource { - continue - } - } else { - continue - } + let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] + var usKeyboardLayoutItem: NSMenuItem? = nil + var chosenBaseKeyboardLayoutItem: NSMenuItem? = nil - if let asciiCapablePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsASCIICapable) { - let asciiCapable = Unmanaged.fromOpaque(asciiCapablePtr).takeUnretainedValue() - if asciiCapable != kCFBooleanTrue { - continue - } - } else { - continue - } + basisKeyboardLayoutButton.menu?.removeAllItems() - if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { - let sourceType = Unmanaged.fromOpaque(sourceTypePtr).takeUnretainedValue() - if sourceType != kTISTypeKeyboardLayout { - continue - } - } else { - continue - } + let itmAppleZhuyinBopomofo = NSMenuItem() + itmAppleZhuyinBopomofo.title = String( + format: NSLocalizedString("Apple Zhuyin Bopomofo", comment: "")) + itmAppleZhuyinBopomofo.representedObject = String( + "com.apple.keylayout.ZhuyinBopomofo") + basisKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinBopomofo) - guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), - let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) else { - continue - } + let itmAppleZhuyinEten = NSMenuItem() + itmAppleZhuyinEten.title = String( + format: NSLocalizedString("Apple Zhuyin Eten", comment: "")) + itmAppleZhuyinEten.representedObject = String("com.apple.keylayout.ZhuyinEten") + basisKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinEten) - let sourceID = String(Unmanaged.fromOpaque(sourceIDPtr).takeUnretainedValue()) - let localizedName = String(Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue()) + let basisKeyboardLayoutID = mgrPrefs.basisKeyboardLayout - let menuItem = NSMenuItem() - menuItem.title = localizedName - menuItem.representedObject = sourceID + for source in list { + if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { + let category = Unmanaged.fromOpaque(categoryPtr).takeUnretainedValue() + if category != kTISCategoryKeyboardInputSource { + continue + } + } else { + continue + } - if sourceID == "com.apple.keylayout.US" { - usKeyboardLayoutItem = menuItem - } - if basisKeyboardLayoutID == sourceID { - chosenBaseKeyboardLayoutItem = menuItem - } - basisKeyboardLayoutButton.menu?.addItem(menuItem) - } + if let asciiCapablePtr = TISGetInputSourceProperty( + source, kTISPropertyInputSourceIsASCIICapable) + { + let asciiCapable = Unmanaged.fromOpaque(asciiCapablePtr) + .takeUnretainedValue() + if asciiCapable != kCFBooleanTrue { + continue + } + } else { + continue + } - switch basisKeyboardLayoutID { - case "com.apple.keylayout.ZhuyinBopomofo": - chosenBaseKeyboardLayoutItem = menuItem_AppleZhuyinBopomofo - case "com.apple.keylayout.ZhuyinEten": - chosenBaseKeyboardLayoutItem = menuItem_AppleZhuyinEten - default: - break // nothing to do - } + if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { + let sourceType = Unmanaged.fromOpaque(sourceTypePtr).takeUnretainedValue() + if sourceType != kTISTypeKeyboardLayout { + continue + } + } else { + continue + } - basisKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem) + guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), + let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) + else { + continue + } - selectionKeyComboBox.usesDataSource = false - selectionKeyComboBox.removeAllItems() - selectionKeyComboBox.addItems(withObjectValues: mgrPrefs.suggestedCandidateKeys) + let sourceID = String(Unmanaged.fromOpaque(sourceIDPtr).takeUnretainedValue()) + let localizedName = String( + Unmanaged.fromOpaque(localizedNamePtr).takeUnretainedValue()) - var candidateSelectionKeys = mgrPrefs.candidateKeys - if candidateSelectionKeys.isEmpty { - candidateSelectionKeys = mgrPrefs.defaultCandidateKeys - } + let menuItem = NSMenuItem() + menuItem.title = localizedName + menuItem.representedObject = sourceID - selectionKeyComboBox.stringValue = candidateSelectionKeys - } + if sourceID == "com.apple.keylayout.US" { + usKeyboardLayoutItem = menuItem + } + if basisKeyboardLayoutID == sourceID { + chosenBaseKeyboardLayoutItem = menuItem + } + basisKeyboardLayoutButton.menu?.addItem(menuItem) + } - // 這裡有必要加上這段處理,用來確保藉由偏好設定介面動過的 CNS 開關能夠立刻生效。 - // 所有涉及到語言模型開關的內容均需要這樣處理。 - @IBAction func toggleCNSSupport(_ sender: Any) { - mgrLangModel.setCNSEnabled(mgrPrefs.cns11643Enabled) - } + switch basisKeyboardLayoutID { + case "com.apple.keylayout.ZhuyinBopomofo": + chosenBaseKeyboardLayoutItem = itmAppleZhuyinBopomofo + case "com.apple.keylayout.ZhuyinEten": + chosenBaseKeyboardLayoutItem = itmAppleZhuyinEten + default: + break // nothing to do + } - @IBAction func toggleSymbolInputEnabled(_ sender: Any) { - mgrLangModel.setSymbolEnabled(mgrPrefs.symbolInputEnabled) - } + basisKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem) - @IBAction func toggleTrad2KangXiAction(_ sender: Any) { - if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on { - mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() - } - } + selectionKeyComboBox.usesDataSource = false + selectionKeyComboBox.removeAllItems() + selectionKeyComboBox.addItems(withObjectValues: mgrPrefs.suggestedCandidateKeys) - @IBAction func toggleTrad2JISShinjitaiAction(_ sender: Any) { - if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on { - mgrPrefs.toggleChineseConversionEnabled() - } - } + var candidateSelectionKeys = mgrPrefs.candidateKeys + if candidateSelectionKeys.isEmpty { + candidateSelectionKeys = mgrPrefs.defaultCandidateKeys + } - @IBAction func updateBasisKeyboardLayoutAction(_ sender: Any) { - if let sourceID = basisKeyboardLayoutButton.selectedItem?.representedObject as? String { - mgrPrefs.basisKeyboardLayout = sourceID - } - } - - @IBAction func updateUiLanguageAction(_ sender: Any) { - if let selectItem = uiLanguageButton.selectedItem { - if currentLanguageSelectItem == selectItem { - return - } - } - if let language = uiLanguageButton.selectedItem?.representedObject as? String { - if (language != "auto") { - mgrPrefs.appleLanguages = [language] - } - else { - UserDefaults.standard.removeObject(forKey: "AppleLanguages") - } - - NSLog("vChewing App self-terminated due to UI language change.") - NSApplication.shared.terminate(nil) - } - } + selectionKeyComboBox.stringValue = candidateSelectionKeys + } - @IBAction func clickedWhetherIMEShouldNotFartToggleAction(_ sender: Any) { - clsSFX.beep() - } + // 這裡有必要加上這段處理,用來確保藉由偏好設定介面動過的 CNS 開關能夠立刻生效。 + // 所有涉及到語言模型開關的內容均需要這樣處理。 + @IBAction func toggleCNSSupport(_ sender: Any) { + mgrLangModel.setCNSEnabled(mgrPrefs.cns11643Enabled) + } - @IBAction func changeSelectionKeyAction(_ sender: Any) { - guard let keys = (sender as AnyObject).stringValue?.trimmingCharacters(in: .whitespacesAndNewlines).charDeDuplicate else { - return - } - do { - try mgrPrefs.validate(candidateKeys: keys) - mgrPrefs.candidateKeys = keys - selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys - } - catch mgrPrefs.CandidateKeyError.empty { - selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys - } - catch { - if let window = window { - let alert = NSAlert(error: error) - alert.beginSheetModal(for: window) { response in - self.selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys - } - clsSFX.beep() - } - } - } + @IBAction func toggleSymbolInputEnabled(_ sender: Any) { + mgrLangModel.setSymbolEnabled(mgrPrefs.symbolInputEnabled) + } - @IBAction func resetSpecifiedUserDataFolder(_ sender: Any) { - UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified") - IME.initLangModels(userOnly: true) - } + @IBAction func toggleTrad2KangXiAction(_ sender: Any) { + if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on { + mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() + } + } - @IBAction func chooseUserDataFolderToSpecify(_ sender: Any) { - IME.dlgOpenPath.title = NSLocalizedString("Choose your desired user data folder.", comment: ""); - IME.dlgOpenPath.showsResizeIndicator = true; - IME.dlgOpenPath.showsHiddenFiles = true; - IME.dlgOpenPath.canChooseFiles = false; - IME.dlgOpenPath.canChooseDirectories = true; + @IBAction func toggleTrad2JISShinjitaiAction(_ sender: Any) { + if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on { + mgrPrefs.toggleChineseConversionEnabled() + } + } - let PreviousFolderValidity = mgrLangModel.checkIfSpecifiedUserDataFolderValid(NSString(string: mgrPrefs.userDataFolderSpecified).expandingTildeInPath) + @IBAction func updateBasisKeyboardLayoutAction(_ sender: Any) { + if let sourceID = basisKeyboardLayoutButton.selectedItem?.representedObject as? String { + mgrPrefs.basisKeyboardLayout = sourceID + } + } - if self.window != nil { - IME.dlgOpenPath.beginSheetModal(for: self.window!) { result in - if result == NSApplication.ModalResponse.OK { - if (IME.dlgOpenPath.url != nil) { - if (mgrLangModel.checkIfSpecifiedUserDataFolderValid(IME.dlgOpenPath.url!.path)) { - mgrPrefs.userDataFolderSpecified = IME.dlgOpenPath.url!.path - IME.initLangModels(userOnly: true) - } else { - clsSFX.beep() - if !PreviousFolderValidity { - self.resetSpecifiedUserDataFolder(self) - } - return - } - } - } else { - if !PreviousFolderValidity { - self.resetSpecifiedUserDataFolder(self) - } - return - } - } - } // End If self.window != nil - } // End IBAction + @IBAction func updateUiLanguageAction(_ sender: Any) { + if let selectItem = uiLanguageButton.selectedItem { + if currentLanguageSelectItem == selectItem { + return + } + } + if let language = uiLanguageButton.selectedItem?.representedObject as? String { + if language != "auto" { + mgrPrefs.appleLanguages = [language] + } else { + UserDefaults.standard.removeObject(forKey: "AppleLanguages") + } + + NSLog("vChewing App self-terminated due to UI language change.") + NSApplication.shared.terminate(nil) + } + } + + @IBAction func clickedWhetherIMEShouldNotFartToggleAction(_ sender: Any) { + clsSFX.beep() + } + + @IBAction func changeSelectionKeyAction(_ sender: Any) { + guard + let keys = (sender as AnyObject).stringValue?.trimmingCharacters( + in: .whitespacesAndNewlines + ) + .charDeDuplicate + else { + return + } + do { + try mgrPrefs.validate(candidateKeys: keys) + mgrPrefs.candidateKeys = keys + selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys + } catch mgrPrefs.CandidateKeyError.empty { + selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys + } catch { + if let window = window { + let alert = NSAlert(error: error) + alert.beginSheetModal(for: window) { response in + self.selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys + } + clsSFX.beep() + } + } + } + + @IBAction func resetSpecifiedUserDataFolder(_ sender: Any) { + UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified") + IME.initLangModels(userOnly: true) + } + + @IBAction func chooseUserDataFolderToSpecify(_ sender: Any) { + IME.dlgOpenPath.title = NSLocalizedString( + "Choose your desired user data folder.", comment: "") + IME.dlgOpenPath.showsResizeIndicator = true + IME.dlgOpenPath.showsHiddenFiles = true + IME.dlgOpenPath.canChooseFiles = false + IME.dlgOpenPath.canChooseDirectories = true + + let bolPreviousFolderValidity = mgrLangModel.checkIfSpecifiedUserDataFolderValid( + NSString(string: mgrPrefs.userDataFolderSpecified).expandingTildeInPath) + + if self.window != nil { + IME.dlgOpenPath.beginSheetModal(for: self.window!) { result in + if result == NSApplication.ModalResponse.OK { + if IME.dlgOpenPath.url != nil { + if mgrLangModel.checkIfSpecifiedUserDataFolderValid( + IME.dlgOpenPath.url!.path) + { + mgrPrefs.userDataFolderSpecified = IME.dlgOpenPath.url!.path + IME.initLangModels(userOnly: true) + } else { + clsSFX.beep() + if !bolPreviousFolderValidity { + self.resetSpecifiedUserDataFolder(self) + } + return + } + } + } else { + if !bolPreviousFolderValidity { + self.resetSpecifiedUserDataFolder(self) + } + return + } + } + } // End If self.window != nil + } // End IBAction } diff --git a/UserPhraseEditor/AppDelegate.swift b/UserPhraseEditor/AppDelegate.swift index dd33c82a..e5e44d25 100644 --- a/UserPhraseEditor/AppDelegate.swift +++ b/UserPhraseEditor/AppDelegate.swift @@ -1,19 +1,25 @@ // 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: +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. +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. +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. +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 @@ -21,31 +27,31 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window - - func applicationDidFinishLaunching(_ aNotification: Notification) { - // Insert code here to initialize your application - } + private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application - } + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + } - func applicationShouldTerminate(_ sender: NSApplication)-> NSApplication.TerminateReply { - return .terminateNow - } - // New About Window - @objc func showAbout() { - if (ctlAboutWindowInstance == nil) { - ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow") - } - ctlAboutWindowInstance?.window?.center() - ctlAboutWindowInstance?.window?.orderFrontRegardless() // 逼著關於視窗往最前方顯示 - ctlAboutWindowInstance?.window?.level = .statusBar - } - // Call the New About Window - @IBAction func about(_ sender: Any) { - (NSApp.delegate as? AppDelegate)?.showAbout() - NSApplication.shared.activate(ignoringOtherApps: true) - } + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + return .terminateNow + } + // New About Window + @objc func showAbout() { + if ctlAboutWindowInstance == nil { + ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow") + } + ctlAboutWindowInstance?.window?.center() + ctlAboutWindowInstance?.window?.orderFrontRegardless() // 逼著關於視窗往最前方顯示 + ctlAboutWindowInstance?.window?.level = .statusBar + } + // Call the New About Window + @IBAction func about(_ sender: Any) { + (NSApp.delegate as? AppDelegate)?.showAbout() + NSApplication.shared.activate(ignoringOtherApps: true) + } } diff --git a/UserPhraseEditor/Content.swift b/UserPhraseEditor/Content.swift index 50155a53..3cf4acf6 100644 --- a/UserPhraseEditor/Content.swift +++ b/UserPhraseEditor/Content.swift @@ -1,41 +1,47 @@ // 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: +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. +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. +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. +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 import Cocoa +import Foundation class Content: NSObject { - @objc dynamic var contentString = "" - - public init(contentString: String) { - self.contentString = contentString - } - + @objc dynamic var contentString = "" + + public init(contentString: String) { + self.contentString = contentString + } + } extension Content { - - func read(from data: Data) { - contentString = String(bytes: data, encoding: .utf8)! - } - - func data() -> Data? { - return contentString.data(using: .utf8) - } - + + func read(from data: Data) { + contentString = String(bytes: data, encoding: .utf8)! + } + + func data() -> Data? { + return contentString.data(using: .utf8) + } + } diff --git a/UserPhraseEditor/Document.swift b/UserPhraseEditor/Document.swift index 9f498207..6777e77f 100644 --- a/UserPhraseEditor/Document.swift +++ b/UserPhraseEditor/Document.swift @@ -1,131 +1,145 @@ // 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: +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. +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. +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. +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 class Document: NSDocument { - - @objc var content = Content(contentString: "") - var contentViewController: ViewController! - - override init() { - super.init() - // Add your subclass-specific initialization here. - } - - // MARK: - Enablers - - // This enables auto save. - override class var autosavesInPlace: Bool { - return true - } - - // This enables asynchronous-writing. - override func canAsynchronouslyWrite(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType) -> Bool { - return true - } - - // This enables asynchronous reading. - override class func canConcurrentlyReadDocuments(ofType: String) -> Bool { - return ofType == "public.plain-text" - } - - // MARK: - User Interface - - /// - Tag: makeWindowControllersExample - override func makeWindowControllers() { - // Returns the storyboard that contains your document window. - let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) - if let windowController = - storyboard.instantiateController( - withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as? NSWindowController { - addWindowController(windowController) - - // Set the view controller's represented object as your document. - if let contentVC = windowController.contentViewController as? ViewController { - contentVC.representedObject = content - contentViewController = contentVC - } - } - } - - // MARK: - Reading and Writing - - /// - Tag: readExample - override func read(from data: Data, ofType typeName: String) throws { - var strToDealWith = String(decoding: data, as: UTF8.self) - strToDealWith.formatConsolidate(HYPY2BPMF: false) - let processedIncomingData = Data(strToDealWith.utf8) - content.read(from: processedIncomingData) - } - - /// - Tag: writeExample - override func data(ofType typeName: String) throws -> Data { - var strToDealWith = content.contentString - strToDealWith.formatConsolidate(HYPY2BPMF: true) - let outputData = Data(strToDealWith.utf8) - return outputData - } - - // MARK: - Printing - - func thePrintInfo() -> NSPrintInfo { - let thePrintInfo = NSPrintInfo() - thePrintInfo.horizontalPagination = .fit - thePrintInfo.isHorizontallyCentered = false - thePrintInfo.isVerticallyCentered = false - - // One inch margin all the way around. - thePrintInfo.leftMargin = 72.0 - thePrintInfo.rightMargin = 72.0 - thePrintInfo.topMargin = 72.0 - thePrintInfo.bottomMargin = 72.0 - - printInfo.dictionary().setObject(NSNumber(value: true), - forKey: NSPrintInfo.AttributeKey.headerAndFooter as NSCopying) - - return thePrintInfo - } - - @objc - func printOperationDidRun( - _ printOperation: NSPrintOperation, success: Bool, contextInfo: UnsafeMutableRawPointer?) { - // Printing finished... - } - - @IBAction override func printDocument(_ sender: Any?) { - // Print the NSTextView. - - // Create a copy to manipulate for printing. - let pageSize = NSSize(width: (printInfo.paperSize.width), height: (printInfo.paperSize.height)) - let textView = NSTextView(frame: NSRect(x: 0.0, y: 0.0, width: pageSize.width, height: pageSize.height)) - - // Make sure we print on a white background. - textView.appearance = NSAppearance(named: .aqua) - - // Copy the attributed string. - textView.textStorage?.append(NSAttributedString(string: content.contentString)) - - let printOperation = NSPrintOperation(view: textView) - printOperation.runModal( - for: windowControllers[0].window!, - delegate: self, - didRun: #selector(printOperationDidRun(_:success:contextInfo:)), contextInfo: nil) - } - + + @objc var content = Content(contentString: "") + var contentViewController: ViewController! + + override init() { + super.init() + // Add your subclass-specific initialization here. + } + + // MARK: - Enablers + + // This enables auto save. + override class var autosavesInPlace: Bool { + return true + } + + // This enables asynchronous-writing. + override func canAsynchronouslyWrite( + to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType + ) -> Bool { + return true + } + + // This enables asynchronous reading. + override class func canConcurrentlyReadDocuments(ofType: String) -> Bool { + return ofType == "public.plain-text" + } + + // MARK: - User Interface + + /// - Tag: makeWindowControllersExample + override func makeWindowControllers() { + // Returns the storyboard that contains your document window. + let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) + if let windowController = + storyboard.instantiateController( + withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) + as? NSWindowController + { + addWindowController(windowController) + + // Set the view controller's represented object as your document. + if let contentVC = windowController.contentViewController as? ViewController { + contentVC.representedObject = content + contentViewController = contentVC + } + } + } + + // MARK: - Reading and Writing + + /// - Tag: readExample + override func read(from data: Data, ofType typeName: String) throws { + var strToDealWith = String(decoding: data, as: UTF8.self) + strToDealWith.formatConsolidate(cnvHYPYtoBPMF: false) + let processedIncomingData = Data(strToDealWith.utf8) + content.read(from: processedIncomingData) + } + + /// - Tag: writeExample + override func data(ofType typeName: String) throws -> Data { + var strToDealWith = content.contentString + strToDealWith.formatConsolidate(cnvHYPYtoBPMF: true) + let outputData = Data(strToDealWith.utf8) + return outputData + } + + // MARK: - Printing + + func thePrintInfo() -> NSPrintInfo { + let thePrintInfo = NSPrintInfo() + thePrintInfo.horizontalPagination = .fit + thePrintInfo.isHorizontallyCentered = false + thePrintInfo.isVerticallyCentered = false + + // One inch margin all the way around. + thePrintInfo.leftMargin = 72.0 + thePrintInfo.rightMargin = 72.0 + thePrintInfo.topMargin = 72.0 + thePrintInfo.bottomMargin = 72.0 + + printInfo.dictionary().setObject( + NSNumber(value: true), + forKey: NSPrintInfo.AttributeKey.headerAndFooter as NSCopying) + + return thePrintInfo + } + + @objc + func printOperationDidRun( + _ printOperation: NSPrintOperation, success: Bool, contextInfo: UnsafeMutableRawPointer? + ) { + // Printing finished... + } + + @IBAction override func printDocument(_ sender: Any?) { + // Print the NSTextView. + + // Create a copy to manipulate for printing. + let pageSize = NSSize( + width: (printInfo.paperSize.width), height: (printInfo.paperSize.height)) + let textView = NSTextView( + frame: NSRect(x: 0.0, y: 0.0, width: pageSize.width, height: pageSize.height)) + + // Make sure we print on a white background. + textView.appearance = NSAppearance(named: .aqua) + + // Copy the attributed string. + textView.textStorage?.append(NSAttributedString(string: content.contentString)) + + let printOperation = NSPrintOperation(view: textView) + printOperation.runModal( + for: windowControllers[0].window!, + delegate: self, + didRun: #selector(printOperationDidRun(_:success:contextInfo:)), contextInfo: nil) + } + } diff --git a/UserPhraseEditor/StringExtension.swift b/UserPhraseEditor/StringExtension.swift index 08b1db62..bc6aa222 100644 --- a/UserPhraseEditor/StringExtension.swift +++ b/UserPhraseEditor/StringExtension.swift @@ -1,515 +1,523 @@ // 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: +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. +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. +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. +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 String { - 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(self.startIndex..., in: self) - self = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) - } catch { return } - } - mutating func selfReplace(_ strOf: String, _ strWith: String = "") { - self = self.replacingOccurrences(of: strOf, with: strWith) - } - mutating func formatConsolidate(HYPY2BPMF: Bool) { - // Step 1: Consolidating formats per line. - var strProcessed = self - // 預處理格式 - strProcessed = strProcessed.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 - // 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: "") // 去除行尾行首空格 - strProcessed.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, 且去除重複行 - if strProcessed.prefix(1) == " " { // 去除檔案開頭空格 - strProcessed.removeFirst() - } - if strProcessed.suffix(1) == " " { // 去除檔案結尾空格 - strProcessed.removeLast() - } - var arrData = [""] - if HYPY2BPMF { - // Step 0: Convert HanyuPinyin to Bopomofo. - arrData = strProcessed.components(separatedBy: "\n") - strProcessed = "" // Reset its value - for lineData in arrData { - var varLineData = lineData - // 漢語拼音轉注音,得先從最長的可能的拼音組合開始轉起, - // 這樣等轉換到更短的可能的漢語拼音組合時就不會出錯。 - // 依此類推,聲調放在最後來轉換。 - varLineData.selfReplace("chuang", "ㄔㄨㄤ") - varLineData.selfReplace("shuang", "ㄕㄨㄤ") - varLineData.selfReplace("zhuang", "ㄓㄨㄤ") - varLineData.selfReplace("chang", "ㄔㄤ") - varLineData.selfReplace("cheng", "ㄔㄥ") - varLineData.selfReplace("chong", "ㄔㄨㄥ") - varLineData.selfReplace("chuai", "ㄔㄨㄞ") - varLineData.selfReplace("chuan", "ㄔㄨㄢ") - varLineData.selfReplace("guang", "ㄍㄨㄤ") - varLineData.selfReplace("huang", "ㄏㄨㄤ") - varLineData.selfReplace("jiang", "ㄐㄧㄤ") - varLineData.selfReplace("jiong", "ㄐㄩㄥ") - varLineData.selfReplace("kuang", "ㄎㄨㄤ") - varLineData.selfReplace("liang", "ㄌㄧㄤ") - varLineData.selfReplace("niang", "ㄋㄧㄤ") - varLineData.selfReplace("qiang", "ㄑㄧㄤ") - varLineData.selfReplace("qiong", "ㄑㄩㄥ") - varLineData.selfReplace("shang", "ㄕㄤ") - varLineData.selfReplace("sheng", "ㄕㄥ") - varLineData.selfReplace("shuai", "ㄕㄨㄞ") - varLineData.selfReplace("shuan", "ㄕㄨㄢ") - varLineData.selfReplace("xiang", "ㄒㄧㄤ") - varLineData.selfReplace("xiong", "ㄒㄩㄥ") - varLineData.selfReplace("zhang", "ㄓㄤ") - varLineData.selfReplace("zheng", "ㄓㄥ") - varLineData.selfReplace("zhong", "ㄓㄨㄥ") - varLineData.selfReplace("zhuai", "ㄓㄨㄞ") - varLineData.selfReplace("zhuan", "ㄓㄨㄢ") - varLineData.selfReplace("bang", "ㄅㄤ") - varLineData.selfReplace("beng", "ㄅㄥ") - varLineData.selfReplace("bian", "ㄅㄧㄢ") - varLineData.selfReplace("biao", "ㄅㄧㄠ") - varLineData.selfReplace("bing", "ㄅㄧㄥ") - varLineData.selfReplace("cang", "ㄘㄤ") - varLineData.selfReplace("ceng", "ㄘㄥ") - varLineData.selfReplace("chai", "ㄔㄞ") - varLineData.selfReplace("chan", "ㄔㄢ") - varLineData.selfReplace("chao", "ㄔㄠ") - varLineData.selfReplace("chen", "ㄔㄣ") - varLineData.selfReplace("chou", "ㄔㄡ") - varLineData.selfReplace("chua", "ㄔㄨㄚ") - varLineData.selfReplace("chui", "ㄔㄨㄟ") - varLineData.selfReplace("chun", "ㄔㄨㄣ") - varLineData.selfReplace("chuo", "ㄔㄨㄛ") - varLineData.selfReplace("cong", "ㄘㄨㄥ") - varLineData.selfReplace("cuan", "ㄘㄨㄢ") - varLineData.selfReplace("dang", "ㄉㄤ") - varLineData.selfReplace("deng", "ㄉㄥ") - varLineData.selfReplace("dian", "ㄉㄧㄢ") - varLineData.selfReplace("diao", "ㄉㄧㄠ") - varLineData.selfReplace("ding", "ㄉㄧㄥ") - varLineData.selfReplace("dong", "ㄉㄨㄥ") - varLineData.selfReplace("duan", "ㄉㄨㄢ") - varLineData.selfReplace("fang", "ㄈㄤ") - varLineData.selfReplace("feng", "ㄈㄥ") - varLineData.selfReplace("fiao", "ㄈㄧㄠ") - varLineData.selfReplace("fong", "ㄈㄨㄥ") - varLineData.selfReplace("gang", "ㄍㄤ") - varLineData.selfReplace("geng", "ㄍㄥ") - varLineData.selfReplace("giao", "ㄍㄧㄠ") - varLineData.selfReplace("gong", "ㄍㄨㄥ") - varLineData.selfReplace("guai", "ㄍㄨㄞ") - varLineData.selfReplace("guan", "ㄍㄨㄢ") - varLineData.selfReplace("hang", "ㄏㄤ") - varLineData.selfReplace("heng", "ㄏㄥ") - varLineData.selfReplace("hong", "ㄏㄨㄥ") - varLineData.selfReplace("huai", "ㄏㄨㄞ") - varLineData.selfReplace("huan", "ㄏㄨㄢ") - varLineData.selfReplace("jian", "ㄐㄧㄢ") - varLineData.selfReplace("jiao", "ㄐㄧㄠ") - varLineData.selfReplace("jing", "ㄐㄧㄥ") - varLineData.selfReplace("juan", "ㄐㄩㄢ") - varLineData.selfReplace("kang", "ㄎㄤ") - varLineData.selfReplace("keng", "ㄎㄥ") - varLineData.selfReplace("kong", "ㄎㄨㄥ") - varLineData.selfReplace("kuai", "ㄎㄨㄞ") - varLineData.selfReplace("kuan", "ㄎㄨㄢ") - varLineData.selfReplace("lang", "ㄌㄤ") - varLineData.selfReplace("leng", "ㄌㄥ") - varLineData.selfReplace("lian", "ㄌㄧㄢ") - varLineData.selfReplace("liao", "ㄌㄧㄠ") - varLineData.selfReplace("ling", "ㄌㄧㄥ") - varLineData.selfReplace("long", "ㄌㄨㄥ") - varLineData.selfReplace("luan", "ㄌㄨㄢ") - varLineData.selfReplace("lvan", "ㄌㄩㄢ") - varLineData.selfReplace("mang", "ㄇㄤ") - varLineData.selfReplace("meng", "ㄇㄥ") - varLineData.selfReplace("mian", "ㄇㄧㄢ") - varLineData.selfReplace("miao", "ㄇㄧㄠ") - varLineData.selfReplace("ming", "ㄇㄧㄥ") - varLineData.selfReplace("nang", "ㄋㄤ") - varLineData.selfReplace("neng", "ㄋㄥ") - varLineData.selfReplace("nian", "ㄋㄧㄢ") - varLineData.selfReplace("niao", "ㄋㄧㄠ") - varLineData.selfReplace("ning", "ㄋㄧㄥ") - varLineData.selfReplace("nong", "ㄋㄨㄥ") - varLineData.selfReplace("nuan", "ㄋㄨㄢ") - varLineData.selfReplace("pang", "ㄆㄤ") - varLineData.selfReplace("peng", "ㄆㄥ") - varLineData.selfReplace("pian", "ㄆㄧㄢ") - varLineData.selfReplace("piao", "ㄆㄧㄠ") - varLineData.selfReplace("ping", "ㄆㄧㄥ") - varLineData.selfReplace("qian", "ㄑㄧㄢ") - varLineData.selfReplace("qiao", "ㄑㄧㄠ") - varLineData.selfReplace("qing", "ㄑㄧㄥ") - varLineData.selfReplace("quan", "ㄑㄩㄢ") - varLineData.selfReplace("rang", "ㄖㄤ") - varLineData.selfReplace("reng", "ㄖㄥ") - varLineData.selfReplace("rong", "ㄖㄨㄥ") - varLineData.selfReplace("ruan", "ㄖㄨㄢ") - varLineData.selfReplace("sang", "ㄙㄤ") - varLineData.selfReplace("seng", "ㄙㄥ") - varLineData.selfReplace("shai", "ㄕㄞ") - varLineData.selfReplace("shan", "ㄕㄢ") - varLineData.selfReplace("shao", "ㄕㄠ") - varLineData.selfReplace("shei", "ㄕㄟ") - varLineData.selfReplace("shen", "ㄕㄣ") - varLineData.selfReplace("shou", "ㄕㄡ") - varLineData.selfReplace("shua", "ㄕㄨㄚ") - varLineData.selfReplace("shui", "ㄕㄨㄟ") - varLineData.selfReplace("shun", "ㄕㄨㄣ") - varLineData.selfReplace("shuo", "ㄕㄨㄛ") - varLineData.selfReplace("song", "ㄙㄨㄥ") - varLineData.selfReplace("suan", "ㄙㄨㄢ") - varLineData.selfReplace("tang", "ㄊㄤ") - varLineData.selfReplace("teng", "ㄊㄥ") - varLineData.selfReplace("tian", "ㄊㄧㄢ") - varLineData.selfReplace("tiao", "ㄊㄧㄠ") - varLineData.selfReplace("ting", "ㄊㄧㄥ") - varLineData.selfReplace("tong", "ㄊㄨㄥ") - varLineData.selfReplace("tuan", "ㄊㄨㄢ") - varLineData.selfReplace("wang", "ㄨㄤ") - varLineData.selfReplace("weng", "ㄨㄥ") - varLineData.selfReplace("xian", "ㄒㄧㄢ") - varLineData.selfReplace("xiao", "ㄒㄧㄠ") - varLineData.selfReplace("xing", "ㄒㄧㄥ") - varLineData.selfReplace("xuan", "ㄒㄩㄢ") - varLineData.selfReplace("yang", "ㄧㄤ") - varLineData.selfReplace("ying", "ㄧㄥ") - varLineData.selfReplace("yong", "ㄩㄥ") - varLineData.selfReplace("yuan", "ㄩㄢ") - varLineData.selfReplace("zang", "ㄗㄤ") - varLineData.selfReplace("zeng", "ㄗㄥ") - varLineData.selfReplace("zhai", "ㄓㄞ") - varLineData.selfReplace("zhan", "ㄓㄢ") - varLineData.selfReplace("zhao", "ㄓㄠ") - varLineData.selfReplace("zhei", "ㄓㄟ") - varLineData.selfReplace("zhen", "ㄓㄣ") - varLineData.selfReplace("zhou", "ㄓㄡ") - varLineData.selfReplace("zhua", "ㄓㄨㄚ") - varLineData.selfReplace("zhui", "ㄓㄨㄟ") - varLineData.selfReplace("zhun", "ㄓㄨㄣ") - varLineData.selfReplace("zhuo", "ㄓㄨㄛ") - varLineData.selfReplace("zong", "ㄗㄨㄥ") - varLineData.selfReplace("zuan", "ㄗㄨㄢ") - varLineData.selfReplace("jun", "ㄐㄩㄣ") - varLineData.selfReplace("ang", "ㄤ") - varLineData.selfReplace("bai", "ㄅㄞ") - varLineData.selfReplace("ban", "ㄅㄢ") - varLineData.selfReplace("bao", "ㄅㄠ") - varLineData.selfReplace("bei", "ㄅㄟ") - varLineData.selfReplace("ben", "ㄅㄣ") - varLineData.selfReplace("bie", "ㄅㄧㄝ") - varLineData.selfReplace("bin", "ㄅㄧㄣ") - varLineData.selfReplace("cai", "ㄘㄞ") - varLineData.selfReplace("can", "ㄘㄢ") - varLineData.selfReplace("cao", "ㄘㄠ") - varLineData.selfReplace("cei", "ㄘㄟ") - varLineData.selfReplace("cen", "ㄘㄣ") - varLineData.selfReplace("cha", "ㄔㄚ") - varLineData.selfReplace("che", "ㄔㄜ") - varLineData.selfReplace("chi", "ㄔ") - varLineData.selfReplace("chu", "ㄔㄨ") - varLineData.selfReplace("cou", "ㄘㄡ") - varLineData.selfReplace("cui", "ㄘㄨㄟ") - varLineData.selfReplace("cun", "ㄘㄨㄣ") - varLineData.selfReplace("cuo", "ㄘㄨㄛ") - varLineData.selfReplace("dai", "ㄉㄞ") - varLineData.selfReplace("dan", "ㄉㄢ") - varLineData.selfReplace("dao", "ㄉㄠ") - varLineData.selfReplace("dei", "ㄉㄟ") - varLineData.selfReplace("den", "ㄉㄣ") - varLineData.selfReplace("dia", "ㄉㄧㄚ") - varLineData.selfReplace("die", "ㄉㄧㄝ") - varLineData.selfReplace("diu", "ㄉㄧㄡ") - varLineData.selfReplace("dou", "ㄉㄡ") - varLineData.selfReplace("dui", "ㄉㄨㄟ") - varLineData.selfReplace("dun", "ㄉㄨㄣ") - varLineData.selfReplace("duo", "ㄉㄨㄛ") - varLineData.selfReplace("eng", "ㄥ") - varLineData.selfReplace("fan", "ㄈㄢ") - varLineData.selfReplace("fei", "ㄈㄟ") - varLineData.selfReplace("fen", "ㄈㄣ") - varLineData.selfReplace("fou", "ㄈㄡ") - varLineData.selfReplace("gai", "ㄍㄞ") - varLineData.selfReplace("gan", "ㄍㄢ") - varLineData.selfReplace("gao", "ㄍㄠ") - varLineData.selfReplace("gei", "ㄍㄟ") - varLineData.selfReplace("gin", "ㄍㄧㄣ") - varLineData.selfReplace("gen", "ㄍㄣ") - varLineData.selfReplace("gou", "ㄍㄡ") - varLineData.selfReplace("gua", "ㄍㄨㄚ") - varLineData.selfReplace("gue", "ㄍㄨㄜ") - varLineData.selfReplace("gui", "ㄍㄨㄟ") - varLineData.selfReplace("gun", "ㄍㄨㄣ") - varLineData.selfReplace("guo", "ㄍㄨㄛ") - varLineData.selfReplace("hai", "ㄏㄞ") - varLineData.selfReplace("han", "ㄏㄢ") - varLineData.selfReplace("hao", "ㄏㄠ") - varLineData.selfReplace("hei", "ㄏㄟ") - varLineData.selfReplace("hen", "ㄏㄣ") - varLineData.selfReplace("hou", "ㄏㄡ") - varLineData.selfReplace("hua", "ㄏㄨㄚ") - varLineData.selfReplace("hui", "ㄏㄨㄟ") - varLineData.selfReplace("hun", "ㄏㄨㄣ") - varLineData.selfReplace("huo", "ㄏㄨㄛ") - varLineData.selfReplace("jia", "ㄐㄧㄚ") - varLineData.selfReplace("jie", "ㄐㄧㄝ") - varLineData.selfReplace("jin", "ㄐㄧㄣ") - varLineData.selfReplace("jiu", "ㄐㄧㄡ") - varLineData.selfReplace("jue", "ㄐㄩㄝ") - varLineData.selfReplace("kai", "ㄎㄞ") - varLineData.selfReplace("kan", "ㄎㄢ") - varLineData.selfReplace("kao", "ㄎㄠ") - varLineData.selfReplace("ken", "ㄎㄣ") - varLineData.selfReplace("kiu", "ㄎㄧㄡ") - varLineData.selfReplace("kou", "ㄎㄡ") - varLineData.selfReplace("kua", "ㄎㄨㄚ") - varLineData.selfReplace("kui", "ㄎㄨㄟ") - varLineData.selfReplace("kun", "ㄎㄨㄣ") - varLineData.selfReplace("kuo", "ㄎㄨㄛ") - varLineData.selfReplace("lai", "ㄌㄞ") - varLineData.selfReplace("lan", "ㄌㄢ") - varLineData.selfReplace("lao", "ㄌㄠ") - varLineData.selfReplace("lei", "ㄌㄟ") - varLineData.selfReplace("lia", "ㄌㄧㄚ") - varLineData.selfReplace("lie", "ㄌㄧㄝ") - varLineData.selfReplace("lin", "ㄌㄧㄣ") - varLineData.selfReplace("liu", "ㄌㄧㄡ") - varLineData.selfReplace("lou", "ㄌㄡ") - varLineData.selfReplace("lun", "ㄌㄨㄣ") - varLineData.selfReplace("luo", "ㄌㄨㄛ") - varLineData.selfReplace("lve", "ㄌㄩㄝ") - varLineData.selfReplace("mai", "ㄇㄞ") - varLineData.selfReplace("man", "ㄇㄢ") - varLineData.selfReplace("mao", "ㄇㄠ") - varLineData.selfReplace("mei", "ㄇㄟ") - varLineData.selfReplace("men", "ㄇㄣ") - varLineData.selfReplace("mie", "ㄇㄧㄝ") - varLineData.selfReplace("min", "ㄇㄧㄣ") - varLineData.selfReplace("miu", "ㄇㄧㄡ") - varLineData.selfReplace("mou", "ㄇㄡ") - varLineData.selfReplace("nai", "ㄋㄞ") - varLineData.selfReplace("nan", "ㄋㄢ") - varLineData.selfReplace("nao", "ㄋㄠ") - varLineData.selfReplace("nei", "ㄋㄟ") - varLineData.selfReplace("nen", "ㄋㄣ") - varLineData.selfReplace("nie", "ㄋㄧㄝ") - varLineData.selfReplace("nin", "ㄋㄧㄣ") - varLineData.selfReplace("niu", "ㄋㄧㄡ") - varLineData.selfReplace("nou", "ㄋㄡ") - varLineData.selfReplace("nui", "ㄋㄨㄟ") - varLineData.selfReplace("nun", "ㄋㄨㄣ") - varLineData.selfReplace("nuo", "ㄋㄨㄛ") - varLineData.selfReplace("nve", "ㄋㄩㄝ") - varLineData.selfReplace("pai", "ㄆㄞ") - varLineData.selfReplace("pan", "ㄆㄢ") - varLineData.selfReplace("pao", "ㄆㄠ") - varLineData.selfReplace("pei", "ㄆㄟ") - varLineData.selfReplace("pen", "ㄆㄣ") - varLineData.selfReplace("pia", "ㄆㄧㄚ") - varLineData.selfReplace("pie", "ㄆㄧㄝ") - varLineData.selfReplace("pin", "ㄆㄧㄣ") - varLineData.selfReplace("pou", "ㄆㄡ") - varLineData.selfReplace("qia", "ㄑㄧㄚ") - varLineData.selfReplace("qie", "ㄑㄧㄝ") - varLineData.selfReplace("qin", "ㄑㄧㄣ") - varLineData.selfReplace("qiu", "ㄑㄧㄡ") - varLineData.selfReplace("que", "ㄑㄩㄝ") - varLineData.selfReplace("qun", "ㄑㄩㄣ") - varLineData.selfReplace("ran", "ㄖㄢ") - varLineData.selfReplace("rao", "ㄖㄠ") - varLineData.selfReplace("ren", "ㄖㄣ") - varLineData.selfReplace("rou", "ㄖㄡ") - varLineData.selfReplace("rui", "ㄖㄨㄟ") - varLineData.selfReplace("run", "ㄖㄨㄣ") - varLineData.selfReplace("ruo", "ㄖㄨㄛ") - varLineData.selfReplace("sai", "ㄙㄞ") - varLineData.selfReplace("san", "ㄙㄢ") - varLineData.selfReplace("sao", "ㄙㄠ") - varLineData.selfReplace("sei", "ㄙㄟ") - varLineData.selfReplace("sen", "ㄙㄣ") - varLineData.selfReplace("sha", "ㄕㄚ") - varLineData.selfReplace("she", "ㄕㄜ") - varLineData.selfReplace("shi", "ㄕ") - varLineData.selfReplace("shu", "ㄕㄨ") - varLineData.selfReplace("sou", "ㄙㄡ") - varLineData.selfReplace("sui", "ㄙㄨㄟ") - varLineData.selfReplace("sun", "ㄙㄨㄣ") - varLineData.selfReplace("suo", "ㄙㄨㄛ") - varLineData.selfReplace("tai", "ㄊㄞ") - varLineData.selfReplace("tan", "ㄊㄢ") - varLineData.selfReplace("tao", "ㄊㄠ") - varLineData.selfReplace("tie", "ㄊㄧㄝ") - varLineData.selfReplace("tou", "ㄊㄡ") - varLineData.selfReplace("tui", "ㄊㄨㄟ") - varLineData.selfReplace("tun", "ㄊㄨㄣ") - varLineData.selfReplace("tuo", "ㄊㄨㄛ") - varLineData.selfReplace("wai", "ㄨㄞ") - varLineData.selfReplace("wan", "ㄨㄢ") - varLineData.selfReplace("wei", "ㄨㄟ") - varLineData.selfReplace("wen", "ㄨㄣ") - varLineData.selfReplace("xia", "ㄒㄧㄚ") - varLineData.selfReplace("xie", "ㄒㄧㄝ") - varLineData.selfReplace("xin", "ㄒㄧㄣ") - varLineData.selfReplace("xiu", "ㄒㄧㄡ") - varLineData.selfReplace("xue", "ㄒㄩㄝ") - varLineData.selfReplace("xun", "ㄒㄩㄣ") - varLineData.selfReplace("yai", "ㄧㄞ") - varLineData.selfReplace("yan", "ㄧㄢ") - varLineData.selfReplace("yao", "ㄧㄠ") - varLineData.selfReplace("yin", "ㄧㄣ") - varLineData.selfReplace("you", "ㄧㄡ") - varLineData.selfReplace("yue", "ㄩㄝ") - varLineData.selfReplace("yun", "ㄩㄣ") - varLineData.selfReplace("zai", "ㄗㄞ") - varLineData.selfReplace("zan", "ㄗㄢ") - varLineData.selfReplace("zao", "ㄗㄠ") - varLineData.selfReplace("zei", "ㄗㄟ") - varLineData.selfReplace("zen", "ㄗㄣ") - varLineData.selfReplace("zha", "ㄓㄚ") - varLineData.selfReplace("zhe", "ㄓㄜ") - varLineData.selfReplace("zhi", "ㄓ") - varLineData.selfReplace("zhu", "ㄓㄨ") - varLineData.selfReplace("zou", "ㄗㄡ") - varLineData.selfReplace("zui", "ㄗㄨㄟ") - varLineData.selfReplace("zun", "ㄗㄨㄣ") - varLineData.selfReplace("zuo", "ㄗㄨㄛ") - varLineData.selfReplace("ai", "ㄞ") - varLineData.selfReplace("an", "ㄢ") - varLineData.selfReplace("ao", "ㄠ") - varLineData.selfReplace("ba", "ㄅㄚ") - varLineData.selfReplace("bi", "ㄅㄧ") - varLineData.selfReplace("bo", "ㄅㄛ") - varLineData.selfReplace("bu", "ㄅㄨ") - varLineData.selfReplace("ca", "ㄘㄚ") - varLineData.selfReplace("ce", "ㄘㄜ") - varLineData.selfReplace("ci", "ㄘ") - varLineData.selfReplace("cu", "ㄘㄨ") - varLineData.selfReplace("da", "ㄉㄚ") - varLineData.selfReplace("de", "ㄉㄜ") - varLineData.selfReplace("di", "ㄉㄧ") - varLineData.selfReplace("du", "ㄉㄨ") - varLineData.selfReplace("eh", "ㄝ") - varLineData.selfReplace("ei", "ㄟ") - varLineData.selfReplace("en", "ㄣ") - varLineData.selfReplace("er", "ㄦ") - varLineData.selfReplace("fa", "ㄈㄚ") - varLineData.selfReplace("fo", "ㄈㄛ") - varLineData.selfReplace("fu", "ㄈㄨ") - varLineData.selfReplace("ga", "ㄍㄚ") - varLineData.selfReplace("ge", "ㄍㄜ") - varLineData.selfReplace("gi", "ㄍㄧ") - varLineData.selfReplace("gu", "ㄍㄨ") - varLineData.selfReplace("ha", "ㄏㄚ") - varLineData.selfReplace("he", "ㄏㄜ") - varLineData.selfReplace("hu", "ㄏㄨ") - varLineData.selfReplace("ji", "ㄐㄧ") - varLineData.selfReplace("ju", "ㄐㄩ") - varLineData.selfReplace("ka", "ㄎㄚ") - varLineData.selfReplace("ke", "ㄎㄜ") - varLineData.selfReplace("ku", "ㄎㄨ") - varLineData.selfReplace("la", "ㄌㄚ") - varLineData.selfReplace("le", "ㄌㄜ") - varLineData.selfReplace("li", "ㄌㄧ") - varLineData.selfReplace("lo", "ㄌㄛ") - varLineData.selfReplace("lu", "ㄌㄨ") - varLineData.selfReplace("lv", "ㄌㄩ") - varLineData.selfReplace("ma", "ㄇㄚ") - varLineData.selfReplace("me", "ㄇㄜ") - varLineData.selfReplace("mi", "ㄇㄧ") - varLineData.selfReplace("mo", "ㄇㄛ") - varLineData.selfReplace("mu", "ㄇㄨ") - varLineData.selfReplace("na", "ㄋㄚ") - varLineData.selfReplace("ne", "ㄋㄜ") - varLineData.selfReplace("ni", "ㄋㄧ") - varLineData.selfReplace("nu", "ㄋㄨ") - varLineData.selfReplace("nv", "ㄋㄩ") - varLineData.selfReplace("ou", "ㄡ") - varLineData.selfReplace("pa", "ㄆㄚ") - varLineData.selfReplace("pi", "ㄆㄧ") - varLineData.selfReplace("po", "ㄆㄛ") - varLineData.selfReplace("pu", "ㄆㄨ") - varLineData.selfReplace("qi", "ㄑㄧ") - varLineData.selfReplace("qu", "ㄑㄩ") - varLineData.selfReplace("re", "ㄖㄜ") - varLineData.selfReplace("ri", "ㄖ") - varLineData.selfReplace("ru", "ㄖㄨ") - varLineData.selfReplace("sa", "ㄙㄚ") - varLineData.selfReplace("se", "ㄙㄜ") - varLineData.selfReplace("si", "ㄙ") - varLineData.selfReplace("su", "ㄙㄨ") - varLineData.selfReplace("ta", "ㄊㄚ") - varLineData.selfReplace("te", "ㄊㄜ") - varLineData.selfReplace("ti", "ㄊㄧ") - varLineData.selfReplace("tu", "ㄊㄨ") - varLineData.selfReplace("wa", "ㄨㄚ") - varLineData.selfReplace("wo", "ㄨㄛ") - varLineData.selfReplace("wu", "ㄨ") - varLineData.selfReplace("xi", "ㄒㄧ") - varLineData.selfReplace("xu", "ㄒㄩ") - varLineData.selfReplace("ya", "ㄧㄚ") - varLineData.selfReplace("ye", "ㄧㄝ") - varLineData.selfReplace("yi", "ㄧ") - varLineData.selfReplace("yo", "ㄧㄛ") - varLineData.selfReplace("yu", "ㄩ") - varLineData.selfReplace("za", "ㄗㄚ") - varLineData.selfReplace("ze", "ㄗㄜ") - varLineData.selfReplace("zi", "ㄗ") - varLineData.selfReplace("zu", "ㄗㄨ") - varLineData.selfReplace("a", "ㄚ") - varLineData.selfReplace("e", "ㄜ") - varLineData.selfReplace("o", "ㄛ") - varLineData.selfReplace("q", "ㄑ") - varLineData.selfReplace("2", "ˊ") - varLineData.selfReplace("3", "ˇ") - varLineData.selfReplace("4", "ˋ") - varLineData.selfReplace("5", "˙") - varLineData.selfReplace("1", "") - strProcessed += varLineData - strProcessed += "\n" - } - } - - // Step 3: Add Formatted Pragma - let hdrFormatted = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍\n" // Sorted Header - strProcessed = hdrFormatted + strProcessed // Add Sorted Header - - // Step 4: Deduplication. - 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: Commit Formatted Contents. - self = strProcessed - } + 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(self.startIndex..., in: self) + self = regex.stringByReplacingMatches( + in: self, options: [], range: range, withTemplate: replaceWith) + } catch { return } + } + mutating func selfReplace(_ strOf: String, _ strWith: String = "") { + self = self.replacingOccurrences(of: strOf, with: strWith) + } + mutating func formatConsolidate(cnvHYPYtoBPMF: Bool) { + // Step 1: Consolidating formats per line. + var strProcessed = self + // 預處理格式 + strProcessed = strProcessed.replacingOccurrences(of: " #MACOS", with: "") // 去掉 macOS 標記 + // 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: "") // 去除行尾行首空格 + strProcessed.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & FF to LF, 且去除重複行 + if strProcessed.prefix(1) == " " { // 去除檔案開頭空格 + strProcessed.removeFirst() + } + if strProcessed.suffix(1) == " " { // 去除檔案結尾空格 + strProcessed.removeLast() + } + var arrData = [""] + if cnvHYPYtoBPMF { + // Step 0: Convert HanyuPinyin to Bopomofo. + arrData = strProcessed.components(separatedBy: "\n") + strProcessed = "" // Reset its value + for lineData in arrData { + var varLineData = lineData + // 漢語拼音轉注音,得先從最長的可能的拼音組合開始轉起, + // 這樣等轉換到更短的可能的漢語拼音組合時就不會出錯。 + // 依此類推,聲調放在最後來轉換。 + varLineData.selfReplace("chuang", "ㄔㄨㄤ") + varLineData.selfReplace("shuang", "ㄕㄨㄤ") + varLineData.selfReplace("zhuang", "ㄓㄨㄤ") + varLineData.selfReplace("chang", "ㄔㄤ") + varLineData.selfReplace("cheng", "ㄔㄥ") + varLineData.selfReplace("chong", "ㄔㄨㄥ") + varLineData.selfReplace("chuai", "ㄔㄨㄞ") + varLineData.selfReplace("chuan", "ㄔㄨㄢ") + varLineData.selfReplace("guang", "ㄍㄨㄤ") + varLineData.selfReplace("huang", "ㄏㄨㄤ") + varLineData.selfReplace("jiang", "ㄐㄧㄤ") + varLineData.selfReplace("jiong", "ㄐㄩㄥ") + varLineData.selfReplace("kuang", "ㄎㄨㄤ") + varLineData.selfReplace("liang", "ㄌㄧㄤ") + varLineData.selfReplace("niang", "ㄋㄧㄤ") + varLineData.selfReplace("qiang", "ㄑㄧㄤ") + varLineData.selfReplace("qiong", "ㄑㄩㄥ") + varLineData.selfReplace("shang", "ㄕㄤ") + varLineData.selfReplace("sheng", "ㄕㄥ") + varLineData.selfReplace("shuai", "ㄕㄨㄞ") + varLineData.selfReplace("shuan", "ㄕㄨㄢ") + varLineData.selfReplace("xiang", "ㄒㄧㄤ") + varLineData.selfReplace("xiong", "ㄒㄩㄥ") + varLineData.selfReplace("zhang", "ㄓㄤ") + varLineData.selfReplace("zheng", "ㄓㄥ") + varLineData.selfReplace("zhong", "ㄓㄨㄥ") + varLineData.selfReplace("zhuai", "ㄓㄨㄞ") + varLineData.selfReplace("zhuan", "ㄓㄨㄢ") + varLineData.selfReplace("bang", "ㄅㄤ") + varLineData.selfReplace("beng", "ㄅㄥ") + varLineData.selfReplace("bian", "ㄅㄧㄢ") + varLineData.selfReplace("biao", "ㄅㄧㄠ") + varLineData.selfReplace("bing", "ㄅㄧㄥ") + varLineData.selfReplace("cang", "ㄘㄤ") + varLineData.selfReplace("ceng", "ㄘㄥ") + varLineData.selfReplace("chai", "ㄔㄞ") + varLineData.selfReplace("chan", "ㄔㄢ") + varLineData.selfReplace("chao", "ㄔㄠ") + varLineData.selfReplace("chen", "ㄔㄣ") + varLineData.selfReplace("chou", "ㄔㄡ") + varLineData.selfReplace("chua", "ㄔㄨㄚ") + varLineData.selfReplace("chui", "ㄔㄨㄟ") + varLineData.selfReplace("chun", "ㄔㄨㄣ") + varLineData.selfReplace("chuo", "ㄔㄨㄛ") + varLineData.selfReplace("cong", "ㄘㄨㄥ") + varLineData.selfReplace("cuan", "ㄘㄨㄢ") + varLineData.selfReplace("dang", "ㄉㄤ") + varLineData.selfReplace("deng", "ㄉㄥ") + varLineData.selfReplace("dian", "ㄉㄧㄢ") + varLineData.selfReplace("diao", "ㄉㄧㄠ") + varLineData.selfReplace("ding", "ㄉㄧㄥ") + varLineData.selfReplace("dong", "ㄉㄨㄥ") + varLineData.selfReplace("duan", "ㄉㄨㄢ") + varLineData.selfReplace("fang", "ㄈㄤ") + varLineData.selfReplace("feng", "ㄈㄥ") + varLineData.selfReplace("fiao", "ㄈㄧㄠ") + varLineData.selfReplace("fong", "ㄈㄨㄥ") + varLineData.selfReplace("gang", "ㄍㄤ") + varLineData.selfReplace("geng", "ㄍㄥ") + varLineData.selfReplace("giao", "ㄍㄧㄠ") + varLineData.selfReplace("gong", "ㄍㄨㄥ") + varLineData.selfReplace("guai", "ㄍㄨㄞ") + varLineData.selfReplace("guan", "ㄍㄨㄢ") + varLineData.selfReplace("hang", "ㄏㄤ") + varLineData.selfReplace("heng", "ㄏㄥ") + varLineData.selfReplace("hong", "ㄏㄨㄥ") + varLineData.selfReplace("huai", "ㄏㄨㄞ") + varLineData.selfReplace("huan", "ㄏㄨㄢ") + varLineData.selfReplace("jian", "ㄐㄧㄢ") + varLineData.selfReplace("jiao", "ㄐㄧㄠ") + varLineData.selfReplace("jing", "ㄐㄧㄥ") + varLineData.selfReplace("juan", "ㄐㄩㄢ") + varLineData.selfReplace("kang", "ㄎㄤ") + varLineData.selfReplace("keng", "ㄎㄥ") + varLineData.selfReplace("kong", "ㄎㄨㄥ") + varLineData.selfReplace("kuai", "ㄎㄨㄞ") + varLineData.selfReplace("kuan", "ㄎㄨㄢ") + varLineData.selfReplace("lang", "ㄌㄤ") + varLineData.selfReplace("leng", "ㄌㄥ") + varLineData.selfReplace("lian", "ㄌㄧㄢ") + varLineData.selfReplace("liao", "ㄌㄧㄠ") + varLineData.selfReplace("ling", "ㄌㄧㄥ") + varLineData.selfReplace("long", "ㄌㄨㄥ") + varLineData.selfReplace("luan", "ㄌㄨㄢ") + varLineData.selfReplace("lvan", "ㄌㄩㄢ") + varLineData.selfReplace("mang", "ㄇㄤ") + varLineData.selfReplace("meng", "ㄇㄥ") + varLineData.selfReplace("mian", "ㄇㄧㄢ") + varLineData.selfReplace("miao", "ㄇㄧㄠ") + varLineData.selfReplace("ming", "ㄇㄧㄥ") + varLineData.selfReplace("nang", "ㄋㄤ") + varLineData.selfReplace("neng", "ㄋㄥ") + varLineData.selfReplace("nian", "ㄋㄧㄢ") + varLineData.selfReplace("niao", "ㄋㄧㄠ") + varLineData.selfReplace("ning", "ㄋㄧㄥ") + varLineData.selfReplace("nong", "ㄋㄨㄥ") + varLineData.selfReplace("nuan", "ㄋㄨㄢ") + varLineData.selfReplace("pang", "ㄆㄤ") + varLineData.selfReplace("peng", "ㄆㄥ") + varLineData.selfReplace("pian", "ㄆㄧㄢ") + varLineData.selfReplace("piao", "ㄆㄧㄠ") + varLineData.selfReplace("ping", "ㄆㄧㄥ") + varLineData.selfReplace("qian", "ㄑㄧㄢ") + varLineData.selfReplace("qiao", "ㄑㄧㄠ") + varLineData.selfReplace("qing", "ㄑㄧㄥ") + varLineData.selfReplace("quan", "ㄑㄩㄢ") + varLineData.selfReplace("rang", "ㄖㄤ") + varLineData.selfReplace("reng", "ㄖㄥ") + varLineData.selfReplace("rong", "ㄖㄨㄥ") + varLineData.selfReplace("ruan", "ㄖㄨㄢ") + varLineData.selfReplace("sang", "ㄙㄤ") + varLineData.selfReplace("seng", "ㄙㄥ") + varLineData.selfReplace("shai", "ㄕㄞ") + varLineData.selfReplace("shan", "ㄕㄢ") + varLineData.selfReplace("shao", "ㄕㄠ") + varLineData.selfReplace("shei", "ㄕㄟ") + varLineData.selfReplace("shen", "ㄕㄣ") + varLineData.selfReplace("shou", "ㄕㄡ") + varLineData.selfReplace("shua", "ㄕㄨㄚ") + varLineData.selfReplace("shui", "ㄕㄨㄟ") + varLineData.selfReplace("shun", "ㄕㄨㄣ") + varLineData.selfReplace("shuo", "ㄕㄨㄛ") + varLineData.selfReplace("song", "ㄙㄨㄥ") + varLineData.selfReplace("suan", "ㄙㄨㄢ") + varLineData.selfReplace("tang", "ㄊㄤ") + varLineData.selfReplace("teng", "ㄊㄥ") + varLineData.selfReplace("tian", "ㄊㄧㄢ") + varLineData.selfReplace("tiao", "ㄊㄧㄠ") + varLineData.selfReplace("ting", "ㄊㄧㄥ") + varLineData.selfReplace("tong", "ㄊㄨㄥ") + varLineData.selfReplace("tuan", "ㄊㄨㄢ") + varLineData.selfReplace("wang", "ㄨㄤ") + varLineData.selfReplace("weng", "ㄨㄥ") + varLineData.selfReplace("xian", "ㄒㄧㄢ") + varLineData.selfReplace("xiao", "ㄒㄧㄠ") + varLineData.selfReplace("xing", "ㄒㄧㄥ") + varLineData.selfReplace("xuan", "ㄒㄩㄢ") + varLineData.selfReplace("yang", "ㄧㄤ") + varLineData.selfReplace("ying", "ㄧㄥ") + varLineData.selfReplace("yong", "ㄩㄥ") + varLineData.selfReplace("yuan", "ㄩㄢ") + varLineData.selfReplace("zang", "ㄗㄤ") + varLineData.selfReplace("zeng", "ㄗㄥ") + varLineData.selfReplace("zhai", "ㄓㄞ") + varLineData.selfReplace("zhan", "ㄓㄢ") + varLineData.selfReplace("zhao", "ㄓㄠ") + varLineData.selfReplace("zhei", "ㄓㄟ") + varLineData.selfReplace("zhen", "ㄓㄣ") + varLineData.selfReplace("zhou", "ㄓㄡ") + varLineData.selfReplace("zhua", "ㄓㄨㄚ") + varLineData.selfReplace("zhui", "ㄓㄨㄟ") + varLineData.selfReplace("zhun", "ㄓㄨㄣ") + varLineData.selfReplace("zhuo", "ㄓㄨㄛ") + varLineData.selfReplace("zong", "ㄗㄨㄥ") + varLineData.selfReplace("zuan", "ㄗㄨㄢ") + varLineData.selfReplace("jun", "ㄐㄩㄣ") + varLineData.selfReplace("ang", "ㄤ") + varLineData.selfReplace("bai", "ㄅㄞ") + varLineData.selfReplace("ban", "ㄅㄢ") + varLineData.selfReplace("bao", "ㄅㄠ") + varLineData.selfReplace("bei", "ㄅㄟ") + varLineData.selfReplace("ben", "ㄅㄣ") + varLineData.selfReplace("bie", "ㄅㄧㄝ") + varLineData.selfReplace("bin", "ㄅㄧㄣ") + varLineData.selfReplace("cai", "ㄘㄞ") + varLineData.selfReplace("can", "ㄘㄢ") + varLineData.selfReplace("cao", "ㄘㄠ") + varLineData.selfReplace("cei", "ㄘㄟ") + varLineData.selfReplace("cen", "ㄘㄣ") + varLineData.selfReplace("cha", "ㄔㄚ") + varLineData.selfReplace("che", "ㄔㄜ") + varLineData.selfReplace("chi", "ㄔ") + varLineData.selfReplace("chu", "ㄔㄨ") + varLineData.selfReplace("cou", "ㄘㄡ") + varLineData.selfReplace("cui", "ㄘㄨㄟ") + varLineData.selfReplace("cun", "ㄘㄨㄣ") + varLineData.selfReplace("cuo", "ㄘㄨㄛ") + varLineData.selfReplace("dai", "ㄉㄞ") + varLineData.selfReplace("dan", "ㄉㄢ") + varLineData.selfReplace("dao", "ㄉㄠ") + varLineData.selfReplace("dei", "ㄉㄟ") + varLineData.selfReplace("den", "ㄉㄣ") + varLineData.selfReplace("dia", "ㄉㄧㄚ") + varLineData.selfReplace("die", "ㄉㄧㄝ") + varLineData.selfReplace("diu", "ㄉㄧㄡ") + varLineData.selfReplace("dou", "ㄉㄡ") + varLineData.selfReplace("dui", "ㄉㄨㄟ") + varLineData.selfReplace("dun", "ㄉㄨㄣ") + varLineData.selfReplace("duo", "ㄉㄨㄛ") + varLineData.selfReplace("eng", "ㄥ") + varLineData.selfReplace("fan", "ㄈㄢ") + varLineData.selfReplace("fei", "ㄈㄟ") + varLineData.selfReplace("fen", "ㄈㄣ") + varLineData.selfReplace("fou", "ㄈㄡ") + varLineData.selfReplace("gai", "ㄍㄞ") + varLineData.selfReplace("gan", "ㄍㄢ") + varLineData.selfReplace("gao", "ㄍㄠ") + varLineData.selfReplace("gei", "ㄍㄟ") + varLineData.selfReplace("gin", "ㄍㄧㄣ") + varLineData.selfReplace("gen", "ㄍㄣ") + varLineData.selfReplace("gou", "ㄍㄡ") + varLineData.selfReplace("gua", "ㄍㄨㄚ") + varLineData.selfReplace("gue", "ㄍㄨㄜ") + varLineData.selfReplace("gui", "ㄍㄨㄟ") + varLineData.selfReplace("gun", "ㄍㄨㄣ") + varLineData.selfReplace("guo", "ㄍㄨㄛ") + varLineData.selfReplace("hai", "ㄏㄞ") + varLineData.selfReplace("han", "ㄏㄢ") + varLineData.selfReplace("hao", "ㄏㄠ") + varLineData.selfReplace("hei", "ㄏㄟ") + varLineData.selfReplace("hen", "ㄏㄣ") + varLineData.selfReplace("hou", "ㄏㄡ") + varLineData.selfReplace("hua", "ㄏㄨㄚ") + varLineData.selfReplace("hui", "ㄏㄨㄟ") + varLineData.selfReplace("hun", "ㄏㄨㄣ") + varLineData.selfReplace("huo", "ㄏㄨㄛ") + varLineData.selfReplace("jia", "ㄐㄧㄚ") + varLineData.selfReplace("jie", "ㄐㄧㄝ") + varLineData.selfReplace("jin", "ㄐㄧㄣ") + varLineData.selfReplace("jiu", "ㄐㄧㄡ") + varLineData.selfReplace("jue", "ㄐㄩㄝ") + varLineData.selfReplace("kai", "ㄎㄞ") + varLineData.selfReplace("kan", "ㄎㄢ") + varLineData.selfReplace("kao", "ㄎㄠ") + varLineData.selfReplace("ken", "ㄎㄣ") + varLineData.selfReplace("kiu", "ㄎㄧㄡ") + varLineData.selfReplace("kou", "ㄎㄡ") + varLineData.selfReplace("kua", "ㄎㄨㄚ") + varLineData.selfReplace("kui", "ㄎㄨㄟ") + varLineData.selfReplace("kun", "ㄎㄨㄣ") + varLineData.selfReplace("kuo", "ㄎㄨㄛ") + varLineData.selfReplace("lai", "ㄌㄞ") + varLineData.selfReplace("lan", "ㄌㄢ") + varLineData.selfReplace("lao", "ㄌㄠ") + varLineData.selfReplace("lei", "ㄌㄟ") + varLineData.selfReplace("lia", "ㄌㄧㄚ") + varLineData.selfReplace("lie", "ㄌㄧㄝ") + varLineData.selfReplace("lin", "ㄌㄧㄣ") + varLineData.selfReplace("liu", "ㄌㄧㄡ") + varLineData.selfReplace("lou", "ㄌㄡ") + varLineData.selfReplace("lun", "ㄌㄨㄣ") + varLineData.selfReplace("luo", "ㄌㄨㄛ") + varLineData.selfReplace("lve", "ㄌㄩㄝ") + varLineData.selfReplace("mai", "ㄇㄞ") + varLineData.selfReplace("man", "ㄇㄢ") + varLineData.selfReplace("mao", "ㄇㄠ") + varLineData.selfReplace("mei", "ㄇㄟ") + varLineData.selfReplace("men", "ㄇㄣ") + varLineData.selfReplace("mie", "ㄇㄧㄝ") + varLineData.selfReplace("min", "ㄇㄧㄣ") + varLineData.selfReplace("miu", "ㄇㄧㄡ") + varLineData.selfReplace("mou", "ㄇㄡ") + varLineData.selfReplace("nai", "ㄋㄞ") + varLineData.selfReplace("nan", "ㄋㄢ") + varLineData.selfReplace("nao", "ㄋㄠ") + varLineData.selfReplace("nei", "ㄋㄟ") + varLineData.selfReplace("nen", "ㄋㄣ") + varLineData.selfReplace("nie", "ㄋㄧㄝ") + varLineData.selfReplace("nin", "ㄋㄧㄣ") + varLineData.selfReplace("niu", "ㄋㄧㄡ") + varLineData.selfReplace("nou", "ㄋㄡ") + varLineData.selfReplace("nui", "ㄋㄨㄟ") + varLineData.selfReplace("nun", "ㄋㄨㄣ") + varLineData.selfReplace("nuo", "ㄋㄨㄛ") + varLineData.selfReplace("nve", "ㄋㄩㄝ") + varLineData.selfReplace("pai", "ㄆㄞ") + varLineData.selfReplace("pan", "ㄆㄢ") + varLineData.selfReplace("pao", "ㄆㄠ") + varLineData.selfReplace("pei", "ㄆㄟ") + varLineData.selfReplace("pen", "ㄆㄣ") + varLineData.selfReplace("pia", "ㄆㄧㄚ") + varLineData.selfReplace("pie", "ㄆㄧㄝ") + varLineData.selfReplace("pin", "ㄆㄧㄣ") + varLineData.selfReplace("pou", "ㄆㄡ") + varLineData.selfReplace("qia", "ㄑㄧㄚ") + varLineData.selfReplace("qie", "ㄑㄧㄝ") + varLineData.selfReplace("qin", "ㄑㄧㄣ") + varLineData.selfReplace("qiu", "ㄑㄧㄡ") + varLineData.selfReplace("que", "ㄑㄩㄝ") + varLineData.selfReplace("qun", "ㄑㄩㄣ") + varLineData.selfReplace("ran", "ㄖㄢ") + varLineData.selfReplace("rao", "ㄖㄠ") + varLineData.selfReplace("ren", "ㄖㄣ") + varLineData.selfReplace("rou", "ㄖㄡ") + varLineData.selfReplace("rui", "ㄖㄨㄟ") + varLineData.selfReplace("run", "ㄖㄨㄣ") + varLineData.selfReplace("ruo", "ㄖㄨㄛ") + varLineData.selfReplace("sai", "ㄙㄞ") + varLineData.selfReplace("san", "ㄙㄢ") + varLineData.selfReplace("sao", "ㄙㄠ") + varLineData.selfReplace("sei", "ㄙㄟ") + varLineData.selfReplace("sen", "ㄙㄣ") + varLineData.selfReplace("sha", "ㄕㄚ") + varLineData.selfReplace("she", "ㄕㄜ") + varLineData.selfReplace("shi", "ㄕ") + varLineData.selfReplace("shu", "ㄕㄨ") + varLineData.selfReplace("sou", "ㄙㄡ") + varLineData.selfReplace("sui", "ㄙㄨㄟ") + varLineData.selfReplace("sun", "ㄙㄨㄣ") + varLineData.selfReplace("suo", "ㄙㄨㄛ") + varLineData.selfReplace("tai", "ㄊㄞ") + varLineData.selfReplace("tan", "ㄊㄢ") + varLineData.selfReplace("tao", "ㄊㄠ") + varLineData.selfReplace("tie", "ㄊㄧㄝ") + varLineData.selfReplace("tou", "ㄊㄡ") + varLineData.selfReplace("tui", "ㄊㄨㄟ") + varLineData.selfReplace("tun", "ㄊㄨㄣ") + varLineData.selfReplace("tuo", "ㄊㄨㄛ") + varLineData.selfReplace("wai", "ㄨㄞ") + varLineData.selfReplace("wan", "ㄨㄢ") + varLineData.selfReplace("wei", "ㄨㄟ") + varLineData.selfReplace("wen", "ㄨㄣ") + varLineData.selfReplace("xia", "ㄒㄧㄚ") + varLineData.selfReplace("xie", "ㄒㄧㄝ") + varLineData.selfReplace("xin", "ㄒㄧㄣ") + varLineData.selfReplace("xiu", "ㄒㄧㄡ") + varLineData.selfReplace("xue", "ㄒㄩㄝ") + varLineData.selfReplace("xun", "ㄒㄩㄣ") + varLineData.selfReplace("yai", "ㄧㄞ") + varLineData.selfReplace("yan", "ㄧㄢ") + varLineData.selfReplace("yao", "ㄧㄠ") + varLineData.selfReplace("yin", "ㄧㄣ") + varLineData.selfReplace("you", "ㄧㄡ") + varLineData.selfReplace("yue", "ㄩㄝ") + varLineData.selfReplace("yun", "ㄩㄣ") + varLineData.selfReplace("zai", "ㄗㄞ") + varLineData.selfReplace("zan", "ㄗㄢ") + varLineData.selfReplace("zao", "ㄗㄠ") + varLineData.selfReplace("zei", "ㄗㄟ") + varLineData.selfReplace("zen", "ㄗㄣ") + varLineData.selfReplace("zha", "ㄓㄚ") + varLineData.selfReplace("zhe", "ㄓㄜ") + varLineData.selfReplace("zhi", "ㄓ") + varLineData.selfReplace("zhu", "ㄓㄨ") + varLineData.selfReplace("zou", "ㄗㄡ") + varLineData.selfReplace("zui", "ㄗㄨㄟ") + varLineData.selfReplace("zun", "ㄗㄨㄣ") + varLineData.selfReplace("zuo", "ㄗㄨㄛ") + varLineData.selfReplace("ai", "ㄞ") + varLineData.selfReplace("an", "ㄢ") + varLineData.selfReplace("ao", "ㄠ") + varLineData.selfReplace("ba", "ㄅㄚ") + varLineData.selfReplace("bi", "ㄅㄧ") + varLineData.selfReplace("bo", "ㄅㄛ") + varLineData.selfReplace("bu", "ㄅㄨ") + varLineData.selfReplace("ca", "ㄘㄚ") + varLineData.selfReplace("ce", "ㄘㄜ") + varLineData.selfReplace("ci", "ㄘ") + varLineData.selfReplace("cu", "ㄘㄨ") + varLineData.selfReplace("da", "ㄉㄚ") + varLineData.selfReplace("de", "ㄉㄜ") + varLineData.selfReplace("di", "ㄉㄧ") + varLineData.selfReplace("du", "ㄉㄨ") + varLineData.selfReplace("eh", "ㄝ") + varLineData.selfReplace("ei", "ㄟ") + varLineData.selfReplace("en", "ㄣ") + varLineData.selfReplace("er", "ㄦ") + varLineData.selfReplace("fa", "ㄈㄚ") + varLineData.selfReplace("fo", "ㄈㄛ") + varLineData.selfReplace("fu", "ㄈㄨ") + varLineData.selfReplace("ga", "ㄍㄚ") + varLineData.selfReplace("ge", "ㄍㄜ") + varLineData.selfReplace("gi", "ㄍㄧ") + varLineData.selfReplace("gu", "ㄍㄨ") + varLineData.selfReplace("ha", "ㄏㄚ") + varLineData.selfReplace("he", "ㄏㄜ") + varLineData.selfReplace("hu", "ㄏㄨ") + varLineData.selfReplace("ji", "ㄐㄧ") + varLineData.selfReplace("ju", "ㄐㄩ") + varLineData.selfReplace("ka", "ㄎㄚ") + varLineData.selfReplace("ke", "ㄎㄜ") + varLineData.selfReplace("ku", "ㄎㄨ") + varLineData.selfReplace("la", "ㄌㄚ") + varLineData.selfReplace("le", "ㄌㄜ") + varLineData.selfReplace("li", "ㄌㄧ") + varLineData.selfReplace("lo", "ㄌㄛ") + varLineData.selfReplace("lu", "ㄌㄨ") + varLineData.selfReplace("lv", "ㄌㄩ") + varLineData.selfReplace("ma", "ㄇㄚ") + varLineData.selfReplace("me", "ㄇㄜ") + varLineData.selfReplace("mi", "ㄇㄧ") + varLineData.selfReplace("mo", "ㄇㄛ") + varLineData.selfReplace("mu", "ㄇㄨ") + varLineData.selfReplace("na", "ㄋㄚ") + varLineData.selfReplace("ne", "ㄋㄜ") + varLineData.selfReplace("ni", "ㄋㄧ") + varLineData.selfReplace("nu", "ㄋㄨ") + varLineData.selfReplace("nv", "ㄋㄩ") + varLineData.selfReplace("ou", "ㄡ") + varLineData.selfReplace("pa", "ㄆㄚ") + varLineData.selfReplace("pi", "ㄆㄧ") + varLineData.selfReplace("po", "ㄆㄛ") + varLineData.selfReplace("pu", "ㄆㄨ") + varLineData.selfReplace("qi", "ㄑㄧ") + varLineData.selfReplace("qu", "ㄑㄩ") + varLineData.selfReplace("re", "ㄖㄜ") + varLineData.selfReplace("ri", "ㄖ") + varLineData.selfReplace("ru", "ㄖㄨ") + varLineData.selfReplace("sa", "ㄙㄚ") + varLineData.selfReplace("se", "ㄙㄜ") + varLineData.selfReplace("si", "ㄙ") + varLineData.selfReplace("su", "ㄙㄨ") + varLineData.selfReplace("ta", "ㄊㄚ") + varLineData.selfReplace("te", "ㄊㄜ") + varLineData.selfReplace("ti", "ㄊㄧ") + varLineData.selfReplace("tu", "ㄊㄨ") + varLineData.selfReplace("wa", "ㄨㄚ") + varLineData.selfReplace("wo", "ㄨㄛ") + varLineData.selfReplace("wu", "ㄨ") + varLineData.selfReplace("xi", "ㄒㄧ") + varLineData.selfReplace("xu", "ㄒㄩ") + varLineData.selfReplace("ya", "ㄧㄚ") + varLineData.selfReplace("ye", "ㄧㄝ") + varLineData.selfReplace("yi", "ㄧ") + varLineData.selfReplace("yo", "ㄧㄛ") + varLineData.selfReplace("yu", "ㄩ") + varLineData.selfReplace("za", "ㄗㄚ") + varLineData.selfReplace("ze", "ㄗㄜ") + varLineData.selfReplace("zi", "ㄗ") + varLineData.selfReplace("zu", "ㄗㄨ") + varLineData.selfReplace("a", "ㄚ") + varLineData.selfReplace("e", "ㄜ") + varLineData.selfReplace("o", "ㄛ") + varLineData.selfReplace("q", "ㄑ") + varLineData.selfReplace("2", "ˊ") + varLineData.selfReplace("3", "ˇ") + varLineData.selfReplace("4", "ˋ") + varLineData.selfReplace("5", "˙") + varLineData.selfReplace("1", "") + strProcessed += varLineData + strProcessed += "\n" + } + } + + // Step 3: Add Formatted Pragma, the Sorted Header: + let hdrFormatted = "# 𝙵𝙾𝚁𝙼𝙰𝚃 𝚘𝚛𝚐.𝚊𝚝𝚎𝚕𝚒𝚎𝚛𝙸𝚗𝚖𝚞.𝚟𝚌𝚑𝚎𝚠𝚒𝚗𝚐.𝚞𝚜𝚎𝚛𝙻𝚊𝚗𝚐𝚞𝚊𝚐𝚎𝙼𝚘𝚍𝚎𝚕𝙳𝚊𝚝𝚊.𝚏𝚘𝚛𝚖𝚊𝚝𝚝𝚎𝚍\n" + strProcessed = hdrFormatted + strProcessed // Add Sorted Header + + // Step 4: Deduplication. + 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: Commit Formatted Contents. + self = strProcessed + } } diff --git a/UserPhraseEditor/ViewController.swift b/UserPhraseEditor/ViewController.swift index 20831686..84c8e345 100644 --- a/UserPhraseEditor/ViewController.swift +++ b/UserPhraseEditor/ViewController.swift @@ -1,59 +1,65 @@ // 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: +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. +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. +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. +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 class ViewController: NSViewController, NSTextViewDelegate { - /// - Tag: setRepresentedObjectExample - override var representedObject: Any? { - didSet { - // Pass down the represented object to all of the child view controllers. - for child in children { - child.representedObject = representedObject - } - } - } + /// - Tag: setRepresentedObjectExample + override var representedObject: Any? { + didSet { + // Pass down the represented object to all of the child view controllers. + for child in children { + child.representedObject = representedObject + } + } + } - weak var document: Document? { - if let docRepresentedObject = representedObject as? Document { - return docRepresentedObject - } - return nil - } + weak var document: Document? { + if let docRepresentedObject = representedObject as? Document { + return docRepresentedObject + } + return nil + } - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } - override func viewDidAppear() { - super.viewDidAppear() - } + override func viewDidAppear() { + super.viewDidAppear() + } - // MARK: - NSTextViewDelegate + // MARK: - NSTextViewDelegate - func textDidBeginEditing(_ notification: Notification) { - document?.objectDidBeginEditing(self) - } + func textDidBeginEditing(_ notification: Notification) { + document?.objectDidBeginEditing(self) + } - func textDidEndEditing(_ notification: Notification) { - document?.objectDidEndEditing(self) - } + func textDidEndEditing(_ notification: Notification) { + document?.objectDidEndEditing(self) + } } diff --git a/UserPhraseEditor/WindowController.swift b/UserPhraseEditor/WindowController.swift index 3d101202..12a8b6cb 100644 --- a/UserPhraseEditor/WindowController.swift +++ b/UserPhraseEditor/WindowController.swift @@ -1,35 +1,41 @@ // 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: +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. +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. +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. +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 class WindowController: NSWindowController, NSWindowDelegate { - - override func windowDidLoad() { - super.windowDidLoad() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - /** NSWindows loaded from the storyboard will be cascaded + + override func windowDidLoad() { + super.windowDidLoad() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + /** NSWindows loaded from the storyboard will be cascaded based on the original frame of the window in the storyboard. */ - shouldCascadeWindows = true - } - + shouldCascadeWindows = true + } + } diff --git a/UserPhraseEditor/ctlAboutWindow.swift b/UserPhraseEditor/ctlAboutWindow.swift index c144e201..e38945b5 100644 --- a/UserPhraseEditor/ctlAboutWindow.swift +++ b/UserPhraseEditor/ctlAboutWindow.swift @@ -1,51 +1,64 @@ // Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). -// All possible vChewing-specific modifications are (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 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: -1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +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. +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. +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 @objc(AboutWindow) class ctlAboutWindow: NSWindowController { - @IBOutlet weak var appVersionLabel: NSTextField! - @IBOutlet weak var appCopyrightLabel: NSTextField! - @IBOutlet var appEULAContent: NSTextView! + @IBOutlet weak var appVersionLabel: NSTextField! + @IBOutlet weak var appCopyrightLabel: NSTextField! + @IBOutlet var appEULAContent: NSTextView! - override func windowDidLoad() { - super.windowDidLoad() - - window?.standardWindowButton(.closeButton)?.isHidden = true - window?.standardWindowButton(.miniaturizeButton)?.isHidden = true - window?.standardWindowButton(.zoomButton)?.isHidden = true - guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String, - let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { - return - } - if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String { - appCopyrightLabel.stringValue = copyrightLabel - } - if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { - appEULAContent.string = eulaContent - } - appVersionLabel.stringValue = String(format: "%@ Build %@", versionString, installingVersion) - } + override func windowDidLoad() { + super.windowDidLoad() - @IBAction func btnWiki(_ sender: NSButton) { - if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") { - NSWorkspace.shared.open(url) - } - } + window?.standardWindowButton(.closeButton)?.isHidden = true + window?.standardWindowButton(.miniaturizeButton)?.isHidden = true + window?.standardWindowButton(.zoomButton)?.isHidden = true + guard + let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] + as? String, + let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + else { + return + } + if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] + as? String + { + appCopyrightLabel.stringValue = copyrightLabel + } + if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { + appEULAContent.string = eulaContent + } + appVersionLabel.stringValue = String( + format: "%@ Build %@", versionString, installingVersion) + } + + @IBAction func btnWiki(_ sender: NSButton) { + if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") { + NSWorkspace.shared.open(url) + } + } }