Swift // Clang-Format.

This commit is contained in:
ShikiSuen 2022-04-03 11:23:50 +08:00
parent f2db36206a
commit 1803a5a5b0
34 changed files with 7137 additions and 6280 deletions

56
.clang-format-swift.json Normal file
View File

@ -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
}

View File

@ -1,383 +1,421 @@
#!/usr/bin/env swift
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Foundation
// MARK: - // MARK: -
fileprivate extension String { extension String {
mutating func regReplace(pattern: String, replaceWith: String = "") { fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914 // Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do { do {
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]) let regex = try NSRegularExpression(
let range = NSRange(self.startIndex..., in: self) pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines])
self = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) let range = NSRange(self.startIndex..., in: self)
} catch { return } self = regex.stringByReplacingMatches(
} in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return }
}
} }
fileprivate func getDocumentsDirectory() -> URL { private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0] return paths[0]
} }
// MARK: - // MARK: -
// Ref: https://stackoverflow.com/a/32581409/4162914 // Ref: https://stackoverflow.com/a/32581409/4162914
fileprivate extension Float { extension Float {
func rounded(toPlaces places:Int) -> Float { fileprivate func rounded(toPlaces places: Int) -> Float {
let divisor = pow(10.0, Float(places)) let divisor = pow(10.0, Float(places))
return (self * divisor).rounded() / divisor return (self * divisor).rounded() / divisor
} }
} }
// MARK: - // MARK: -
// Ref: https://stackoverflow.com/a/41581695/4162914 // Ref: https://stackoverflow.com/a/41581695/4162914
precedencegroup ExponentiationPrecedence { precedencegroup ExponentiationPrecedence {
associativity: right associativity: right
higherThan: MultiplicationPrecedence higherThan: MultiplicationPrecedence
} }
infix operator ** : ExponentiationPrecedence infix operator **: ExponentiationPrecedence
func ** (_ base: Double, _ exp: Double) -> Double { func ** (_ base: Double, _ exp: Double) -> Double {
return pow(base, exp) return pow(base, exp)
} }
func ** (_ base: Float, _ exp: Float) -> Float { func ** (_ base: Float, _ exp: Float) -> Float {
return pow(base, exp) return pow(base, exp)
} }
// MARK: - // MARK: -
struct Entry { struct Entry {
var valPhone: String = "" var valPhone: String = ""
var valPhrase: String = "" var valPhrase: String = ""
var valWeight: Float = -1.0 var valWeight: Float = -1.0
var valCount: Int = 0 var valCount: Int = 0
} }
// MARK: - // MARK: -
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" private let urlCHSforCustom: String = "./components/chs/phrases-custom-chs.txt"
fileprivate let url_CHS_MCBP: String = "./components/chs/phrases-mcbp-chs.txt" private let urlCHSforMCBP: String = "./components/chs/phrases-mcbp-chs.txt"
fileprivate let url_CHS_MOE: String = "./components/chs/phrases-moe-chs.txt" private let urlCHSforMOE: String = "./components/chs/phrases-moe-chs.txt"
fileprivate let url_CHS_VCHEW: String = "./components/chs/phrases-vchewing-chs.txt" private let urlCHSforVCHEW: String = "./components/chs/phrases-vchewing-chs.txt"
fileprivate let url_CHT_Custom: String = "./components/cht/phrases-custom-cht.txt" private let urlCHTforCustom: String = "./components/cht/phrases-custom-cht.txt"
fileprivate let url_CHT_MCBP: String = "./components/cht/phrases-mcbp-cht.txt" private let urlCHTforMCBP: String = "./components/cht/phrases-mcbp-cht.txt"
fileprivate let url_CHT_MOE: String = "./components/cht/phrases-moe-cht.txt" private let urlCHTforMOE: String = "./components/cht/phrases-moe-cht.txt"
fileprivate let url_CHT_VCHEW: String = "./components/cht/phrases-vchewing-cht.txt" private let urlCHTforVCHEW: String = "./components/cht/phrases-vchewing-cht.txt"
fileprivate let urlKanjiCore: String = "./components/common/char-kanji-core.txt" private let urlKanjiCore: String = "./components/common/char-kanji-core.txt"
fileprivate let urlPunctuation: String = "./components/common/data-punctuations.txt" private let urlPunctuation: String = "./components/common/data-punctuations.txt"
fileprivate let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt" private let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt"
fileprivate let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt" private let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt"
fileprivate let urlOutputCHS: String = "./data-chs.txt" private let urlOutputCHS: String = "./data-chs.txt"
fileprivate let urlOutputCHT: String = "./data-cht.txt" private let urlOutputCHT: String = "./data-cht.txt"
// MARK: - // MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Entry] { func rawDictForPhrases(isCHS: Bool) -> [Entry] {
var arrEntryRAW: [Entry] = [] var arrEntryRAW: [Entry] = []
var strRAW: String = "" var strRAW: String = ""
let urlCustom: String = isCHS ? url_CHS_Custom : url_CHT_Custom let urlCustom: String = isCHS ? urlCHSforCustom : urlCHTforCustom
let urlMCBP: String = isCHS ? url_CHS_MCBP : url_CHT_MCBP let urlMCBP: String = isCHS ? urlCHSforMCBP : urlCHTforMCBP
let urlMOE: String = isCHS ? url_CHS_MOE : url_CHT_MOE let urlMOE: String = isCHS ? urlCHSforMOE : urlCHTforMOE
let urlVCHEW: String = isCHS ? url_CHS_VCHEW : url_CHT_VCHEW let urlVCHEW: String = isCHS ? urlCHSforVCHEW : urlCHTforVCHEW
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
// //
do { do {
strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8) strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlMCBP, encoding: .utf8) strRAW += try String(contentsOfFile: urlMCBP, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8) strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8) strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8)
} } catch {
catch { NSLog(" - Exception happened when reading raw phrases data.")
NSLog(" - Exception happened when reading raw phrases data.") return []
return [] }
} //
// strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS // CJKWhiteSpace (\x{3000}) to ASCII Space
// CJKWhiteSpace (\x{3000}) to ASCII Space // NonBreakWhiteSpace (\x{A0}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space // Tab to ASCII Space
// Tab to ASCII Space // ASCII
// ASCII strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32 //
// let arrData = Array(
let arrData = Array(NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
for lineData in arrData { for lineData in arrData {
// //
let arrLineData = lineData.components(separatedBy: " ") let arrLineData = lineData.components(separatedBy: " ")
var varLineDataProcessed: String = "" var varLineDataProcessed: String = ""
var count = 0 var count = 0
for currentCell in arrLineData { for currentCell in arrLineData {
count += 1 count += 1
if count < 3 { if count < 3 {
varLineDataProcessed += currentCell + "\t" varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count { } else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-" varLineDataProcessed += currentCell + "-"
} else { } else {
varLineDataProcessed += currentCell varLineDataProcessed += currentCell
} }
} }
// Entry // Entry
let arrCells : [String] = varLineDataProcessed.components(separatedBy: "\t") let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 // count = 0 //
var phone = "" var phone = ""
var phrase = "" var phrase = ""
var occurrence = 0 var occurrence = 0
for cell in arrCells { for cell in arrCells {
count += 1 count += 1
switch count { switch count {
case 1: phrase = cell case 1: phrase = cell
case 3: phone = cell case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0 case 2: occurrence = Int(cell) ?? 0
default: break default: break
} }
} }
if phrase != "" { // if phrase != "" { //
arrEntryRAW += [Entry.init(valPhone: phone, valPhrase: phrase, valWeight: 0.0, valCount: occurrence)] arrEntryRAW += [
} Entry.init(
} valPhone: phone, valPhrase: phrase, valWeight: 0.0,
NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。") valCount: occurrence)
return arrEntryRAW ]
}
}
NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。")
return arrEntryRAW
} }
// MARK: - // MARK: -
func rawDictForKanjis(isCHS: Bool) -> [Entry] { func rawDictForKanjis(isCHS: Bool) -> [Entry] {
var arrEntryRAW: [Entry] = [] var arrEntryRAW: [Entry] = []
var strRAW: String = "" var strRAW: String = ""
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
// //
do { do {
strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8) strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8)
} } catch {
catch { NSLog(" - Exception happened when reading raw core kanji data.")
NSLog(" - Exception happened when reading raw core kanji data.") return []
return [] }
} //
// strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS // CJKWhiteSpace (\x{3000}) to ASCII Space
// CJKWhiteSpace (\x{3000}) to ASCII Space // NonBreakWhiteSpace (\x{A0}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space // Tab to ASCII Space
// Tab to ASCII Space // ASCII
// ASCII strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32 //
// let arrData = Array(
let arrData = Array(NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
var varLineData: String = "" var varLineData: String = ""
for lineData in arrData { for lineData in arrData {
// 1,2,4 1,3,4 // 1,2,4 1,3,4
let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1).joined(separator: "\t") let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1)
let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2).joined(separator: "\t") .joined(
varLineData = varLineDataPre + "\t" + varLineDataPost separator: "\t")
let arrLineData = varLineData.components(separatedBy: " ") let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2)
var varLineDataProcessed: String = "" .joined(
var count = 0 separator: "\t")
for currentCell in arrLineData { varLineData = varLineDataPre + "\t" + varLineDataPost
count += 1 let arrLineData = varLineData.components(separatedBy: " ")
if count < 3 { var varLineDataProcessed: String = ""
varLineDataProcessed += currentCell + "\t" var count = 0
} else if count < arrLineData.count { for currentCell in arrLineData {
varLineDataProcessed += currentCell + "-" count += 1
} else { if count < 3 {
varLineDataProcessed += currentCell varLineDataProcessed += currentCell + "\t"
} } else if count < arrLineData.count {
} varLineDataProcessed += currentCell + "-"
// Entry } else {
let arrCells : [String] = varLineDataProcessed.components(separatedBy: "\t") varLineDataProcessed += currentCell
count = 0 // }
var phone = "" }
var phrase = "" // Entry
var occurrence = 0 let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
for cell in arrCells { count = 0 //
count += 1 var phone = ""
switch count { var phrase = ""
case 1: phrase = cell var occurrence = 0
case 3: phone = cell for cell in arrCells {
case 2: occurrence = Int(cell) ?? 0 count += 1
default: break switch count {
} case 1: phrase = cell
} case 3: phone = cell
if phrase != "" { // case 2: occurrence = Int(cell) ?? 0
arrEntryRAW += [Entry.init(valPhone: phone, valPhrase: phrase, valWeight: 0.0, valCount: occurrence)] default: break
} }
} }
NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。") if phrase != "" { //
return arrEntryRAW arrEntryRAW += [
Entry.init(
valPhone: phone, valPhrase: phrase, valWeight: 0.0,
valCount: occurrence)
]
}
}
NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。")
return arrEntryRAW
} }
// MARK: - // MARK: -
func rawDictForNonKanjis(isCHS: Bool) -> [Entry] { func rawDictForNonKanjis(isCHS: Bool) -> [Entry] {
var arrEntryRAW: [Entry] = [] var arrEntryRAW: [Entry] = []
var strRAW: String = "" var strRAW: String = ""
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
// //
do { do {
strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8) strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8)
strRAW += "\n" strRAW += "\n"
strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8) strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8)
} } catch {
catch { NSLog(" - Exception happened when reading raw core kanji data.")
NSLog(" - Exception happened when reading raw core kanji data.") return []
return [] }
} //
// strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS // CJKWhiteSpace (\x{3000}) to ASCII Space
// CJKWhiteSpace (\x{3000}) to ASCII Space // NonBreakWhiteSpace (\x{A0}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space // Tab to ASCII Space
// Tab to ASCII Space // ASCII
// ASCII strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ") strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") // strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF, strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32 //
// let arrData = Array(
let arrData = Array(NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String]) NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
var varLineData: String = "" var varLineData: String = ""
for lineData in arrData { for lineData in arrData {
varLineData = lineData varLineData = lineData
// //
varLineData = varLineData.components(separatedBy: " ").prefix(3).joined(separator: "\t") // varLineData = varLineData.components(separatedBy: " ").prefix(3).joined(
let arrLineData = varLineData.components(separatedBy: " ") separator: "\t") //
var varLineDataProcessed: String = "" let arrLineData = varLineData.components(separatedBy: " ")
var count = 0 var varLineDataProcessed: String = ""
for currentCell in arrLineData { var count = 0
count += 1 for currentCell in arrLineData {
if count < 3 { count += 1
varLineDataProcessed += currentCell + "\t" if count < 3 {
} else if count < arrLineData.count { varLineDataProcessed += currentCell + "\t"
varLineDataProcessed += currentCell + "-" } else if count < arrLineData.count {
} else { varLineDataProcessed += currentCell + "-"
varLineDataProcessed += currentCell } else {
} varLineDataProcessed += currentCell
} }
// Entry }
let arrCells : [String] = varLineDataProcessed.components(separatedBy: "\t") // Entry
count = 0 // let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
var phone = "" count = 0 //
var phrase = "" var phone = ""
var occurrence = 0 var phrase = ""
for cell in arrCells { var occurrence = 0
count += 1 for cell in arrCells {
switch count { count += 1
case 1: phrase = cell switch count {
case 3: phone = cell case 1: phrase = cell
case 2: occurrence = Int(cell) ?? 0 case 3: phone = cell
default: break case 2: occurrence = Int(cell) ?? 0
} default: break
} }
if phrase != "" { // }
arrEntryRAW += [Entry.init(valPhone: phone, valPhrase: phrase, valWeight: 0.0, valCount: occurrence)] if phrase != "" { //
} arrEntryRAW += [
} Entry.init(
NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。") valPhone: phone, valPhrase: phrase, valWeight: 0.0,
return arrEntryRAW valCount: occurrence)
]
}
}
NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。")
return arrEntryRAW
} }
func weightAndSort(_ arrStructUncalculated: [Entry], isCHS: Bool) -> [Entry] { func weightAndSort(_ arrStructUncalculated: [Entry], isCHS: Bool) -> [Entry] {
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
var arrStructCalculated: [Entry] = [] var arrStructCalculated: [Entry] = []
let fscale: Float = 2.7 let fscale: Float = 2.7
var norm: Float = 0.0 var norm: Float = 0.0
for entry in arrStructUncalculated { for entry in arrStructUncalculated {
if entry.valCount >= 0 { if entry.valCount >= 0 {
norm += fscale**(Float(entry.valPhrase.count) / 3.0 - 1.0) * Float(entry.valCount) norm += fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0)
} * Float(entry.valCount)
} }
// norm norm }
// // norm norm
// 1 0 0.5 //
for entry in arrStructUncalculated { // 1 0 0.5
var weight: Float = 0 for entry in arrStructUncalculated {
switch entry.valCount { var weight: Float = 0
case -2: // switch entry.valCount {
weight = -13 case -2: //
case -1: // weight = -13
weight = -13 case -1: //
case 0: // weight = -13
weight = log10(fscale**(Float(entry.valPhrase.count) / 3.0 - 1.0) * 0.5 / norm) case 0: //
default: weight = log10(
weight = log10(fscale**(Float(entry.valPhrase.count) / 3.0 - 1.0) * Float(entry.valCount) / norm) // Credit: MJHsieh. fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0) * 0.5 / norm)
} default:
let weightRounded: Float = weight.rounded(toPlaces: 3) // weight = log10(
arrStructCalculated += [Entry.init(valPhone: entry.valPhone, valPhrase: entry.valPhrase, valWeight: weightRounded, valCount: entry.valCount)] fscale ** (Float(entry.valPhrase.count) / 3.0 - 1.0)
} * Float(entry.valCount) / norm) // Credit: MJHsieh.
NSLog(" - \(i18n): 成功計算權重。") }
// ========================================== let weightRounded: Float = weight.rounded(toPlaces: 3) //
// arrStructCalculated += [
let arrStructSorted: [Entry] = arrStructCalculated.sorted(by: {(lhs, rhs) -> Bool in return (lhs.valPhone, rhs.valCount) < (rhs.valPhone, lhs.valCount)}) Entry.init(
NSLog(" - \(i18n): 排序整理完畢,準備編譯要寫入的檔案內容。") valPhone: entry.valPhone, valPhrase: entry.valPhrase, valWeight: weightRounded,
return arrStructSorted 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) { func fileOutput(isCHS: Bool) {
let i18n: String = isCHS ? "簡體中文" : "繁體中文" let i18n: String = isCHS ? "簡體中文" : "繁體中文"
let pathOutput = urlCurrentFolder.appendingPathComponent(isCHS ? urlOutputCHS : urlOutputCHT) let pathOutput = urlCurrentFolder.appendingPathComponent(
var strPrintLine = "" isCHS ? urlOutputCHS : urlOutputCHT)
// var strPrintLine = ""
do { //
strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8) do {
} strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8)
catch { } catch {
NSLog(" - \(i18n): Exception happened when reading raw punctuation data.") NSLog(" - \(i18n): Exception happened when reading raw punctuation data.")
} }
NSLog(" - \(i18n): 成功插入標點符號與西文字母數據。") NSLog(" - \(i18n): 成功插入標點符號與西文字母數據。")
// //
var arrStructUnified: [Entry] = [] var arrStructUnified: [Entry] = []
arrStructUnified += rawDictForKanjis(isCHS: isCHS) arrStructUnified += rawDictForKanjis(isCHS: isCHS)
arrStructUnified += rawDictForNonKanjis(isCHS: isCHS) arrStructUnified += rawDictForNonKanjis(isCHS: isCHS)
arrStructUnified += rawDictForPhrases(isCHS: isCHS) arrStructUnified += rawDictForPhrases(isCHS: isCHS)
// //
arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS) arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS)
for entry in arrStructUnified { for entry in arrStructUnified {
strPrintLine += entry.valPhone + " " + entry.valPhrase + " " + String(entry.valWeight) + "\n" strPrintLine +=
} entry.valPhone + " " + entry.valPhrase + " " + String(entry.valWeight)
NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。") + "\n"
do { }
try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8) NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。")
} do {
catch { try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8)
NSLog(" - \(i18n): Error on writing strings to file: \(error)") } catch {
} NSLog(" - \(i18n): Error on writing strings to file: \(error)")
NSLog(" - \(i18n): 寫入完成。") }
NSLog(" - \(i18n): 寫入完成。")
} }
// MARK: - // MARK: -
func main() { func main() {
NSLog("// 準備編譯繁體中文核心語料檔案。") NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false) fileOutput(isCHS: false)
NSLog("// 準備編譯簡體中文核心語料檔案。") NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true) fileOutput(isCHS: true)
} }
main() main()

View File

@ -1,20 +1,27 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@ -23,9 +30,11 @@ private let kTargetBin = "vChewing"
private let kTargetType = "app" private let kTargetType = "app"
private let kTargetBundle = "vChewing.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 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 kDestinationPartial = urlDestinationPartial.path
private let kTargetPartialPath = urlTargetPartial.path private let kTargetPartialPath = urlTargetPartial.path
@ -35,251 +44,300 @@ private let kTranslocationRemovalTickInterval: TimeInterval = 0.5
private let kTranslocationRemovalDeadline: TimeInterval = 60.0 private let kTranslocationRemovalDeadline: TimeInterval = 60.0
@NSApplicationMain @NSApplicationMain
@objc (AppDelegate) @objc(AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate { class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet weak private var installButton: NSButton! @IBOutlet weak private var installButton: NSButton!
@IBOutlet weak private var cancelButton: NSButton! @IBOutlet weak private var cancelButton: NSButton!
@IBOutlet weak private var progressSheet: NSWindow! @IBOutlet weak private var progressSheet: NSWindow!
@IBOutlet weak private var progressIndicator: NSProgressIndicator! @IBOutlet weak private var progressIndicator: NSProgressIndicator!
@IBOutlet weak private var appVersionLabel: NSTextField! @IBOutlet weak private var appVersionLabel: NSTextField!
@IBOutlet weak private var appCopyrightLabel: NSTextField! @IBOutlet weak private var appCopyrightLabel: NSTextField!
@IBOutlet private var appEULAContent: NSTextView! @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
func runAlertPanel(title: String, message: String, buttonTitle: String) { private var archiveUtil: ArchiveUtil?
let alert = NSAlert() private var installingVersion = ""
alert.alertStyle = .informational private var upgrading = false
alert.messageText = title private var translocationRemovalStartTime: Date?
alert.informativeText = message private var currentVersionNumber: Int = 0
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
func applicationDidFinishLaunching(_ notification: Notification) { func runAlertPanel(title: String, message: String, buttonTitle: String) {
guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String, let alert = NSAlert()
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { alert.alertStyle = .informational
return alert.messageText = title
} alert.informativeText = message
self.installingVersion = installingVersion alert.addButton(withTitle: buttonTitle)
self.archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle) alert.runModal()
_ = archiveUtil?.validateIfNotarizedArchiveExists() }
cancelButton.nextKeyView = installButton func applicationDidFinishLaunching(_ notification: Notification) {
installButton.nextKeyView = cancelButton guard
if let cell = installButton.cell as? NSButtonCell { let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
window?.defaultButtonCell = cell as? String,
} let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
else {
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String { return
appCopyrightLabel.stringValue = copyrightLabel }
} self.installingVersion = installingVersion
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String { self.archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle)
appEULAContent.string = eulaContent _ = archiveUtil?.validateIfNotarizedArchiveExists()
}
appVersionLabel.stringValue = String(format: "%@ Build %@", versionString, installingVersion)
window?.title = String(format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "", versionString, installingVersion) cancelButton.nextKeyView = installButton
window?.standardWindowButton(.closeButton)?.isHidden = true installButton.nextKeyView = cancelButton
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true if let cell = installButton.cell as? NSButtonCell {
window?.standardWindowButton(.zoomButton)?.isHidden = true window?.defaultButtonCell = cell
}
if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) { if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"]
let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath) as? String
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String {
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String appCopyrightLabel.stringValue = copyrightLabel
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0 }
if shortVersion != nil, let currentVersion = currentVersion, currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending { if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
upgrading = true appEULAContent.string = eulaContent
} }
} appVersionLabel.stringValue = String(
format: "%@ Build %@", versionString, installingVersion)
if upgrading { window?.title = String(
installButton.title = NSLocalizedString("Upgrade", comment: "") 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() if FileManager.default.fileExists(
window?.orderFront(self) atPath: (kTargetPartialPath as NSString).expandingTildeInPath)
NSApp.activate(ignoringOtherApps: true) {
} 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) { if upgrading {
cancelButton.isEnabled = false installButton.title = NSLocalizedString("Upgrade", comment: "")
installButton.isEnabled = false }
removeThenInstallInputMethod()
}
@objc func timerTick(_ timer: Timer) { window?.center()
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date()) window?.orderFront(self)
if elapsed >= kTranslocationRemovalDeadline { NSApp.activate(ignoringOtherApps: true)
timer.invalidate() }
window?.endSheet(progressSheet, returnCode: .cancel)
} else if appBundleChronoshiftedToARandomizedPath(kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0
timer.invalidate()
window?.endSheet(progressSheet, returnCode: .continue)
}
}
func removeThenInstallInputMethod() { @IBAction func agreeAndInstallAction(_ sender: AnyObject) {
if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) == false { cancelButton.isEnabled = false
self.installInputMethod(previousExists: false, previousVersionNotFullyDeactivatedWarning: false) installButton.isEnabled = false
return 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)
}
}
// func removeThenInstallInputMethod() {
do { if FileManager.default.fileExists(
let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath atPath: (kTargetPartialPath as NSString).expandingTildeInPath)
let fileManager = FileManager.default == false
let fileURLString = String(format: "%@/%@", sourceDir, kTargetBundle) {
let fileURL = URL(fileURLWithPath: fileURLString) self.installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false)
// return
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() let shouldWaitForTranslocationRemoval =
killTask.launchPath = "/usr/bin/killall" appBundleChronoshiftedToARandomizedPath(kTargetPartialPath)
killTask.arguments = ["-9", kTargetBin] && (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false)
killTask.launch()
killTask.waitUntilExit()
if shouldWaitForTranslocationRemoval { //
progressIndicator.startAnimation(self) do {
window?.beginSheet(progressSheet) { returnCode in let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath
DispatchQueue.main.async { let fileManager = FileManager.default
if returnCode == .continue { let fileURLString = String(format: "%@/%@", sourceDir, kTargetBundle)
self.installInputMethod(previousExists: true, previousVersionNotFullyDeactivatedWarning: false) let fileURL = URL(fileURLWithPath: fileURLString)
} else {
self.installInputMethod(previousExists: true, previousVersionNotFullyDeactivatedWarning: true)
}
}
}
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 { } else {
self.installInputMethod(previousExists: false, previousVersionNotFullyDeactivatedWarning: false) self.installInputMethod(
} previousExists: false, previousVersionNotFullyDeactivatedWarning: false)
} }
}
func installInputMethod(previousExists: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool) { func installInputMethod(
guard let targetBundle = archiveUtil?.unzipNotarizedArchive() ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) else { previousExists: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
return ) {
} guard
let cpTask = Process() let targetBundle = archiveUtil?.unzipNotarizedArchive()
cpTask.launchPath = "/bin/cp" ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
cpTask.arguments = ["-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath] else {
cpTask.launch() return
cpTask.waitUntilExit() }
let cpTask = Process()
cpTask.launchPath = "/bin/cp"
cpTask.arguments = [
"-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath,
]
cpTask.launch()
cpTask.waitUntilExit()
if cpTask.terminationStatus != 0 { if cpTask.terminationStatus != 0 {
runAlertPanel(title: NSLocalizedString("Install Failed", comment: ""), runAlertPanel(
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), title: NSLocalizedString("Install Failed", comment: ""),
buttonTitle: NSLocalizedString("Cancel", comment: "")) message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
endAppWithDelay() buttonTitle: NSLocalizedString("Cancel", comment: ""))
} endAppWithDelay()
}
guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath), guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath),
let imeIdentifier = imeBundle.bundleIdentifier let imeIdentifier = imeBundle.bundleIdentifier
else { else {
endAppWithDelay() endAppWithDelay()
return return
} }
let imeBundleURL = imeBundle.bundleURL let imeBundleURL = imeBundle.bundleURL
var inputSource = InputSourceHelper.inputSource(for: imeIdentifier) var inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
if inputSource == nil { if inputSource == nil {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString)."); NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
let status = InputSourceHelper.registerTnputSource(at: imeBundleURL) let status = InputSourceHelper.registerTnputSource(at: imeBundleURL)
if !status { if !status {
let message = String(format: NSLocalizedString("Cannot find input source %@ after registration.", comment: ""), imeIdentifier) let message = String(
runAlertPanel(title: NSLocalizedString("Fatal Error", comment: ""), message: message, buttonTitle: NSLocalizedString("Abort", comment: "")) format: NSLocalizedString(
endAppWithDelay() "Cannot find input source %@ after registration.", comment: ""),
return imeIdentifier)
} runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: ""))
endAppWithDelay()
return
}
inputSource = InputSourceHelper.inputSource(for: imeIdentifier) inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
if inputSource == nil { if inputSource == nil {
let message = String(format: NSLocalizedString("Cannot find input source %@ after registration.", comment: ""), imeIdentifier) let message = String(
runAlertPanel(title: NSLocalizedString("Fatal Error", comment: ""), message: message, buttonTitle: NSLocalizedString("Abort", comment: "")) 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 var isMacOS12OrAbove = false
if #available(macOS 12.0, *) { if #available(macOS 12.0, *) {
NSLog("macOS 12 or later detected."); NSLog("macOS 12 or later detected.")
isMacOS12OrAbove = true isMacOS12OrAbove = true
} else { } else {
NSLog("Installer runs with the pre-macOS 12 flow."); NSLog("Installer runs with the pre-macOS 12 flow.")
} }
// If the IME is not enabled, enable it. Also, unconditionally enable it on macOS 12.0+, // If the IME is not enabled, enable it. Also, unconditionally enable it on macOS 12.0+,
// as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not*
// enabled in the user's current set of IMEs (which means the IME does not show up in // enabled in the user's current set of IMEs (which means the IME does not show up in
// the user's input menu). // the user's input menu).
var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!) var mainInputSourceEnabled = InputSourceHelper.inputSourceEnabled(for: inputSource!)
if !mainInputSourceEnabled || isMacOS12OrAbove { if !mainInputSourceEnabled || isMacOS12OrAbove {
mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!) mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!)
if (mainInputSourceEnabled) { if mainInputSourceEnabled {
NSLog("Input method enabled: \(imeIdentifier)"); NSLog("Input method enabled: \(imeIdentifier)")
} else { } else {
NSLog("Failed to enable input method: \(imeIdentifier)"); NSLog("Failed to enable input method: \(imeIdentifier)")
} }
} }
// Alert Panel
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()
}
}
func endAppWithDelay() { // Alert Panel
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { let ntfPostInstall = NSAlert()
NSApp.terminate(self) 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) { func endAppWithDelay() {
NSApp.terminate(self) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
} NSApp.terminate(self)
}
}
func windowWillClose(_ Notification: Notification) { @IBAction func cancelAction(_ sender: AnyObject) {
NSApp.terminate(self) NSApp.terminate(self)
} }
func windowWillClose(_ notification: Notification) {
NSApp.terminate(self)
}
} }

