Swift // Clang-Format.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,69 +1,76 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor,
except as required to fulfill notice requirements above.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
public extension NSString {
extension NSString {
/// Converts the index in an NSString to the index in a Swift string.
///
/// An Emoji might be compose by more than one UTF-16 code points, however
/// the length of an NSString is only the sum of the UTF-16 code points. It
/// causes that the NSString and Swift string representation of the same
/// string have different lengths once the string contains such Emoji. The
/// method helps to find the index in a Swift string by passing the index
/// in an NSString.
func characterIndex(from utf16Index:Int) -> (Int, String) {
let string = (self as String)
var length = 0
for (i, character) in string.enumerated() {
length += character.utf16.count
if length > utf16Index {
return (i, string)
}
}
return (string.count, string)
}
/// Converts the index in an NSString to the index in a Swift string.
///
/// An Emoji might be compose by more than one UTF-16 code points, however
/// the length of an NSString is only the sum of the UTF-16 code points. It
/// causes that the NSString and Swift string representation of the same
/// string have different lengths once the string contains such Emoji. The
/// method helps to find the index in a Swift string by passing the index
/// in an NSString.
public func characterIndex(from utf16Index: Int) -> (Int, String) {
let string = (self as String)
var length = 0
for (i, character) in string.enumerated() {
length += character.utf16.count
if length > utf16Index {
return (i, string)
}
}
return (string.count, string)
}
@objc func nextUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex < string.count {
fixedIndex += 1
}
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
}
@objc public func nextUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex < string.count {
fixedIndex += 1
}
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
}
@objc func previousUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex > 0 {
fixedIndex -= 1
}
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
}
@objc public func previousUtf16Position(for index: Int) -> Int {
var (fixedIndex, string) = characterIndex(from: index)
if fixedIndex > 0 {
fixedIndex -= 1
}
return string[..<string.index(string.startIndex, offsetBy: fixedIndex)].utf16.count
}
@objc var count: Int {
(self as String).count
}
@objc public var count: Int {
(self as String).count
}
@objc func split() -> [NSString] {
Array(self as String).map {
NSString(string: String($0))
}
}
@objc public func split() -> [NSString] {
Array(self as String).map {
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).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor,
except as required to fulfill notice requirements above.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
public protocol FSEventStreamHelperDelegate: AnyObject {
func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event])
func helper(_ helper: FSEventStreamHelper, didReceive events: [FSEventStreamHelper.Event])
}
public class FSEventStreamHelper : NSObject {
public class FSEventStreamHelper: NSObject {
public struct Event {
var path: String
var flags: FSEventStreamEventFlags
var id: FSEventStreamEventId
}
public struct Event {
var path: String
var flags: FSEventStreamEventFlags
var id: FSEventStreamEventId
}
public let path: String
public let dispatchQueue: DispatchQueue
public weak var delegate: FSEventStreamHelperDelegate?
public let path: String
public let dispatchQueue: DispatchQueue
public weak var delegate: FSEventStreamHelperDelegate?
@objc public init(path: String, queue: DispatchQueue) {
self.path = path
self.dispatchQueue = queue
}
@objc public init(path: String, queue: DispatchQueue) {
self.path = path
self.dispatchQueue = queue
}
private var stream: FSEventStreamRef? = nil
private var stream: FSEventStreamRef? = nil
public func start() -> Bool {
if stream != nil {
return false
}
var context = FSEventStreamContext()
context.info = Unmanaged.passUnretained(self).toOpaque()
guard let stream = FSEventStreamCreate(nil, {
(stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in
let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!).takeUnretainedValue()
let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount)
let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount)
let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount)
let events = (0..<eventCount).map {
FSEventStreamHelper.Event(path: String(cString: pathsPtr[$0]),
flags: flagsPtr[$0],
id: eventIDsPtr[$0] )
}
helper.delegate?.helper(helper, didReceive: events)
},
&context,
[path] as CFArray,
UInt64(kFSEventStreamEventIdSinceNow),
1.0,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
) else {
return false
}
public func start() -> Bool {
if stream != nil {
return false
}
var context = FSEventStreamContext()
context.info = Unmanaged.passUnretained(self).toOpaque()
guard
let stream = FSEventStreamCreate(
nil,
{
(stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in
let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!)
.takeUnretainedValue()
let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
let pathsPtr = UnsafeBufferPointer(start: pathsBase, count: eventCount)
let flagsPtr = UnsafeBufferPointer(start: eventFlags, count: eventCount)
let eventIDsPtr = UnsafeBufferPointer(start: eventIds, count: eventCount)
let events = (0..<eventCount).map {
FSEventStreamHelper.Event(
path: String(cString: pathsPtr[$0]),
flags: flagsPtr[$0],
id: eventIDsPtr[$0])
}
helper.delegate?.helper(helper, didReceive: events)
},
&context,
[path] as CFArray,
UInt64(kFSEventStreamEventIdSinceNow),
1.0,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
)
else {
return false
}
FSEventStreamSetDispatchQueue(stream, dispatchQueue)
if !FSEventStreamStart(stream) {
FSEventStreamInvalidate(stream)
return false
}
self.stream = stream
return true
}
FSEventStreamSetDispatchQueue(stream, dispatchQueue)
if !FSEventStreamStart(stream) {
FSEventStreamInvalidate(stream)
return false
}
self.stream = stream
return true
}
func stop() {
guard let stream = stream else {
return
}
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
self.stream = nil
}
func stop() {
guard let stream = stream else {
return
}
FSEventStreamStop(stream)
FSEventStreamInvalidate(stream)
self.stream = nil
}
}