View File

@ -1,117 +1,134 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
struct ArchiveUtil { struct ArchiveUtil {
var appName: String var appName: String
var targetAppBundleName: String var targetAppBundleName: String
init(appName: String, targetAppBundleName: String) { init(appName: String, targetAppBundleName: String) {
self.appName = appName self.appName = appName
self.targetAppBundleName = targetAppBundleName self.targetAppBundleName = targetAppBundleName
} }
// Returns YES if (1) a zip file under // Returns YES if (1) a zip file under
// Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if // Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if
// Resources/$_invalidAppBundleName does not exist. // Resources/$_invalidAppBundleName does not exist.
func validateIfNotarizedArchiveExists() -> Bool { func validateIfNotarizedArchiveExists() -> Bool {
guard let resourePath = Bundle.main.resourcePath, guard let resourePath = Bundle.main.resourcePath,
let notarizedArchivesPath = notarizedArchivesPath, let notarizedArchivesPath = notarizedArchivesPath,
let notarizedArchive = notarizedArchive, let notarizedArchive = notarizedArchive,
let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(atPath: notarizedArchivesPath) let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(
else { atPath: notarizedArchivesPath)
return false else {
} return false
}
let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName) let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName)
let count = notarizedArchivesContent.count let count = notarizedArchivesContent.count
let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive) let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive)
let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath) let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath)
if count > 0 { if count > 0 {
if count != 1 || !notarizedArchiveExists || devModeAppBundleExists { if count != 1 || !notarizedArchiveExists || devModeAppBundleExists {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .informational alert.alertStyle = .informational
alert.messageText = "Internal Error" alert.messageText = "Internal Error"
alert.informativeText = "devMode installer, expected archive name: \(notarizedArchive), " + alert.informativeText =
"archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)" "devMode installer, expected archive name: \(notarizedArchive), "
alert.addButton(withTitle: "Terminate") + "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)"
alert.runModal() alert.addButton(withTitle: "Terminate")
NSApp.terminate(nil) alert.runModal()
} else { NSApp.terminate(nil)
return true } else {
} return true
} }
}
if !devModeAppBundleExists { if !devModeAppBundleExists {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .informational alert.alertStyle = .informational
alert.messageText = "Internal Error" alert.messageText = "Internal Error"
alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)" alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)"
alert.addButton(withTitle: "Terminate") alert.addButton(withTitle: "Terminate")
alert.runModal() alert.runModal()
NSApp.terminate(nil) NSApp.terminate(nil)
} }
return false return false
} }
func unzipNotarizedArchive() -> String? { func unzipNotarizedArchive() -> String? {
if !self.validateIfNotarizedArchiveExists() { if !self.validateIfNotarizedArchiveExists() {
return nil return nil
} }
guard let notarizedArchive = notarizedArchive, guard let notarizedArchive = notarizedArchive,
let resourcePath = Bundle.main.resourcePath else { let resourcePath = Bundle.main.resourcePath
return nil else {
} return nil
let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(UUID().uuidString) }
let arguments: [String] = [notarizedArchive, "-d", tempFilePath] let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(
let unzipTask = Process() UUID().uuidString)
unzipTask.launchPath = "/usr/bin/unzip" let arguments: [String] = [notarizedArchive, "-d", tempFilePath]
unzipTask.currentDirectoryPath = resourcePath let unzipTask = Process()
unzipTask.arguments = arguments unzipTask.launchPath = "/usr/bin/unzip"
unzipTask.launch() unzipTask.currentDirectoryPath = resourcePath
unzipTask.waitUntilExit() unzipTask.arguments = arguments
unzipTask.launch()
unzipTask.waitUntilExit()
assert(unzipTask.terminationStatus == 0, "Must successfully unzipped") assert(unzipTask.terminationStatus == 0, "Must successfully unzipped")
let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName) let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName)
assert(FileManager.default.fileExists(atPath: result), "App bundle must be unzipped at \(result).") assert(
return result FileManager.default.fileExists(atPath: result),
} "App bundle must be unzipped at \(result).")
return result
}
private var notarizedArchivesPath: String? { private var notarizedArchivesPath: String? {
guard let resourePath = Bundle.main.resourcePath else { guard let resourePath = Bundle.main.resourcePath else {
return nil return nil
} }
let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent("NotarizedArchives") let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent(
return notarizedArchivesPath "NotarizedArchives")
} return notarizedArchivesPath
}
private var notarizedArchive: String? { private var notarizedArchive: String? {
guard let notarizedArchivesPath = notarizedArchivesPath, guard let notarizedArchivesPath = notarizedArchivesPath,
let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String else { let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
return nil as? String
} else {
let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip" return nil
let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent(notarizedArchiveBasename) }
return notarizedArchive let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip"
} let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent(
notarizedArchiveBasename)
return notarizedArchive
}
} }

View File

@ -1,20 +1,27 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Foundation
@ -25,28 +32,28 @@ import OpenCC
/// Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass /// Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass
/// in Swift in order to bridge the Swift classes into our Objective-C++ project. /// in Swift in order to bridge the Swift classes into our Objective-C++ project.
public class OpenCCBridge: NSObject { public class OpenCCBridge: NSObject {
private static let shared = OpenCCBridge() private static let shared = OpenCCBridge()
private var simplify: ChineseConverter? private var simplify: ChineseConverter?
private var traditionalize: ChineseConverter? private var traditionalize: ChineseConverter?
private override init() {
try? simplify = ChineseConverter(options: .simplify)
try? traditionalize = ChineseConverter(options: [.traditionalize, .twStandard])
super.init()
}
/// CrossConvert. private override init() {
/// try? simplify = ChineseConverter(options: .simplify)
/// - Parameter string: Text in Original Script. try? traditionalize = ChineseConverter(options: [.traditionalize, .twStandard])
/// - Returns: Text converted to Different Script. super.init()
@objc public static func crossConvert(_ string: String) -> String? { }
switch ctlInputMethod.currentKeyHandler.inputMode {
case InputMode.imeModeCHS: /// CrossConvert.
return shared.traditionalize?.convert(string) ///
case InputMode.imeModeCHT: /// - Parameter string: Text in Original Script.
return shared.simplify?.convert(string) /// - Returns: Text converted to Different Script.
default: @objc public static func crossConvert(_ string: String) -> String? {
return string switch ctlInputMethod.currentKeyHandler.inputMode {
} case InputMode.imeModeCHS:
} return shared.traditionalize?.convert(string)
case InputMode.imeModeCHT:
return shared.simplify?.convert(string)
default:
return string
}
}
} }

View File

@ -1,20 +1,27 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@ -29,287 +36,346 @@ private let kNextCheckInterval: TimeInterval = 86400.0
private let kTimeoutInterval: TimeInterval = 60.0 private let kTimeoutInterval: TimeInterval = 60.0
struct VersionUpdateReport { struct VersionUpdateReport {
var siteUrl: URL? var siteUrl: URL?
var currentShortVersion: String = "" var currentShortVersion: String = ""
var currentVersion: String = "" var currentVersion: String = ""
var remoteShortVersion: String = "" var remoteShortVersion: String = ""
var remoteVersion: String = "" var remoteVersion: String = ""
var versionDescription: String = "" var versionDescription: String = ""
} }
enum VersionUpdateApiResult { enum VersionUpdateApiResult {
case shouldUpdate(report: VersionUpdateReport) case shouldUpdate(report: VersionUpdateReport)
case noNeedToUpdate case noNeedToUpdate
case ignored case ignored
} }
enum VersionUpdateApiError: Error, LocalizedError { enum VersionUpdateApiError: Error, LocalizedError {
case connectionError(message: String) case connectionError(message: String)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .connectionError(let message): case .connectionError(let message):
return String(format: NSLocalizedString("There may be no internet connection or the server failed to respond.\n\nError message: %@", comment: ""), message) return String(
} format: NSLocalizedString(
} "There may be no internet connection or the server failed to respond.\n\nError message: %@",
comment: ""), message)
}
}
} }
struct VersionUpdateApi { struct VersionUpdateApi {
static func check(forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> ()) -> URLSessionTask? { static func check(
guard let infoDict = Bundle.main.infoDictionary, forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String, ) -> URLSessionTask? {
let updateInfoURL = URL(string: updateInfoURLString) else { guard let infoDict = Bundle.main.infoDictionary,
return nil let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
} let updateInfoURL = URL(string: updateInfoURLString)
else {
return nil
}
let request = URLRequest(url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: kTimeoutInterval) let request = URLRequest(
let task = URLSession.shared.dataTask(with: request) { data, response, error in url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData,
if let error = error { timeoutInterval: kTimeoutInterval)
DispatchQueue.main.async { let task = URLSession.shared.dataTask(with: request) { data, response, error in
forced ? if let error = error {
callback(.failure(VersionUpdateApiError.connectionError(message: error.localizedDescription))) : DispatchQueue.main.async {
callback(.success(.ignored)) forced
} ? callback(
return .failure(
} VersionUpdateApiError.connectionError(
message: error.localizedDescription)))
: callback(.success(.ignored))
}
return
}
do { do {
guard let plist = try PropertyListSerialization.propertyList(from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any], guard
let remoteVersion = plist[kCFBundleVersionKey] as? String, let plist = try PropertyListSerialization.propertyList(
let infoDict = Bundle.main.infoDictionary from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
else { let remoteVersion = plist[kCFBundleVersionKey] as? String,
DispatchQueue.main.async { let infoDict = Bundle.main.infoDictionary
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) else {
} DispatchQueue.main.async {
return forced
} ? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
return
}
// TODO: Validate info (e.g. bundle identifier) // TODO: Validate info (e.g. bundle identifier)
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this // TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? "" let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
let result = currentVersion.compare(remoteVersion, options: .numeric, range: nil, locale: nil) let result = currentVersion.compare(
remoteVersion, options: .numeric, range: nil, locale: nil)
if result != .orderedAscending { if result != .orderedAscending {
DispatchQueue.main.async { DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) forced
} ? callback(.success(.noNeedToUpdate))
IME.prtDebugIntel("vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available.") : callback(.success(.ignored))
return }
} IME.prtDebugIntel(
IME.prtDebugIntel("vChewingDebug: Update // New version detected, proceeding to the next phase.") "vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available."
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String, )
let siteInfoURL = URL(string: siteInfoURLString) return
else { }
DispatchQueue.main.async { IME.prtDebugIntel(
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) "vChewingDebug: Update // New version detected, proceeding to the next phase.")
} guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
IME.prtDebugIntel("vChewingDebug: Update // Failed from retrieving / parsing URL intel.") let siteInfoURL = URL(string: siteInfoURLString)
return else {
} DispatchQueue.main.async {
IME.prtDebugIntel("vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.") forced
var report = VersionUpdateReport(siteUrl: siteInfoURL) ? callback(.success(.noNeedToUpdate))
var versionDescription = "" : callback(.success(.ignored))
let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any] }
if let versionDescriptions = versionDescriptions { IME.prtDebugIntel(
var locale = "en" "vChewingDebug: Update // Failed from retrieving / parsing URL intel.")
let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"] return
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales) }
if let first = preferredTags.first { IME.prtDebugIntel(
locale = first "vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.")
} var report = VersionUpdateReport(siteUrl: siteInfoURL)
versionDescription = versionDescriptions[locale] as? String ?? versionDescriptions["en"] as? String ?? "" var versionDescription = ""
if !versionDescription.isEmpty { let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any]
versionDescription = "\n\n" + versionDescription if let versionDescriptions = versionDescriptions {
} var locale = "en"
} let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"]
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
report.currentVersion = currentVersion if let first = preferredTags.first {
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? "" locale = first
report.remoteVersion = remoteVersion }
report.versionDescription = versionDescription versionDescription =
DispatchQueue.main.async { versionDescriptions[locale] as? String ?? versionDescriptions["en"]
callback(.success(.shouldUpdate(report: report))) as? String ?? ""
} if !versionDescription.isEmpty {
IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.") versionDescription = "\n\n" + versionDescription
} catch { }
DispatchQueue.main.async { }
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored)) report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
} report.currentVersion = currentVersion
} report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
} report.remoteVersion = remoteVersion
task.resume() report.versionDescription = versionDescription
return task 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) @objc(AppDelegate)
class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate, FSEventStreamHelperDelegate { class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate,
func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) { FSEventStreamHelperDelegate
// 100ms 使使 {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) {
if mgrPrefs.shouldAutoReloadUserDataFiles { // 100ms 使使
IME.initLangModels(userOnly: true) 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? @IBOutlet weak var window: NSWindow?
private var ctlPrefWindowInstance: ctlPrefWindow? private var ctlPrefWindowInstance: ctlPrefWindow?
private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window
private var checkTask: URLSessionTask? private var checkTask: URLSessionTask?
private var updateNextStepURL: URL? private var updateNextStepURL: URL?
private var fsStreamHelper = FSEventStreamHelper(path: mgrLangModel.dataFolderPath(isDefaultFolder: false), queue: DispatchQueue(label: "vChewing User Phrases")) private var fsStreamHelper = FSEventStreamHelper(
private var currentAlertType: String = "" path: mgrLangModel.dataFolderPath(isDefaultFolder: false),
queue: DispatchQueue(label: "vChewing User Phrases"))
private var currentAlertType: String = ""
// dealloc // dealloc
deinit { deinit {
ctlPrefWindowInstance = nil ctlPrefWindowInstance = nil
ctlAboutWindowInstance = nil ctlAboutWindowInstance = nil
checkTask = nil checkTask = nil
updateNextStepURL = nil updateNextStepURL = nil
fsStreamHelper.stop() fsStreamHelper.stop()
fsStreamHelper.delegate = nil fsStreamHelper.delegate = nil
} }
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
IME.initLangModels(userOnly: false) IME.initLangModels(userOnly: false)
fsStreamHelper.delegate = self fsStreamHelper.delegate = self
_ = fsStreamHelper.start() _ = fsStreamHelper.start()
mgrPrefs.setMissingDefaults() mgrPrefs.setMissingDefaults()
// 使
if (UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) != nil) == true {
checkForUpdate()
}
}
@objc func showPreferences() { // 使
if (ctlPrefWindowInstance == nil) { if (UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) != nil) == true {
ctlPrefWindowInstance = ctlPrefWindow.init(windowNibName: "frmPrefWindow") checkForUpdate()
} }
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)
}
@objc(checkForUpdate) @objc func showPreferences() {
func checkForUpdate() { if ctlPrefWindowInstance == nil {
checkForUpdate(forced: false) ctlPrefWindowInstance = ctlPrefWindow.init(windowNibName: "frmPrefWindow")
} }
ctlPrefWindowInstance?.window?.center()
ctlPrefWindowInstance?.window?.orderFrontRegardless() //
ctlPrefWindowInstance?.window?.level = .statusBar
ctlPrefWindowInstance?.window?.titlebarAppearsTransparent = true
NSApp.setActivationPolicy(.accessory)
}
@objc(checkForUpdateForced:) // New About Window
func checkForUpdate(forced: Bool) { @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 { @objc(checkForUpdate)
// busy func checkForUpdate() {
return checkForUpdate(forced: false)
} }
// time for update? @objc(checkForUpdateForced:)
if !forced { func checkForUpdate(forced: Bool) {
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
}
}
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date()) if checkTask != nil {
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey) // busy
return
}
checkTask = VersionUpdateApi.check(forced: forced) { [self] result in // time for update?
defer { if !forced {
self.checkTask = nil if UserDefaults.standard.bool(forKey: kCheckUpdateAutomatically) == false {
} return
switch result { }
case .success(let apiResult): let now = Date()
switch apiResult { let date = UserDefaults.standard.object(forKey: kNextUpdateCheckDateKey) as? Date ?? now
case .shouldUpdate(let report): if now.compare(date) == .orderedAscending {
self.updateNextStepURL = report.siteUrl return
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 selfUninstall() { let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
self.currentAlertType = "Uninstall" UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
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)
}
func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) { checkTask = VersionUpdateApi.check(forced: forced) { [self] result in
switch self.currentAlertType { defer {
case "Uninstall": self.checkTask = nil
NSWorkspace.shared.openFile(mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder") }
IME.uninstall(isSudo: false, selfKill: true) switch result {
case "Update": case .success(let apiResult):
if let updateNextStepURL = self.updateNextStepURL { switch apiResult {
NSWorkspace.shared.open(updateNextStepURL) case .shouldUpdate(let report):
} self.updateNextStepURL = report.siteUrl
self.updateNextStepURL = nil let content = String(
default: format: NSLocalizedString(
break "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) { func selfUninstall() {
switch self.currentAlertType { self.currentAlertType = "Uninstall"
case "Update": let content = String(
self.updateNextStepURL = nil format: NSLocalizedString(
default: "This will remove vChewing Input Method from this user account, requiring your confirmation.",
break 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 func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) {
@IBAction func about(_ sender: Any) { switch self.currentAlertType {
(NSApp.delegate as? AppDelegate)?.showAbout() case "Uninstall":
NSApplication.shared.activate(ignoringOtherApps: true) 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)
}
} }

View File

@ -1,329 +1,339 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc class AppleKeyboardConverter: NSObject { @objc class AppleKeyboardConverter: NSObject {
@objc class func isDynamicBaseKeyboardLayoutEnabled() -> Bool { @objc class func isDynamicBaseKeyboardLayoutEnabled() -> Bool {
switch mgrPrefs.basisKeyboardLayout { switch mgrPrefs.basisKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo": case "com.apple.keylayout.ZhuyinBopomofo":
return true return true
case "com.apple.keylayout.ZhuyinEten": case "com.apple.keylayout.ZhuyinEten":
return true return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingdachen": case "org.atelierInmu.vChewing.keyLayouts.vchewingdachen":
return true return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingmitac": case "org.atelierInmu.vChewing.keyLayouts.vchewingmitac":
return true return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingibm": case "org.atelierInmu.vChewing.keyLayouts.vchewingibm":
return true return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou": case "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou":
return true return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingeten": case "org.atelierInmu.vChewing.keyLayouts.vchewingeten":
return true return true
case "org.unknown.keylayout.vChewingDachen": case "org.unknown.keylayout.vChewingDachen":
return true return true
case "org.unknown.keylayout.vChewingFakeSeigyou": case "org.unknown.keylayout.vChewingFakeSeigyou":
return true return true
case "org.unknown.keylayout.vChewingETen": case "org.unknown.keylayout.vChewingETen":
return true return true
case "org.unknown.keylayout.vChewingIBM": case "org.unknown.keylayout.vChewingIBM":
return true return true
case "org.unknown.keylayout.vChewingMiTAC": case "org.unknown.keylayout.vChewingMiTAC":
return true return true
default: default:
return false return false
} }
} }
// Apple // Apple
@objc class func cnvApple2ABC(_ charCode: UniChar) -> UniChar { @objc class func cnvApple2ABC(_ charCode: UniChar) -> UniChar {
var charCode = charCode var charCode = charCode
// OVMandarin OVMandarin // OVMandarin OVMandarin
if self.isDynamicBaseKeyboardLayoutEnabled() { if self.isDynamicBaseKeyboardLayoutEnabled() {
// Apple // Apple
switch mgrPrefs.basisKeyboardLayout { switch mgrPrefs.basisKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo": do { case "com.apple.keylayout.ZhuyinBopomofo":
if (charCode == 97) {charCode = UniChar(65)} do {
if (charCode == 98) {charCode = UniChar(66)} if charCode == 97 { charCode = UniChar(65) }
if (charCode == 99) {charCode = UniChar(67)} if charCode == 98 { charCode = UniChar(66) }
if (charCode == 100) {charCode = UniChar(68)} if charCode == 99 { charCode = UniChar(67) }
if (charCode == 101) {charCode = UniChar(69)} if charCode == 100 { charCode = UniChar(68) }
if (charCode == 102) {charCode = UniChar(70)} if charCode == 101 { charCode = UniChar(69) }
if (charCode == 103) {charCode = UniChar(71)} if charCode == 102 { charCode = UniChar(70) }
if (charCode == 104) {charCode = UniChar(72)} if charCode == 103 { charCode = UniChar(71) }
if (charCode == 105) {charCode = UniChar(73)} if charCode == 104 { charCode = UniChar(72) }
if (charCode == 106) {charCode = UniChar(74)} if charCode == 105 { charCode = UniChar(73) }
if (charCode == 107) {charCode = UniChar(75)} if charCode == 106 { charCode = UniChar(74) }
if (charCode == 108) {charCode = UniChar(76)} if charCode == 107 { charCode = UniChar(75) }
if (charCode == 109) {charCode = UniChar(77)} if charCode == 108 { charCode = UniChar(76) }
if (charCode == 110) {charCode = UniChar(78)} if charCode == 109 { charCode = UniChar(77) }
if (charCode == 111) {charCode = UniChar(79)} if charCode == 110 { charCode = UniChar(78) }
if (charCode == 112) {charCode = UniChar(80)} if charCode == 111 { charCode = UniChar(79) }
if (charCode == 113) {charCode = UniChar(81)} if charCode == 112 { charCode = UniChar(80) }
if (charCode == 114) {charCode = UniChar(82)} if charCode == 113 { charCode = UniChar(81) }
if (charCode == 115) {charCode = UniChar(83)} if charCode == 114 { charCode = UniChar(82) }
if (charCode == 116) {charCode = UniChar(84)} if charCode == 115 { charCode = UniChar(83) }
if (charCode == 117) {charCode = UniChar(85)} if charCode == 116 { charCode = UniChar(84) }
if (charCode == 118) {charCode = UniChar(86)} if charCode == 117 { charCode = UniChar(85) }
if (charCode == 119) {charCode = UniChar(87)} if charCode == 118 { charCode = UniChar(86) }
if (charCode == 120) {charCode = UniChar(88)} if charCode == 119 { charCode = UniChar(87) }
if (charCode == 121) {charCode = UniChar(89)} if charCode == 120 { charCode = UniChar(88) }
if (charCode == 122) {charCode = UniChar(90)} if charCode == 121 { charCode = UniChar(89) }
} if charCode == 122 { charCode = UniChar(90) }
case "com.apple.keylayout.ZhuyinEten": do { }
if (charCode == 65345) {charCode = UniChar(65)} case "com.apple.keylayout.ZhuyinEten":
if (charCode == 65346) {charCode = UniChar(66)} do {
if (charCode == 65347) {charCode = UniChar(67)} if charCode == 65345 { charCode = UniChar(65) }
if (charCode == 65348) {charCode = UniChar(68)} if charCode == 65346 { charCode = UniChar(66) }
if (charCode == 65349) {charCode = UniChar(69)} if charCode == 65347 { charCode = UniChar(67) }
if (charCode == 65350) {charCode = UniChar(70)} if charCode == 65348 { charCode = UniChar(68) }
if (charCode == 65351) {charCode = UniChar(71)} if charCode == 65349 { charCode = UniChar(69) }
if (charCode == 65352) {charCode = UniChar(72)} if charCode == 65350 { charCode = UniChar(70) }
if (charCode == 65353) {charCode = UniChar(73)} if charCode == 65351 { charCode = UniChar(71) }
if (charCode == 65354) {charCode = UniChar(74)} if charCode == 65352 { charCode = UniChar(72) }
if (charCode == 65355) {charCode = UniChar(75)} if charCode == 65353 { charCode = UniChar(73) }
if (charCode == 65356) {charCode = UniChar(76)} if charCode == 65354 { charCode = UniChar(74) }
if (charCode == 65357) {charCode = UniChar(77)} if charCode == 65355 { charCode = UniChar(75) }
if (charCode == 65358) {charCode = UniChar(78)} if charCode == 65356 { charCode = UniChar(76) }
if (charCode == 65359) {charCode = UniChar(79)} if charCode == 65357 { charCode = UniChar(77) }
if (charCode == 65360) {charCode = UniChar(80)} if charCode == 65358 { charCode = UniChar(78) }
if (charCode == 65361) {charCode = UniChar(81)} if charCode == 65359 { charCode = UniChar(79) }
if (charCode == 65362) {charCode = UniChar(82)} if charCode == 65360 { charCode = UniChar(80) }
if (charCode == 65363) {charCode = UniChar(83)} if charCode == 65361 { charCode = UniChar(81) }
if (charCode == 65364) {charCode = UniChar(84)} if charCode == 65362 { charCode = UniChar(82) }
if (charCode == 65365) {charCode = UniChar(85)} if charCode == 65363 { charCode = UniChar(83) }
if (charCode == 65366) {charCode = UniChar(86)} if charCode == 65364 { charCode = UniChar(84) }
if (charCode == 65367) {charCode = UniChar(87)} if charCode == 65365 { charCode = UniChar(85) }
if (charCode == 65368) {charCode = UniChar(88)} if charCode == 65366 { charCode = UniChar(86) }
if (charCode == 65369) {charCode = UniChar(89)} if charCode == 65367 { charCode = UniChar(87) }
if (charCode == 65370) {charCode = UniChar(90)} if charCode == 65368 { charCode = UniChar(88) }
} if charCode == 65369 { charCode = UniChar(89) }
default: break 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 == 12573 { charCode = UniChar(44) }
if (charCode == 12581) {charCode = UniChar(47)} if charCode == 12582 { charCode = UniChar(45) }
if (charCode == 12578) {charCode = UniChar(48)} if charCode == 12577 { charCode = UniChar(46) }
if (charCode == 12549) {charCode = UniChar(49)} if charCode == 12581 { charCode = UniChar(47) }
if (charCode == 12553) {charCode = UniChar(50)} if charCode == 12578 { charCode = UniChar(48) }
if (charCode == 711) {charCode = UniChar(51)} if charCode == 12549 { charCode = UniChar(49) }
if (charCode == 715) {charCode = UniChar(52)} if charCode == 12553 { charCode = UniChar(50) }
if (charCode == 12563) {charCode = UniChar(53)} if charCode == 711 { charCode = UniChar(51) }
if (charCode == 714) {charCode = UniChar(54)} if charCode == 715 { charCode = UniChar(52) }
if (charCode == 729) {charCode = UniChar(55)} if charCode == 12563 { charCode = UniChar(53) }
if (charCode == 12570) {charCode = UniChar(56)} if charCode == 714 { charCode = UniChar(54) }
if (charCode == 12574) {charCode = UniChar(57)} if charCode == 729 { charCode = UniChar(55) }
if (charCode == 12580) {charCode = UniChar(59)} if charCode == 12570 { charCode = UniChar(56) }
if (charCode == 12551) {charCode = UniChar(97)} if charCode == 12574 { charCode = UniChar(57) }
if (charCode == 12566) {charCode = UniChar(98)} if charCode == 12580 { charCode = UniChar(59) }
if (charCode == 12559) {charCode = UniChar(99)} if charCode == 12551 { charCode = UniChar(97) }
if (charCode == 12558) {charCode = UniChar(100)} if charCode == 12566 { charCode = UniChar(98) }
if (charCode == 12557) {charCode = UniChar(101)} if charCode == 12559 { charCode = UniChar(99) }
if (charCode == 12561) {charCode = UniChar(102)} if charCode == 12558 { charCode = UniChar(100) }
if (charCode == 12565) {charCode = UniChar(103)} if charCode == 12557 { charCode = UniChar(101) }
if (charCode == 12568) {charCode = UniChar(104)} if charCode == 12561 { charCode = UniChar(102) }
if (charCode == 12571) {charCode = UniChar(105)} if charCode == 12565 { charCode = UniChar(103) }
if (charCode == 12584) {charCode = UniChar(106)} if charCode == 12568 { charCode = UniChar(104) }
if (charCode == 12572) {charCode = UniChar(107)} if charCode == 12571 { charCode = UniChar(105) }
if (charCode == 12576) {charCode = UniChar(108)} if charCode == 12584 { charCode = UniChar(106) }
if (charCode == 12585) {charCode = UniChar(109)} if charCode == 12572 { charCode = UniChar(107) }
if (charCode == 12569) {charCode = UniChar(110)} if charCode == 12576 { charCode = UniChar(108) }
if (charCode == 12575) {charCode = UniChar(111)} if charCode == 12585 { charCode = UniChar(109) }
if (charCode == 12579) {charCode = UniChar(112)} if charCode == 12569 { charCode = UniChar(110) }
if (charCode == 12550) {charCode = UniChar(113)} if charCode == 12575 { charCode = UniChar(111) }
if (charCode == 12560) {charCode = UniChar(114)} if charCode == 12579 { charCode = UniChar(112) }
if (charCode == 12555) {charCode = UniChar(115)} if charCode == 12550 { charCode = UniChar(113) }
if (charCode == 12564) {charCode = UniChar(116)} if charCode == 12560 { charCode = UniChar(114) }
if (charCode == 12583) {charCode = UniChar(117)} if charCode == 12555 { charCode = UniChar(115) }
if (charCode == 12562) {charCode = UniChar(118)} if charCode == 12564 { charCode = UniChar(116) }
if (charCode == 12554) {charCode = UniChar(119)} if charCode == 12583 { charCode = UniChar(117) }
if (charCode == 12556) {charCode = UniChar(120)} if charCode == 12562 { charCode = UniChar(118) }
if (charCode == 12567) {charCode = UniChar(121)} if charCode == 12554 { charCode = UniChar(119) }
if (charCode == 12552) {charCode = UniChar(122)} if charCode == 12556 { charCode = UniChar(120) }
// if charCode == 12567 { charCode = UniChar(121) }
if (charCode == 12289) {charCode = UniChar(92)} if charCode == 12552 { charCode = UniChar(122) }
if (charCode == 12300) {charCode = UniChar(91)} //
if (charCode == 12301) {charCode = UniChar(93)} if charCode == 12289 { charCode = UniChar(92) }
if (charCode == 12302) {charCode = UniChar(123)} if charCode == 12300 { charCode = UniChar(91) }
if (charCode == 12303) {charCode = UniChar(125)} if charCode == 12301 { charCode = UniChar(93) }
if (charCode == 65292) {charCode = UniChar(60)} if charCode == 12302 { charCode = UniChar(123) }
if (charCode == 12290) {charCode = UniChar(62)} if charCode == 12303 { charCode = UniChar(125) }
// SHIFT if charCode == 65292 { charCode = UniChar(60) }
if (charCode == 65281) {charCode = UniChar(33)} if charCode == 12290 { charCode = UniChar(62) }
if (charCode == 65312) {charCode = UniChar(64)} // SHIFT
if (charCode == 65283) {charCode = UniChar(35)} if charCode == 65281 { charCode = UniChar(33) }
if (charCode == 65284) {charCode = UniChar(36)} if charCode == 65312 { charCode = UniChar(64) }
if (charCode == 65285) {charCode = UniChar(37)} if charCode == 65283 { charCode = UniChar(35) }
if (charCode == 65087) {charCode = UniChar(94)} if charCode == 65284 { charCode = UniChar(36) }
if (charCode == 65286) {charCode = UniChar(38)} if charCode == 65285 { charCode = UniChar(37) }
if (charCode == 65290) {charCode = UniChar(42)} if charCode == 65087 { charCode = UniChar(94) }
if (charCode == 65288) {charCode = UniChar(40)} if charCode == 65286 { charCode = UniChar(38) }
if (charCode == 65289) {charCode = UniChar(41)} if charCode == 65290 { charCode = UniChar(42) }
// Alt if charCode == 65288 { charCode = UniChar(40) }
if (charCode == 8212) {charCode = UniChar(45)} if charCode == 65289 { charCode = UniChar(41) }
// Apple // Alt
if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { if charCode == 8212 { charCode = UniChar(45) }
if (charCode == 65343) {charCode = UniChar(95)} // Apple
if (charCode == 65306) {charCode = UniChar(58)} if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if (charCode == 65311) {charCode = UniChar(63)} if charCode == 65343 { charCode = UniChar(95) }
if (charCode == 65291) {charCode = UniChar(43)} if charCode == 65306 { charCode = UniChar(58) }
if (charCode == 65372) {charCode = UniChar(124)} if charCode == 65311 { charCode = UniChar(63) }
} if charCode == 65291 { charCode = UniChar(43) }
} if charCode == 65372 { charCode = UniChar(124) }
return charCode }
} }
return charCode
}
@objc class func cnvStringApple2ABC(_ strProcessed: String) -> String { @objc class func cnvStringApple2ABC(_ strProcessed: String) -> String {
var strProcessed = strProcessed var strProcessed = strProcessed
if self.isDynamicBaseKeyboardLayoutEnabled() { if self.isDynamicBaseKeyboardLayoutEnabled() {
// Apple // Apple
switch mgrPrefs.basisKeyboardLayout { switch mgrPrefs.basisKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo": do { case "com.apple.keylayout.ZhuyinBopomofo":
if (strProcessed == "a") {strProcessed = "A"} do {
if (strProcessed == "b") {strProcessed = "B"} if strProcessed == "a" { strProcessed = "A" }
if (strProcessed == "c") {strProcessed = "C"} if strProcessed == "b" { strProcessed = "B" }
if (strProcessed == "d") {strProcessed = "D"} if strProcessed == "c" { strProcessed = "C" }
if (strProcessed == "e") {strProcessed = "E"} if strProcessed == "d" { strProcessed = "D" }
if (strProcessed == "f") {strProcessed = "F"} if strProcessed == "e" { strProcessed = "E" }
if (strProcessed == "g") {strProcessed = "G"} if strProcessed == "f" { strProcessed = "F" }
if (strProcessed == "h") {strProcessed = "H"} if strProcessed == "g" { strProcessed = "G" }
if (strProcessed == "i") {strProcessed = "I"} if strProcessed == "h" { strProcessed = "H" }
if (strProcessed == "j") {strProcessed = "J"} if strProcessed == "i" { strProcessed = "I" }
if (strProcessed == "k") {strProcessed = "K"} if strProcessed == "j" { strProcessed = "J" }
if (strProcessed == "l") {strProcessed = "L"} if strProcessed == "k" { strProcessed = "K" }
if (strProcessed == "m") {strProcessed = "M"} if strProcessed == "l" { strProcessed = "L" }
if (strProcessed == "n") {strProcessed = "N"} if strProcessed == "m" { strProcessed = "M" }
if (strProcessed == "o") {strProcessed = "O"} if strProcessed == "n" { strProcessed = "N" }
if (strProcessed == "p") {strProcessed = "P"} if strProcessed == "o" { strProcessed = "O" }
if (strProcessed == "q") {strProcessed = "Q"} if strProcessed == "p" { strProcessed = "P" }
if (strProcessed == "r") {strProcessed = "R"} if strProcessed == "q" { strProcessed = "Q" }
if (strProcessed == "s") {strProcessed = "S"} if strProcessed == "r" { strProcessed = "R" }
if (strProcessed == "t") {strProcessed = "T"} if strProcessed == "s" { strProcessed = "S" }
if (strProcessed == "u") {strProcessed = "U"} if strProcessed == "t" { strProcessed = "T" }
if (strProcessed == "v") {strProcessed = "V"} if strProcessed == "u" { strProcessed = "U" }
if (strProcessed == "w") {strProcessed = "W"} if strProcessed == "v" { strProcessed = "V" }
if (strProcessed == "x") {strProcessed = "X"} if strProcessed == "w" { strProcessed = "W" }
if (strProcessed == "y") {strProcessed = "Y"} if strProcessed == "x" { strProcessed = "X" }
if (strProcessed == "z") {strProcessed = "Z"} if strProcessed == "y" { strProcessed = "Y" }
} if strProcessed == "z" { strProcessed = "Z" }
case "com.apple.keylayout.ZhuyinEten": do { }
if (strProcessed == "") {strProcessed = "A"} case "com.apple.keylayout.ZhuyinEten":
if (strProcessed == "") {strProcessed = "B"} do {
if (strProcessed == "") {strProcessed = "C"} if strProcessed == "" { strProcessed = "A" }
if (strProcessed == "") {strProcessed = "D"} if strProcessed == "" { strProcessed = "B" }
if (strProcessed == "") {strProcessed = "E"} if strProcessed == "" { strProcessed = "C" }
if (strProcessed == "") {strProcessed = "F"} if strProcessed == "" { strProcessed = "D" }
if (strProcessed == "") {strProcessed = "G"} if strProcessed == "" { strProcessed = "E" }
if (strProcessed == "") {strProcessed = "H"} if strProcessed == "" { strProcessed = "F" }
if (strProcessed == "") {strProcessed = "I"} if strProcessed == "" { strProcessed = "G" }
if (strProcessed == "") {strProcessed = "J"} if strProcessed == "" { strProcessed = "H" }
if (strProcessed == "") {strProcessed = "K"} if strProcessed == "" { strProcessed = "I" }
if (strProcessed == "") {strProcessed = "L"} if strProcessed == "" { strProcessed = "J" }
if (strProcessed == "") {strProcessed = "M"} if strProcessed == "" { strProcessed = "K" }
if (strProcessed == "") {strProcessed = "N"} if strProcessed == "" { strProcessed = "L" }
if (strProcessed == "") {strProcessed = "O"} if strProcessed == "" { strProcessed = "M" }
if (strProcessed == "") {strProcessed = "P"} if strProcessed == "" { strProcessed = "N" }
if (strProcessed == "") {strProcessed = "Q"} if strProcessed == "" { strProcessed = "O" }
if (strProcessed == "") {strProcessed = "R"} if strProcessed == "" { strProcessed = "P" }
if (strProcessed == "") {strProcessed = "S"} if strProcessed == "" { strProcessed = "Q" }
if (strProcessed == "") {strProcessed = "T"} if strProcessed == "" { strProcessed = "R" }
if (strProcessed == "") {strProcessed = "U"} if strProcessed == "" { strProcessed = "S" }
if (strProcessed == "") {strProcessed = "V"} if strProcessed == "" { strProcessed = "T" }
if (strProcessed == "") {strProcessed = "W"} if strProcessed == "" { strProcessed = "U" }
if (strProcessed == "") {strProcessed = "X"} if strProcessed == "" { strProcessed = "V" }
if (strProcessed == "") {strProcessed = "Y"} if strProcessed == "" { strProcessed = "W" }
if (strProcessed == "") {strProcessed = "Z"} if strProcessed == "" { strProcessed = "X" }
} if strProcessed == "" { strProcessed = "Y" }
default: break if strProcessed == "" { strProcessed = "Z" }
} }
// default: break
if (strProcessed == "") {strProcessed = ","} }
if (strProcessed == "") {strProcessed = "-"} //
if (strProcessed == "") {strProcessed = "."} if strProcessed == "" { strProcessed = "," }
if (strProcessed == "") {strProcessed = "/"} if strProcessed == "" { strProcessed = "-" }
if (strProcessed == "") {strProcessed = "0"} if strProcessed == "" { strProcessed = "." }
if (strProcessed == "") {strProcessed = "1"} if strProcessed == "" { strProcessed = "/" }
if (strProcessed == "") {strProcessed = "2"} if strProcessed == "" { strProcessed = "0" }
if (strProcessed == "ˇ") {strProcessed = "3"} if strProcessed == "" { strProcessed = "1" }
if (strProcessed == "ˋ") {strProcessed = "4"} if strProcessed == "" { strProcessed = "2" }
if (strProcessed == "") {strProcessed = "5"} if strProcessed == "ˇ" { strProcessed = "3" }
if (strProcessed == "ˊ") {strProcessed = "6"} if strProcessed == "ˋ" { strProcessed = "4" }
if (strProcessed == "˙") {strProcessed = "7"} if strProcessed == "" { strProcessed = "5" }
if (strProcessed == "") {strProcessed = "8"} if strProcessed == "ˊ" { strProcessed = "6" }
if (strProcessed == "") {strProcessed = "9"} if strProcessed == "˙" { strProcessed = "7" }
if (strProcessed == "") {strProcessed = ";"} if strProcessed == "" { strProcessed = "8" }
if (strProcessed == "") {strProcessed = "a"} if strProcessed == "" { strProcessed = "9" }
if (strProcessed == "") {strProcessed = "b"} if strProcessed == "" { strProcessed = ";" }
if (strProcessed == "") {strProcessed = "c"} if strProcessed == "" { strProcessed = "a" }
if (strProcessed == "") {strProcessed = "d"} if strProcessed == "" { strProcessed = "b" }
if (strProcessed == "") {strProcessed = "e"} if strProcessed == "" { strProcessed = "c" }
if (strProcessed == "") {strProcessed = "f"} if strProcessed == "" { strProcessed = "d" }
if (strProcessed == "") {strProcessed = "g"} if strProcessed == "" { strProcessed = "e" }
if (strProcessed == "") {strProcessed = "h"} if strProcessed == "" { strProcessed = "f" }
if (strProcessed == "") {strProcessed = "i"} if strProcessed == "" { strProcessed = "g" }
if (strProcessed == "") {strProcessed = "j"} if strProcessed == "" { strProcessed = "h" }
if (strProcessed == "") {strProcessed = "k"} if strProcessed == "" { strProcessed = "i" }
if (strProcessed == "") {strProcessed = "l"} if strProcessed == "" { strProcessed = "j" }
if (strProcessed == "") {strProcessed = "m"} if strProcessed == "" { strProcessed = "k" }
if (strProcessed == "") {strProcessed = "n"} if strProcessed == "" { strProcessed = "l" }
if (strProcessed == "") {strProcessed = "o"} if strProcessed == "" { strProcessed = "m" }
if (strProcessed == "") {strProcessed = "p"} if strProcessed == "" { strProcessed = "n" }
if (strProcessed == "") {strProcessed = "q"} if strProcessed == "" { strProcessed = "o" }
if (strProcessed == "") {strProcessed = "r"} if strProcessed == "" { strProcessed = "p" }
if (strProcessed == "") {strProcessed = "s"} if strProcessed == "" { strProcessed = "q" }
if (strProcessed == "") {strProcessed = "t"} if strProcessed == "" { strProcessed = "r" }
if (strProcessed == "") {strProcessed = "u"} if strProcessed == "" { strProcessed = "s" }
if (strProcessed == "") {strProcessed = "v"} if strProcessed == "" { strProcessed = "t" }
if (strProcessed == "") {strProcessed = "w"} if strProcessed == "" { strProcessed = "u" }
if (strProcessed == "") {strProcessed = "x"} if strProcessed == "" { strProcessed = "v" }
if (strProcessed == "") {strProcessed = "y"} if strProcessed == "" { strProcessed = "w" }
if (strProcessed == "") {strProcessed = "z"} if strProcessed == "" { strProcessed = "x" }
// if strProcessed == "" { strProcessed = "y" }
if (strProcessed == "") {strProcessed = "\\"} if strProcessed == "" { strProcessed = "z" }
if (strProcessed == "") {strProcessed = "["} //
if (strProcessed == "") {strProcessed = "]"} if strProcessed == "" { strProcessed = "\\" }
if (strProcessed == "") {strProcessed = "{"} if strProcessed == "" { strProcessed = "[" }
if (strProcessed == "") {strProcessed = "}"} if strProcessed == "" { strProcessed = "]" }
if (strProcessed == "") {strProcessed = "<"} if strProcessed == "" { strProcessed = "{" }
if (strProcessed == "") {strProcessed = ">"} if strProcessed == "" { strProcessed = "}" }
// SHIFT 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 = "%" }
if (strProcessed == "") {strProcessed = "*"} if strProcessed == "︿" { strProcessed = "^" }
if (strProcessed == "") {strProcessed = "("} if strProcessed == "" { strProcessed = "&" }
if (strProcessed == "") {strProcessed = ")"} if strProcessed == "" { strProcessed = "*" }
// Alt if strProcessed == "" { strProcessed = "(" }
if (strProcessed == "") {strProcessed = "-"} if strProcessed == "" { strProcessed = ")" }
// Apple // Alt
if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" { if strProcessed == "" { strProcessed = "-" }
if (strProcessed == "_") {strProcessed = "_"} // Apple
if (strProcessed == "") {strProcessed = ":"} if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if (strProcessed == "") {strProcessed = "?"} if strProcessed == "_" { strProcessed = "_" }
if (strProcessed == "") {strProcessed = "+"} if strProcessed == "" { strProcessed = ":" }
if (strProcessed == "") {strProcessed = "|"} if strProcessed == "" { strProcessed = "?" }
} if strProcessed == "" { strProcessed = "+" }
} if strProcessed == "" { strProcessed = "|" }
return strProcessed }
} }
return strProcessed
}
} }

View File

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

View File

@ -1,20 +1,27 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 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. // Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts.
// KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes // KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes
@objc enum KeyCode: UInt16 { @objc enum KeyCode: UInt16 {
case none = 0 case none = 0
case space = 49 case space = 49
case backSpace = 51 case backSpace = 51
case esc = 53 case esc = 53
case tab = 48 case tab = 48
case enterLF = 76 case enterLF = 76
case enterCR = 36 case enterCR = 36
case up = 126 case up = 126
case down = 125 case down = 125
case left = 123 case left = 123
case right = 124 case right = 124
case pageUp = 116 case pageUp = 116
case pageDown = 121 case pageDown = 121
case home = 115 case home = 115
case end = 119 case end = 119
case delete = 117 case delete = 117
case leftShift = 56 case leftShift = 56
case rightShift = 60 case rightShift = 60
case capsLock = 57 case capsLock = 57
case symbolMenuPhysicalKey = 50 case symbolMenuPhysicalKey = 50
} }
// CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html // CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html
enum CharCode: UInt/*16*/ { enum CharCode: UInt /*16*/ {
case yajuusenpai = 1145141919810893 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. // - 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 { class keyParser: NSObject {
@objc private (set) var useVerticalMode: Bool @objc private(set) var useVerticalMode: Bool
@objc private (set) var inputText: String? @objc private(set) var inputText: String?
@objc private (set) var inputTextIgnoringModifiers: String? @objc private(set) var inputTextIgnoringModifiers: String?
@objc private (set) var charCode: UInt16 @objc private(set) var charCode: UInt16
@objc private (set) var keyCode: UInt16 @objc private(set) var keyCode: UInt16
private var isFlagChanged: Bool private var isFlagChanged: Bool
private var flags: NSEvent.ModifierFlags private var flags: NSEvent.ModifierFlags
private var cursorForwardKey: KeyCode private var cursorForwardKey: KeyCode
private var cursorBackwardKey: KeyCode private var cursorBackwardKey: KeyCode
private var extraChooseCandidateKey: KeyCode private var extraChooseCandidateKey: KeyCode
private var extraChooseCandidateKeyReverse: KeyCode private var extraChooseCandidateKeyReverse: KeyCode
private var absorbedArrowKey: KeyCode private var absorbedArrowKey: KeyCode
private var verticalModeOnlyChooseCandidateKey: KeyCode private var verticalModeOnlyChooseCandidateKey: KeyCode
@objc private (set) var emacsKey: vChewingEmacsKey @objc private(set) var emacsKey: vChewingEmacsKey
@objc init(inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags, isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil) { @objc init(
let inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") inputText: String?, keyCode: UInt16, charCode: UInt16, flags: NSEvent.ModifierFlags,
let inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(inputTextIgnoringModifiers ?? inputText) isVerticalMode: Bool, inputTextIgnoringModifiers: String? = nil
self.inputText = inputText ) {
self.inputTextIgnoringModifiers = inputTextIgnoringModifiers let inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "")
self.keyCode = keyCode let inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(
self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) inputTextIgnoringModifiers ?? inputText)
self.flags = flags self.inputText = inputText
self.isFlagChanged = false self.inputTextIgnoringModifiers = inputTextIgnoringModifiers
useVerticalMode = isVerticalMode self.keyCode = keyCode
emacsKey = EmacsKeyHelper.detect(charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags) self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
cursorForwardKey = useVerticalMode ? .down : .right self.flags = flags
cursorBackwardKey = useVerticalMode ? .up : .left self.isFlagChanged = false
extraChooseCandidateKey = useVerticalMode ? .left : .down useVerticalMode = isVerticalMode
extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up emacsKey = EmacsKeyHelper.detect(
absorbedArrowKey = useVerticalMode ? .right : .up charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: flags)
verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none cursorForwardKey = useVerticalMode ? .down : .right
super.init() 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) { @objc init(event: NSEvent, isVerticalMode: Bool) {
inputText = AppleKeyboardConverter.cnvStringApple2ABC(event.characters ?? "") inputText = AppleKeyboardConverter.cnvStringApple2ABC(event.characters ?? "")
inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(event.charactersIgnoringModifiers ?? "") inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(
keyCode = event.keyCode event.charactersIgnoringModifiers ?? "")
flags = event.modifierFlags keyCode = event.keyCode
isFlagChanged = (event.type == .flagsChanged) ? true : false flags = event.modifierFlags
useVerticalMode = isVerticalMode isFlagChanged = (event.type == .flagsChanged) ? true : false
let charCode: UInt16 = { useVerticalMode = isVerticalMode
guard let inputText = event.characters, inputText.count > 0 else { let charCode: UInt16 = {
return 0 guard let inputText = event.characters, inputText.count > 0 else {
} return 0
let first = inputText[inputText.startIndex].utf16.first! }
return first let first = inputText[inputText.startIndex].utf16.first!
}() return first
self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) }()
emacsKey = EmacsKeyHelper.detect(charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: event.modifierFlags) self.charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
cursorForwardKey = useVerticalMode ? .down : .right emacsKey = EmacsKeyHelper.detect(
cursorBackwardKey = useVerticalMode ? .up : .left charCode: AppleKeyboardConverter.cnvApple2ABC(charCode), flags: event.modifierFlags)
extraChooseCandidateKey = useVerticalMode ? .left : .down cursorForwardKey = useVerticalMode ? .down : .right
extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up cursorBackwardKey = useVerticalMode ? .up : .left
absorbedArrowKey = useVerticalMode ? .right : .up extraChooseCandidateKey = useVerticalMode ? .left : .down
verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none extraChooseCandidateKeyReverse = useVerticalMode ? .right : .up
super.init() absorbedArrowKey = useVerticalMode ? .right : .up
} verticalModeOnlyChooseCandidateKey = useVerticalMode ? absorbedArrowKey : .none
super.init()
}
override var description: String { override var description: String {
charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "") inputText = AppleKeyboardConverter.cnvStringApple2ABC(inputText ?? "")
inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(inputTextIgnoringModifiers ?? "") inputTextIgnoringModifiers = AppleKeyboardConverter.cnvStringApple2ABC(
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)>" 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 { @objc var isShiftHold: Bool {
flags.contains([.shift]) flags.contains([.shift])
} }
@objc var isCommandHold: Bool { @objc var isCommandHold: Bool {
flags.contains([.command]) flags.contains([.command])
} }
@objc var isControlHold: Bool { @objc var isControlHold: Bool {
flags.contains([.control]) flags.contains([.control])
} }
@objc var isControlHotKey: Bool { @objc var isControlHotKey: Bool {
flags.contains([.control]) && inputText?.first?.isLetter ?? false flags.contains([.control]) && inputText?.first?.isLetter ?? false
} }
@objc var isOptionHotKey: Bool { @objc var isOptionHotKey: Bool {
flags.contains([.option]) && inputText?.first?.isLetter ?? false flags.contains([.option]) && inputText?.first?.isLetter ?? false
} }
@objc var isOptionHold: Bool { @objc var isOptionHold: Bool {
flags.contains([.option]) flags.contains([.option])
} }
@objc var isCapsLockOn: Bool { @objc var isCapsLockOn: Bool {
flags.contains([.capsLock]) flags.contains([.capsLock])
} }
@objc var isNumericPad: Bool { @objc var isNumericPad: Bool {
flags.contains([.numericPad]) flags.contains([.numericPad])
} }
@objc var isFunctionKeyHold: Bool { @objc var isFunctionKeyHold: Bool {
flags.contains([.function]) flags.contains([.function])
} }
@objc var isReservedKey: Bool { @objc var isReservedKey: Bool {
guard let code = KeyCode(rawValue: keyCode) else { guard let code = KeyCode(rawValue: keyCode) else {
return false return false
} }
return code.rawValue != KeyCode.none.rawValue return code.rawValue != KeyCode.none.rawValue
} }
@objc var isTab: Bool { @objc var isTab: Bool {
KeyCode(rawValue: keyCode) == KeyCode.tab KeyCode(rawValue: keyCode) == KeyCode.tab
} }
@objc var isEnter: Bool { @objc var isEnter: Bool {
(KeyCode(rawValue: keyCode) == KeyCode.enterCR) || (KeyCode(rawValue: keyCode) == KeyCode.enterLF) (KeyCode(rawValue: keyCode) == KeyCode.enterCR)
} || (KeyCode(rawValue: keyCode) == KeyCode.enterLF)
}
@objc var isUp: Bool { @objc var isUp: Bool {
KeyCode(rawValue: keyCode) == KeyCode.up KeyCode(rawValue: keyCode) == KeyCode.up
} }
@objc var isDown: Bool { @objc var isDown: Bool {
KeyCode(rawValue: keyCode) == KeyCode.down KeyCode(rawValue: keyCode) == KeyCode.down
} }
@objc var isLeft: Bool { @objc var isLeft: Bool {
KeyCode(rawValue: keyCode) == KeyCode.left KeyCode(rawValue: keyCode) == KeyCode.left
} }
@objc var isRight: Bool { @objc var isRight: Bool {
KeyCode(rawValue: keyCode) == KeyCode.right KeyCode(rawValue: keyCode) == KeyCode.right
} }
@objc var isPageUp: Bool { @objc var isPageUp: Bool {
KeyCode(rawValue: keyCode) == KeyCode.pageUp KeyCode(rawValue: keyCode) == KeyCode.pageUp
} }
@objc var isPageDown: Bool { @objc var isPageDown: Bool {
KeyCode(rawValue: keyCode) == KeyCode.pageDown KeyCode(rawValue: keyCode) == KeyCode.pageDown
} }
@objc var isSpace: Bool { @objc var isSpace: Bool {
KeyCode(rawValue: keyCode) == KeyCode.space KeyCode(rawValue: keyCode) == KeyCode.space
} }
@objc var isBackSpace: Bool { @objc var isBackSpace: Bool {
KeyCode(rawValue: keyCode) == KeyCode.backSpace KeyCode(rawValue: keyCode) == KeyCode.backSpace
} }
@objc var isESC: Bool { @objc var isESC: Bool {
KeyCode(rawValue: keyCode) == KeyCode.esc KeyCode(rawValue: keyCode) == KeyCode.esc
} }
@objc var isHome: Bool { @objc var isHome: Bool {
KeyCode(rawValue: keyCode) == KeyCode.home KeyCode(rawValue: keyCode) == KeyCode.home
} }
@objc var isEnd: Bool { @objc var isEnd: Bool {
KeyCode(rawValue: keyCode) == KeyCode.end KeyCode(rawValue: keyCode) == KeyCode.end
} }
@objc var isDelete: Bool { @objc var isDelete: Bool {
KeyCode(rawValue: keyCode) == KeyCode.delete KeyCode(rawValue: keyCode) == KeyCode.delete
} }
@objc var isCursorBackward: Bool { @objc var isCursorBackward: Bool {
KeyCode(rawValue: keyCode) == cursorBackwardKey KeyCode(rawValue: keyCode) == cursorBackwardKey
} }
@objc var isCursorForward: Bool { @objc var isCursorForward: Bool {
KeyCode(rawValue: keyCode) == cursorForwardKey KeyCode(rawValue: keyCode) == cursorForwardKey
} }
@objc var isAbsorbedArrowKey: Bool { @objc var isAbsorbedArrowKey: Bool {
KeyCode(rawValue: keyCode) == absorbedArrowKey KeyCode(rawValue: keyCode) == absorbedArrowKey
} }
@objc var isExtraChooseCandidateKey: Bool { @objc var isExtraChooseCandidateKey: Bool {
KeyCode(rawValue: keyCode) == extraChooseCandidateKey KeyCode(rawValue: keyCode) == extraChooseCandidateKey
} }
@objc var isExtraChooseCandidateKeyReverse: Bool { @objc var isExtraChooseCandidateKeyReverse: Bool {
KeyCode(rawValue: keyCode) == extraChooseCandidateKeyReverse KeyCode(rawValue: keyCode) == extraChooseCandidateKeyReverse
} }
@objc var isVerticalModeOnlyChooseCandidateKey: Bool { @objc var isVerticalModeOnlyChooseCandidateKey: Bool {
KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey KeyCode(rawValue: keyCode) == verticalModeOnlyChooseCandidateKey
} }
@objc var isUpperCaseASCIILetterKey: Bool { @objc var isUpperCaseASCIILetterKey: Bool {
// flags == .shift Shift // flags == .shift Shift
self.charCode >= 65 && self.charCode <= 90 && flags == .shift self.charCode >= 65 && self.charCode <= 90 && flags == .shift
} }
@objc var isSymbolMenuPhysicalKey: Bool { @objc var isSymbolMenuPhysicalKey: Bool {
// KeyCode macOS Apple // KeyCode macOS Apple
// ![input isShift] 使 Shift // ![input isShift] 使 Shift
KeyCode(rawValue: keyCode) == KeyCode.symbolMenuPhysicalKey KeyCode(rawValue: keyCode) == KeyCode.symbolMenuPhysicalKey
} }
} }
@objc enum vChewingEmacsKey: UInt16 { @objc enum vChewingEmacsKey: UInt16 {
case none = 0 case none = 0
case forward = 6 // F case forward = 6 // F
case backward = 2 // B case backward = 2 // B
case home = 1 // A case home = 1 // A
case end = 5 // E case end = 5 // E
case delete = 4 // D case delete = 4 // D
case nextPage = 22 // V case nextPage = 22 // V
} }
class EmacsKeyHelper: NSObject { class EmacsKeyHelper: NSObject {
@objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey { @objc static func detect(charCode: UniChar, flags: NSEvent.ModifierFlags) -> vChewingEmacsKey {
let charCode = AppleKeyboardConverter.cnvApple2ABC(charCode) let charCode = AppleKeyboardConverter.cnvApple2ABC(charCode)
if flags.contains(.control) { if flags.contains(.control) {
return vChewingEmacsKey(rawValue: charCode) ?? .none return vChewingEmacsKey(rawValue: charCode) ?? .none
} }
return .none; return .none
} }
} }

View File

@ -1,69 +1,76 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
public extension NSString { extension NSString {
/// Converts the index in an NSString to the index in a Swift string. /// Converts the index in an NSString to the index in a Swift string.
/// ///
/// An Emoji might be compose by more than one UTF-16 code points, however /// An Emoji might be compose by more than one UTF-16 code points, however
/// the length of an NSString is only the sum of the UTF-16 code points. It /// the length of an NSString is only the sum of the UTF-16 code points. It
/// causes that the NSString and Swift string representation of the same /// causes that the NSString and Swift string representation of the same
/// string have different lengths once the string contains such Emoji. The /// string have different lengths once the string contains such Emoji. The
/// method helps to find the index in a Swift string by passing the index /// method helps to find the index in a Swift string by passing the index
/// in an NSString. /// in an NSString.
func characterIndex(from utf16Index:Int) -> (Int, String) { public func characterIndex(from utf16Index: Int) -> (Int, String) {
let string = (self as String) let string = (self as String)
var length = 0 var length = 0
for (i, character) in string.enumerated() { for (i, character) in string.enumerated() {
length += character.utf16.count length += character.utf16.count
if length > utf16Index { if length > utf16Index {
return (i, string) return (i, string)
} }
} }
return (string.count, string) return (string.count, string)
} }
@objc func nextUtf16Position(for index: Int) -> Int { @objc public func nextUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index) var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex < string.count { if fixedIndex < string.count {
fixedIndex += 1 fixedIndex += 1
} }
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
} }
@objc func previousUtf16Position(for index: Int) -> Int { @objc public func previousUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index) var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex > 0 { if fixedIndex > 0 {
fixedIndex -= 1 fixedIndex -= 1
} }
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
} }
@objc var count: Int { @objc public var count: Int {
(self as String).count (self as String).count
} }
@objc func split() -> [NSString] { @objc public func split() -> [NSString] {
Array(self as String).map { Array(self as String).map {
NSString(string: String($0)) NSString(string: String($0))
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,92 +1,104 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
public protocol FSEventStreamHelperDelegate: AnyObject { public protocol FSEventStreamHelperDelegate: AnyObject {
func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event]) func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event])
} }
public class FSEventStreamHelper : NSObject { public class FSEventStreamHelper: NSObject {
public struct Event { public struct Event {
var path: String var path: String
var flags: FSEventStreamEventFlags var flags: FSEventStreamEventFlags
var id: FSEventStreamEventId var id: FSEventStreamEventId
} }
public let path: String public let path: String
public let dispatchQueue: DispatchQueue public let dispatchQueue: DispatchQueue
public weak var delegate: FSEventStreamHelperDelegate? public weak var delegate: FSEventStreamHelperDelegate?
@objc public init(path: String, queue: DispatchQueue) { @objc public init(path: String, queue: DispatchQueue) {
self.path = path self.path = path
self.dispatchQueue = queue self.dispatchQueue = queue
} }
private var stream: FSEventStreamRef? = nil private var stream: FSEventStreamRef? = nil
public func start() -> Bool { public func start() -> Bool {
if stream != nil { if stream != nil {
return false return false
} }
var context = FSEventStreamContext() var context = FSEventStreamContext()
context.info = Unmanaged.passUnretained(self).toOpaque() context.info = Unmanaged.passUnretained(self).toOpaque()
guard let stream = FSEventStreamCreate(nil, { guard
(stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in let stream = FSEventStreamCreate(
let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!).takeUnretainedValue() nil,
let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self) {
let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount) (stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in
let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount) let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!)
let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount) .takeUnretainedValue()
let events = (0..<eventCount).map { let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
FSEventStreamHelper.Event(path: String(cString: pathsPtr[$0]), let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount)
flags: flagsPtr[$0], let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount)
id: eventIDsPtr[$0] ) let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount)
} let events = (0..<eventCount).map {
helper.delegate?.helper(helper, didReceive: events) FSEventStreamHelper.Event(
}, path: String(cString: pathsPtr[$0]),
&context, flags: flagsPtr[$0],
[path] as CFArray, id: eventIDsPtr[$0])
UInt64(kFSEventStreamEventIdSinceNow), }
1.0, helper.delegate?.helper(helper, didReceive: events)
FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone) },
) else { &context,
return false [path] as CFArray,
} UInt64(kFSEventStreamEventIdSinceNow),
1.0,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
)
else {
return false
}
FSEventStreamSetDispatchQueue(stream, dispatchQueue) FSEventStreamSetDispatchQueue(stream, dispatchQueue)
if !FSEventStreamStart(stream) { if !FSEventStreamStart(stream) {
FSEventStreamInvalidate(stream) FSEventStreamInvalidate(stream)
return false return false
} }
self.stream = stream self.stream = stream
return true return true
} }
func stop() { func stop() {
guard let stream = stream else { guard let stream = stream else {
return return
} }
FSEventStreamStop(stream) FSEventStreamStop(stream)
FSEventStreamInvalidate(stream) FSEventStreamInvalidate(stream)
self.stream = nil self.stream = nil
} }
} }