View File

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

View File

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

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

View File

@ -235,7 +235,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
[currentMarkedPhrase appendString:userPhrase];
if (areWeDuplicating && !areWeDeleting) {
// Do not use ASCII characters to comment here.
// Otherwise, it will be scrambled by HYPY2BPMF module shipped in the vChewing Phrase Editor.
// Otherwise, it will be scrambled by cnvHYPYtoBPMF module shipped in the vChewing Phrase Editor.
[currentMarkedPhrase appendString:@"\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"];
}
[currentMarkedPhrase appendString:@"\n"];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,51 +1,64 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor,
except as required to fulfill notice requirements above.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
@objc(AboutWindow) class ctlAboutWindow: NSWindowController {
@IBOutlet weak var appVersionLabel: NSTextField!
@IBOutlet weak var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
@IBOutlet weak var appVersionLabel: NSTextField!
@IBOutlet weak var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
override func windowDidLoad() {
super.windowDidLoad()
window?.standardWindowButton(.closeButton)?.isHidden = true
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
window?.standardWindowButton(.zoomButton)?.isHidden = true
guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return
}
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String {
appCopyrightLabel.stringValue = copyrightLabel
}
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
appEULAContent.string = eulaContent
}
appVersionLabel.stringValue = String(format: "%@ Build %@", versionString, installingVersion)
}
override func windowDidLoad() {
super.windowDidLoad()
@IBAction func btnWiki(_ sender: NSButton) {
if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") {
NSWorkspace.shared.open(url)
}
}
window?.standardWindowButton(.closeButton)?.isHidden = true
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
window?.standardWindowButton(.zoomButton)?.isHidden = true
guard
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
else {
return
}
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"]
as? String
{
appCopyrightLabel.stringValue = copyrightLabel
}
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
appEULAContent.string = eulaContent
}
appVersionLabel.stringValue = String(
format: "%@ Build %@", versionString, installingVersion)
}
@IBAction func btnWiki(_ sender: NSButton) {
if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") {
NSWorkspace.shared.open(url)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,51 +1,64 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor,
except as required to fulfill notice requirements above.
2. No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
@objc(AboutWindow) class ctlAboutWindow: NSWindowController {
@IBOutlet weak var appVersionLabel: NSTextField!
@IBOutlet weak var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
@IBOutlet weak var appVersionLabel: NSTextField!
@IBOutlet weak var appCopyrightLabel: NSTextField!
@IBOutlet var appEULAContent: NSTextView!
override func windowDidLoad() {
super.windowDidLoad()
window?.standardWindowButton(.closeButton)?.isHidden = true
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
window?.standardWindowButton(.zoomButton)?.isHidden = true
guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return
}
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"] as? String {
appCopyrightLabel.stringValue = copyrightLabel
}
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
appEULAContent.string = eulaContent
}
appVersionLabel.stringValue = String(format: "%@ Build %@", versionString, installingVersion)
}
override func windowDidLoad() {
super.windowDidLoad()
@IBAction func btnWiki(_ sender: NSButton) {
if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") {
NSWorkspace.shared.open(url)
}
}
window?.standardWindowButton(.closeButton)?.isHidden = true
window?.standardWindowButton(.miniaturizeButton)?.isHidden = true
window?.standardWindowButton(.zoomButton)?.isHidden = true
guard
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
else {
return
}
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"]
as? String
{
appCopyrightLabel.stringValue = copyrightLabel
}
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
appEULAContent.string = eulaContent
}
appVersionLabel.stringValue = String(
format: "%@ Build %@", versionString, installingVersion)
}
@IBAction func btnWiki(_ sender: NSButton) {
if let url = URL(string: "https://gitee.com/vchewing/vChewing-macOS/wikis") {
NSWorkspace.shared.open(url)
}
}
}