View File

@ -1,168 +1,195 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc public class IME: NSObject { @objc public class IME: NSObject {
static let dlgOpenPath = NSOpenPanel(); static let dlgOpenPath = NSOpenPanel()
// MARK: - Print debug information to the console. // MARK: - Print debug information to the console.
@objc static func prtDebugIntel(_ strPrint: String) { @objc static func prtDebugIntel(_ strPrint: String) {
if mgrPrefs.isDebugModeEnabled { if mgrPrefs.isDebugModeEnabled {
NSLog("vChewingErrorCallback: %@", strPrint) NSLog("vChewingErrorCallback: %@", strPrint)
} }
} }
// MARK: - Tell whether this IME is running with Root privileges. // MARK: - Tell whether this IME is running with Root privileges.
@objc static var isSudoMode: Bool { @objc static var isSudoMode: Bool {
NSUserName() == "root" NSUserName() == "root"
} }
// MARK: - Initializing Language Models. // MARK: - Initializing Language Models.
@objc static func initLangModels(userOnly: Bool) { @objc static func initLangModels(userOnly: Bool) {
if !userOnly { if !userOnly {
mgrLangModel.loadDataModels() // mgrLangModel.loadDataModels() //
} }
// mgrLangModel loadUserPhrases dataFolderPath // mgrLangModel loadUserPhrases dataFolderPath
// //
// //
mgrLangModel.loadUserPhrases() mgrLangModel.loadUserPhrases()
mgrLangModel.loadUserPhraseReplacement() mgrLangModel.loadUserPhraseReplacement()
mgrLangModel.loadUserAssociatedPhrases() mgrLangModel.loadUserAssociatedPhrases()
} }
// MARK: - System Dark Mode Status Detector. // MARK: - System Dark Mode Status Detector.
@objc static func isDarkMode() -> Bool { @objc static func isDarkMode() -> Bool {
if #available(macOS 10.15, *) { if #available(macOS 10.15, *) {
let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription.lowercased() let appearanceDescription = NSApplication.shared.effectiveAppearance.debugDescription
if appearanceDescription.contains("dark") { .lowercased()
return true if appearanceDescription.contains("dark") {
} return true
} else if #available(macOS 10.14, *) { }
if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle") as? String { } else if #available(macOS 10.14, *) {
if appleInterfaceStyle.lowercased().contains("dark") { if let appleInterfaceStyle = UserDefaults.standard.object(forKey: "AppleInterfaceStyle")
return true as? String
} {
} if appleInterfaceStyle.lowercased().contains("dark") {
} return true
return false }
} }
}
return false
}
// MARK: - Trash a file if it exists. // MARK: - Trash a file if it exists.
@discardableResult static func trashTargetIfExists(_ path: String) -> Bool { @discardableResult static func trashTargetIfExists(_ path: String) -> Bool {
do { do {
if FileManager.default.fileExists(atPath: path) { if FileManager.default.fileExists(atPath: path) {
// //
try FileManager.default.trashItem(at: URL(fileURLWithPath: path), resultingItemURL: nil) try FileManager.default.trashItem(
} else { at: URL(fileURLWithPath: path), resultingItemURL: nil)
NSLog("Item doesn't exist: \(path)") } else {
} NSLog("Item doesn't exist: \(path)")
} catch let error as NSError { }
NSLog("Failed from removing this object: \(path) || Error: \(error)") } catch let error as NSError {
return false NSLog("Failed from removing this object: \(path) || Error: \(error)")
} return false
return true }
} 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
}
let kTargetBin = "vChewing" // MARK: - Uninstall the input method.
let kTargetBundle = "/vChewing.app" @discardableResult static func uninstall(isSudo: Bool = false, selfKill: Bool = true) -> Int32 {
let pathLibrary = isSudo ? "/Library" : FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].path // Bundle.main.bundleURL便使 sudo
let pathIMELibrary = isSudo ? "/Library/Input Methods" : FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0].path guard let bundleID = Bundle.main.bundleIdentifier else {
let pathUnitKeyboardLayouts = "/Keyboard Layouts" NSLog("Failed to ensure the bundle identifier.")
let arrKeyLayoutFiles = ["/vChewing ETen.keylayout", "/vChewingKeyLayout.bundle", "/vChewing MiTAC.keylayout", "/vChewing IBM.keylayout", "/vChewing FakeSeigyou.keylayout", "/vChewing Dachen.keylayout"] return -1
}
// let kTargetBin = "vChewing"
for objPath in arrKeyLayoutFiles { let kTargetBundle = "/vChewing.app"
let objFullPath = pathLibrary + pathUnitKeyboardLayouts + objPath let pathLibrary =
if !IME.trashTargetIfExists(objFullPath) { return -1 } isSudo
} ? "/Library"
if CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "--all" && CommandLine.arguments[1] == "uninstall" { : FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].path
// 使 let pathIMELibrary =
// 使 symbol link isSudo
// symbol link ? "/Library/Input Methods"
IME.trashTargetIfExists(mgrLangModel.dataFolderPath(isDefaultFolder: true)) : FileManager.default.urls(for: .inputMethodsDirectory, in: .userDomainMask)[0].path
IME.trashTargetIfExists(pathLibrary + "/Preferences/" + bundleID + ".plist") // App let pathUnitKeyboardLayouts = "/Keyboard Layouts"
} let arrKeyLayoutFiles = [
if !IME.trashTargetIfExists(pathIMELibrary + kTargetBundle) { return -1 } // App "/vChewing ETen.keylayout", "/vChewingKeyLayout.bundle", "/vChewing MiTAC.keylayout",
// "/vChewing IBM.keylayout", "/vChewing FakeSeigyou.keylayout",
if selfKill { "/vChewing Dachen.keylayout",
let killTask = Process() ]
killTask.launchPath = "/usr/bin/killall"
killTask.arguments = ["-9", kTargetBin]
killTask.launch()
killTask.waitUntilExit()
}
return 0
}
// MARK: - Registering the input method. //
@discardableResult static func registerInputMethod() -> Int32 { for objPath in arrKeyLayoutFiles {
guard let bundleID = Bundle.main.bundleIdentifier else { let objFullPath = pathLibrary + pathUnitKeyboardLayouts + objPath
return -1 if !IME.trashTargetIfExists(objFullPath) { return -1 }
} }
let bundleUrl = Bundle.main.bundleURL if CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "--all"
var maybeInputSource = InputSourceHelper.inputSource(for: bundleID) && 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 { // MARK: - Registering the input method.
NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)"); @discardableResult static func registerInputMethod() -> Int32 {
// then register guard let bundleID = Bundle.main.bundleIdentifier else {
let status = InputSourceHelper.registerTnputSource(at: bundleUrl) return -1
}
let bundleUrl = Bundle.main.bundleURL
var maybeInputSource = InputSourceHelper.inputSource(for: bundleID)
if !status { if maybeInputSource == nil {
NSLog("Fatal error: Cannot register input source \(bundleID) at \(bundleUrl.absoluteString).") NSLog("Registering input source \(bundleID) at \(bundleUrl.absoluteString)")
return -1 // 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 { maybeInputSource = InputSourceHelper.inputSource(for: bundleID)
NSLog("Fatal error: Cannot find input source \(bundleID) after registration.") }
return -1
}
if !InputSourceHelper.inputSourceEnabled(for: inputSource) { guard let inputSource = maybeInputSource else {
NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).") NSLog("Fatal error: Cannot find input source \(bundleID) after registration.")
let status = InputSourceHelper.enable(inputSource: inputSource) return -1
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" { if !InputSourceHelper.inputSourceEnabled(for: inputSource) {
let enabled = InputSourceHelper.enableAllInputMode(for: bundleID) NSLog("Enabling input source \(bundleID) at \(bundleUrl.absoluteString).")
NSLog(enabled ? "All input sources enabled for \(bundleID)" : "Cannot enable all input sources for \(bundleID), but this is ignored") let status = InputSourceHelper.enable(inputSource: inputSource)
} if !status {
return 0 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
}
} }

View File

@ -1,127 +1,140 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Carbon
import Cocoa
public class InputSourceHelper: NSObject { public class InputSourceHelper: NSObject {
@available(*, unavailable) @available(*, unavailable)
public override init() { public override init() {
super.init() super.init()
} }
public static func allInstalledInputSources() -> [TISInputSource] { public static func allInstalledInputSources() -> [TISInputSource] {
TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
} }
@objc(inputSourceForProperty:stringValue:) @objc(inputSourceForProperty:stringValue:)
public static func inputSource(for propertyKey: CFString, stringValue: String) -> TISInputSource? { public static func inputSource(for propertyKey: CFString, stringValue: String)
let stringID = CFStringGetTypeID() -> TISInputSource?
for source in allInstalledInputSources() { {
if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) { let stringID = CFStringGetTypeID()
let property = Unmanaged<CFTypeRef>.fromOpaque(propertyPtr).takeUnretainedValue() for source in allInstalledInputSources() {
let typeID = CFGetTypeID(property) if let propertyPtr = TISGetInputSourceProperty(source, propertyKey) {
if typeID != stringID { let property = Unmanaged<CFTypeRef>.fromOpaque(propertyPtr).takeUnretainedValue()
continue let typeID = CFGetTypeID(property)
} if typeID != stringID {
if stringValue == property as? String { continue
return source }
} if stringValue == property as? String {
} return source
} }
return nil }
} }
return nil
}
@objc(inputSourceForInputSourceID:) @objc(inputSourceForInputSourceID:)
public static func inputSource(for sourceID: String) -> TISInputSource? { public static func inputSource(for sourceID: String) -> TISInputSource? {
inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID) inputSource(for: kTISPropertyInputSourceID, stringValue: sourceID)
} }
@objc(inputSourceEnabled:) @objc(inputSourceEnabled:)
public static func inputSourceEnabled(for source: TISInputSource) -> Bool { public static func inputSourceEnabled(for source: TISInputSource) -> Bool {
if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) { if let valuePts = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled) {
let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue() let value = Unmanaged<CFBoolean>.fromOpaque(valuePts).takeUnretainedValue()
return value == kCFBooleanTrue return value == kCFBooleanTrue
} }
return false return false
} }
@objc(enableInputSource:) @objc(enableInputSource:)
public static func enable(inputSource: TISInputSource) -> Bool { public static func enable(inputSource: TISInputSource) -> Bool {
let status = TISEnableInputSource(inputSource) let status = TISEnableInputSource(inputSource)
return status == noErr return status == noErr
} }
@objc(enableAllInputModesForInputSourceBundleID:) @objc(enableAllInputModesForInputSourceBundleID:)
public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool { public static func enableAllInputMode(for inputSourceBundleD: String) -> Bool {
var enabled = false var enabled = false
for source in allInstalledInputSources() { for source in allInstalledInputSources() {
guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID),
let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { let _ = TISGetInputSourceProperty(source, kTISPropertyInputModeID)
continue else {
} continue
let bundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr).takeUnretainedValue() }
if String(bundleID) == inputSourceBundleD { let bundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr).takeUnretainedValue()
let modeEnabled = self.enable(inputSource: source) if String(bundleID) == inputSourceBundleD {
if !modeEnabled { let modeEnabled = self.enable(inputSource: source)
return false if !modeEnabled {
} return false
enabled = true }
} enabled = true
} }
}
return enabled return enabled
} }
@objc(enableInputMode:forInputSourceBundleID:) @objc(enableInputMode:forInputSourceBundleID:)
public static func enable(inputMode modeID: String, for bundleID: String) -> Bool { public static func enable(inputMode modeID: String, for bundleID: String) -> Bool {
for source in allInstalledInputSources() { for source in allInstalledInputSources() {
guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID), guard let bundleIDPtr = TISGetInputSourceProperty(source, kTISPropertyBundleID),
let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { let modePtr = TISGetInputSourceProperty(source, kTISPropertyInputModeID)
continue else {
} continue
let inputsSourceBundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr).takeUnretainedValue() }
let inputsSourceModeID = Unmanaged<CFString>.fromOpaque(modePtr).takeUnretainedValue() let inputsSourceBundleID = Unmanaged<CFString>.fromOpaque(bundleIDPtr)
if modeID == String(inputsSourceModeID) && bundleID == String(inputsSourceBundleID) { .takeUnretainedValue()
let enabled = enable(inputSource: source) let inputsSourceModeID = Unmanaged<CFString>.fromOpaque(modePtr).takeUnretainedValue()
print("Attempt to enable input source of mode: \(modeID), bundle ID: \(bundleID), result: \(enabled)") if modeID == String(inputsSourceModeID) && bundleID == String(inputsSourceBundleID) {
return enabled 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)") print("Failed to find any matching input source of mode: \(modeID), bundle ID: \(bundleID)")
return false return false
} }
@objc(disableInputSource:) @objc(disableInputSource:)
public static func disable(inputSource: TISInputSource) -> Bool { public static func disable(inputSource: TISInputSource) -> Bool {
let status = TISDisableInputSource(inputSource) let status = TISDisableInputSource(inputSource)
return status == noErr return status == noErr
} }
@objc(registerInputSource:) @objc(registerInputSource:)
public static func registerTnputSource(at url: URL) -> Bool { public static func registerTnputSource(at url: URL) -> Bool {
let status = TISRegisterInputSource(url as CFURL) let status = TISRegisterInputSource(url as CFURL)
return status == noErr return status == noErr
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,27 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@ -72,464 +79,492 @@ private let kDefaultKeys = "123456789"
@propertyWrapper @propertyWrapper
struct UserDefault<Value> { struct UserDefault<Value> {
let key: String let key: String
let defaultValue: Value let defaultValue: Value
var container: UserDefaults = .standard var container: UserDefaults = .standard
var wrappedValue: Value { var wrappedValue: Value {
get { get {
container.object(forKey: key) as? Value ?? defaultValue container.object(forKey: key) as? Value ?? defaultValue
} }
set { set {
container.set(newValue, forKey: key) container.set(newValue, forKey: key)
} }
} }
} }
@propertyWrapper @propertyWrapper
struct CandidateListTextSize { struct CandidateListTextSize {
let key: String let key: String
let defaultValue: CGFloat = kDefaultCandidateListTextSize let defaultValue: CGFloat = kDefaultCandidateListTextSize
lazy var container: UserDefault = { lazy var container: UserDefault = {
UserDefault(key: key, defaultValue: defaultValue) UserDefault(key: key, defaultValue: defaultValue)
}() }()
var wrappedValue: CGFloat { var wrappedValue: CGFloat {
mutating get { mutating get {
var value = container.wrappedValue var value = container.wrappedValue
if value < kMinCandidateListTextSize { if value < kMinCandidateListTextSize {
value = kMinCandidateListTextSize value = kMinCandidateListTextSize
} else if value > kMaxCandidateListTextSize { } else if value > kMaxCandidateListTextSize {
value = kMaxCandidateListTextSize value = kMaxCandidateListTextSize
} }
return value return value
} }
set { set {
var value = newValue var value = newValue
if value < kMinCandidateListTextSize { if value < kMinCandidateListTextSize {
value = kMinCandidateListTextSize value = kMinCandidateListTextSize
} else if value > kMaxCandidateListTextSize { } else if value > kMaxCandidateListTextSize {
value = kMaxCandidateListTextSize value = kMaxCandidateListTextSize
} }
container.wrappedValue = value container.wrappedValue = value
} }
} }
} }
@propertyWrapper @propertyWrapper
struct ComposingBufferSize { struct ComposingBufferSize {
let key: String let key: String
let defaultValue: Int = kDefaultComposingBufferSize let defaultValue: Int = kDefaultComposingBufferSize
lazy var container: UserDefault = { lazy var container: UserDefault = {
UserDefault(key: key, defaultValue: defaultValue) UserDefault(key: key, defaultValue: defaultValue)
}() }()
var wrappedValue: Int { var wrappedValue: Int {
mutating get { mutating get {
let currentValue = container.wrappedValue let currentValue = container.wrappedValue
if currentValue < kMinComposingBufferSize { if currentValue < kMinComposingBufferSize {
return kMinComposingBufferSize return kMinComposingBufferSize
} else if currentValue > kMaxComposingBufferSize { } else if currentValue > kMaxComposingBufferSize {
return kMaxComposingBufferSize return kMaxComposingBufferSize
} }
return currentValue return currentValue
} }
set { set {
var value = newValue var value = newValue
if value < kMinComposingBufferSize { if value < kMinComposingBufferSize {
value = kMinComposingBufferSize value = kMinComposingBufferSize
} else if value > kMaxComposingBufferSize { } else if value > kMaxComposingBufferSize {
value = kMaxComposingBufferSize value = kMaxComposingBufferSize
} }
container.wrappedValue = value container.wrappedValue = value
} }
} }
} }
// MARK: - // MARK: -
@objc enum KeyboardLayout: Int { @objc enum KeyboardLayout: Int {
case standard = 0 case standard = 0
case eten = 1 case eten = 1
case hsu = 2 case hsu = 2
case eten26 = 3 case eten26 = 3
case IBM = 4 case ibm = 4
case MiTAC = 5 case mitac = 5
case FakeSeigyou = 6 case fakeSeigyou = 6
case hanyuPinyin = 10 case hanyuPinyin = 10
var name: String { var name: String {
switch (self) { switch self {
case .standard: case .standard:
return "Standard" return "Standard"
case .eten: case .eten:
return "ETen" return "ETen"
case .hsu: case .hsu:
return "Hsu" return "Hsu"
case .eten26: case .eten26:
return "ETen26" return "ETen26"
case .IBM: case .ibm:
return "IBM" return "IBM"
case .MiTAC: case .mitac:
return "MiTAC" return "MiTAC"
case .FakeSeigyou: case .fakeSeigyou:
return "FakeSeigyou" return "FakeSeigyou"
case .hanyuPinyin: case .hanyuPinyin:
return "HanyuPinyin" return "HanyuPinyin"
} }
} }
} }
// MARK: - // MARK: -
@objc public class mgrPrefs: NSObject { @objc public class mgrPrefs: NSObject {
static var allKeys:[String] { static var allKeys: [String] {
[kIsDebugModeEnabled, [
kUserDataFolderSpecified, kIsDebugModeEnabled,
kKeyboardLayoutPreference, kUserDataFolderSpecified,
kBasisKeyboardLayoutPreference, kKeyboardLayoutPreference,
kShowPageButtonsInCandidateWindow, kBasisKeyboardLayoutPreference,
kCandidateListTextSize, kShowPageButtonsInCandidateWindow,
kAppleLanguagesPreferences, kCandidateListTextSize,
kShouldAutoReloadUserDataFiles, kAppleLanguagesPreferences,
kSelectPhraseAfterCursorAsCandidatePreference, kShouldAutoReloadUserDataFiles,
kUseHorizontalCandidateListPreference, kSelectPhraseAfterCursorAsCandidatePreference,
kComposingBufferSizePreference, kUseHorizontalCandidateListPreference,
kChooseCandidateUsingSpace, kComposingBufferSizePreference,
kCNS11643Enabled, kChooseCandidateUsingSpace,
kSymbolInputEnabled, kCNS11643Enabled,
kChineseConversionEnabled, kSymbolInputEnabled,
kShiftJISShinjitaiOutputEnabled, kChineseConversionEnabled,
kHalfWidthPunctuationEnabled, kShiftJISShinjitaiOutputEnabled,
kSpecifyTabKeyBehavior, kHalfWidthPunctuationEnabled,
kSpecifySpaceKeyBehavior, kSpecifyTabKeyBehavior,
kEscToCleanInputBuffer, kSpecifySpaceKeyBehavior,
kCandidateTextFontName, kEscToCleanInputBuffer,
kCandidateKeyLabelFontName, kCandidateTextFontName,
kCandidateKeys, kCandidateKeyLabelFontName,
kMoveCursorAfterSelectingCandidate, kCandidateKeys,
kPhraseReplacementEnabled, kMoveCursorAfterSelectingCandidate,
kUseSCPCTypingMode, kPhraseReplacementEnabled,
kMaxCandidateLength, kUseSCPCTypingMode,
kShouldNotFartInLieuOfBeep, kMaxCandidateLength,
kAssociatedPhrasesEnabled] kShouldNotFartInLieuOfBeep,
} kAssociatedPhrasesEnabled,
]
@objc public static func setMissingDefaults () { }
// Preferences Module plist private
@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: 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: 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: 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: kSymbolInputEnabled) == nil {
if UserDefaults.standard.object(forKey: kCandidateListTextSize) == nil { UserDefaults.standard.set(mgrPrefs.symbolInputEnabled, forKey: kSymbolInputEnabled)
UserDefaults.standard.set(mgrPrefs.candidateListTextSize, forKey: kCandidateListTextSize) }
}
// 18
// true if UserDefaults.standard.object(forKey: kCandidateListTextSize) == nil {
if UserDefaults.standard.object(forKey: kChooseCandidateUsingSpace) == nil { UserDefaults.standard.set(
UserDefaults.standard.set(mgrPrefs.chooseCandidateUsingSpace, forKey: kChooseCandidateUsingSpace) mgrPrefs.candidateListTextSize, forKey: kCandidateListTextSize)
} }
// 使 // true
if UserDefaults.standard.object(forKey: kShouldAutoReloadUserDataFiles) == nil { if UserDefaults.standard.object(forKey: kChooseCandidateUsingSpace) == nil {
UserDefaults.standard.set(mgrPrefs.shouldAutoReloadUserDataFiles, forKey: kShouldAutoReloadUserDataFiles) UserDefaults.standard.set(
} mgrPrefs.chooseCandidateUsingSpace, forKey: kChooseCandidateUsingSpace)
}
// Tab
if UserDefaults.standard.object(forKey: kSpecifyTabKeyBehavior) == nil { // 使
UserDefaults.standard.set(mgrPrefs.specifyTabKeyBehavior, forKey: kSpecifyTabKeyBehavior) if UserDefaults.standard.object(forKey: kShouldAutoReloadUserDataFiles) == nil {
} UserDefaults.standard.set(
mgrPrefs.shouldAutoReloadUserDataFiles, forKey: kShouldAutoReloadUserDataFiles)
// Space }
if UserDefaults.standard.object(forKey: kSpecifySpaceKeyBehavior) == nil {
UserDefaults.standard.set(mgrPrefs.specifySpaceKeyBehavior, forKey: kSpecifySpaceKeyBehavior) // Tab
} if UserDefaults.standard.object(forKey: kSpecifyTabKeyBehavior) == nil {
UserDefaults.standard.set(
// false mgrPrefs.specifyTabKeyBehavior, forKey: kSpecifyTabKeyBehavior)
if UserDefaults.standard.object(forKey: kUseSCPCTypingMode) == nil { }
UserDefaults.standard.set(mgrPrefs.useSCPCTypingMode, forKey: kUseSCPCTypingMode)
} // Space
if UserDefaults.standard.object(forKey: kSpecifySpaceKeyBehavior) == nil {
// false UserDefaults.standard.set(
if UserDefaults.standard.object(forKey: kAssociatedPhrasesEnabled) == nil { mgrPrefs.specifySpaceKeyBehavior, forKey: kSpecifySpaceKeyBehavior)
UserDefaults.standard.set(mgrPrefs.associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled) }
}
// false
// 0 if UserDefaults.standard.object(forKey: kUseSCPCTypingMode) == nil {
if UserDefaults.standard.object(forKey: kSelectPhraseAfterCursorAsCandidatePreference) == nil { UserDefaults.standard.set(mgrPrefs.useSCPCTypingMode, forKey: kUseSCPCTypingMode)
UserDefaults.standard.set(mgrPrefs.selectPhraseAfterCursorAsCandidate, forKey: kSelectPhraseAfterCursorAsCandidatePreference) }
}
// false
// if UserDefaults.standard.object(forKey: kAssociatedPhrasesEnabled) == nil {
if UserDefaults.standard.object(forKey: kMoveCursorAfterSelectingCandidate) == nil { UserDefaults.standard.set(
UserDefaults.standard.set(mgrPrefs.moveCursorAfterSelectingCandidate, forKey: kMoveCursorAfterSelectingCandidate) mgrPrefs.associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled)
} }
// // 0
if UserDefaults.standard.object(forKey: kUseHorizontalCandidateListPreference) == nil { if UserDefaults.standard.object(forKey: kSelectPhraseAfterCursorAsCandidatePreference)
UserDefaults.standard.set(mgrPrefs.useHorizontalCandidateList, forKey: kUseHorizontalCandidateListPreference) == nil
} {
UserDefaults.standard.set(
// mgrPrefs.selectPhraseAfterCursorAsCandidate,
if UserDefaults.standard.object(forKey: kCNS11643Enabled) == nil { forKey: kSelectPhraseAfterCursorAsCandidatePreference)
UserDefaults.standard.set(mgrPrefs.cns11643Enabled, forKey: kCNS11643Enabled) }
}
//
// if UserDefaults.standard.object(forKey: kMoveCursorAfterSelectingCandidate) == nil {
if UserDefaults.standard.object(forKey: kChineseConversionEnabled) == nil { UserDefaults.standard.set(
UserDefaults.standard.set(mgrPrefs.chineseConversionEnabled, forKey: kChineseConversionEnabled) mgrPrefs.moveCursorAfterSelectingCandidate,
} forKey: kMoveCursorAfterSelectingCandidate)
}
// JIS
if UserDefaults.standard.object(forKey: kShiftJISShinjitaiOutputEnabled) == nil { //
UserDefaults.standard.set(mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) if UserDefaults.standard.object(forKey: kUseHorizontalCandidateListPreference) == nil {
} UserDefaults.standard.set(
mgrPrefs.useHorizontalCandidateList, forKey: kUseHorizontalCandidateListPreference)
// }
if UserDefaults.standard.object(forKey: kPhraseReplacementEnabled) == nil {
UserDefaults.standard.set(mgrPrefs.phraseReplacementEnabled, forKey: kPhraseReplacementEnabled) //
} if UserDefaults.standard.object(forKey: kCNS11643Enabled) == nil {
UserDefaults.standard.set(mgrPrefs.cns11643Enabled, forKey: kCNS11643Enabled)
// }
if UserDefaults.standard.object(forKey: kShouldNotFartInLieuOfBeep) == nil {
UserDefaults.standard.set(mgrPrefs.shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep) //
} if UserDefaults.standard.object(forKey: kChineseConversionEnabled) == nil {
UserDefaults.standard.set(
UserDefaults.standard.synchronize() mgrPrefs.chineseConversionEnabled, forKey: kChineseConversionEnabled)
} }
@UserDefault(key: kIsDebugModeEnabled, defaultValue: false) // JIS
@objc static var isDebugModeEnabled: Bool if UserDefaults.standard.object(forKey: kShiftJISShinjitaiOutputEnabled) == nil {
UserDefaults.standard.set(
@UserDefault(key: kUserDataFolderSpecified, defaultValue: "") mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
@objc static var userDataFolderSpecified: String }
@objc static func ifSpecifiedUserDataPathExistsInPlist() -> Bool { //
UserDefaults.standard.object(forKey: kUserDataFolderSpecified) != nil if UserDefaults.standard.object(forKey: kPhraseReplacementEnabled) == nil {
} UserDefaults.standard.set(
mgrPrefs.phraseReplacementEnabled, forKey: kPhraseReplacementEnabled)
@UserDefault(key: kAppleLanguagesPreferences, defaultValue: []) }
@objc static var appleLanguages: Array<String>
//
@UserDefault(key: kKeyboardLayoutPreference, defaultValue: 0) if UserDefaults.standard.object(forKey: kShouldNotFartInLieuOfBeep) == nil {
@objc static var keyboardLayout: Int UserDefaults.standard.set(
mgrPrefs.shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep)
@objc static var keyboardLayoutName: String { }
(KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name
} UserDefaults.standard.synchronize()
}
@UserDefault(key: kBasisKeyboardLayoutPreference, defaultValue: "com.apple.keylayout.ZhuyinBopomofo")
@objc static var basisKeyboardLayout: String @UserDefault(key: kIsDebugModeEnabled, defaultValue: false)
@objc static var isDebugModeEnabled: Bool
@UserDefault(key: kShowPageButtonsInCandidateWindow, defaultValue: true)
@objc static var showPageButtonsInCandidateWindow: Bool @UserDefault(key: kUserDataFolderSpecified, defaultValue: "")
@objc static var userDataFolderSpecified: String
@CandidateListTextSize(key: kCandidateListTextSize)
@objc static var candidateListTextSize: CGFloat @objc static func ifSpecifiedUserDataPathExistsInPlist() -> Bool {
UserDefaults.standard.object(forKey: kUserDataFolderSpecified) != nil
@UserDefault(key: kShouldAutoReloadUserDataFiles, defaultValue: true) }
@objc static var shouldAutoReloadUserDataFiles: Bool
@UserDefault(key: kAppleLanguagesPreferences, defaultValue: [])
@UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreference, defaultValue: false) @objc static var appleLanguages: [String]
@objc static var selectPhraseAfterCursorAsCandidate: Bool
@UserDefault(key: kKeyboardLayoutPreference, defaultValue: 0)
@UserDefault(key: kMoveCursorAfterSelectingCandidate, defaultValue: false) @objc static var keyboardLayout: Int
@objc static var moveCursorAfterSelectingCandidate: Bool
@objc static var keyboardLayoutName: String {
@UserDefault(key: kUseHorizontalCandidateListPreference, defaultValue: true) (KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.standard).name
@objc static var useHorizontalCandidateList: Bool }
@ComposingBufferSize(key: kComposingBufferSizePreference) @UserDefault(
@objc static var composingBufferSize: Int key: kBasisKeyboardLayoutPreference, defaultValue: "com.apple.keylayout.ZhuyinBopomofo")
@objc static var basisKeyboardLayout: String
@UserDefault(key: kChooseCandidateUsingSpace, defaultValue: true)
@objc static var chooseCandidateUsingSpace: Bool @UserDefault(key: kShowPageButtonsInCandidateWindow, defaultValue: true)
@objc static var showPageButtonsInCandidateWindow: Bool
@UserDefault(key: kUseSCPCTypingMode, defaultValue: false)
@objc static var useSCPCTypingMode: Bool @CandidateListTextSize(key: kCandidateListTextSize)
@objc static var candidateListTextSize: CGFloat
@objc static func toggleSCPCTypingModeEnabled() -> Bool {
useSCPCTypingMode = !useSCPCTypingMode @UserDefault(key: kShouldAutoReloadUserDataFiles, defaultValue: true)
UserDefaults.standard.set(useSCPCTypingMode, forKey: kUseSCPCTypingMode) @objc static var shouldAutoReloadUserDataFiles: Bool
return useSCPCTypingMode
} @UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreference, defaultValue: false)
@objc static var selectPhraseAfterCursorAsCandidate: Bool
@UserDefault(key: kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2)
@objc static var maxCandidateLength: Int @UserDefault(key: kMoveCursorAfterSelectingCandidate, defaultValue: false)
@objc static var moveCursorAfterSelectingCandidate: Bool
@UserDefault(key: kShouldNotFartInLieuOfBeep, defaultValue: true)
@objc static var shouldNotFartInLieuOfBeep: Bool @UserDefault(key: kUseHorizontalCandidateListPreference, defaultValue: true)
@objc static var useHorizontalCandidateList: Bool
@objc static func toggleShouldNotFartInLieuOfBeep() -> Bool {
shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep @ComposingBufferSize(key: kComposingBufferSizePreference)
UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep) @objc static var composingBufferSize: Int
return shouldNotFartInLieuOfBeep
} @UserDefault(key: kChooseCandidateUsingSpace, defaultValue: true)
@objc static var chooseCandidateUsingSpace: Bool
@UserDefault(key: kCNS11643Enabled, defaultValue: false)
@objc static var cns11643Enabled: Bool @UserDefault(key: kUseSCPCTypingMode, defaultValue: false)
@objc static var useSCPCTypingMode: Bool
@objc static func toggleCNS11643Enabled() -> Bool {
cns11643Enabled = !cns11643Enabled @objc static func toggleSCPCTypingModeEnabled() -> Bool {
mgrLangModel.setCNSEnabled(cns11643Enabled) // useSCPCTypingMode = !useSCPCTypingMode
UserDefaults.standard.set(cns11643Enabled, forKey: kCNS11643Enabled) UserDefaults.standard.set(useSCPCTypingMode, forKey: kUseSCPCTypingMode)
return cns11643Enabled return useSCPCTypingMode
} }
@UserDefault(key: kSymbolInputEnabled, defaultValue: true) @UserDefault(key: kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2)
@objc static var symbolInputEnabled: Bool @objc static var maxCandidateLength: Int
@objc static func toggleSymbolInputEnabled() -> Bool { @UserDefault(key: kShouldNotFartInLieuOfBeep, defaultValue: true)
symbolInputEnabled = !symbolInputEnabled @objc static var shouldNotFartInLieuOfBeep: Bool
mgrLangModel.setSymbolEnabled(symbolInputEnabled) //
UserDefaults.standard.set(symbolInputEnabled, forKey: kSymbolInputEnabled) @objc static func toggleShouldNotFartInLieuOfBeep() -> Bool {
return symbolInputEnabled shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep
} UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep)
return shouldNotFartInLieuOfBeep
@UserDefault(key: kChineseConversionEnabled, defaultValue: false) }
@objc static var chineseConversionEnabled: Bool
@UserDefault(key: kCNS11643Enabled, defaultValue: false)
@objc @discardableResult static func toggleChineseConversionEnabled() -> Bool { @objc static var cns11643Enabled: Bool
chineseConversionEnabled = !chineseConversionEnabled
// JIS @objc static func toggleCNS11643Enabled() -> Bool {
if chineseConversionEnabled && shiftJISShinjitaiOutputEnabled { cns11643Enabled = !cns11643Enabled
self.toggleShiftJISShinjitaiOutputEnabled() mgrLangModel.setCNSEnabled(cns11643Enabled) //
UserDefaults.standard.set(shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled) UserDefaults.standard.set(cns11643Enabled, forKey: kCNS11643Enabled)
} return cns11643Enabled
UserDefaults.standard.set(chineseConversionEnabled, forKey: kChineseConversionEnabled) }
return chineseConversionEnabled
} @UserDefault(key: kSymbolInputEnabled, defaultValue: true)
@objc static var symbolInputEnabled: Bool
@UserDefault(key: kShiftJISShinjitaiOutputEnabled, defaultValue: false)
@objc static var shiftJISShinjitaiOutputEnabled: Bool @objc static func toggleSymbolInputEnabled() -> Bool {
symbolInputEnabled = !symbolInputEnabled
@objc @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool { mgrLangModel.setSymbolEnabled(symbolInputEnabled) //
shiftJISShinjitaiOutputEnabled = !shiftJISShinjitaiOutputEnabled UserDefaults.standard.set(symbolInputEnabled, forKey: kSymbolInputEnabled)
// JIS return symbolInputEnabled
if shiftJISShinjitaiOutputEnabled && chineseConversionEnabled {self.toggleChineseConversionEnabled()} }
UserDefaults.standard.set(shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
return shiftJISShinjitaiOutputEnabled @UserDefault(key: kChineseConversionEnabled, defaultValue: false)
} @objc static var chineseConversionEnabled: Bool
@UserDefault(key: kHalfWidthPunctuationEnabled, defaultValue: false) @objc @discardableResult static func toggleChineseConversionEnabled() -> Bool {
@objc static var halfWidthPunctuationEnabled: Bool chineseConversionEnabled = !chineseConversionEnabled
// JIS
@objc static func toggleHalfWidthPunctuationEnabled() -> Bool { if chineseConversionEnabled && shiftJISShinjitaiOutputEnabled {
halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled self.toggleShiftJISShinjitaiOutputEnabled()
return halfWidthPunctuationEnabled UserDefaults.standard.set(
} shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
}
@UserDefault(key: kEscToCleanInputBuffer, defaultValue: true) UserDefaults.standard.set(chineseConversionEnabled, forKey: kChineseConversionEnabled)
@objc static var escToCleanInputBuffer: Bool return chineseConversionEnabled
}
@UserDefault(key: kSpecifyTabKeyBehavior, defaultValue: false) @UserDefault(key: kShiftJISShinjitaiOutputEnabled, defaultValue: false)
@objc static var specifyTabKeyBehavior: Bool @objc static var shiftJISShinjitaiOutputEnabled: Bool
@UserDefault(key: kSpecifySpaceKeyBehavior, defaultValue: false) @objc @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool {
@objc static var specifySpaceKeyBehavior: Bool shiftJISShinjitaiOutputEnabled = !shiftJISShinjitaiOutputEnabled
// JIS
// MARK: - Optional settings if shiftJISShinjitaiOutputEnabled && chineseConversionEnabled {
@UserDefault(key: kCandidateTextFontName, defaultValue: nil) self.toggleChineseConversionEnabled()
@objc static var candidateTextFontName: String? }
UserDefaults.standard.set(
@UserDefault(key: kCandidateKeyLabelFontName, defaultValue: nil) shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
@objc static var candidateKeyLabelFontName: String? return shiftJISShinjitaiOutputEnabled
}
@UserDefault(key: kCandidateKeys, defaultValue: kDefaultKeys)
@objc static var candidateKeys: String @UserDefault(key: kHalfWidthPunctuationEnabled, defaultValue: false)
@objc static var halfWidthPunctuationEnabled: Bool
@objc static var defaultCandidateKeys: String {
kDefaultKeys @objc static func toggleHalfWidthPunctuationEnabled() -> Bool {
} halfWidthPunctuationEnabled = !halfWidthPunctuationEnabled
@objc static var suggestedCandidateKeys: [String] { return halfWidthPunctuationEnabled
[kDefaultKeys, "234567890", "QWERTYUIO", "QWERTASDF", "ASDFGHJKL", "ASDFZXCVB"] }
}
@UserDefault(key: kEscToCleanInputBuffer, defaultValue: true)
@objc static func validate(candidateKeys: String) throws { @objc static var escToCleanInputBuffer: Bool
let trimmed = candidateKeys.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { @UserDefault(key: kSpecifyTabKeyBehavior, defaultValue: false)
throw CandidateKeyError.empty @objc static var specifyTabKeyBehavior: Bool
}
if !trimmed.canBeConverted(to: .ascii) { @UserDefault(key: kSpecifySpaceKeyBehavior, defaultValue: false)
throw CandidateKeyError.invalidCharacters @objc static var specifySpaceKeyBehavior: Bool
}
if trimmed.contains(" ") { // MARK: - Optional settings
throw CandidateKeyError.containSpace @UserDefault(key: kCandidateTextFontName, defaultValue: nil)
} @objc static var candidateTextFontName: String?
if trimmed.count < 4 {
throw CandidateKeyError.tooShort @UserDefault(key: kCandidateKeyLabelFontName, defaultValue: nil)
} @objc static var candidateKeyLabelFontName: String?
if trimmed.count > 15 {
throw CandidateKeyError.tooLong @UserDefault(key: kCandidateKeys, defaultValue: kDefaultKeys)
} @objc static var candidateKeys: String
let set = Set(Array(trimmed))
if set.count != trimmed.count { @objc static var defaultCandidateKeys: String {
throw CandidateKeyError.duplicatedCharacters kDefaultKeys
} }
} @objc static var suggestedCandidateKeys: [String] {
[kDefaultKeys, "234567890", "QWERTYUIO", "QWERTASDF", "ASDFGHJKL", "ASDFZXCVB"]
enum CandidateKeyError: Error, LocalizedError { }
case empty
case invalidCharacters @objc static func validate(candidateKeys: String) throws {
case containSpace let trimmed = candidateKeys.trimmingCharacters(in: .whitespacesAndNewlines)
case duplicatedCharacters if trimmed.isEmpty {
case tooShort throw CandidateKeyError.empty
case tooLong }
if !trimmed.canBeConverted(to: .ascii) {
var errorDescription: String? { throw CandidateKeyError.invalidCharacters
switch self { }
case .empty: if trimmed.contains(" ") {
return NSLocalizedString("Candidates keys cannot be empty.", comment: "") throw CandidateKeyError.containSpace
case .invalidCharacters: }
return NSLocalizedString("Candidate keys can only contain ASCII characters like alphanumericals.", comment: "") if trimmed.count < 4 {
case .containSpace: throw CandidateKeyError.tooShort
return NSLocalizedString("Candidate keys cannot contain space.", comment: "") }
case .duplicatedCharacters: if trimmed.count > 15 {
return NSLocalizedString("There should not be duplicated keys.", comment: "") throw CandidateKeyError.tooLong
case .tooShort: }
return NSLocalizedString("Please specify at least 4 candidate keys.", comment: "") let set = Set(Array(trimmed))
case .tooLong: if set.count != trimmed.count {
return NSLocalizedString("Maximum 15 candidate keys allowed.", comment: "") throw CandidateKeyError.duplicatedCharacters
} }
} }
} enum CandidateKeyError: Error, LocalizedError {
case empty
@UserDefault(key: kPhraseReplacementEnabled, defaultValue: false) case invalidCharacters
@objc static var phraseReplacementEnabled: Bool case containSpace
case duplicatedCharacters
@objc static func togglePhraseReplacementEnabled() -> Bool { case tooShort
phraseReplacementEnabled = !phraseReplacementEnabled case tooLong
mgrLangModel.setPhraseReplacementEnabled(phraseReplacementEnabled)
UserDefaults.standard.set(phraseReplacementEnabled, forKey: kPhraseReplacementEnabled) var errorDescription: String? {
return phraseReplacementEnabled switch self {
} case .empty:
return NSLocalizedString("Candidates keys cannot be empty.", comment: "")
@UserDefault(key: kAssociatedPhrasesEnabled, defaultValue: false) case .invalidCharacters:
@objc static var associatedPhrasesEnabled: Bool return NSLocalizedString(
"Candidate keys can only contain ASCII characters like alphanumericals.",
@objc static func toggleAssociatedPhrasesEnabled() -> Bool { comment: "")
associatedPhrasesEnabled = !associatedPhrasesEnabled case .containSpace:
UserDefaults.standard.set(associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled) return NSLocalizedString("Candidate keys cannot contain space.", comment: "")
return associatedPhrasesEnabled 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
}
} }

View File

@ -235,7 +235,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
[currentMarkedPhrase appendString:userPhrase]; [currentMarkedPhrase appendString:userPhrase];
if (areWeDuplicating && !areWeDeleting) { if (areWeDuplicating && !areWeDeleting) {
// Do not use ASCII characters to comment here. // 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:@"\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"];
} }
[currentMarkedPhrase appendString:@"\n"]; [currentMarkedPhrase appendString:@"\n"];

View File

@ -1,59 +1,65 @@
// Copyright (c) 2022 and onwards Isaac Xen (MIT License). // 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). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc public class clsSFX: NSObject, NSSoundDelegate { @objc public class clsSFX: NSObject, NSSoundDelegate {
private static let shared = clsSFX() private static let shared = clsSFX()
private override init(){ private override init() {
super.init() super.init()
} }
private var currentBeep: NSSound? private var currentBeep: NSSound?
private func beep() { private func beep() {
// Stop existing beep // Stop existing beep
if let beep = currentBeep { if let beep = currentBeep {
if beep.isPlaying { if beep.isPlaying {
beep.stop() beep.stop()
} }
} }
// Create a new beep sound if possible // Create a new beep sound if possible
var sndBeep:String var sndBeep: String
if mgrPrefs.shouldNotFartInLieuOfBeep == false { if mgrPrefs.shouldNotFartInLieuOfBeep == false {
sndBeep = "Fart" sndBeep = "Fart"
} else { } else {
sndBeep = "Beep" sndBeep = "Beep"
} }
guard guard
let beep = NSSound(named:sndBeep) let beep = NSSound(named: sndBeep)
else { else {
NSSound.beep() NSSound.beep()
return return
} }
beep.delegate = self beep.delegate = self
beep.volume = 0.4 beep.volume = 0.4
beep.play() beep.play()
currentBeep = beep currentBeep = beep
} }
@objc public func sound(_ sound: NSSound, didFinishPlaying flag: Bool) { @objc public func sound(_ sound: NSSound, didFinishPlaying flag: Bool) {
currentBeep = nil currentBeep = nil
} }
@objc static func beep() { @objc static func beep() {
shared.beep() shared.beep()
} }
} }

View File

@ -1,20 +1,27 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@ -23,30 +30,32 @@ import InputMethodKit
let kConnectionName = "vChewing_1_Connection" let kConnectionName = "vChewing_1_Connection"
if CommandLine.arguments.count > 1 { if CommandLine.arguments.count > 1 {
if CommandLine.arguments[1] == "install" { if CommandLine.arguments[1] == "install" {
let exitCode = IME.registerInputMethod() let exitCode = IME.registerInputMethod()
exit(exitCode) exit(exitCode)
} }
if CommandLine.arguments[1] == "uninstall" { if CommandLine.arguments[1] == "uninstall" {
let exitCode = IME.uninstall(isSudo: IME.isSudoMode) let exitCode = IME.uninstall(isSudo: IME.isSudoMode)
exit(exitCode) exit(exitCode)
} }
} }
guard let mainNibName = Bundle.main.infoDictionary?["NSMainNibFile"] as? String else { guard let mainNibName = Bundle.main.infoDictionary?["NSMainNibFile"] as? String else {
NSLog("Fatal error: NSMainNibFile key not defined in Info.plist."); NSLog("Fatal error: NSMainNibFile key not defined in Info.plist.")
exit(-1) exit(-1)
} }
let loaded = Bundle.main.loadNibNamed(mainNibName, owner: NSApp, topLevelObjects: nil) let loaded = Bundle.main.loadNibNamed(mainNibName, owner: NSApp, topLevelObjects: nil)
if !loaded { if !loaded {
NSLog("Fatal error: Cannot load \(mainNibName).") NSLog("Fatal error: Cannot load \(mainNibName).")
exit(-1) exit(-1)
} }
guard let bundleID = Bundle.main.bundleIdentifier, let server = IMKServer(name: kConnectionName, bundleIdentifier: bundleID) else { guard let bundleID = Bundle.main.bundleIdentifier,
NSLog("Fatal error: Cannot initialize input method server with connection \(kConnectionName).") let server = IMKServer(name: kConnectionName, bundleIdentifier: bundleID)
exit(-1) else {
NSLog("Fatal error: Cannot initialize input method server with connection \(kConnectionName).")
exit(-1)
} }
NSApp.run() NSApp.run()

View File

@ -1,165 +1,176 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc(VTCandidateKeyLabel) @objc(VTCandidateKeyLabel)
public class CandidateKeyLabel: NSObject { public class CandidateKeyLabel: NSObject {
@objc public private(set) var key: String @objc public private(set) var key: String
@objc public private(set) var displayedText: String @objc public private(set) var displayedText: String
public init(key: String, displayedText: String) { public init(key: String, displayedText: String) {
self.key = key self.key = key
self.displayedText = displayedText self.displayedText = displayedText
super.init() super.init()
} }
} }
@objc(VTCandidateControllerDelegate) @objc(VTCandidateControllerDelegate)
public protocol CandidateControllerDelegate: AnyObject { public protocol CandidateControllerDelegate: AnyObject {
func candidateCountForController(_ controller: CandidateController) -> UInt func candidateCountForController(_ controller: CandidateController) -> UInt
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt)
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) -> String
func candidateController(
_ controller: CandidateController, didSelectCandidateAtIndex index: UInt)
} }
@objc(VTCandidateController) @objc(VTCandidateController)
public class CandidateController: NSWindowController { public class CandidateController: NSWindowController {
@objc public weak var delegate: CandidateControllerDelegate? { @objc public weak var delegate: CandidateControllerDelegate? {
didSet { didSet {
reloadData() reloadData()
} }
} }
@objc public var selectedCandidateIndex: UInt = UInt.max @objc public var selectedCandidateIndex: UInt = UInt.max
@objc public var visible: Bool = false { @objc public var visible: Bool = false {
didSet { didSet {
NSObject.cancelPreviousPerformRequests(withTarget: self) NSObject.cancelPreviousPerformRequests(withTarget: self)
if visible { if visible {
window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0) window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0)
} else { } else {
window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0) window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0)
} }
} }
} }
@objc public var windowTopLeftPoint: NSPoint { @objc public var windowTopLeftPoint: NSPoint {
get { get {
guard let frameRect = window?.frame else { guard let frameRect = window?.frame else {
return NSPoint.zero return NSPoint.zero
} }
return NSPoint(x: frameRect.minX, y: frameRect.maxY) return NSPoint(x: frameRect.minX, y: frameRect.maxY)
} }
set { set {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0) self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0)
} }
} }
} }
@objc public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map { @objc public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
CandidateKeyLabel(key: $0, displayedText: $0) .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 keyLabelFont: NSFont = NSFont.monospacedDigitSystemFont(
@objc public var tooltip: String = "" 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 { @objc public func showNextPage() -> Bool {
false false
} }
@objc public func showPreviousPage() -> Bool { @objc public func showPreviousPage() -> Bool {
false false
} }
@objc public func highlightNextCandidate() -> Bool { @objc public func highlightNextCandidate() -> Bool {
false false
} }
@objc public func highlightPreviousCandidate() -> Bool { @objc public func highlightPreviousCandidate() -> Bool {
false false
} }
@objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { @objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
UInt.max UInt.max
} }
/// Sets the location of the candidate window. /// Sets the location of the candidate window.
/// ///
/// Please note that the method has side effects that modifies /// Please note that the method has side effects that modifies
/// `windowTopLeftPoint` to make the candidate window to stay in at least /// `windowTopLeftPoint` to make the candidate window to stay in at least
/// in a screen. /// in a screen.
/// ///
/// - Parameters: /// - Parameters:
/// - windowTopLeftPoint: The given location. /// - windowTopLeftPoint: The given location.
/// - height: The height that helps the window not to be out of the bottom /// - height: The height that helps the window not to be out of the bottom
/// of a screen. /// of a screen.
@objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:) @objc(setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:)
public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height) self.doSet(
} windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height)
} }
}
func doSet(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) { func doSet(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
var adjustedPoint = windowTopLeftPoint var adjustedPoint = windowTopLeftPoint
var adjustedHeight = height var adjustedHeight = height
var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero
for screen in NSScreen.screens { for screen in NSScreen.screens {
let frame = screen.visibleFrame let frame = screen.visibleFrame
if windowTopLeftPoint.x >= frame.minX && if windowTopLeftPoint.x >= frame.minX && windowTopLeftPoint.x <= frame.maxX
windowTopLeftPoint.x <= frame.maxX && && windowTopLeftPoint.y >= frame.minY && windowTopLeftPoint.y <= frame.maxY
windowTopLeftPoint.y >= frame.minY && {
windowTopLeftPoint.y <= frame.maxY { screenFrame = frame
screenFrame = frame break
break }
} }
}
if adjustedHeight > screenFrame.size.height / 2.0 { if adjustedHeight > screenFrame.size.height / 2.0 {
adjustedHeight = 0.0 adjustedHeight = 0.0
} }
let windowSize = window?.frame.size ?? NSSize.zero let windowSize = window?.frame.size ?? NSSize.zero
// bottom beneath the screen? // bottom beneath the screen?
if adjustedPoint.y - windowSize.height < screenFrame.minY { if adjustedPoint.y - windowSize.height < screenFrame.minY {
adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height
} }
// top over the screen? // top over the screen?
if adjustedPoint.y >= screenFrame.maxY { if adjustedPoint.y >= screenFrame.maxY {
adjustedPoint.y = screenFrame.maxY - 1.0 adjustedPoint.y = screenFrame.maxY - 1.0
} }
// right // right
if adjustedPoint.x + windowSize.width >= screenFrame.maxX { if adjustedPoint.x + windowSize.width >= screenFrame.maxX {
adjustedPoint.x = screenFrame.maxX - windowSize.width adjustedPoint.x = screenFrame.maxX - windowSize.width
} }
// left // left
if adjustedPoint.x < screenFrame.minX { if adjustedPoint.x < screenFrame.minX {
adjustedPoint.x = screenFrame.minX adjustedPoint.x = screenFrame.minX
} }
window?.setFrameTopLeftPoint(adjustedPoint) window?.setFrameTopLeftPoint(adjustedPoint)
} }
} }

View File

@ -1,411 +1,462 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
fileprivate class HorizontalCandidateView: NSView { private class HorizontalCandidateView: NSView {
var highlightedIndex: UInt = 0 var highlightedIndex: UInt = 0
var action: Selector? var action: Selector?
weak var target: AnyObject? weak var target: AnyObject?
private var keyLabels: [String] = [] private var keyLabels: [String] = []
private var displayedCandidates: [String] = [] private var displayedCandidates: [String] = []
private var dispCandidatesWithLabels: [String] = [] private var dispCandidatesWithLabels: [String] = []
private var keyLabelHeight: CGFloat = 0 private var keyLabelHeight: CGFloat = 0
private var keyLabelWidth: CGFloat = 0 private var keyLabelWidth: CGFloat = 0
private var candidateTextHeight: CGFloat = 0 private var candidateTextHeight: CGFloat = 0
private var cellPadding: CGFloat = 0 private var cellPadding: CGFloat = 0
private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:] private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:]
private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
private var elementWidths: [CGFloat] = [] private var elementWidths: [CGFloat] = []
private var trackingHighlightedIndex: UInt = UInt.max private var trackingHighlightedIndex: UInt = UInt.max
override var isFlipped: Bool { override var isFlipped: Bool {
true true
} }
var sizeForView: NSSize { var sizeForView: NSSize {
var result = NSSize.zero var result = NSSize.zero
if !elementWidths.isEmpty { if !elementWidths.isEmpty {
result.width = elementWidths.reduce(0, +) result.width = elementWidths.reduce(0, +)
result.width += CGFloat(elementWidths.count) result.width += CGFloat(elementWidths.count)
result.height = candidateTextHeight + cellPadding result.height = candidateTextHeight + cellPadding
} }
return result return result
} }
@objc(setKeyLabels:displayedCandidates:) @objc(setKeyLabels:displayedCandidates:)
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
let count = min(labels.count, candidates.count) let count = min(labels.count, candidates.count)
keyLabels = Array(labels[0..<count]) keyLabels = Array(labels[0..<count])
displayedCandidates = Array(candidates[0..<count]) displayedCandidates = Array(candidates[0..<count])
dispCandidatesWithLabels = zip(keyLabels,displayedCandidates).map() {$0 + $1} dispCandidatesWithLabels = zip(keyLabels, displayedCandidates).map { $0 + $1 }
var newWidths = [CGFloat]() var newWidths = [CGFloat]()
let baseSize = NSSize(width: 10240.0, height: 10240.0) let baseSize = NSSize(width: 10240.0, height: 10240.0)
for index in 0..<count { for index in 0..<count {
let rctCandidate = (dispCandidatesWithLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateWithLabelAttrDict) let rctCandidate = (dispCandidatesWithLabels[index] as NSString).boundingRect(
var cellWidth = rctCandidate.size.width + cellPadding with: baseSize, options: .usesLineFragmentOrigin,
let cellHeight = rctCandidate.size.height + cellPadding attributes: candidateWithLabelAttrDict)
if cellWidth < cellHeight * 1.35 { var cellWidth = rctCandidate.size.width + cellPadding
cellWidth = cellHeight * 1.35 let cellHeight = rctCandidate.size.height + cellPadding
} if cellWidth < cellHeight * 1.35 {
newWidths.append(cellWidth) cellWidth = cellHeight * 1.35
} }
elementWidths = newWidths newWidths.append(cellWidth)
} }
elementWidths = newWidths
}
@objc(setKeyLabelFont:candidateFont:) @objc(setKeyLabelFont:candidateFont:)
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) { func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
let paraStyle = NSMutableParagraphStyle() let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default) paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .center paraStyle.alignment = .center
candidateWithLabelAttrDict = [.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.labelColor] // We still need this dummy section to make sure the space occupations of the candidates are correct.
keyLabelAttrDict = [.font: labelFont,
.paragraphStyle: paraStyle,
.verticalGlyphForm: true as AnyObject,
.foregroundColor: NSColor.secondaryLabelColor] // Candidate phrase text color
candidateAttrDict = [.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.labelColor] // Candidate index text color
let labelFontSize = labelFont.pointSize
let candidateFontSize = candidateFont.pointSize
let biggestSize = max(labelFontSize, candidateFontSize)
keyLabelWidth = ceil(labelFontSize)
keyLabelHeight = ceil(labelFontSize * 2)
candidateTextHeight = ceil(candidateFontSize * 1.20)
cellPadding = ceil(biggestSize / 2.0)
}
override func draw(_ dirtyRect: NSRect) { candidateWithLabelAttrDict = [
let bounds = self.bounds .font: candidateFont,
NSColor.controlBackgroundColor.setFill() // Candidate list panel base background .paragraphStyle: paraStyle,
NSBezierPath.fill(bounds) .foregroundColor: NSColor.labelColor,
] // We still need this dummy section to make sure that
// the space occupations of the candidates are correct.
NSColor.systemGray.withAlphaComponent(0.75).setStroke() keyLabelAttrDict = [
.font: labelFont,
.paragraphStyle: paraStyle,
.verticalGlyphForm: true as AnyObject,
.foregroundColor: NSColor.secondaryLabelColor,
] // Candidate phrase text color
candidateAttrDict = [
.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.labelColor,
] // Candidate index text color
let labelFontSize = labelFont.pointSize
let candidateFontSize = candidateFont.pointSize
let biggestSize = max(labelFontSize, candidateFontSize)
keyLabelWidth = ceil(labelFontSize)
keyLabelHeight = ceil(labelFontSize * 2)
candidateTextHeight = ceil(candidateFontSize * 1.20)
cellPadding = ceil(biggestSize / 2.0)
}
NSBezierPath.strokeLine(from: NSPoint(x: bounds.size.width, y: 0.0), to: NSPoint(x: bounds.size.width, y: bounds.size.height)) override func draw(_ dirtyRect: NSRect) {
let bounds = self.bounds
NSColor.controlBackgroundColor.setFill() // Candidate list panel base background
NSBezierPath.fill(bounds)
var accuWidth: CGFloat = 0 NSColor.systemGray.withAlphaComponent(0.75).setStroke()
for index in 0..<elementWidths.count {
let currentWidth = elementWidths[index]
let rctCandidateArea = NSRect(x: accuWidth, y: 0.0, width: currentWidth + 1.0, height: candidateTextHeight + cellPadding)
let rctLabel = NSRect(x: accuWidth + cellPadding / 2 - 1, y: cellPadding / 2 , width: keyLabelWidth, height: keyLabelHeight * 2.0)
let rctCandidatePhrase = NSRect(x: accuWidth + keyLabelWidth - 1, y: cellPadding / 2 , width: currentWidth - keyLabelWidth, height: candidateTextHeight)
var activeCandidateIndexAttr = keyLabelAttrDict NSBezierPath.strokeLine(
var activeCandidateAttr = candidateAttrDict from: NSPoint(x: bounds.size.width, y: 0.0),
if index == highlightedIndex { to: NSPoint(x: bounds.size.width, y: bounds.size.height))
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()
}
activeCandidateIndexAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.84) // The index text color of the highlightened candidate
activeCandidateAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor // The phrase text color of the highlightened candidate
} 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? { var accuWidth: CGFloat = 0
let location = convert(event.locationInWindow, to: nil) for index in 0..<elementWidths.count {
if !NSPointInRect(location, self.bounds) { let currentWidth = elementWidths[index]
return nil let rctCandidateArea = NSRect(
} x: accuWidth, y: 0.0, width: currentWidth + 1.0,
var accuWidth: CGFloat = 0.0 height: candidateTextHeight + cellPadding)
for index in 0..<elementWidths.count { let rctLabel = NSRect(
let currentWidth = elementWidths[index] x: accuWidth + cellPadding / 2 - 1, y: cellPadding / 2, width: keyLabelWidth,
height: keyLabelHeight * 2.0)
let rctCandidatePhrase = NSRect(
x: accuWidth + keyLabelWidth - 1, y: cellPadding / 2,
width: currentWidth - keyLabelWidth,
height: candidateTextHeight)
if location.x >= accuWidth && location.x <= accuWidth + currentWidth { var activeCandidateIndexAttr = keyLabelAttrDict
return UInt(index) var activeCandidateAttr = candidateAttrDict
} if index == highlightedIndex {
accuWidth += currentWidth + 1.0 let colorBlendAmount: CGFloat = IME.isDarkMode() ? 0.25 : 0
} // The background color of the highlightened candidate
return nil 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..<elementWidths.count {
let currentWidth = elementWidths[index]
override func mouseUp(with event: NSEvent) { if location.x >= accuWidth && location.x <= accuWidth + currentWidth {
trackingHighlightedIndex = highlightedIndex return UInt(index)
guard let newIndex = findHitIndex(event: event) else { }
return accuWidth += currentWidth + 1.0
} }
highlightedIndex = newIndex return nil
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 override func mouseUp(with event: NSEvent) {
self.setNeedsDisplay(self.bounds) trackingHighlightedIndex = highlightedIndex
if triggerAction { guard let newIndex = findHitIndex(event: event) else {
if let target = target as? NSObject, let action = action { return
target.perform(action, with: self) }
} 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) @objc(VTHorizontalCandidateController)
public class HorizontalCandidateController: CandidateController { public class HorizontalCandidateController: CandidateController {
private var candidateView: HorizontalCandidateView private var candidateView: HorizontalCandidateView
private var prevPageButton: NSButton private var prevPageButton: NSButton
private var nextPageButton: NSButton private var nextPageButton: NSButton
private var currentPage: UInt = 0 private var currentPage: UInt = 0
public init() { public init() {
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] let styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) let panel = NSPanel(
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
panel.hasShadow = true panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
panel.isOpaque = false panel.hasShadow = true
panel.backgroundColor = NSColor.clear panel.isOpaque = false
panel.backgroundColor = NSColor.clear
contentRect.origin = NSPoint.zero
candidateView = HorizontalCandidateView(frame: contentRect)
candidateView.wantsLayer = true contentRect.origin = NSPoint.zero
candidateView.layer?.borderColor = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor candidateView = HorizontalCandidateView(frame: contentRect)
candidateView.layer?.borderWidth = 1.0
if #available(macOS 10.13, *) {
candidateView.layer?.cornerRadius = 6.0
}
panel.contentView?.addSubview(candidateView) candidateView.wantsLayer = true
candidateView.layer?.borderColor =
contentRect.size = NSSize(width: 20.0, height: 10.0) // Reduce the button width NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor
let buttonAttribute: [NSAttributedString.Key : Any] = [.font : NSFont.systemFont(ofSize: 9.0)] candidateView.layer?.borderWidth = 1.0
if #available(macOS 10.13, *) {
candidateView.layer?.cornerRadius = 6.0
}
nextPageButton = NSButton(frame: contentRect) panel.contentView?.addSubview(candidateView)
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)
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 nextPageButton = NSButton(frame: contentRect)
candidateView.action = #selector(candidateViewMouseDidClick(_:)) 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 super.init(window: panel)
nextPageButton.action = #selector(pageButtonAction(_:))
prevPageButton.target = self candidateView.target = self
prevPageButton.action = #selector(pageButtonAction(_:)) candidateView.action = #selector(candidateViewMouseDidClick(_:))
}
required init?(coder: NSCoder) { nextPageButton.target = self
fatalError("init(coder:) has not been implemented") nextPageButton.action = #selector(pageButtonAction(_:))
}
public override func reloadData() { prevPageButton.target = self
candidateView.highlightedIndex = 0 prevPageButton.action = #selector(pageButtonAction(_:))
currentPage = 0 }
layoutCandidateView()
}
public override func showNextPage() -> Bool { required init?(coder: NSCoder) {
guard delegate != nil else {return false} fatalError("init(coder:) has not been implemented")
if pageCount == 1 {return highlightNextCandidate()} }
currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1
candidateView.highlightedIndex = 0
layoutCandidateView()
return true
}
public override func showPreviousPage() -> Bool { public override func reloadData() {
guard delegate != nil else {return false} candidateView.highlightedIndex = 0
if pageCount == 1 {return highlightPreviousCandidate()} currentPage = 0
currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1 layoutCandidateView()
candidateView.highlightedIndex = 0 }
layoutCandidateView()
return true
}
public override func highlightNextCandidate() -> Bool { public override func showNextPage() -> Bool {
guard let delegate = delegate else {return false} guard delegate != nil else { return false }
selectedCandidateIndex = (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self)) ? 0 : selectedCandidateIndex + 1 if pageCount == 1 { return highlightNextCandidate() }
return true currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1
} candidateView.highlightedIndex = 0
layoutCandidateView()
return true
}
public override func highlightPreviousCandidate() -> Bool { public override func showPreviousPage() -> Bool {
guard let delegate = delegate else {return false} guard delegate != nil else { return false }
selectedCandidateIndex = (selectedCandidateIndex == 0) ? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1 if pageCount == 1 { return highlightPreviousCandidate() }
return true currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1
} candidateView.highlightedIndex = 0
layoutCandidateView()
return true
}
public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { public override func highlightNextCandidate() -> Bool {
guard let delegate = delegate else { guard let delegate = delegate else { return false }
return UInt.max selectedCandidateIndex =
} (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self))
? 0 : selectedCandidateIndex + 1
return true
}
let result = currentPage * UInt(keyLabels.count) + index public override func highlightPreviousCandidate() -> Bool {
return result < delegate.candidateCountForController(self) ? result : UInt.max guard let delegate = delegate else { return false }
} selectedCandidateIndex =
(selectedCandidateIndex == 0)
? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1
return true
}
public override var selectedCandidateIndex: UInt { public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
get { guard let delegate = delegate else {
currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex return UInt.max
} }
set {
guard let delegate = delegate else { let result = currentPage * UInt(keyLabels.count) + index
return return result < delegate.candidateCountForController(self) ? result : UInt.max
} }
let keyLabelCount = UInt(keyLabels.count)
if newValue < delegate.candidateCountForController(self) { public override var selectedCandidateIndex: UInt {
currentPage = newValue / keyLabelCount get {
candidateView.highlightedIndex = newValue % keyLabelCount currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex
layoutCandidateView() }
} 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 { extension HorizontalCandidateController {
private var pageCount: UInt { private var pageCount: UInt {
guard let delegate = delegate else { guard let delegate = delegate else {
return 0 return 0
} }
let totalCount = delegate.candidateCountForController(self) let totalCount = delegate.candidateCountForController(self)
let keyLabelCount = UInt(keyLabels.count) let keyLabelCount = UInt(keyLabels.count)
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
} }
private func layoutCandidateView() { private func layoutCandidateView() {
guard let delegate = delegate else { guard let delegate = delegate else {
return return
} }
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
var candidates = [String]() var candidates = [String]()
let count = delegate.candidateCountForController(self) let count = delegate.candidateCountForController(self)
let keyLabelCount = UInt(keyLabels.count) let keyLabelCount = UInt(keyLabels.count)
let begin = currentPage * keyLabelCount let begin = currentPage * keyLabelCount
for index in begin..<min(begin + keyLabelCount, count) { for index in begin..<min(begin + keyLabelCount, count) {
let candidate = delegate.candidateController(self, candidateAtIndex: index) let candidate = delegate.candidateController(self, candidateAtIndex: index)
candidates.append(candidate) candidates.append(candidate)
} }
candidateView.set(keyLabels: keyLabels.map { $0.displayedText}, displayedCandidates: candidates) candidateView.set(
var newSize = candidateView.sizeForView keyLabels: keyLabels.map { $0.displayedText }, displayedCandidates: candidates)
var frameRect = candidateView.frame var newSize = candidateView.sizeForView
frameRect.size = newSize var frameRect = candidateView.frame
candidateView.frame = frameRect frameRect.size = newSize
candidateView.frame = frameRect
if pageCount > 1 && mgrPrefs.showPageButtonsInCandidateWindow { if pageCount > 1 && mgrPrefs.showPageButtonsInCandidateWindow {
var buttonRect = nextPageButton.frame var buttonRect = nextPageButton.frame
let spacing:CGFloat = 0.0 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 let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0
buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY) buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY)
nextPageButton.frame = buttonRect nextPageButton.frame = buttonRect
buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing) buttonRect.origin = NSPoint(
prevPageButton.frame = buttonRect x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing)
prevPageButton.frame = buttonRect
newSize.width += 20 newSize.width += 20
nextPageButton.isHidden = false nextPageButton.isHidden = false
prevPageButton.isHidden = false prevPageButton.isHidden = false
} else { } else {
nextPageButton.isHidden = true nextPageButton.isHidden = true
prevPageButton.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) let topLeftPoint = NSMakePoint(
frameRect.size = newSize frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) frameRect.size = newSize
self.window?.setFrame(frameRect, display: false) frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
candidateView.setNeedsDisplay(candidateView.bounds) self.window?.setFrame(frameRect, display: false)
} candidateView.setNeedsDisplay(candidateView.bounds)
}
@objc fileprivate func pageButtonAction(_ sender: Any) { @objc fileprivate func pageButtonAction(_ sender: Any) {
guard let sender = sender as? NSButton else { guard let sender = sender as? NSButton else {
return return
} }
if sender == nextPageButton { if sender == nextPageButton {
_ = showNextPage() _ = showNextPage()
} else if sender == prevPageButton { } else if sender == prevPageButton {
_ = showPreviousPage() _ = showPreviousPage()
} }
} }
@objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) {
delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex) delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex)
} }
} }

View File

@ -1,417 +1,467 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
fileprivate class VerticalCandidateView: NSView { private class VerticalCandidateView: NSView {
var highlightedIndex: UInt = 0 var highlightedIndex: UInt = 0
var action: Selector? var action: Selector?
weak var target: AnyObject? weak var target: AnyObject?
private var keyLabels: [String] = [] private var keyLabels: [String] = []
private var displayedCandidates: [String] = [] private var displayedCandidates: [String] = []
private var dispCandidatesWithLabels: [String] = [] private var dispCandidatesWithLabels: [String] = []
private var keyLabelHeight: CGFloat = 0 private var keyLabelHeight: CGFloat = 0
private var keyLabelWidth: CGFloat = 0 private var keyLabelWidth: CGFloat = 0
private var candidateTextHeight: CGFloat = 0 private var candidateTextHeight: CGFloat = 0
private var cellPadding: CGFloat = 0 private var cellPadding: CGFloat = 0
private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:] private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:]
private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:] private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
private var windowWidth: CGFloat = 0 private var windowWidth: CGFloat = 0
private var elementWidths: [CGFloat] = [] private var elementWidths: [CGFloat] = []
private var elementHeights: [CGFloat] = [] private var elementHeights: [CGFloat] = []
private var trackingHighlightedIndex: UInt = UInt.max private var trackingHighlightedIndex: UInt = UInt.max
override var isFlipped: Bool { override var isFlipped: Bool {
true true
} }
var sizeForView: NSSize { var sizeForView: NSSize {
var result = NSSize.zero var result = NSSize.zero
if !elementWidths.isEmpty { if !elementWidths.isEmpty {
result.width = windowWidth result.width = windowWidth
result.height = elementHeights.reduce(0, +) result.height = elementHeights.reduce(0, +)
} }
return result return result
} }
@objc(setKeyLabels:displayedCandidates:) @objc(setKeyLabels:displayedCandidates:)
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
let count = min(labels.count, candidates.count) let count = min(labels.count, candidates.count)
keyLabels = Array(labels[0..<count]) keyLabels = Array(labels[0..<count])
displayedCandidates = Array(candidates[0..<count]) displayedCandidates = Array(candidates[0..<count])
dispCandidatesWithLabels = zip(keyLabels,displayedCandidates).map() {$0 + $1} dispCandidatesWithLabels = zip(keyLabels, displayedCandidates).map { $0 + $1 }
var newWidths = [CGFloat]() var newWidths = [CGFloat]()
var calculatedWindowWidth = CGFloat() var calculatedWindowWidth = CGFloat()
var newHeights = [CGFloat]() var newHeights = [CGFloat]()
let baseSize = NSSize(width: 10240.0, height: 10240.0) let baseSize = NSSize(width: 10240.0, height: 10240.0)
for index in 0..<count { for index in 0..<count {
let rctCandidate = (dispCandidatesWithLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateWithLabelAttrDict) let rctCandidate = (dispCandidatesWithLabels[index] as NSString).boundingRect(
let cellWidth = rctCandidate.size.width + cellPadding with: baseSize, options: .usesLineFragmentOrigin,
let cellHeight = rctCandidate.size.height + cellPadding attributes: candidateWithLabelAttrDict)
if (calculatedWindowWidth < rctCandidate.size.width) { let cellWidth = rctCandidate.size.width + cellPadding
calculatedWindowWidth = rctCandidate.size.width + cellPadding let cellHeight = rctCandidate.size.height + cellPadding
} if calculatedWindowWidth < rctCandidate.size.width {
newWidths.append(cellWidth) calculatedWindowWidth = rctCandidate.size.width + cellPadding
newHeights.append(cellHeight) }
} newWidths.append(cellWidth)
elementWidths = newWidths newHeights.append(cellHeight)
elementHeights = newHeights }
windowWidth = calculatedWindowWidth + cellPadding; elementWidths = newWidths
} elementHeights = newHeights
windowWidth = calculatedWindowWidth + cellPadding
}
@objc(setKeyLabelFont:candidateFont:) @objc(setKeyLabelFont:candidateFont:)
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) { func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
let paraStyle = NSMutableParagraphStyle() let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default) paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .left paraStyle.alignment = .left
candidateWithLabelAttrDict = [.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.labelColor] // We still need this dummy section to make sure the space occupations of the candidates are correct.
keyLabelAttrDict = [.font: labelFont,
.paragraphStyle: paraStyle,
.verticalGlyphForm: true as AnyObject,
.foregroundColor: NSColor.secondaryLabelColor] // Candidate phrase text color
candidateAttrDict = [.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.labelColor] // Candidate index text color
let labelFontSize = labelFont.pointSize
let candidateFontSize = candidateFont.pointSize
let biggestSize = max(labelFontSize, candidateFontSize)
keyLabelWidth = ceil(labelFontSize)
keyLabelHeight = ceil(labelFontSize * 2)
candidateTextHeight = ceil(candidateFontSize * 1.20)
cellPadding = ceil(biggestSize / 2.0)
}
override func draw(_ dirtyRect: NSRect) { candidateWithLabelAttrDict = [
let bounds = self.bounds .font: candidateFont,
NSColor.controlBackgroundColor.setFill() // Candidate list panel base background .paragraphStyle: paraStyle,
NSBezierPath.fill(bounds) .foregroundColor: NSColor.labelColor,
] // We still need this dummy section to make sure that
// the space occupations of the candidates are correct.
NSColor.systemGray.withAlphaComponent(0.75).setStroke() keyLabelAttrDict = [
.font: labelFont,
.paragraphStyle: paraStyle,
.verticalGlyphForm: true as AnyObject,
.foregroundColor: NSColor.secondaryLabelColor,
] // Candidate phrase text color
candidateAttrDict = [
.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.labelColor,
] // Candidate index text color
let labelFontSize = labelFont.pointSize
let candidateFontSize = candidateFont.pointSize
let biggestSize = max(labelFontSize, candidateFontSize)
keyLabelWidth = ceil(labelFontSize)
keyLabelHeight = ceil(labelFontSize * 2)
candidateTextHeight = ceil(candidateFontSize * 1.20)
cellPadding = ceil(biggestSize / 2.0)
}
NSBezierPath.strokeLine(from: NSPoint(x: bounds.size.width, y: 0.0), to: NSPoint(x: bounds.size.width, y: bounds.size.height)) override func draw(_ dirtyRect: NSRect) {
let bounds = self.bounds
NSColor.controlBackgroundColor.setFill() // Candidate list panel base background
NSBezierPath.fill(bounds)
var accuHeight: CGFloat = 0 NSColor.systemGray.withAlphaComponent(0.75).setStroke()
for index in 0..<elementHeights.count {
let currentHeight = elementHeights[index]
let rctCandidateArea = NSRect(x: 0.0, y: accuHeight, width: windowWidth, height: candidateTextHeight + cellPadding)
let rctLabel = NSRect(x: cellPadding / 2 - 1, y: accuHeight + cellPadding / 2, width: keyLabelWidth, height: keyLabelHeight * 2.0)
let rctCandidatePhrase = NSRect(x: cellPadding / 2 - 1 + keyLabelWidth, y: accuHeight + cellPadding / 2 - 1, width: windowWidth - keyLabelWidth, height: candidateTextHeight)
var activeCandidateIndexAttr = keyLabelAttrDict NSBezierPath.strokeLine(
var activeCandidateAttr = candidateAttrDict from: NSPoint(x: bounds.size.width, y: 0.0),
if index == highlightedIndex { to: NSPoint(x: bounds.size.width, y: bounds.size.height))
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()
}
activeCandidateIndexAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.84) // The index text color of the highlightened candidate
activeCandidateAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor // The phrase text color of the highlightened candidate
} 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? { var accuHeight: CGFloat = 0
let location = convert(event.locationInWindow, to: nil) for index in 0..<elementHeights.count {
if !NSPointInRect(location, self.bounds) { let currentHeight = elementHeights[index]
return nil let rctCandidateArea = NSRect(
} x: 0.0, y: accuHeight, width: windowWidth, height: candidateTextHeight + cellPadding
var accuHeight: CGFloat = 0.0 )
for index in 0..<elementHeights.count { let rctLabel = NSRect(
let currentHeight = elementHeights[index] x: cellPadding / 2 - 1, y: accuHeight + cellPadding / 2, width: keyLabelWidth,
height: keyLabelHeight * 2.0)
let rctCandidatePhrase = NSRect(
x: cellPadding / 2 - 1 + keyLabelWidth, y: accuHeight + cellPadding / 2 - 1,
width: windowWidth - keyLabelWidth, height: candidateTextHeight)
if location.y >= accuHeight && location.y <= accuHeight + currentHeight { var activeCandidateIndexAttr = keyLabelAttrDict
return UInt(index) var activeCandidateAttr = candidateAttrDict
} if index == highlightedIndex {
accuHeight += currentHeight let colorBlendAmount: CGFloat = IME.isDarkMode() ? 0.25 : 0
} // The background color of the highlightened candidate
return nil 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..<elementHeights.count {
let currentHeight = elementHeights[index]
override func mouseUp(with event: NSEvent) { if location.y >= accuHeight && location.y <= accuHeight + currentHeight {
trackingHighlightedIndex = highlightedIndex return UInt(index)
guard let newIndex = findHitIndex(event: event) else { }
return accuHeight += currentHeight
} }
highlightedIndex = newIndex return nil
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 override func mouseUp(with event: NSEvent) {
self.setNeedsDisplay(self.bounds) trackingHighlightedIndex = highlightedIndex
if triggerAction { guard let newIndex = findHitIndex(event: event) else {
if let target = target as? NSObject, let action = action { return
target.perform(action, with: self) }
} 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) @objc(VTVerticalCandidateController)
public class VerticalCandidateController: CandidateController { public class VerticalCandidateController: CandidateController {
private var candidateView: VerticalCandidateView private var candidateView: VerticalCandidateView
private var prevPageButton: NSButton private var prevPageButton: NSButton
private var nextPageButton: NSButton private var nextPageButton: NSButton
private var currentPage: UInt = 0 private var currentPage: UInt = 0
public init() { public init() {
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] let styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) let panel = NSPanel(
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
panel.hasShadow = true panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
panel.isOpaque = false panel.hasShadow = true
panel.backgroundColor = NSColor.clear panel.isOpaque = false
panel.backgroundColor = NSColor.clear
contentRect.origin = NSPoint.zero
candidateView = VerticalCandidateView(frame: contentRect)
candidateView.wantsLayer = true contentRect.origin = NSPoint.zero
candidateView.layer?.borderColor = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor candidateView = VerticalCandidateView(frame: contentRect)
candidateView.layer?.borderWidth = 1.0
if #available(macOS 10.13, *) {
candidateView.layer?.cornerRadius = 6.0
}
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 panel.contentView?.addSubview(candidateView)
let buttonAttribute: [NSAttributedString.Key : Any] = [.font : NSFont.systemFont(ofSize: 9.0)]
nextPageButton = NSButton(frame: contentRect) contentRect.size = NSSize(width: 20.0, height: 10.0) // Reduce the button width
NSColor.controlBackgroundColor.setFill() let buttonAttribute: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: 9.0)]
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)
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 super.init(window: panel)
candidateView.action = #selector(candidateViewMouseDidClick(_:))
nextPageButton.target = self candidateView.target = self
nextPageButton.action = #selector(pageButtonAction(_:)) candidateView.action = #selector(candidateViewMouseDidClick(_:))
prevPageButton.target = self nextPageButton.target = self
prevPageButton.action = #selector(pageButtonAction(_:)) nextPageButton.action = #selector(pageButtonAction(_:))
}
required init?(coder: NSCoder) { prevPageButton.target = self
fatalError("init(coder:) has not been implemented") prevPageButton.action = #selector(pageButtonAction(_:))
} }
public override func reloadData() { required init?(coder: NSCoder) {
candidateView.highlightedIndex = 0 fatalError("init(coder:) has not been implemented")
currentPage = 0 }
layoutCandidateView()
}
public override func showNextPage() -> Bool { public override func reloadData() {
guard delegate != nil else {return false} candidateView.highlightedIndex = 0
if pageCount == 1 {return highlightNextCandidate()} currentPage = 0
currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1 layoutCandidateView()
candidateView.highlightedIndex = 0 }
layoutCandidateView()
return true
}
public override func showPreviousPage() -> Bool { public override func showNextPage() -> Bool {
guard delegate != nil else {return false} guard delegate != nil else { return false }
if pageCount == 1 {return highlightPreviousCandidate()} if pageCount == 1 { return highlightNextCandidate() }
currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1 currentPage = (currentPage + 1 >= pageCount) ? 0 : currentPage + 1
candidateView.highlightedIndex = 0 candidateView.highlightedIndex = 0
layoutCandidateView() layoutCandidateView()
return true return true
} }
public override func highlightNextCandidate() -> Bool { public override func showPreviousPage() -> Bool {
guard let delegate = delegate else {return false} guard delegate != nil else { return false }
selectedCandidateIndex = (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self)) ? 0 : selectedCandidateIndex + 1 if pageCount == 1 { return highlightPreviousCandidate() }
return true currentPage = (currentPage == 0) ? pageCount - 1 : currentPage - 1
} candidateView.highlightedIndex = 0
layoutCandidateView()
return true
}
public override func highlightPreviousCandidate() -> Bool { public override func highlightNextCandidate() -> Bool {
guard let delegate = delegate else {return false} guard let delegate = delegate else { return false }
selectedCandidateIndex = (selectedCandidateIndex == 0) ? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1 selectedCandidateIndex =
return true (selectedCandidateIndex + 1 >= delegate.candidateCountForController(self))
} ? 0 : selectedCandidateIndex + 1
return true
}
public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { public override func highlightPreviousCandidate() -> Bool {
guard let delegate = delegate else { guard let delegate = delegate else { return false }
return UInt.max selectedCandidateIndex =
} (selectedCandidateIndex == 0)
? delegate.candidateCountForController(self) - 1 : selectedCandidateIndex - 1
return true
}
let result = currentPage * UInt(keyLabels.count) + index public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
return result < delegate.candidateCountForController(self) ? result : UInt.max guard let delegate = delegate else {
} return UInt.max
}
public override var selectedCandidateIndex: UInt { let result = currentPage * UInt(keyLabels.count) + index
get { return result < delegate.candidateCountForController(self) ? result : UInt.max
currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex }
}
set { public override var selectedCandidateIndex: UInt {
guard let delegate = delegate else { get {
return currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex
} }
let keyLabelCount = UInt(keyLabels.count) set {
if newValue < delegate.candidateCountForController(self) { guard let delegate = delegate else {
currentPage = newValue / keyLabelCount return
candidateView.highlightedIndex = newValue % keyLabelCount }
layoutCandidateView() let keyLabelCount = UInt(keyLabels.count)
} if newValue < delegate.candidateCountForController(self) {
} currentPage = newValue / keyLabelCount
} candidateView.highlightedIndex = newValue % keyLabelCount
layoutCandidateView()
}
}
}
} }
extension VerticalCandidateController { extension VerticalCandidateController {
private var pageCount: UInt { private var pageCount: UInt {
guard let delegate = delegate else { guard let delegate = delegate else {
return 0 return 0
} }
let totalCount = delegate.candidateCountForController(self) let totalCount = delegate.candidateCountForController(self)
let keyLabelCount = UInt(keyLabels.count) let keyLabelCount = UInt(keyLabels.count)
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
} }
private func layoutCandidateView() { private func layoutCandidateView() {
guard let delegate = delegate else { guard let delegate = delegate else {
return return
} }
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
var candidates = [String]() var candidates = [String]()
let count = delegate.candidateCountForController(self) let count = delegate.candidateCountForController(self)
let keyLabelCount = UInt(keyLabels.count) let keyLabelCount = UInt(keyLabels.count)
let begin = currentPage * keyLabelCount let begin = currentPage * keyLabelCount
for index in begin..<min(begin + keyLabelCount, count) { for index in begin..<min(begin + keyLabelCount, count) {
let candidate = delegate.candidateController(self, candidateAtIndex: index) let candidate = delegate.candidateController(self, candidateAtIndex: index)
candidates.append(candidate) candidates.append(candidate)
} }
candidateView.set(keyLabels: keyLabels.map { $0.displayedText}, displayedCandidates: candidates) candidateView.set(
var newSize = candidateView.sizeForView keyLabels: keyLabels.map { $0.displayedText }, displayedCandidates: candidates)
var frameRect = candidateView.frame var newSize = candidateView.sizeForView
frameRect.size = newSize var frameRect = candidateView.frame
candidateView.frame = frameRect frameRect.size = newSize
candidateView.frame = frameRect
if pageCount > 1 && mgrPrefs.showPageButtonsInCandidateWindow { if pageCount > 1 && mgrPrefs.showPageButtonsInCandidateWindow {
var buttonRect = nextPageButton.frame var buttonRect = nextPageButton.frame
let spacing:CGFloat = 0.0 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 let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) // / 2.0
buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY) buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY)
nextPageButton.frame = buttonRect nextPageButton.frame = buttonRect
buttonRect.origin = NSPoint(x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing) buttonRect.origin = NSPoint(
prevPageButton.frame = buttonRect x: newSize.width, y: buttonOriginY + buttonRect.size.height + spacing)
prevPageButton.frame = buttonRect
newSize.width += 20 newSize.width += 20
nextPageButton.isHidden = false nextPageButton.isHidden = false
prevPageButton.isHidden = false prevPageButton.isHidden = false
} else { } else {
nextPageButton.isHidden = true nextPageButton.isHidden = true
prevPageButton.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) let topLeftPoint = NSMakePoint(
frameRect.size = newSize frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) frameRect.size = newSize
self.window?.setFrame(frameRect, display: false) frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
candidateView.setNeedsDisplay(candidateView.bounds) self.window?.setFrame(frameRect, display: false)
} candidateView.setNeedsDisplay(candidateView.bounds)
}
@objc fileprivate func pageButtonAction(_ sender: Any) { @objc fileprivate func pageButtonAction(_ sender: Any) {
guard let sender = sender as? NSButton else { guard let sender = sender as? NSButton else {
return return
} }
if sender == nextPageButton { if sender == nextPageButton {
_ = showNextPage() _ = showNextPage()
} else if sender == prevPageButton { } else if sender == prevPageButton {
_ = showPreviousPage() _ = showPreviousPage()
} }
} }
@objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) {
delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex) delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex)
} }
} }

View File

@ -1,203 +1,216 @@
// Copyright (c) 2021 and onwards Weizhong Yang (MIT License). // 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). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
private protocol NotifierWindowDelegate: AnyObject { private protocol NotifierWindowDelegate: AnyObject {
func windowDidBecomeClicked(_ window: NotifierWindow) func windowDidBecomeClicked(_ window: NotifierWindow)
} }
private class NotifierWindow: NSWindow { private class NotifierWindow: NSWindow {
weak var clickDelegate: NotifierWindowDelegate? weak var clickDelegate: NotifierWindowDelegate?
override func mouseDown(with event: NSEvent) { override func mouseDown(with event: NSEvent) {
clickDelegate?.windowDidBecomeClicked(self) clickDelegate?.windowDidBecomeClicked(self)
} }
} }
private let kWindowWidth: CGFloat = 213.0 private let kWindowWidth: CGFloat = 213.0
private let kWindowHeight: CGFloat = 60.0 private let kWindowHeight: CGFloat = 60.0
public class NotifierController: NSWindowController, NotifierWindowDelegate { public class NotifierController: NSWindowController, NotifierWindowDelegate {
private var messageTextField: NSTextField private var messageTextField: NSTextField
private var message: String = "" { private var message: String = "" {
didSet { didSet {
let paraStyle = NSMutableParagraphStyle() let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default) paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .center paraStyle.alignment = .center
let attr: [NSAttributedString.Key: AnyObject] = [ let attr: [NSAttributedString.Key: AnyObject] = [
.foregroundColor: foregroundColor, .foregroundColor: foregroundColor,
.font: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)), .font: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)),
.paragraphStyle: paraStyle .paragraphStyle: paraStyle,
] ]
let attrString = NSAttributedString(string: message, attributes: attr) let attrString = NSAttributedString(string: message, attributes: attr)
messageTextField.attributedStringValue = attrString messageTextField.attributedStringValue = attrString
let width = window?.frame.width ?? kWindowWidth let width = window?.frame.width ?? kWindowWidth
let rect = attrString.boundingRect(with: NSSize(width: width, height: 1600), options: .usesLineFragmentOrigin) let rect = attrString.boundingRect(
let height = rect.height with: NSSize(width: width, height: 1600), options: .usesLineFragmentOrigin)
let x = messageTextField.frame.origin.x let height = rect.height
let y = ((window?.frame.height ?? kWindowHeight) - height) / 2 let x = messageTextField.frame.origin.x
let newFrame = NSRect(x: x, y: y, width: width, height: height) let y = ((window?.frame.height ?? kWindowHeight) - height) / 2
messageTextField.frame = newFrame let newFrame = NSRect(x: x, y: y, width: width, height: height)
} messageTextField.frame = newFrame
} }
private var shouldStay: Bool = false }
private var backgroundColor: NSColor = .textBackgroundColor { private var shouldStay: Bool = false
didSet { private var backgroundColor: NSColor = .textBackgroundColor {
self.window?.backgroundColor = backgroundColor didSet {
} self.window?.backgroundColor = backgroundColor
} }
private var foregroundColor: NSColor = .controlTextColor { }
didSet { private var foregroundColor: NSColor = .controlTextColor {
self.messageTextField.textColor = foregroundColor didSet {
} self.messageTextField.textColor = foregroundColor
} }
private var waitTimer: Timer? }
private var fadeTimer: Timer? private var waitTimer: Timer?
private var fadeTimer: Timer?
private static var instanceCount = 0 private static var instanceCount = 0
private static var lastLocation = NSPoint.zero private static var lastLocation = NSPoint.zero
@objc public static func notify(message: String, stay: Bool = false) { @objc public static func notify(message: String, stay: Bool = false) {
let controller = NotifierController() let controller = NotifierController()
controller.message = message controller.message = message
controller.shouldStay = stay controller.shouldStay = stay
controller.show() controller.show()
} }
private static func increaseInstanceCount() { private static func increaseInstanceCount() {
instanceCount += 1 instanceCount += 1
} }
private static func decreaseInstanceCount() { private static func decreaseInstanceCount() {
instanceCount -= 1 instanceCount -= 1
if instanceCount < 0 { if instanceCount < 0 {
instanceCount = 0 instanceCount = 0
} }
} }
private init() { private init() {
let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero
let contentRect = NSRect(x: 0, y: 0, width: kWindowWidth, height: kWindowHeight) let contentRect = NSRect(x: 0, y: 0, width: kWindowWidth, height: kWindowHeight)
var windowRect = contentRect var windowRect = contentRect
windowRect.origin.x = screenRect.maxX - windowRect.width - 10 windowRect.origin.x = screenRect.maxX - windowRect.width - 10
windowRect.origin.y = screenRect.maxY - windowRect.height - 10 windowRect.origin.y = screenRect.maxY - windowRect.height - 10
let styleMask: NSWindow.StyleMask = [.fullSizeContentView, .titled] let styleMask: NSWindow.StyleMask = [.fullSizeContentView, .titled]
let transparentVisualEffect = NSVisualEffectView() let transparentVisualEffect = NSVisualEffectView()
transparentVisualEffect.blendingMode = .behindWindow transparentVisualEffect.blendingMode = .behindWindow
transparentVisualEffect.state = .active transparentVisualEffect.state = .active
let panel = NotifierWindow(contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false) let panel = NotifierWindow(
panel.contentView = transparentVisualEffect contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false)
panel.isMovableByWindowBackground = true panel.contentView = transparentVisualEffect
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel)) panel.isMovableByWindowBackground = true
panel.hasShadow = true panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel))
panel.backgroundColor = backgroundColor panel.hasShadow = true
panel.title = "" panel.backgroundColor = backgroundColor
panel.titlebarAppearsTransparent = true panel.title = ""
panel.titleVisibility = .hidden panel.titlebarAppearsTransparent = true
panel.showsToolbarButton = false panel.titleVisibility = .hidden
panel.standardWindowButton(NSWindow.ButtonType.fullScreenButton)?.isHidden = true panel.showsToolbarButton = false
panel.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true panel.standardWindowButton(NSWindow.ButtonType.fullScreenButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true panel.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true panel.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true
messageTextField = NSTextField() messageTextField = NSTextField()
messageTextField.frame = contentRect messageTextField.frame = contentRect
messageTextField.isEditable = false messageTextField.isEditable = false
messageTextField.isSelectable = false messageTextField.isSelectable = false
messageTextField.isBezeled = false messageTextField.isBezeled = false
messageTextField.textColor = foregroundColor messageTextField.textColor = foregroundColor
messageTextField.drawsBackground = false messageTextField.drawsBackground = false
messageTextField.font = .boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular)) messageTextField.font = .boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular))
panel.contentView?.addSubview(messageTextField) panel.contentView?.addSubview(messageTextField)
super.init(window: panel) super.init(window: panel)
panel.clickDelegate = self panel.clickDelegate = self
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
private func show() { private func show() {
func setStartLocation() { func setStartLocation() {
if NotifierController.instanceCount == 0 { if NotifierController.instanceCount == 0 {
return return
} }
let lastLocation = NotifierController.lastLocation let lastLocation = NotifierController.lastLocation
let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero
var windowRect = self.window?.frame ?? NSRect.zero var windowRect = self.window?.frame ?? NSRect.zero
windowRect.origin.x = lastLocation.x windowRect.origin.x = lastLocation.x
windowRect.origin.y = lastLocation.y - 10 - windowRect.height windowRect.origin.y = lastLocation.y - 10 - windowRect.height
if windowRect.origin.y < screenRect.minY { if windowRect.origin.y < screenRect.minY {
return return
} }
self.window?.setFrame(windowRect, display: true) self.window?.setFrame(windowRect, display: true)
} }
func moveIn() { func moveIn() {
let afterRect = self.window?.frame ?? NSRect.zero let afterRect = self.window?.frame ?? NSRect.zero
NotifierController.lastLocation = afterRect.origin NotifierController.lastLocation = afterRect.origin
var beforeRect = afterRect var beforeRect = afterRect
beforeRect.origin.y += 10 beforeRect.origin.y += 10
window?.setFrame(beforeRect, display: true) window?.setFrame(beforeRect, display: true)
window?.orderFront(self) window?.orderFront(self)
window?.setFrame(afterRect, display: true, animate: true) window?.setFrame(afterRect, display: true, animate: true)
} }
setStartLocation() setStartLocation()
moveIn() moveIn()
NotifierController.increaseInstanceCount() NotifierController.increaseInstanceCount()
waitTimer = Timer.scheduledTimer(timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut), userInfo: nil, repeats: false) waitTimer = Timer.scheduledTimer(
} timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut),
userInfo: nil,
repeats: false)
}
@objc private func doFadeOut(_ timer: Timer) { @objc private func doFadeOut(_ timer: Timer) {
let opacity = self.window?.alphaValue ?? 0 let opacity = self.window?.alphaValue ?? 0
if opacity <= 0 { if opacity <= 0 {
self.close() self.close()
} else { } else {
self.window?.alphaValue = opacity - 0.2 self.window?.alphaValue = opacity - 0.2
} }
} }
@objc private func fadeOut() { @objc private func fadeOut() {
waitTimer?.invalidate() waitTimer?.invalidate()
waitTimer = nil waitTimer = nil
NotifierController.decreaseInstanceCount() NotifierController.decreaseInstanceCount()
fadeTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(doFadeOut(_:)), userInfo: nil, repeats: true) fadeTimer = Timer.scheduledTimer(
} timeInterval: 0.01, target: self, selector: #selector(doFadeOut(_:)), userInfo: nil,
repeats: true)
}
public override func close() { public override func close() {
waitTimer?.invalidate() waitTimer?.invalidate()
waitTimer = nil waitTimer = nil
fadeTimer?.invalidate() fadeTimer?.invalidate()
fadeTimer = nil fadeTimer = nil
super.close() super.close()
} }
fileprivate func windowDidBecomeClicked(_ window: NotifierWindow) { fileprivate func windowDidBecomeClicked(_ window: NotifierWindow) {
self.fadeOut() self.fadeOut()
} }
} }

View File

@ -1,122 +1,129 @@
// Copyright (c) 2021 and onwards Weizhong Yang (MIT License). // 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). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
public class TooltipController: NSWindowController { public class TooltipController: NSWindowController {
static var backgroundColor = NSColor.windowBackgroundColor static var backgroundColor = NSColor.windowBackgroundColor
static var textColor = NSColor.windowBackgroundColor static var textColor = NSColor.windowBackgroundColor
private var messageTextField: NSTextField private var messageTextField: NSTextField
private var tooltip: String = "" { private var tooltip: String = "" {
didSet { didSet {
messageTextField.stringValue = tooltip messageTextField.stringValue = tooltip
adjustSize() adjustSize()
} }
} }
public init() { public init() {
let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0) let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0)
let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel]
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) let panel = NSPanel(
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
panel.hasShadow = true panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
panel.hasShadow = true
messageTextField = NSTextField() messageTextField = NSTextField()
messageTextField.isEditable = false messageTextField.isEditable = false
messageTextField.isSelectable = false messageTextField.isSelectable = false
messageTextField.isBezeled = false messageTextField.isBezeled = false
messageTextField.textColor = TooltipController.textColor messageTextField.textColor = TooltipController.textColor
messageTextField.drawsBackground = true messageTextField.drawsBackground = true
messageTextField.backgroundColor = TooltipController.backgroundColor messageTextField.backgroundColor = TooltipController.backgroundColor
messageTextField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) messageTextField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small))
panel.contentView?.addSubview(messageTextField) panel.contentView?.addSubview(messageTextField)
super.init(window: panel) super.init(window: panel)
} }
public required init?(coder: NSCoder) { public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc(showTooltip:atPoint:) @objc(showTooltip:atPoint:)
public func show(tooltip: String, at point: NSPoint) { public func show(tooltip: String, at point: NSPoint) {
messageTextField.textColor = TooltipController.textColor messageTextField.textColor = TooltipController.textColor
messageTextField.backgroundColor = TooltipController.backgroundColor messageTextField.backgroundColor = TooltipController.backgroundColor
self.tooltip = tooltip self.tooltip = tooltip
window?.orderFront(nil) window?.orderFront(nil)
set(windowLocation: point) set(windowLocation: point)
} }
@objc @objc
public func hide() { public func hide() {
window?.orderOut(nil) window?.orderOut(nil)
} }
private func set(windowLocation windowTopLeftPoint: NSPoint) { private func set(windowLocation windowTopLeftPoint: NSPoint) {
var adjustedPoint = windowTopLeftPoint var adjustedPoint = windowTopLeftPoint
adjustedPoint.y -= 5 adjustedPoint.y -= 5
var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero
for screen in NSScreen.screens { for screen in NSScreen.screens {
let frame = screen.visibleFrame let frame = screen.visibleFrame
if windowTopLeftPoint.x >= frame.minX && if windowTopLeftPoint.x >= frame.minX && windowTopLeftPoint.x <= frame.maxX
windowTopLeftPoint.x <= frame.maxX && && windowTopLeftPoint.y >= frame.minY && windowTopLeftPoint.y <= frame.maxY
windowTopLeftPoint.y >= frame.minY && {
windowTopLeftPoint.y <= frame.maxY { screenFrame = frame
screenFrame = frame break
break }
} }
}
let windowSize = window?.frame.size ?? NSSize.zero let windowSize = window?.frame.size ?? NSSize.zero
// bottom beneath the screen? // bottom beneath the screen?
if adjustedPoint.y - windowSize.height < screenFrame.minY { if adjustedPoint.y - windowSize.height < screenFrame.minY {
adjustedPoint.y = screenFrame.minY + windowSize.height adjustedPoint.y = screenFrame.minY + windowSize.height
} }
// top over the screen? // top over the screen?
if adjustedPoint.y >= screenFrame.maxY { if adjustedPoint.y >= screenFrame.maxY {
adjustedPoint.y = screenFrame.maxY - 1.0 adjustedPoint.y = screenFrame.maxY - 1.0
} }
// right // right
if adjustedPoint.x + windowSize.width >= screenFrame.maxX { if adjustedPoint.x + windowSize.width >= screenFrame.maxX {
adjustedPoint.x = screenFrame.maxX - windowSize.width adjustedPoint.x = screenFrame.maxX - windowSize.width
} }
// left // left
if adjustedPoint.x < screenFrame.minX { if adjustedPoint.x < screenFrame.minX {
adjustedPoint.x = screenFrame.minX adjustedPoint.x = screenFrame.minX
} }
window?.setFrameTopLeftPoint(adjustedPoint) window?.setFrameTopLeftPoint(adjustedPoint)
} }
private func adjustSize() { private func adjustSize() {
let attrString = messageTextField.attributedStringValue; let attrString = messageTextField.attributedStringValue
var rect = attrString.boundingRect(with: NSSize(width: 1600.0, height: 1600.0), options: .usesLineFragmentOrigin) var rect = attrString.boundingRect(
rect.size.width += 10 with: NSSize(width: 1600.0, height: 1600.0), options: .usesLineFragmentOrigin)
messageTextField.frame = rect rect.size.width += 10
window?.setFrame(rect, display: true) messageTextField.frame = rect
} window?.setFrame(rect, display: true)
}
} }

View File

@ -1,51 +1,64 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc(AboutWindow) class ctlAboutWindow: NSWindowController { @objc(AboutWindow) class ctlAboutWindow: NSWindowController {
@IBOutlet weak var appVersionLabel: NSTextField! @IBOutlet weak var appVersionLabel: NSTextField!
@IBOutlet weak var appCopyrightLabel: NSTextField! @IBOutlet weak var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView! @IBOutlet var appEULAContent: NSTextView!
override func windowDidLoad() { override func windowDidLoad() {
super.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)
}
@IBAction func btnWiki(_ sender: NSButton) { window?.standardWindowButton(.closeButton)?.isHidden = true
if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") { window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
NSWorkspace.shared.open(url) 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)
}
}
} }

View File

@ -1,118 +1,130 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc protocol ctlNonModalAlertWindowDelegate: AnyObject { @objc protocol ctlNonModalAlertWindowDelegate: AnyObject {
func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow) func ctlNonModalAlertWindowDidConfirm(_ controller: ctlNonModalAlertWindow)
func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow) func ctlNonModalAlertWindowDidCancel(_ controller: ctlNonModalAlertWindow)
} }
class ctlNonModalAlertWindow: NSWindowController { class ctlNonModalAlertWindow: NSWindowController {
@objc(sharedInstance) @objc(sharedInstance)
static let shared = ctlNonModalAlertWindow(windowNibName: "frmNonModalAlertWindow") static let shared = ctlNonModalAlertWindow(windowNibName: "frmNonModalAlertWindow")
@IBOutlet weak var titleTextField: NSTextField! @IBOutlet weak var titleTextField: NSTextField!
@IBOutlet weak var contentTextField: NSTextField! @IBOutlet weak var contentTextField: NSTextField!
@IBOutlet weak var confirmButton: NSButton! @IBOutlet weak var confirmButton: NSButton!
@IBOutlet weak var cancelButton: NSButton! @IBOutlet weak var cancelButton: NSButton!
weak var delegate: ctlNonModalAlertWindowDelegate? weak var delegate: ctlNonModalAlertWindowDelegate?
@objc func show(title: String, content: String, confirmButtonTitle: String, cancelButtonTitle: String?, cancelAsDefault: Bool, delegate: ctlNonModalAlertWindowDelegate?) { @objc func show(
if window?.isVisible == true { title: String, content: String, confirmButtonTitle: String, cancelButtonTitle: String?,
self.delegate?.ctlNonModalAlertWindowDidCancel(self) cancelAsDefault: Bool, delegate: ctlNonModalAlertWindowDelegate?
} ) {
if window?.isVisible == true {
self.delegate?.ctlNonModalAlertWindowDidCancel(self)
}
self.delegate = delegate self.delegate = delegate
var oldFrame = confirmButton.frame var oldFrame = confirmButton.frame
confirmButton.title = confirmButtonTitle confirmButton.title = confirmButtonTitle
confirmButton.sizeToFit() confirmButton.sizeToFit()
var newFrame = confirmButton.frame var newFrame = confirmButton.frame
newFrame.size.width = max(90, newFrame.size.width + 10) newFrame.size.width = max(90, newFrame.size.width + 10)
newFrame.origin.x += oldFrame.size.width - newFrame.size.width newFrame.origin.x += oldFrame.size.width - newFrame.size.width
confirmButton.frame = newFrame confirmButton.frame = newFrame
if let cancelButtonTitle = cancelButtonTitle { if let cancelButtonTitle = cancelButtonTitle {
cancelButton.title = cancelButtonTitle cancelButton.title = cancelButtonTitle
cancelButton.sizeToFit() cancelButton.sizeToFit()
var adjustFrame = cancelButton.frame var adjustFrame = cancelButton.frame
adjustFrame.size.width = max(90, adjustFrame.size.width + 10) adjustFrame.size.width = max(90, adjustFrame.size.width + 10)
adjustFrame.origin.x = newFrame.origin.x - adjustFrame.size.width adjustFrame.origin.x = newFrame.origin.x - adjustFrame.size.width
confirmButton.frame = adjustFrame confirmButton.frame = adjustFrame
cancelButton.isHidden = false cancelButton.isHidden = false
} else { } else {
cancelButton.isHidden = true cancelButton.isHidden = true
} }
cancelButton.nextKeyView = confirmButton cancelButton.nextKeyView = confirmButton
confirmButton.nextKeyView = cancelButton confirmButton.nextKeyView = cancelButton
if cancelButtonTitle != nil { if cancelButtonTitle != nil {
if cancelAsDefault { if cancelAsDefault {
window?.defaultButtonCell = cancelButton.cell as? NSButtonCell window?.defaultButtonCell = cancelButton.cell as? NSButtonCell
} else { } else {
cancelButton.keyEquivalent = " " cancelButton.keyEquivalent = " "
window?.defaultButtonCell = confirmButton.cell as? NSButtonCell window?.defaultButtonCell = confirmButton.cell as? NSButtonCell
} }
} else { } else {
window?.defaultButtonCell = confirmButton.cell as? NSButtonCell window?.defaultButtonCell = confirmButton.cell as? NSButtonCell
} }
titleTextField.stringValue = title titleTextField.stringValue = title
oldFrame = contentTextField.frame oldFrame = contentTextField.frame
contentTextField.stringValue = content contentTextField.stringValue = content
var infiniteHeightFrame = oldFrame var infiniteHeightFrame = oldFrame
infiniteHeightFrame.size.width -= 4.0 infiniteHeightFrame.size.width -= 4.0
infiniteHeightFrame.size.height = 10240 infiniteHeightFrame.size.height = 10240
newFrame = (content as NSString).boundingRect(with: infiniteHeightFrame.size, options: [.usesLineFragmentOrigin], attributes: [.font: contentTextField.font!]) newFrame = (content as NSString).boundingRect(
newFrame.size.width = max(newFrame.size.width, oldFrame.size.width) with: infiniteHeightFrame.size, options: [.usesLineFragmentOrigin],
newFrame.size.height += 4.0 attributes: [.font: contentTextField.font!])
newFrame.origin = oldFrame.origin newFrame.size.width = max(newFrame.size.width, oldFrame.size.width)
newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height) newFrame.size.height += 4.0
contentTextField.frame = newFrame newFrame.origin = oldFrame.origin
newFrame.origin.y -= (newFrame.size.height - oldFrame.size.height)
contentTextField.frame = newFrame
var windowFrame = window?.frame ?? NSRect.zero var windowFrame = window?.frame ?? NSRect.zero
windowFrame.size.height += (newFrame.size.height - oldFrame.size.height) windowFrame.size.height += (newFrame.size.height - oldFrame.size.height)
window?.level = NSWindow.Level(Int(CGShieldingWindowLevel()) + 1) window?.level = NSWindow.Level(Int(CGShieldingWindowLevel()) + 1)
window?.setFrame(windowFrame, display: true) window?.setFrame(windowFrame, display: true)
window?.center() window?.center()
window?.makeKeyAndOrderFront(self) window?.makeKeyAndOrderFront(self)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} }
@IBAction func confirmButtonAction(_ sender: Any) { @IBAction func confirmButtonAction(_ sender: Any) {
delegate?.ctlNonModalAlertWindowDidConfirm(self) delegate?.ctlNonModalAlertWindowDidConfirm(self)
window?.orderOut(self) window?.orderOut(self)
} }
@IBAction func cancelButtonAction(_ sender: Any) { @IBAction func cancelButtonAction(_ sender: Any) {
cancel(sender) cancel(sender)
} }
func cancel(_ sender: Any) { func cancel(_ sender: Any) {
delegate?.ctlNonModalAlertWindowDidCancel(self) delegate?.ctlNonModalAlertWindowDidCancel(self)
delegate = nil delegate = nil
window?.orderOut(self) window?.orderOut(self)
} }
} }

View File

@ -1,279 +1,301 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Carbon
import Cocoa
// Extend the RangeReplaceableCollection to allow it clean duplicated characters. // Extend the RangeReplaceableCollection to allow it clean duplicated characters.
extension RangeReplaceableCollection where Element: Hashable { extension RangeReplaceableCollection where Element: Hashable {
var charDeDuplicate: Self { var charDeDuplicate: Self {
var set = Set<Element>() var set = Set<Element>()
return filter{ set.insert($0).inserted } return filter { set.insert($0).inserted }
} }
} }
// Please note that the class should be exposed using the same class name // 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 // in Objective-C in order to let IMK to see the same class name as
// the "InputMethodServerPreferencesWindowControllerClass" in Info.plist. // the "InputMethodServerPreferencesWindowControllerClass" in Info.plist.
@objc(ctlPrefWindow) class ctlPrefWindow: NSWindowController { @objc(ctlPrefWindow) class ctlPrefWindow: NSWindowController {
@IBOutlet weak var fontSizePopUpButton: NSPopUpButton! @IBOutlet weak var fontSizePopUpButton: NSPopUpButton!
@IBOutlet weak var uiLanguageButton: NSPopUpButton! @IBOutlet weak var uiLanguageButton: NSPopUpButton!
@IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton! @IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton!
@IBOutlet weak var selectionKeyComboBox: NSComboBox! @IBOutlet weak var selectionKeyComboBox: NSComboBox!
@IBOutlet weak var chkTrad2KangXi: NSButton! @IBOutlet weak var chkTrad2KangXi: NSButton!
@IBOutlet weak var chkTrad2JISShinjitai: NSButton! @IBOutlet weak var chkTrad2JISShinjitai: NSButton!
@IBOutlet weak var lblCurrentlySpecifiedUserDataFolder: NSTextFieldCell! @IBOutlet weak var lblCurrentlySpecifiedUserDataFolder: NSTextFieldCell!
var currentLanguageSelectItem: NSMenuItem? = nil
override func windowDidLoad() { var currentLanguageSelectItem: NSMenuItem? = nil
super.windowDidLoad()
lblCurrentlySpecifiedUserDataFolder.placeholderString = mgrLangModel.dataFolderPath(isDefaultFolder: true) override func windowDidLoad() {
super.windowDidLoad()
let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"] lblCurrentlySpecifiedUserDataFolder.placeholderString = mgrLangModel.dataFolderPath(
var autoMUISelectItem: NSMenuItem? = nil isDefaultFolder: true)
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)
let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource] let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"]
var usKeyboardLayoutItem: NSMenuItem? = nil var autoMUISelectItem: NSMenuItem? = nil
var chosenBaseKeyboardLayoutItem: 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() if language == "auto" {
menuItem_AppleZhuyinBopomofo.title = String(format: NSLocalizedString("Apple Zhuyin Bopomofo", comment: "")) autoMUISelectItem = menuItem
menuItem_AppleZhuyinBopomofo.representedObject = String("com.apple.keylayout.ZhuyinBopomofo") }
basisKeyboardLayoutButton.menu?.addItem(menuItem_AppleZhuyinBopomofo)
let menuItem_AppleZhuyinEten = NSMenuItem() if !appleLanguages.isEmpty {
menuItem_AppleZhuyinEten.title = String(format: NSLocalizedString("Apple Zhuyin Eten", comment: "")) if appleLanguages[0] == language {
menuItem_AppleZhuyinEten.representedObject = String("com.apple.keylayout.ZhuyinEten") chosenLanguageItem = menuItem
basisKeyboardLayoutButton.menu?.addItem(menuItem_AppleZhuyinEten) }
}
uiLanguageButton.menu?.addItem(menuItem)
}
let basisKeyboardLayoutID = mgrPrefs.basisKeyboardLayout currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem
uiLanguageButton.select(currentLanguageSelectItem)
for source in list { let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) { var usKeyboardLayoutItem: NSMenuItem? = nil
let category = Unmanaged<CFString>.fromOpaque(categoryPtr).takeUnretainedValue() var chosenBaseKeyboardLayoutItem: NSMenuItem? = nil
if category != kTISCategoryKeyboardInputSource {
continue
}
} else {
continue
}
if let asciiCapablePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsASCIICapable) { basisKeyboardLayoutButton.menu?.removeAllItems()
let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(asciiCapablePtr).takeUnretainedValue()
if asciiCapable != kCFBooleanTrue {
continue
}
} else {
continue
}
if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) { let itmAppleZhuyinBopomofo = NSMenuItem()
let sourceType = Unmanaged<CFString>.fromOpaque(sourceTypePtr).takeUnretainedValue() itmAppleZhuyinBopomofo.title = String(
if sourceType != kTISTypeKeyboardLayout { format: NSLocalizedString("Apple Zhuyin Bopomofo", comment: ""))
continue itmAppleZhuyinBopomofo.representedObject = String(
} "com.apple.keylayout.ZhuyinBopomofo")
} else { basisKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinBopomofo)
continue
}
guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID), let itmAppleZhuyinEten = NSMenuItem()
let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName) else { itmAppleZhuyinEten.title = String(
continue format: NSLocalizedString("Apple Zhuyin Eten", comment: ""))
} itmAppleZhuyinEten.representedObject = String("com.apple.keylayout.ZhuyinEten")
basisKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinEten)
let sourceID = String(Unmanaged<CFString>.fromOpaque(sourceIDPtr).takeUnretainedValue()) let basisKeyboardLayoutID = mgrPrefs.basisKeyboardLayout
let localizedName = String(Unmanaged<CFString>.fromOpaque(localizedNamePtr).takeUnretainedValue())
let menuItem = NSMenuItem() for source in list {
menuItem.title = localizedName if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) {
menuItem.representedObject = sourceID let category = Unmanaged<CFString>.fromOpaque(categoryPtr).takeUnretainedValue()
if category != kTISCategoryKeyboardInputSource {
continue
}
} else {
continue
}
if sourceID == "com.apple.keylayout.US" { if let asciiCapablePtr = TISGetInputSourceProperty(
usKeyboardLayoutItem = menuItem source, kTISPropertyInputSourceIsASCIICapable)
} {
if basisKeyboardLayoutID == sourceID { let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(asciiCapablePtr)
chosenBaseKeyboardLayoutItem = menuItem .takeUnretainedValue()
} if asciiCapable != kCFBooleanTrue {
basisKeyboardLayoutButton.menu?.addItem(menuItem) continue
} }
} else {
continue
}
switch basisKeyboardLayoutID { if let sourceTypePtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) {
case "com.apple.keylayout.ZhuyinBopomofo": let sourceType = Unmanaged<CFString>.fromOpaque(sourceTypePtr).takeUnretainedValue()
chosenBaseKeyboardLayoutItem = menuItem_AppleZhuyinBopomofo if sourceType != kTISTypeKeyboardLayout {
case "com.apple.keylayout.ZhuyinEten": continue
chosenBaseKeyboardLayoutItem = menuItem_AppleZhuyinEten }
default: } else {
break // nothing to do continue
} }
basisKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem) guard let sourceIDPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName)
else {
continue
}
selectionKeyComboBox.usesDataSource = false let sourceID = String(Unmanaged<CFString>.fromOpaque(sourceIDPtr).takeUnretainedValue())
selectionKeyComboBox.removeAllItems() let localizedName = String(
selectionKeyComboBox.addItems(withObjectValues: mgrPrefs.suggestedCandidateKeys) Unmanaged<CFString>.fromOpaque(localizedNamePtr).takeUnretainedValue())
var candidateSelectionKeys = mgrPrefs.candidateKeys let menuItem = NSMenuItem()
if candidateSelectionKeys.isEmpty { menuItem.title = localizedName
candidateSelectionKeys = mgrPrefs.defaultCandidateKeys menuItem.representedObject = sourceID
}
selectionKeyComboBox.stringValue = candidateSelectionKeys if sourceID == "com.apple.keylayout.US" {
} usKeyboardLayoutItem = menuItem
}
if basisKeyboardLayoutID == sourceID {
chosenBaseKeyboardLayoutItem = menuItem
}
basisKeyboardLayoutButton.menu?.addItem(menuItem)
}
// CNS switch basisKeyboardLayoutID {
// case "com.apple.keylayout.ZhuyinBopomofo":
@IBAction func toggleCNSSupport(_ sender: Any) { chosenBaseKeyboardLayoutItem = itmAppleZhuyinBopomofo
mgrLangModel.setCNSEnabled(mgrPrefs.cns11643Enabled) case "com.apple.keylayout.ZhuyinEten":
} chosenBaseKeyboardLayoutItem = itmAppleZhuyinEten
default:
break // nothing to do
}
@IBAction func toggleSymbolInputEnabled(_ sender: Any) { basisKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem)
mgrLangModel.setSymbolEnabled(mgrPrefs.symbolInputEnabled)
}
@IBAction func toggleTrad2KangXiAction(_ sender: Any) { selectionKeyComboBox.usesDataSource = false
if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on { selectionKeyComboBox.removeAllItems()
mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() selectionKeyComboBox.addItems(withObjectValues: mgrPrefs.suggestedCandidateKeys)
}
}
@IBAction func toggleTrad2JISShinjitaiAction(_ sender: Any) { var candidateSelectionKeys = mgrPrefs.candidateKeys
if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on { if candidateSelectionKeys.isEmpty {
mgrPrefs.toggleChineseConversionEnabled() candidateSelectionKeys = mgrPrefs.defaultCandidateKeys
} }
}
@IBAction func updateBasisKeyboardLayoutAction(_ sender: Any) { selectionKeyComboBox.stringValue = candidateSelectionKeys
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)
}
}
@IBAction func clickedWhetherIMEShouldNotFartToggleAction(_ sender: Any) { // CNS
clsSFX.beep() //
} @IBAction func toggleCNSSupport(_ sender: Any) {
mgrLangModel.setCNSEnabled(mgrPrefs.cns11643Enabled)
}
@IBAction func changeSelectionKeyAction(_ sender: Any) { @IBAction func toggleSymbolInputEnabled(_ sender: Any) {
guard let keys = (sender as AnyObject).stringValue?.trimmingCharacters(in: .whitespacesAndNewlines).charDeDuplicate else { mgrLangModel.setSymbolEnabled(mgrPrefs.symbolInputEnabled)
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) { @IBAction func toggleTrad2KangXiAction(_ sender: Any) {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified") if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on {
IME.initLangModels(userOnly: true) mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
} }
}
@IBAction func chooseUserDataFolderToSpecify(_ sender: Any) { @IBAction func toggleTrad2JISShinjitaiAction(_ sender: Any) {
IME.dlgOpenPath.title = NSLocalizedString("Choose your desired user data folder.", comment: ""); if chkTrad2KangXi.state == .on && chkTrad2JISShinjitai.state == .on {
IME.dlgOpenPath.showsResizeIndicator = true; mgrPrefs.toggleChineseConversionEnabled()
IME.dlgOpenPath.showsHiddenFiles = true; }
IME.dlgOpenPath.canChooseFiles = false; }
IME.dlgOpenPath.canChooseDirectories = true;
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 { @IBAction func updateUiLanguageAction(_ sender: Any) {
IME.dlgOpenPath.beginSheetModal(for: self.window!) { result in if let selectItem = uiLanguageButton.selectedItem {
if result == NSApplication.ModalResponse.OK { if currentLanguageSelectItem == selectItem {
if (IME.dlgOpenPath.url != nil) { return
if (mgrLangModel.checkIfSpecifiedUserDataFolderValid(IME.dlgOpenPath.url!.path)) { }
mgrPrefs.userDataFolderSpecified = IME.dlgOpenPath.url!.path }
IME.initLangModels(userOnly: true) if let language = uiLanguageButton.selectedItem?.representedObject as? String {
} else { if language != "auto" {
clsSFX.beep() mgrPrefs.appleLanguages = [language]
if !PreviousFolderValidity { } else {
self.resetSpecifiedUserDataFolder(self) UserDefaults.standard.removeObject(forKey: "AppleLanguages")
} }
return
} NSLog("vChewing App self-terminated due to UI language change.")
} NSApplication.shared.terminate(nil)
} else { }
if !PreviousFolderValidity { }
self.resetSpecifiedUserDataFolder(self)
} @IBAction func clickedWhetherIMEShouldNotFartToggleAction(_ sender: Any) {
return clsSFX.beep()
} }
}
} // End If self.window != nil @IBAction func changeSelectionKeyAction(_ sender: Any) {
} // End IBAction 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
} }

View File

@ -1,19 +1,25 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@ -21,31 +27,31 @@ import Cocoa
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to tear down your application // Insert code here to initialize your application
} }
func applicationShouldTerminate(_ sender: NSApplication)-> NSApplication.TerminateReply { func applicationWillTerminate(_ aNotification: Notification) {
return .terminateNow // Insert code here to tear down your application
} }
// New About Window
@objc func showAbout() { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
if (ctlAboutWindowInstance == nil) { return .terminateNow
ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow") }
} // New About Window
ctlAboutWindowInstance?.window?.center() @objc func showAbout() {
ctlAboutWindowInstance?.window?.orderFrontRegardless() // if ctlAboutWindowInstance == nil {
ctlAboutWindowInstance?.window?.level = .statusBar ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow")
} }
// Call the New About Window ctlAboutWindowInstance?.window?.center()
@IBAction func about(_ sender: Any) { ctlAboutWindowInstance?.window?.orderFrontRegardless() //
(NSApp.delegate as? AppDelegate)?.showAbout() ctlAboutWindowInstance?.window?.level = .statusBar
NSApplication.shared.activate(ignoringOtherApps: true) }
} // Call the New About Window
@IBAction func about(_ sender: Any) {
(NSApp.delegate as? AppDelegate)?.showAbout()
NSApplication.shared.activate(ignoringOtherApps: true)
}
} }

View File

@ -1,41 +1,47 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
import Foundation
class Content: NSObject { class Content: NSObject {
@objc dynamic var contentString = "" @objc dynamic var contentString = ""
public init(contentString: String) { public init(contentString: String) {
self.contentString = contentString self.contentString = contentString
} }
} }
extension Content { extension Content {
func read(from data: Data) { func read(from data: Data) {
contentString = String(bytes: data, encoding: .utf8)! contentString = String(bytes: data, encoding: .utf8)!
} }
func data() -> Data? { func data() -> Data? {
return contentString.data(using: .utf8) return contentString.data(using: .utf8)
} }
} }

View File

@ -1,131 +1,145 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
class Document: NSDocument { class Document: NSDocument {
@objc var content = Content(contentString: "") @objc var content = Content(contentString: "")
var contentViewController: ViewController! var contentViewController: ViewController!
override init() { override init() {
super.init() super.init()
// Add your subclass-specific initialization here. // Add your subclass-specific initialization here.
} }
// MARK: - Enablers // MARK: - Enablers
// This enables auto save. // This enables auto save.
override class var autosavesInPlace: Bool { override class var autosavesInPlace: Bool {
return true return true
} }
// This enables asynchronous-writing. // This enables asynchronous-writing.
override func canAsynchronouslyWrite(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType) -> Bool { override func canAsynchronouslyWrite(
return true 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" // This enables asynchronous reading.
} override class func canConcurrentlyReadDocuments(ofType: String) -> Bool {
return ofType == "public.plain-text"
// MARK: - User Interface }
/// - Tag: makeWindowControllersExample // MARK: - User Interface
override func makeWindowControllers() {
// Returns the storyboard that contains your document window. /// - Tag: makeWindowControllersExample
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) override func makeWindowControllers() {
if let windowController = // Returns the storyboard that contains your document window.
storyboard.instantiateController( let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as? NSWindowController { if let windowController =
addWindowController(windowController) storyboard.instantiateController(
withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller"))
// Set the view controller's represented object as your document. as? NSWindowController
if let contentVC = windowController.contentViewController as? ViewController { {
contentVC.representedObject = content addWindowController(windowController)
contentViewController = contentVC
} // 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) // MARK: - Reading and Writing
strToDealWith.formatConsolidate(HYPY2BPMF: false)
let processedIncomingData = Data(strToDealWith.utf8) /// - Tag: readExample
content.read(from: processedIncomingData) override func read(from data: Data, ofType typeName: String) throws {
} var strToDealWith = String(decoding: data, as: UTF8.self)
strToDealWith.formatConsolidate(cnvHYPYtoBPMF: false)
/// - Tag: writeExample let processedIncomingData = Data(strToDealWith.utf8)
override func data(ofType typeName: String) throws -> Data { content.read(from: processedIncomingData)
var strToDealWith = content.contentString }
strToDealWith.formatConsolidate(HYPY2BPMF: true)
let outputData = Data(strToDealWith.utf8) /// - Tag: writeExample
return outputData override func data(ofType typeName: String) throws -> Data {
} var strToDealWith = content.contentString
strToDealWith.formatConsolidate(cnvHYPYtoBPMF: true)
// MARK: - Printing let outputData = Data(strToDealWith.utf8)
return outputData
func thePrintInfo() -> NSPrintInfo { }
let thePrintInfo = NSPrintInfo()
thePrintInfo.horizontalPagination = .fit // MARK: - Printing
thePrintInfo.isHorizontallyCentered = false
thePrintInfo.isVerticallyCentered = false func thePrintInfo() -> NSPrintInfo {
let thePrintInfo = NSPrintInfo()
// One inch margin all the way around. thePrintInfo.horizontalPagination = .fit
thePrintInfo.leftMargin = 72.0 thePrintInfo.isHorizontallyCentered = false
thePrintInfo.rightMargin = 72.0 thePrintInfo.isVerticallyCentered = false
thePrintInfo.topMargin = 72.0
thePrintInfo.bottomMargin = 72.0 // One inch margin all the way around.
thePrintInfo.leftMargin = 72.0
printInfo.dictionary().setObject(NSNumber(value: true), thePrintInfo.rightMargin = 72.0
forKey: NSPrintInfo.AttributeKey.headerAndFooter as NSCopying) thePrintInfo.topMargin = 72.0
thePrintInfo.bottomMargin = 72.0
return thePrintInfo
} printInfo.dictionary().setObject(
NSNumber(value: true),
@objc forKey: NSPrintInfo.AttributeKey.headerAndFooter as NSCopying)
func printOperationDidRun(
_ printOperation: NSPrintOperation, success: Bool, contextInfo: UnsafeMutableRawPointer?) { return thePrintInfo
// Printing finished... }
}
@objc
@IBAction override func printDocument(_ sender: Any?) { func printOperationDidRun(
// Print the NSTextView. _ printOperation: NSPrintOperation, success: Bool, contextInfo: UnsafeMutableRawPointer?
) {
// Create a copy to manipulate for printing. // Printing finished...
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))
@IBAction override func printDocument(_ sender: Any?) {
// Make sure we print on a white background. // Print the NSTextView.
textView.appearance = NSAppearance(named: .aqua)
// Create a copy to manipulate for printing.
// Copy the attributed string. let pageSize = NSSize(
textView.textStorage?.append(NSAttributedString(string: content.contentString)) width: (printInfo.paperSize.width), height: (printInfo.paperSize.height))
let textView = NSTextView(
let printOperation = NSPrintOperation(view: textView) frame: NSRect(x: 0.0, y: 0.0, width: pageSize.width, height: pageSize.height))
printOperation.runModal(
for: windowControllers[0].window!, // Make sure we print on a white background.
delegate: self, textView.appearance = NSAppearance(named: .aqua)
didRun: #selector(printOperationDidRun(_:success:contextInfo:)), contextInfo: nil)
} // 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)
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +1,65 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
class ViewController: NSViewController, NSTextViewDelegate { class ViewController: NSViewController, NSTextViewDelegate {
/// - Tag: setRepresentedObjectExample /// - Tag: setRepresentedObjectExample
override var representedObject: Any? { override var representedObject: Any? {
didSet { didSet {
// Pass down the represented object to all of the child view controllers. // Pass down the represented object to all of the child view controllers.
for child in children { for child in children {
child.representedObject = representedObject child.representedObject = representedObject
} }
} }
} }
weak var document: Document? { weak var document: Document? {
if let docRepresentedObject = representedObject as? Document { if let docRepresentedObject = representedObject as? Document {
return docRepresentedObject return docRepresentedObject
} }
return nil return nil
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
// Do any additional setup after loading the view. // Do any additional setup after loading the view.
} }
override func viewDidAppear() { override func viewDidAppear() {
super.viewDidAppear() super.viewDidAppear()
} }
// MARK: - NSTextViewDelegate // MARK: - NSTextViewDelegate
func textDidBeginEditing(_ notification: Notification) { func textDidBeginEditing(_ notification: Notification) {
document?.objectDidBeginEditing(self) document?.objectDidBeginEditing(self)
} }
func textDidEndEditing(_ notification: Notification) { func textDidEndEditing(_ notification: Notification) {
document?.objectDidEndEditing(self) document?.objectDidEndEditing(self)
} }
} }

View File

@ -1,35 +1,41 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
class WindowController: NSWindowController, NSWindowDelegate { class WindowController: NSWindowController, NSWindowDelegate {
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad() super.windowDidLoad()
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) super.init(coder: aDecoder)
/** NSWindows loaded from the storyboard will be cascaded /** NSWindows loaded from the storyboard will be cascaded
based on the original frame of the window in the storyboard. based on the original frame of the window in the storyboard.
*/ */
shouldCascadeWindows = true shouldCascadeWindows = true
} }
} }

View File

@ -1,51 +1,64 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License). // 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 Permission is hereby granted, free of charge, to any person obtaining a copy of
documentation files (the "Software"), to deal in the Software without restriction, including without limitation this software and associated documentation files (the "Software"), to deal in
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and the Software without restriction, including without limitation the rights to
to permit persons to whom the Software is furnished to do so, subject to the following conditions: 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, 2. No trademark license is granted to use the trade names, trademarks, service
except as required to fulfill notice requirements above. 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 Cocoa
@objc(AboutWindow) class ctlAboutWindow: NSWindowController { @objc(AboutWindow) class ctlAboutWindow: NSWindowController {
@IBOutlet weak var appVersionLabel: NSTextField! @IBOutlet weak var appVersionLabel: NSTextField!
@IBOutlet weak var appCopyrightLabel: NSTextField! @IBOutlet weak var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView! @IBOutlet var appEULAContent: NSTextView!
override func windowDidLoad() { override func windowDidLoad() {
super.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)
}
@IBAction func btnWiki(_ sender: NSButton) { window?.standardWindowButton(.closeButton)?.isHidden = true
if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") { window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
NSWorkspace.shared.open(url) 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)
}
}
} }