Compare commits
44 Commits
Author | SHA1 | Date |
---|---|---|
|
abb17e9ff6 | |
|
afad2076b0 | |
|
40245cf1d5 | |
|
08a54c14c1 | |
|
67a6bb5f87 | |
|
97a9e9aa5c | |
|
c932083c5f | |
|
25d8f7c093 | |
|
14adf03311 | |
|
817df50916 | |
|
e8961ff33f | |
|
2d23deb83a | |
|
fc5243c97f | |
|
9e1d130ba7 | |
|
5a6aee2a25 | |
|
aa4162fa9b | |
|
951f41461a | |
|
f46cfda6f5 | |
|
03edccff4f | |
|
d8aba434d9 | |
|
005116c429 | |
|
93256f0095 | |
|
11bb5a9b66 | |
|
9dc7821708 | |
|
5d62d5b66d | |
|
c63c531f1b | |
|
35d4426730 | |
|
b628ddd082 | |
|
4e00791144 | |
|
1e098cac53 | |
|
0107e7cd78 | |
|
9411686d03 | |
|
5eec7cd604 | |
|
dc79c629a1 | |
|
040c597345 | |
|
923471c8bb | |
|
46d4e7bdb3 | |
|
55dcdc8ce0 | |
|
bd5fdcaa26 | |
|
d5d9167b1e | |
|
c2679735c1 | |
|
4904664277 | |
|
76dd75ce5a | |
|
7be2a85b25 |
|
@ -1058,6 +1058,19 @@ func healthCheck(_ data: [Unigram]) -> String {
|
|||
return result
|
||||
}
|
||||
|
||||
// MARK: - 與主執行緒有關的任務 Flags
|
||||
|
||||
struct TaskFlags: OptionSet {
|
||||
public let rawValue: Int
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let common = TaskFlags(rawValue: 1 << 0)
|
||||
public static let chs = TaskFlags(rawValue: 1 << 1)
|
||||
public static let cht = TaskFlags(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
// MARK: - 主執行緒
|
||||
|
||||
var compileJSON = false
|
||||
|
@ -1080,61 +1093,71 @@ func main() {
|
|||
NSLog("// SQLite 資料庫初期化失敗。")
|
||||
exit(-1)
|
||||
}
|
||||
|
||||
var taskFlags: TaskFlags = [.common, .chs, .cht] {
|
||||
didSet {
|
||||
guard taskFlags.isEmpty else { return }
|
||||
NSLog("// 全部 TXT 辭典檔案建置完畢。")
|
||||
if compileJSON {
|
||||
NSLog("// 全部 JSON 辭典檔案建置完畢。")
|
||||
}
|
||||
if compileSQLite, prepared {
|
||||
NSLog("// 開始整合反查資料。")
|
||||
mapReverseLookupForCheck.forEach { key, values in
|
||||
values.reversed().forEach { valueLiteral in
|
||||
let value = cnvPhonabetToASCII(valueLiteral)
|
||||
if !rangeMapReverseLookup[key, default: []].contains(value) {
|
||||
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
NSLog("// 反查資料整合完畢。")
|
||||
NSLog("// 準備建置 SQL 資料庫。")
|
||||
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
|
||||
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
|
||||
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
|
||||
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
|
||||
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
|
||||
writeRevLookupMapToSQL(rangeMapReverseLookup)
|
||||
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||
assert(committed)
|
||||
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||
assert(compressed)
|
||||
if !dumpSQLDB() {
|
||||
NSLog("// SQLite 辭典傾印失敗。")
|
||||
} else {
|
||||
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
|
||||
}
|
||||
sqlite3_close_v2(ptrSQL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let globalQueue = DispatchQueue.global(qos: .default)
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
|
||||
commonFileOutput()
|
||||
taskFlags.remove(.common)
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
NSLog("// 準備編譯繁體中文核心語料檔案。")
|
||||
fileOutput(isCHS: false)
|
||||
taskFlags.remove(.cht)
|
||||
group.leave()
|
||||
}
|
||||
group.enter()
|
||||
globalQueue.async {
|
||||
NSLog("// 準備編譯簡體中文核心語料檔案。")
|
||||
fileOutput(isCHS: true)
|
||||
taskFlags.remove(.chs)
|
||||
group.leave()
|
||||
}
|
||||
// 一直等待完成
|
||||
group.wait()
|
||||
NSLog("// 全部 TXT 辭典檔案建置完畢。")
|
||||
if compileJSON {
|
||||
NSLog("// 全部 JSON 辭典檔案建置完畢。")
|
||||
}
|
||||
if compileSQLite, prepared {
|
||||
NSLog("// 開始整合反查資料。")
|
||||
mapReverseLookupForCheck.forEach { key, values in
|
||||
values.reversed().forEach { valueLiteral in
|
||||
let value = cnvPhonabetToASCII(valueLiteral)
|
||||
if !rangeMapReverseLookup[key, default: []].contains(value) {
|
||||
rangeMapReverseLookup[key, default: []].insert(value, at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
NSLog("// 反查資料整合完畢。")
|
||||
NSLog("// 準備建置 SQL 資料庫。")
|
||||
writeMainMapToSQL(rangeMapJSONCHS, column: "theDataCHS")
|
||||
writeMainMapToSQL(rangeMapJSONCHT, column: "theDataCHT")
|
||||
writeMainMapToSQL(rangeMapSymbols, column: "theDataSYMB")
|
||||
writeMainMapToSQL(rangeMapZhuyinwen, column: "theDataCHEW")
|
||||
writeMainMapToSQL(rangeMapCNS, column: "theDataCNS")
|
||||
writeRevLookupMapToSQL(rangeMapReverseLookup)
|
||||
let committed = "commit;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||
assert(committed)
|
||||
let compressed = "VACUUM;".runAsSQLExec(dbPointer: &ptrSQL)
|
||||
assert(compressed)
|
||||
if !dumpSQLDB() {
|
||||
NSLog("// SQLite 辭典傾印失敗。")
|
||||
} else {
|
||||
NSLog("// 全部 SQLite 辭典檔案建置完畢。")
|
||||
}
|
||||
sqlite3_close_v2(ptrSQL)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
@ -13,13 +13,13 @@ let package = Package(
|
|||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "NSAttributedTextView",
|
||||
dependencies: [
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
// Modified by The vChewing Project in order to use it with AppKit.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import Foundation
|
||||
@testable import NSAttributedTextView
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import XCTest
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
|
||||
|
||||
import Foundation
|
||||
import SwiftExtension
|
||||
|
||||
public class LineReader {
|
||||
let encoding: String.Encoding
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,35 @@
|
|||
// swift-tools-version: 5.7
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "BrailleSputnik",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "BrailleSputnik",
|
||||
targets: ["BrailleSputnik"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../vChewing_Shared"),
|
||||
.package(path: "../vChewing_Tekkon"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "BrailleSputnik",
|
||||
dependencies: [
|
||||
.product(name: "Shared", package: "vChewing_Shared"),
|
||||
.product(name: "Tekkon", package: "vChewing_Tekkon"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "BrailleSputnikTests",
|
||||
dependencies: ["BrailleSputnik"]
|
||||
),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,284 @@
|
|||
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
extension BrailleSputnik {
|
||||
enum Braille: String {
|
||||
case blank = "⠀" // U+2800
|
||||
case d1 = "⠁"
|
||||
case d2 = "⠂"
|
||||
case d12 = "⠃"
|
||||
case d3 = "⠄"
|
||||
case d13 = "⠅"
|
||||
case d23 = "⠆"
|
||||
case d123 = "⠇"
|
||||
case d4 = "⠈"
|
||||
case d14 = "⠉"
|
||||
case d24 = "⠊"
|
||||
case d124 = "⠋"
|
||||
case d34 = "⠌"
|
||||
case d134 = "⠍"
|
||||
case d234 = "⠎"
|
||||
case d1234 = "⠏"
|
||||
case d5 = "⠐"
|
||||
case d15 = "⠑"
|
||||
case d25 = "⠒"
|
||||
case d125 = "⠓"
|
||||
case d35 = "⠔"
|
||||
case d135 = "⠕"
|
||||
case d235 = "⠖"
|
||||
case d1235 = "⠗"
|
||||
case d45 = "⠘"
|
||||
case d145 = "⠙"
|
||||
case d245 = "⠚"
|
||||
case d1245 = "⠛"
|
||||
case d345 = "⠜"
|
||||
case d1345 = "⠝"
|
||||
case d2345 = "⠞"
|
||||
case d12345 = "⠟"
|
||||
case d6 = "⠠"
|
||||
case d16 = "⠡"
|
||||
case d26 = "⠢"
|
||||
case d126 = "⠣"
|
||||
case d36 = "⠤"
|
||||
case d136 = "⠥"
|
||||
case d236 = "⠦"
|
||||
case d1236 = "⠧"
|
||||
case d46 = "⠨"
|
||||
case d146 = "⠩"
|
||||
case d246 = "⠪"
|
||||
case d1246 = "⠫"
|
||||
case d346 = "⠬"
|
||||
case d1346 = "⠭"
|
||||
case d2346 = "⠮"
|
||||
case d12346 = "⠯"
|
||||
case d56 = "⠰"
|
||||
case d156 = "⠱"
|
||||
case d256 = "⠲"
|
||||
case d1256 = "⠳"
|
||||
case d356 = "⠴"
|
||||
case d1356 = "⠵"
|
||||
case d2356 = "⠶"
|
||||
case d12356 = "⠷"
|
||||
case d456 = "⠸"
|
||||
case d1456 = "⠹"
|
||||
case d2456 = "⠺"
|
||||
case d12456 = "⠻"
|
||||
case d3456 = "⠼"
|
||||
case d13456 = "⠽"
|
||||
case d23456 = "⠾"
|
||||
case d123456 = "⠿"
|
||||
}
|
||||
|
||||
public enum BrailleStandard: Int {
|
||||
case of1947 = 1
|
||||
case of2018 = 2
|
||||
}
|
||||
}
|
||||
|
||||
protocol BrailleProcessingUnit {
|
||||
var mapConsonants: [String: String] { get }
|
||||
var mapSemivowels: [String: String] { get }
|
||||
var mapVowels: [String: String] { get }
|
||||
var mapIntonations: [String: String] { get }
|
||||
var mapIntonationSpecialCases: [String: String] { get }
|
||||
var mapCombinedVowels: [String: String] { get }
|
||||
var mapPunctuations: [String: String] { get }
|
||||
|
||||
func handleSpecialCases(target: inout String, value: String?) -> Bool
|
||||
}
|
||||
|
||||
// MARK: - Static Data conforming to 1947 Standard.
|
||||
|
||||
extension BrailleSputnik {
|
||||
class BrailleProcessingUnit1947: BrailleProcessingUnit {
|
||||
func handleSpecialCases(target _: inout String, value _: String?) -> Bool {
|
||||
// 國語點字標準無最終例外處理步驟。
|
||||
false
|
||||
}
|
||||
|
||||
let mapConsonants: [String: String] = [
|
||||
"ㄎ": "⠇", "ㄋ": "⠝", "ㄕ": "⠊",
|
||||
"ㄌ": "⠉", "ㄆ": "⠏", "ㄇ": "⠍",
|
||||
"ㄓ": "⠁", "ㄏ": "⠗", "ㄖ": "⠛",
|
||||
"ㄅ": "⠕", "ㄑ": "⠚", "ㄘ": "⠚",
|
||||
"ㄗ": "⠓", "ㄙ": "⠑", "ㄐ": "⠅",
|
||||
"ㄉ": "⠙", "ㄈ": "⠟", "ㄔ": "⠃",
|
||||
"ㄒ": "⠑", "ㄊ": "⠋", "ㄍ": "⠅",
|
||||
]
|
||||
|
||||
let mapSemivowels: [String: String] = [
|
||||
"ㄧ": "⠡", "ㄩ": "⠳", "ㄨ": "⠌",
|
||||
]
|
||||
|
||||
let mapVowels: [String: String] = [
|
||||
"ㄤ": "⠭", "ㄛ": "⠣", "ㄠ": "⠩",
|
||||
"ㄞ": "⠺", "ㄜ": "⠮", "ㄡ": "⠷",
|
||||
"ㄟ": "⠴", "ㄣ": "⠥", "ㄥ": "⠵",
|
||||
"ㄢ": "⠧", "ㄚ": "⠜", "ㄦ": "⠱",
|
||||
]
|
||||
|
||||
let mapIntonations: [String: String] = [
|
||||
"˙": "⠱⠁", "ˇ": "⠈", "ˊ": "⠂", " ": "⠄", "ˋ": "⠐",
|
||||
]
|
||||
|
||||
let mapIntonationSpecialCases: [String: String] = [
|
||||
"ㄜ˙": "⠮⠁", "ㄚ˙": "⠜⠁", "ㄛ˙": "⠣⠁", "ㄣ˙": "⠥⠁",
|
||||
]
|
||||
|
||||
let mapCombinedVowels: [String: String] = [
|
||||
"ㄧㄝ": "⠬", "ㄧㄣ": "⠹", "ㄩㄝ": "⠦",
|
||||
"ㄨㄟ": "⠫", "ㄨㄥ": "⠯", "ㄨㄣ": "⠿",
|
||||
"ㄨㄚ": "⠔", "ㄧㄡ": "⠎", "ㄧㄤ": "⠨",
|
||||
"ㄧㄚ": "⠾", "ㄨㄛ": "⠒", "ㄧㄥ": "⠽",
|
||||
"ㄨㄞ": "⠶", "ㄩㄥ": "⠖", "ㄧㄠ": "⠪",
|
||||
"ㄧㄞ": "⠢", "ㄨㄤ": "⠸", "ㄩㄣ": "⠲",
|
||||
"ㄧㄢ": "⠞", "ㄩㄢ": "⠘", "ㄨㄢ": "⠻",
|
||||
]
|
||||
|
||||
let mapPunctuations: [String: String] = [
|
||||
"。": "⠤⠀", "·": "⠤⠀", ",": "⠆", ";": "⠰",
|
||||
"、": "⠠", "?": "⠕⠀", "!": "⠇⠀", ":": "⠒⠒",
|
||||
"╴╴": "⠰⠰", "﹏﹏": "⠠⠤", "……": "⠐⠐⠐",
|
||||
"—": "⠐⠂", "—— ——": "⠐⠂⠐⠂", "※": "⠈⠼", "◎": "⠪⠕",
|
||||
"『": "⠦⠦", "』": "⠴⠴", "「": "⠰⠤", "」": "⠤⠆",
|
||||
"‘": "⠦⠦", "’": "⠴⠴", "“": "⠰⠤", "”": "⠤⠆",
|
||||
"(": "⠪", ")": "⠕", "〔": "⠯", "〕": "⠽",
|
||||
"{": "⠦", "}": "⠴", "[": "⠯", "]": "⠽",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static Data conforming to 2018 Standard (GF0019-2018)
|
||||
|
||||
extension BrailleSputnik {
|
||||
class BrailleProcessingUnit2018: BrailleProcessingUnit {
|
||||
func handleSpecialCases(target: inout String, value: String?) -> Bool {
|
||||
guard let value = value else { return false }
|
||||
switch value {
|
||||
case "他": target = Braille.d2345.rawValue + Braille.d35.rawValue
|
||||
case "它": target = Braille.d4.rawValue + Braille.d2345.rawValue + Braille.d35.rawValue
|
||||
default: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
let mapConsonants: [String: String] = [
|
||||
"ㄅ": Braille.d12.rawValue,
|
||||
"ㄆ": Braille.d1234.rawValue,
|
||||
"ㄇ": Braille.d134.rawValue,
|
||||
"ㄈ": Braille.d124.rawValue,
|
||||
"ㄉ": Braille.d145.rawValue,
|
||||
"ㄊ": Braille.d2345.rawValue,
|
||||
"ㄋ": Braille.d1345.rawValue,
|
||||
"ㄌ": Braille.d123.rawValue,
|
||||
"ㄍ": Braille.d1245.rawValue,
|
||||
"ㄎ": Braille.d13.rawValue,
|
||||
"ㄏ": Braille.d125.rawValue,
|
||||
"ㄐ": Braille.d1245.rawValue,
|
||||
"ㄑ": Braille.d13.rawValue,
|
||||
"ㄒ": Braille.d125.rawValue,
|
||||
"ㄓ": Braille.d34.rawValue,
|
||||
"ㄔ": Braille.d12345.rawValue,
|
||||
"ㄕ": Braille.d156.rawValue,
|
||||
"ㄖ": Braille.d245.rawValue,
|
||||
"ㄗ": Braille.d1356.rawValue,
|
||||
"ㄘ": Braille.d14.rawValue,
|
||||
"ㄙ": Braille.d234.rawValue,
|
||||
]
|
||||
|
||||
let mapSemivowels: [String: String] = [
|
||||
"ㄧ": Braille.d24.rawValue,
|
||||
"ㄨ": Braille.d136.rawValue,
|
||||
"ㄩ": Braille.d346.rawValue,
|
||||
]
|
||||
|
||||
let mapVowels: [String: String] = [
|
||||
"ㄚ": Braille.d35.rawValue,
|
||||
"ㄛ": Braille.d26.rawValue,
|
||||
"ㄜ": Braille.d26.rawValue,
|
||||
"ㄞ": Braille.d246.rawValue,
|
||||
"ㄟ": Braille.d2346.rawValue,
|
||||
"ㄠ": Braille.d235.rawValue,
|
||||
"ㄡ": Braille.d12356.rawValue,
|
||||
"ㄢ": Braille.d1236.rawValue,
|
||||
"ㄣ": Braille.d356.rawValue,
|
||||
"ㄤ": Braille.d236.rawValue,
|
||||
"ㄥ": Braille.d3456.rawValue, // 該注音符號也有合併處理規則。
|
||||
"ㄦ": Braille.d1235.rawValue,
|
||||
]
|
||||
|
||||
let mapIntonations: [String: String] = [
|
||||
" ": Braille.d1.rawValue,
|
||||
"ˊ": Braille.d2.rawValue,
|
||||
"ˇ": Braille.d3.rawValue,
|
||||
"ˋ": Braille.d23.rawValue,
|
||||
// "˙": nil, // 輕聲不設符號。
|
||||
]
|
||||
|
||||
let mapIntonationSpecialCases: [String: String] = [:]
|
||||
|
||||
let mapCombinedVowels: [String: String] = [
|
||||
"ㄧㄚ": Braille.d1246.rawValue,
|
||||
"ㄧㄝ": Braille.d15.rawValue,
|
||||
"ㄧㄞ": Braille.d1246.rawValue, // 此乃特例「崖」,依陸規審音處理。
|
||||
"ㄧㄠ": Braille.d345.rawValue,
|
||||
"ㄧㄡ": Braille.d1256.rawValue,
|
||||
"ㄧㄢ": Braille.d146.rawValue,
|
||||
"ㄧㄣ": Braille.d126.rawValue,
|
||||
"ㄧㄤ": Braille.d1346.rawValue,
|
||||
"ㄧㄥ": Braille.d16.rawValue,
|
||||
"ㄨㄚ": Braille.d123456.rawValue,
|
||||
"ㄨㄛ": Braille.d135.rawValue,
|
||||
"ㄨㄞ": Braille.d13456.rawValue,
|
||||
"ㄨㄟ": Braille.d2456.rawValue,
|
||||
"ㄨㄢ": Braille.d12456.rawValue,
|
||||
"ㄨㄣ": Braille.d25.rawValue,
|
||||
"ㄨㄤ": Braille.d2356.rawValue,
|
||||
"ㄨㄥ": Braille.d256.rawValue,
|
||||
"ㄩㄝ": Braille.d23456.rawValue,
|
||||
"ㄩㄢ": Braille.d12346.rawValue,
|
||||
"ㄩㄣ": Braille.d456.rawValue,
|
||||
"ㄩㄥ": Braille.d1456.rawValue,
|
||||
]
|
||||
|
||||
let mapPunctuations: [String: String] = [
|
||||
"。": Braille.d5.rawValue + Braille.d23.rawValue,
|
||||
"·": Braille.d6.rawValue + Braille.d3.rawValue,
|
||||
",": Braille.d5.rawValue,
|
||||
";": Braille.d56.rawValue,
|
||||
"、": Braille.d4.rawValue,
|
||||
"?": Braille.d5.rawValue + Braille.d3.rawValue,
|
||||
"!": Braille.d56.rawValue + Braille.d2.rawValue,
|
||||
":": Braille.d36.rawValue,
|
||||
"——": Braille.d6.rawValue + Braille.d36.rawValue,
|
||||
"……": Braille.d5.rawValue + Braille.d5.rawValue + Braille.d5.rawValue,
|
||||
"-": Braille.d36.rawValue,
|
||||
"‧": Braille.d5.rawValue, // 著重號。
|
||||
"*": Braille.d2356.rawValue + Braille.d35.rawValue,
|
||||
"《": Braille.d5.rawValue + Braille.d36.rawValue,
|
||||
"》": Braille.d36.rawValue + Braille.d2.rawValue,
|
||||
"〈": Braille.d5.rawValue + Braille.d3.rawValue,
|
||||
"〉": Braille.d6.rawValue + Braille.d2.rawValue,
|
||||
"『": Braille.d45.rawValue + Braille.d45.rawValue,
|
||||
"』": Braille.d45.rawValue + Braille.d45.rawValue,
|
||||
"「": Braille.d45.rawValue,
|
||||
"」": Braille.d45.rawValue,
|
||||
"‘": Braille.d45.rawValue + Braille.d45.rawValue,
|
||||
"’": Braille.d45.rawValue + Braille.d45.rawValue,
|
||||
"“": Braille.d45.rawValue,
|
||||
"”": Braille.d45.rawValue,
|
||||
"(": Braille.d56.rawValue + Braille.d3.rawValue,
|
||||
")": Braille.d6.rawValue + Braille.d23.rawValue,
|
||||
"〔": Braille.d56.rawValue + Braille.d23.rawValue,
|
||||
"〕": Braille.d56.rawValue + Braille.d23.rawValue,
|
||||
"[": Braille.d56.rawValue + Braille.d23.rawValue,
|
||||
"]": Braille.d56.rawValue + Braille.d23.rawValue,
|
||||
// "{": "⠦", "}": "⠴", // 2018 國通標準並未定義花括弧。
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Tekkon
|
||||
|
||||
public class BrailleSputnik {
|
||||
public var standard: BrailleStandard
|
||||
public init(standard: BrailleStandard) {
|
||||
self.standard = standard
|
||||
}
|
||||
|
||||
var staticData: BrailleProcessingUnit {
|
||||
switch standard {
|
||||
case .of1947: return Self.staticData1947
|
||||
case .of2018: return Self.staticData2018
|
||||
}
|
||||
}
|
||||
|
||||
static var sharedComposer = Tekkon.Composer("", arrange: .ofDachen, correction: true)
|
||||
private static let staticData1947: BrailleProcessingUnit = BrailleProcessingUnit1947()
|
||||
private static let staticData2018: BrailleProcessingUnit = BrailleProcessingUnit2018()
|
||||
}
|
||||
|
||||
public extension BrailleSputnik {
|
||||
func convertToBraille(smashedPairs: [(key: String, value: String)], extraInsertion: (reading: String, cursor: Int)? = nil) -> String {
|
||||
var convertedStack: [String?] = []
|
||||
var processedKeysCount = 0
|
||||
var extraInsertion = extraInsertion
|
||||
smashedPairs.forEach { key, value in
|
||||
let subKeys = key.split(separator: "\t")
|
||||
switch subKeys.count {
|
||||
case 0: return
|
||||
case 1:
|
||||
guard !key.isEmpty else { break }
|
||||
let isPunctuation: Bool = key.first == "_" // 檢查是不是標點符號。
|
||||
if isPunctuation {
|
||||
convertedStack.append(convertPunctuationToBraille(value))
|
||||
} else {
|
||||
var key = key.description
|
||||
fixToneOne(target: &key)
|
||||
convertedStack.append(convertPhonabetReadingToBraille(key, value: value))
|
||||
}
|
||||
processedKeysCount += 1
|
||||
default:
|
||||
// 這種情形就是詞音配對不一致的典型情形,此時僅處理注音讀音。
|
||||
subKeys.forEach { subKey in
|
||||
var subKey = subKey.description
|
||||
fixToneOne(target: &subKey)
|
||||
convertedStack.append(convertPhonabetReadingToBraille(subKey))
|
||||
processedKeysCount += 1
|
||||
}
|
||||
}
|
||||
if let theExtraInsertion = extraInsertion, processedKeysCount == theExtraInsertion.cursor {
|
||||
convertedStack.append(convertPhonabetReadingToBraille(theExtraInsertion.reading))
|
||||
extraInsertion = nil
|
||||
}
|
||||
}
|
||||
return convertedStack.compactMap(\.?.description).joined()
|
||||
}
|
||||
|
||||
private func fixToneOne(target key: inout String) {
|
||||
for char in key {
|
||||
guard Tekkon.Phonabet(char.description).type != .null else { return }
|
||||
}
|
||||
if let lastChar = key.last?.description, Tekkon.Phonabet(lastChar).type != .intonation {
|
||||
key += " "
|
||||
}
|
||||
}
|
||||
|
||||
func convertPunctuationToBraille(_ givenTarget: any StringProtocol) -> String? {
|
||||
staticData.mapPunctuations[givenTarget.description]
|
||||
}
|
||||
|
||||
func convertPhonabetReadingToBraille(_ rawReading: any StringProtocol, value referredValue: String? = nil) -> String? {
|
||||
var resultStack = ""
|
||||
// 检查特殊情形。
|
||||
guard !staticData.handleSpecialCases(target: &resultStack, value: referredValue) else { return resultStack }
|
||||
Self.sharedComposer.clear()
|
||||
rawReading.forEach { char in
|
||||
Self.sharedComposer.receiveKey(fromPhonabet: char.description)
|
||||
}
|
||||
let consonant = Self.sharedComposer.consonant.value
|
||||
let semivowel = Self.sharedComposer.semivowel.value
|
||||
let vowel = Self.sharedComposer.vowel.value
|
||||
let intonation = Self.sharedComposer.intonation.value
|
||||
if !consonant.isEmpty {
|
||||
resultStack.append(staticData.mapConsonants[consonant] ?? "")
|
||||
}
|
||||
let combinedVowels = Self.sharedComposer.semivowel.value + Self.sharedComposer.vowel.value
|
||||
if combinedVowels.count == 2 {
|
||||
resultStack.append(staticData.mapCombinedVowels[combinedVowels] ?? "")
|
||||
} else {
|
||||
resultStack.append(staticData.mapSemivowels[semivowel] ?? "")
|
||||
resultStack.append(staticData.mapVowels[vowel] ?? "")
|
||||
}
|
||||
// 聲調處理。
|
||||
if let intonationSpecialCaseMetResult = staticData.mapIntonationSpecialCases[vowel + intonation] {
|
||||
resultStack.append(intonationSpecialCaseMetResult.last?.description ?? "")
|
||||
} else {
|
||||
resultStack.append(staticData.mapIntonations[intonation] ?? "")
|
||||
}
|
||||
return resultStack
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
@testable import BrailleSputnik
|
||||
import XCTest
|
||||
|
||||
final class BrailleSputnikTests: XCTestCase {
|
||||
func testBrailleConversion() throws {
|
||||
// 大丘丘病了二丘丘瞧,三丘丘採藥四丘丘熬。
|
||||
var rawReadingStr = "ㄉㄚˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄅㄧㄥˋ-ㄌㄜ˙-ㄦˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄑㄧㄠˊ-_,"
|
||||
rawReadingStr += "-ㄙㄢ-ㄑㄧㄡ-ㄑㄧㄡ-ㄘㄞˇ-ㄧㄠˋ-ㄙˋ-ㄑㄧㄡ-ㄑㄧㄡ-ㄠˊ-_。"
|
||||
let rawReadingArray: [(key: String, value: String)] = rawReadingStr.split(separator: "-").map {
|
||||
let value: String = $0.first == "_" ? $0.last?.description ?? "" : ""
|
||||
return (key: $0.description, value: value)
|
||||
}
|
||||
let processor = BrailleSputnik(standard: .of1947)
|
||||
let result1947 = processor.convertToBraille(smashedPairs: rawReadingArray)
|
||||
XCTAssertEqual(result1947, "⠙⠜⠐⠚⠎⠄⠚⠎⠄⠕⠽⠐⠉⠮⠁⠱⠐⠚⠎⠄⠚⠎⠄⠚⠪⠂⠆⠑⠧⠄⠚⠎⠄⠚⠎⠄⠚⠺⠈⠪⠐⠑⠐⠚⠎⠄⠚⠎⠄⠩⠂⠤⠀")
|
||||
processor.standard = .of2018
|
||||
let result2018 = processor.convertToBraille(smashedPairs: rawReadingArray)
|
||||
XCTAssertEqual(result2018, "⠙⠔⠆⠅⠳⠁⠅⠳⠁⠃⠡⠆⠇⠢⠗⠆⠅⠳⠁⠅⠳⠁⠅⠜⠂⠐⠎⠧⠁⠅⠳⠁⠅⠳⠁⠉⠪⠄⠜⠆⠎⠆⠅⠳⠁⠅⠳⠁⠖⠂⠐⠆")
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ public class CandidateCellData: Hashable {
|
|||
public static var unifiedSize: Double = 16
|
||||
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
|
||||
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
|
||||
static var internalPrefs = PrefMgr()
|
||||
public var selectionKey: String
|
||||
public let displayedText: String
|
||||
public private(set) var textDimension: NSSize
|
||||
|
@ -81,7 +82,8 @@ public class CandidateCellData: Hashable {
|
|||
}
|
||||
|
||||
public func cellLength(isMatrix: Bool = true) -> Double {
|
||||
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
|
||||
let factor: CGFloat = (Self.internalPrefs.minCellWidthForHorizontalMatrix == 0) ? 1.5 : 2
|
||||
let minLength = ceil(Self.unifiedCharDimension * factor + size * 1.25)
|
||||
if displayedText.count <= 2, isMatrix { return minLength }
|
||||
return textDimension.width
|
||||
}
|
||||
|
|
|
@ -17,26 +17,24 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
|||
open var reverseLookupResult: [String] = []
|
||||
|
||||
open func highlightedColor() -> NSColor {
|
||||
var result = NSColor.controlAccentColor
|
||||
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0
|
||||
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" {
|
||||
colorBlendAmount = 0.15
|
||||
var result = NSColor.clear
|
||||
if #available(macOS 10.14, *) {
|
||||
result = .controlAccentColor
|
||||
} else {
|
||||
result = .alternateSelectedControlTextColor
|
||||
}
|
||||
let colorBlendAmount = 0.3
|
||||
// 設定當前高亮候選字的背景顏色。
|
||||
switch locale {
|
||||
case "zh-Hans":
|
||||
result = NSColor.systemRed
|
||||
result = NSColor.red
|
||||
case "zh-Hant":
|
||||
result = NSColor.systemBlue
|
||||
result = NSColor.blue
|
||||
case "ja":
|
||||
result = NSColor.systemBrown
|
||||
result = NSColor.brown
|
||||
default: break
|
||||
}
|
||||
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
|
||||
if #unavailable(macOS 10.14) {
|
||||
colorBlendAmount = 0.3
|
||||
blendingAgainstTarget = NSColor.white
|
||||
}
|
||||
let blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
|
||||
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
private extension NSUserInterfaceLayoutOrientation {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
/// 田所選字窗的 AppKit 简单版本,繪製效率不受 SwiftUI 的限制。
|
||||
|
|
|
@ -15,8 +15,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
.package(path: "../RMJay_LineReader"),
|
||||
.package(path: "../vChewing_Megrez"),
|
||||
.package(path: "../vChewing_PinyinPhonaConverter"),
|
||||
.package(path: "../vChewing_Shared"),
|
||||
.package(path: "../vChewing_SwiftExtension"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
@ -24,8 +23,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
.product(name: "LineReader", package: "RMJay_LineReader"),
|
||||
.product(name: "Megrez", package: "vChewing_Megrez"),
|
||||
.product(name: "Shared", package: "vChewing_Shared"),
|
||||
.product(name: "PinyinPhonaConverter", package: "vChewing_PinyinPhonaConverter"),
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import Foundation
|
||||
import LineReader
|
||||
import Shared
|
||||
|
||||
public extension LMAssembly {
|
||||
enum LMConsolidator {
|
||||
|
@ -26,19 +25,19 @@ public extension LMAssembly {
|
|||
let lineReader = try LineReader(file: fileHandle)
|
||||
for strLine in lineReader { // 不需要 i=0,因為第一遍迴圈就出結果。
|
||||
if strLine != kPragmaHeader {
|
||||
vCLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||
vCLMLog("Header Mismatch, Starting In-Place Consolidation.")
|
||||
return false
|
||||
} else {
|
||||
vCLog("Header Verification Succeeded: \(strLine).")
|
||||
vCLMLog("Header Verification Succeeded: \(strLine).")
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
vCLog("Header Verification Failed: File Access Error.")
|
||||
vCLMLog("Header Verification Failed: File Access Error.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
vCLog("Header Verification Failed: File Missing.")
|
||||
vCLMLog("Header Verification Failed: File Missing.")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -51,12 +50,12 @@ public extension LMAssembly {
|
|||
let dict = try FileManager.default.attributesOfItem(atPath: path)
|
||||
if let value = dict[FileAttributeKey.size] as? UInt64 { fileSize = value }
|
||||
} catch {
|
||||
vCLog("EOF Fix Failed: File Missing at \(path).")
|
||||
vCLMLog("EOF Fix Failed: File Missing at \(path).")
|
||||
return false
|
||||
}
|
||||
guard let fileSize = fileSize else { return false }
|
||||
guard let writeFile = FileHandle(forUpdatingAtPath: path) else {
|
||||
vCLog("EOF Fix Failed: File Not Writable at \(path).")
|
||||
vCLMLog("EOF Fix Failed: File Not Writable at \(path).")
|
||||
return false
|
||||
}
|
||||
defer { writeFile.closeFile() }
|
||||
|
@ -64,11 +63,11 @@ public extension LMAssembly {
|
|||
/// 但這個函式執行完之後往往就會 consolidate() 整理格式,所以不會有差。
|
||||
writeFile.seek(toFileOffset: fileSize - 1)
|
||||
if writeFile.readDataToEndOfFile().first != 0x0A {
|
||||
vCLog("EOF Missing Confirmed, Start Fixing.")
|
||||
vCLMLog("EOF Missing Confirmed, Start Fixing.")
|
||||
var newData = Data()
|
||||
newData.append(0x0A)
|
||||
writeFile.write(newData)
|
||||
vCLog("EOF Successfully Assured.")
|
||||
vCLMLog("EOF Successfully Assured.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -142,14 +141,29 @@ public extension LMAssembly {
|
|||
// Write consolidated file contents.
|
||||
try strProcessed.write(to: urlPath, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||
vCLMLog("Consolidation Failed w/ File: \(path), error: \(error)")
|
||||
return false
|
||||
}
|
||||
vCLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||
vCLMLog("Either Consolidation Successful Or No-Need-To-Consolidate.")
|
||||
return true
|
||||
}
|
||||
vCLog("Consolidation Failed: File Missing at \(path).")
|
||||
vCLMLog("Consolidation Failed: File Missing at \(path).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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(startIndex..., in: self)
|
||||
self = regex.stringByReplacingMatches(
|
||||
in: self, options: [], range: range, withTemplate: replaceWith
|
||||
)
|
||||
} catch { return }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
import SQLite3
|
||||
|
||||
public extension LMAssembly {
|
||||
/// 語言模組副本化模組(LMInstantiator,下稱「LMI」)自身為符合天權星組字引擎內
|
||||
|
@ -43,17 +41,19 @@ public extension LMAssembly {
|
|||
public var deltaOfCalendarYears: Int = -2000
|
||||
}
|
||||
|
||||
public static var asyncLoadingUserData: Bool = true
|
||||
|
||||
// SQLite 連線所在的記憶體位置。
|
||||
static var ptrSQL: OpaquePointer?
|
||||
|
||||
// SQLite 連線是否已經建立。
|
||||
public private(set) static var isSQLDBConnected: Bool = false
|
||||
public internal(set) static var isSQLDBConnected: Bool = false
|
||||
|
||||
// 簡體中文模型?
|
||||
public let isCHS: Bool
|
||||
|
||||
// 在函式內部用以記錄狀態的開關。
|
||||
public var config = Config()
|
||||
public private(set) var config = Config()
|
||||
|
||||
// 這句需要留著,不然無法被 package 外界存取。
|
||||
public init(
|
||||
|
@ -69,21 +69,8 @@ public extension LMAssembly {
|
|||
return self
|
||||
}
|
||||
|
||||
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
|
||||
if dropPreviousConnection { disconnectSQLDB() }
|
||||
vCLog("Establishing SQLite connection to: \(dbPath)")
|
||||
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
|
||||
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
||||
isSQLDBConnected = true
|
||||
return true
|
||||
}
|
||||
|
||||
public static func disconnectSQLDB() {
|
||||
if Self.ptrSQL != nil {
|
||||
sqlite3_close_v2(Self.ptrSQL)
|
||||
Self.ptrSQL = nil
|
||||
}
|
||||
isSQLDBConnected = false
|
||||
public static func setCassetCandidateKeyValidator(_ validator: @escaping (String) -> Bool) {
|
||||
Self.lmCassette.candidateKeysValidator = validator
|
||||
}
|
||||
|
||||
/// 介紹一下幾個通用的語言模組型別:
|
||||
|
@ -122,23 +109,37 @@ public extension LMAssembly {
|
|||
public func resetFactoryJSONModels() {}
|
||||
|
||||
public func loadUserPhrasesData(path: String, filterPath: String?) {
|
||||
DispatchQueue.main.async {
|
||||
func loadMain() {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
self.lmUserPhrases.clear()
|
||||
self.lmUserPhrases.open(path)
|
||||
vCLog("lmUserPhrases: \(self.lmUserPhrases.count) entries of data loaded from: \(path)")
|
||||
lmUserPhrases.clear()
|
||||
lmUserPhrases.open(path)
|
||||
vCLMLog("lmUserPhrases: \(lmUserPhrases.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmUserPhrases: File access failure: \(path)")
|
||||
vCLMLog("lmUserPhrases: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
loadMain()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
loadMain()
|
||||
}
|
||||
}
|
||||
guard let filterPath = filterPath else { return }
|
||||
DispatchQueue.main.async {
|
||||
func loadFilter() {
|
||||
if FileManager.default.isReadableFile(atPath: filterPath) {
|
||||
self.lmFiltered.clear()
|
||||
self.lmFiltered.open(filterPath)
|
||||
vCLog("lmFiltered: \(self.lmFiltered.count) entries of data loaded from: \(path)")
|
||||
lmFiltered.clear()
|
||||
lmFiltered.open(filterPath)
|
||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmFiltered: File access failure: \(path)")
|
||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
loadFilter()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
loadFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,57 +149,85 @@ public extension LMAssembly {
|
|||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
lmFiltered.clear()
|
||||
lmFiltered.open(path)
|
||||
vCLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
vCLMLog("lmFiltered: \(lmFiltered.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmFiltered: File access failure: \(path)")
|
||||
vCLMLog("lmFiltered: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
|
||||
public func loadUserSymbolData(path: String) {
|
||||
DispatchQueue.main.async {
|
||||
func load() {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
self.lmUserSymbols.clear()
|
||||
self.lmUserSymbols.open(path)
|
||||
vCLog("lmUserSymbol: \(self.lmUserSymbols.count) entries of data loaded from: \(path)")
|
||||
lmUserSymbols.clear()
|
||||
lmUserSymbols.open(path)
|
||||
vCLMLog("lmUserSymbol: \(lmUserSymbols.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmUserSymbol: File access failure: \(path)")
|
||||
vCLMLog("lmUserSymbol: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func loadUserAssociatesData(path: String) {
|
||||
DispatchQueue.main.async {
|
||||
func load() {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
self.lmAssociates.clear()
|
||||
self.lmAssociates.open(path)
|
||||
vCLog("lmAssociates: \(self.lmAssociates.count) entries of data loaded from: \(path)")
|
||||
lmAssociates.clear()
|
||||
lmAssociates.open(path)
|
||||
vCLMLog("lmAssociates: \(lmAssociates.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmAssociates: File access failure: \(path)")
|
||||
vCLMLog("lmAssociates: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func loadReplacementsData(path: String) {
|
||||
DispatchQueue.main.async {
|
||||
func load() {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
self.lmReplacements.clear()
|
||||
self.lmReplacements.open(path)
|
||||
vCLog("lmReplacements: \(self.lmReplacements.count) entries of data loaded from: \(path)")
|
||||
lmReplacements.clear()
|
||||
lmReplacements.open(path)
|
||||
vCLMLog("lmReplacements: \(lmReplacements.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmReplacements: File access failure: \(path)")
|
||||
vCLMLog("lmReplacements: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isCassetteDataLoaded: Bool { Self.lmCassette.isLoaded }
|
||||
public static func loadCassetteData(path: String) {
|
||||
DispatchQueue.main.async {
|
||||
func load() {
|
||||
if FileManager.default.isReadableFile(atPath: path) {
|
||||
Self.lmCassette.clear()
|
||||
Self.lmCassette.open(path)
|
||||
vCLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
|
||||
vCLMLog("lmCassette: \(Self.lmCassette.count) entries of data loaded from: \(path)")
|
||||
} else {
|
||||
vCLog("lmCassette: File access failure: \(path)")
|
||||
vCLMLog("lmCassette: File access failure: \(path)")
|
||||
}
|
||||
}
|
||||
if !Self.asyncLoadingUserData {
|
||||
load()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -329,11 +358,6 @@ public extension LMAssembly {
|
|||
}
|
||||
}
|
||||
|
||||
// 用 reversed 指令讓使用者語彙檔案內的詞條優先順序隨著行數增加而逐漸增高。
|
||||
// 這樣一來就可以在就地新增語彙時徹底複寫優先權。
|
||||
// 將兩句差分也是為了讓 rawUserUnigrams 的類型不受可能的影響。
|
||||
rawAllUnigrams += lmUserPhrases.unigramsFor(key: keyChain).reversed()
|
||||
|
||||
if !config.isCassetteEnabled || config.isCassetteEnabled && keyChain.map(\.description)[0] == "_" {
|
||||
// 先給出 NumPad 的結果。
|
||||
rawAllUnigrams += supplyNumPadUnigrams(key: keyChain)
|
||||
|
@ -362,6 +386,21 @@ public extension LMAssembly {
|
|||
}
|
||||
}
|
||||
|
||||
// 用 reversed 指令讓使用者語彙檔案內的詞條優先順序隨著行數增加而逐漸增高。
|
||||
// 這樣一來就可以在就地新增語彙時徹底複寫優先權。
|
||||
// 將兩句差分也是為了讓 rawUserUnigrams 的類型不受可能的影響。
|
||||
var userPhraseUnigrams = Array(lmUserPhrases.unigramsFor(key: keyChain).reversed())
|
||||
if keyArray.count == 1, let topScore = rawAllUnigrams.map(\.score).max() {
|
||||
// 不再讓使用者自己加入的單漢字讀音權重進入爬軌體系。
|
||||
userPhraseUnigrams = userPhraseUnigrams.map { currentUnigram in
|
||||
Megrez.Unigram(
|
||||
value: currentUnigram.value,
|
||||
score: Swift.min(topScore + 0.000_114_514, currentUnigram.score)
|
||||
)
|
||||
}
|
||||
}
|
||||
rawAllUnigrams = userPhraseUnigrams + rawAllUnigrams
|
||||
|
||||
// 分析且處理可能存在的 InputToken。
|
||||
rawAllUnigrams = rawAllUnigrams.map { unigram in
|
||||
let convertedValues = unigram.value.parseAsInputToken(isCHS: isCHS)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
/// 磁帶模式專用:當前磁帶所規定的花牌鍵。
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
import SQLite3
|
||||
|
||||
/* ==============
|
||||
|
@ -57,6 +56,23 @@ extension LMAssembly.LMInstantiator {
|
|||
}
|
||||
|
||||
extension LMAssembly.LMInstantiator {
|
||||
@discardableResult public static func connectSQLDB(dbPath: String, dropPreviousConnection: Bool = true) -> Bool {
|
||||
if dropPreviousConnection { disconnectSQLDB() }
|
||||
vCLMLog("Establishing SQLite connection to: \(dbPath)")
|
||||
guard sqlite3_open(dbPath, &Self.ptrSQL) == SQLITE_OK else { return false }
|
||||
guard "PRAGMA journal_mode = OFF;".runAsSQLExec(dbPointer: &ptrSQL) else { return false }
|
||||
isSQLDBConnected = true
|
||||
return true
|
||||
}
|
||||
|
||||
public static func disconnectSQLDB() {
|
||||
if Self.ptrSQL != nil {
|
||||
sqlite3_close_v2(Self.ptrSQL)
|
||||
Self.ptrSQL = nil
|
||||
}
|
||||
isSQLDBConnected = false
|
||||
}
|
||||
|
||||
fileprivate static func querySQL(strStmt sqlQuery: String, coreColumn column: CoreColumn, handler: (String) -> Void) {
|
||||
guard Self.ptrSQL != nil else { return }
|
||||
performStatementSansResult { ptrStatement in
|
||||
|
@ -125,9 +141,10 @@ extension LMAssembly.LMInstantiator {
|
|||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取原廠標準資料庫辭典內的對應資料陣列的 UTF8 資料、就地分析、生成單元圖陣列。
|
||||
/// - Remark: 該函式會無損地返回原廠辭典的結果,不受使用者控頻與資料過濾條件的影響,不包含全字庫的資料。
|
||||
/// - parameters:
|
||||
/// - key: 讀音索引鍵。
|
||||
func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
public func factoryCoreUnigramsFor(key: String) -> [Megrez.Unigram] {
|
||||
// 此處需要把 ASCII 單引號換成連續兩個單引號,否則會有 SQLite 語句查詢故障。
|
||||
factoryUnigramsFor(key: key, column: isCHS ? .theDataCHS : .theDataCHT)
|
||||
}
|
||||
|
@ -146,8 +163,10 @@ extension LMAssembly.LMInstantiator {
|
|||
let encryptedKey = Self.cnvPhonabetToASCII(key.replacingOccurrences(of: "'", with: "''"))
|
||||
let sqlQuery = "SELECT * FROM DATA_MAIN WHERE theKey='\(encryptedKey)';"
|
||||
Self.querySQL(strStmt: sqlQuery, coreColumn: column) { currentResult in
|
||||
let arrRangeRecords = currentResult.split(separator: "\t")
|
||||
for strNetaSet in arrRangeRecords {
|
||||
var i: Double = 0
|
||||
var previousScore: Double?
|
||||
currentResult.split(separator: "\t").forEach { strNetaSet in
|
||||
// 這裡假定原廠資料已經經過對權重的 stable sort 排序。
|
||||
let neta = Array(strNetaSet.trimmingCharacters(in: .newlines).split(separator: " ").reversed())
|
||||
let theValue: String = .init(neta[0])
|
||||
var theScore = column.defaultScore
|
||||
|
@ -157,8 +176,15 @@ extension LMAssembly.LMInstantiator {
|
|||
if theScore > 0 {
|
||||
theScore *= -1 // 應對可能忘記寫負號的情形
|
||||
}
|
||||
if previousScore == theScore {
|
||||
theScore -= i * 0.000_001
|
||||
i += 1
|
||||
} else {
|
||||
previousScore = theScore
|
||||
i = 0
|
||||
}
|
||||
grams.append(Megrez.Unigram(value: theValue, score: theScore))
|
||||
if !key.contains("_punctuation") { continue }
|
||||
if !key.contains("_punctuation") { return }
|
||||
let halfValue = theValue.applyingTransformFW2HW(reverse: false)
|
||||
if halfValue != theValue {
|
||||
gramsHW.append(Megrez.Unigram(value: halfValue, score: theScore))
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension LMAssembly {
|
||||
struct UserDictionarySummarized: Codable {
|
||||
let isCHS: Bool
|
||||
let userPhrases: [String: [String]]
|
||||
let filter: [String: [String]]
|
||||
let userSymbols: [String: [String]]
|
||||
let replacements: [String: String]
|
||||
let associates: [String: [String]]
|
||||
}
|
||||
}
|
||||
|
||||
public extension LMAssembly.LMInstantiator {
|
||||
func summarize(all: Bool) -> LMAssembly.UserDictionarySummarized {
|
||||
LMAssembly.UserDictionarySummarized(
|
||||
isCHS: isCHS,
|
||||
userPhrases: lmUserPhrases.dictRepresented,
|
||||
filter: lmFiltered.dictRepresented,
|
||||
userSymbols: lmUserSymbols.dictRepresented,
|
||||
replacements: lmReplacements.dictRepresented,
|
||||
associates: all ? lmAssociates.dictRepresented : [:]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
|
||||
// 該檔案使得 LMAssembly 擺脫對 Tekkon 的依賴。
|
||||
|
||||
private typealias LengthSortedDictionary = [Int: [String: String]]
|
||||
|
||||
private let mapHanyuPinyinToPhonabets: LengthSortedDictionary = {
|
||||
let parsed = try? JSONDecoder().decode(LengthSortedDictionary.self, from: jsnHanyuPinyinToMPS.data(using: .utf8) ?? Data([]))
|
||||
return parsed ?? [:]
|
||||
}()
|
||||
|
||||
extension String {
|
||||
mutating func convertToPhonabets(newToneOne: String = "") {
|
||||
if isEmpty || contains("_") || !isNotPureAlphanumerical { return }
|
||||
let lengths = mapHanyuPinyinToPhonabets.keys.sorted().reversed()
|
||||
lengths.forEach { length in
|
||||
mapHanyuPinyinToPhonabets[length]?.forEach { key, value in
|
||||
self = replacingOccurrences(of: key, with: value)
|
||||
}
|
||||
}
|
||||
self = replacingOccurrences(of: " ", with: newToneOne)
|
||||
}
|
||||
}
|
||||
|
||||
/// 檢測字串是否包含半形英數內容
|
||||
private extension String {
|
||||
var isNotPureAlphanumerical: Bool {
|
||||
let regex = ".*[^A-Za-z0-9].*"
|
||||
let testString = NSPredicate(format: "SELF MATCHES %@", regex)
|
||||
return testString.evaluate(with: self)
|
||||
}
|
||||
}
|
||||
|
||||
private let jsnHanyuPinyinToMPS = #"""
|
||||
{
|
||||
"1":{"1":" ","2":"ˊ","3":"ˇ","4":"ˋ","5":"˙","a":"ㄚ","e":"ㄜ","o":"ㄛ","q":"ㄑ"},
|
||||
"2":{"ai":"ㄞ","an":"ㄢ","ao":"ㄠ","ba":"ㄅㄚ","bi":"ㄅㄧ","bo":"ㄅㄛ","bu":"ㄅㄨ",
|
||||
"ca":"ㄘㄚ","ce":"ㄘㄜ","ci":"ㄘ","cu":"ㄘㄨ","da":"ㄉㄚ","de":"ㄉㄜ","di":"ㄉㄧ",
|
||||
"du":"ㄉㄨ","eh":"ㄝ","ei":"ㄟ","en":"ㄣ","er":"ㄦ","fa":"ㄈㄚ","fo":"ㄈㄛ",
|
||||
"fu":"ㄈㄨ","ga":"ㄍㄚ","ge":"ㄍㄜ","gi":"ㄍㄧ","gu":"ㄍㄨ","ha":"ㄏㄚ","he":"ㄏㄜ",
|
||||
"hu":"ㄏㄨ","ji":"ㄐㄧ","ju":"ㄐㄩ","ka":"ㄎㄚ","ke":"ㄎㄜ","ku":"ㄎㄨ","la":"ㄌㄚ",
|
||||
"le":"ㄌㄜ","li":"ㄌㄧ","lo":"ㄌㄛ","lu":"ㄌㄨ","lv":"ㄌㄩ","ma":"ㄇㄚ","me":"ㄇㄜ",
|
||||
"mi":"ㄇㄧ","mo":"ㄇㄛ","mu":"ㄇㄨ","na":"ㄋㄚ","ne":"ㄋㄜ","ni":"ㄋㄧ","nu":"ㄋㄨ",
|
||||
"nv":"ㄋㄩ","ou":"ㄡ","pa":"ㄆㄚ","pi":"ㄆㄧ","po":"ㄆㄛ","pu":"ㄆㄨ","qi":"ㄑㄧ",
|
||||
"qu":"ㄑㄩ","re":"ㄖㄜ","ri":"ㄖ","ru":"ㄖㄨ","sa":"ㄙㄚ","se":"ㄙㄜ","si":"ㄙ",
|
||||
"su":"ㄙㄨ","ta":"ㄊㄚ","te":"ㄊㄜ","ti":"ㄊㄧ","tu":"ㄊㄨ","wa":"ㄨㄚ","wo":"ㄨㄛ",
|
||||
"wu":"ㄨ","xi":"ㄒㄧ","xu":"ㄒㄩ","ya":"ㄧㄚ","ye":"ㄧㄝ","yi":"ㄧ","yo":"ㄧㄛ",
|
||||
"yu":"ㄩ","za":"ㄗㄚ","ze":"ㄗㄜ","zi":"ㄗ","zu":"ㄗㄨ"},
|
||||
"3":{"ang":"ㄤ","bai":"ㄅㄞ","ban":"ㄅㄢ","bao":"ㄅㄠ","bei":"ㄅㄟ","ben":"ㄅㄣ",
|
||||
"bie":"ㄅㄧㄝ","bin":"ㄅㄧㄣ","cai":"ㄘㄞ","can":"ㄘㄢ","cao":"ㄘㄠ","cei":"ㄘㄟ",
|
||||
"cen":"ㄘㄣ","cha":"ㄔㄚ","che":"ㄔㄜ","chi":"ㄔ","chu":"ㄔㄨ","cou":"ㄘㄡ",
|
||||
"cui":"ㄘㄨㄟ","cun":"ㄘㄨㄣ","cuo":"ㄘㄨㄛ","dai":"ㄉㄞ","dan":"ㄉㄢ","dao":"ㄉㄠ",
|
||||
"dei":"ㄉㄟ","den":"ㄉㄣ","dia":"ㄉㄧㄚ","die":"ㄉㄧㄝ","diu":"ㄉㄧㄡ","dou":"ㄉㄡ",
|
||||
"dui":"ㄉㄨㄟ","dun":"ㄉㄨㄣ","duo":"ㄉㄨㄛ","eng":"ㄥ","fan":"ㄈㄢ","fei":"ㄈㄟ",
|
||||
"fen":"ㄈㄣ","fou":"ㄈㄡ","gai":"ㄍㄞ","gan":"ㄍㄢ","gao":"ㄍㄠ","gei":"ㄍㄟ",
|
||||
"gen":"ㄍㄣ","gin":"ㄍㄧㄣ","gou":"ㄍㄡ","gua":"ㄍㄨㄚ","gue":"ㄍㄨㄜ","gui":"ㄍㄨㄟ",
|
||||
"gun":"ㄍㄨㄣ","guo":"ㄍㄨㄛ","hai":"ㄏㄞ","han":"ㄏㄢ","hao":"ㄏㄠ","hei":"ㄏㄟ",
|
||||
"hen":"ㄏㄣ","hou":"ㄏㄡ","hua":"ㄏㄨㄚ","hui":"ㄏㄨㄟ","hun":"ㄏㄨㄣ","huo":"ㄏㄨㄛ",
|
||||
"jia":"ㄐㄧㄚ","jie":"ㄐㄧㄝ","jin":"ㄐㄧㄣ","jiu":"ㄐㄧㄡ","jue":"ㄐㄩㄝ",
|
||||
"jun":"ㄐㄩㄣ","kai":"ㄎㄞ","kan":"ㄎㄢ","kao":"ㄎㄠ","ken":"ㄎㄣ","kiu":"ㄎㄧㄡ",
|
||||
"kou":"ㄎㄡ","kua":"ㄎㄨㄚ","kui":"ㄎㄨㄟ","kun":"ㄎㄨㄣ","kuo":"ㄎㄨㄛ","lai":"ㄌㄞ",
|
||||
"lan":"ㄌㄢ","lao":"ㄌㄠ","lei":"ㄌㄟ","lia":"ㄌㄧㄚ","lie":"ㄌㄧㄝ","lin":"ㄌㄧㄣ",
|
||||
"liu":"ㄌㄧㄡ","lou":"ㄌㄡ","lun":"ㄌㄨㄣ","luo":"ㄌㄨㄛ","lve":"ㄌㄩㄝ","mai":"ㄇㄞ",
|
||||
"man":"ㄇㄢ","mao":"ㄇㄠ","mei":"ㄇㄟ","men":"ㄇㄣ","mie":"ㄇㄧㄝ","min":"ㄇㄧㄣ",
|
||||
"miu":"ㄇㄧㄡ","mou":"ㄇㄡ","nai":"ㄋㄞ","nan":"ㄋㄢ","nao":"ㄋㄠ","nei":"ㄋㄟ",
|
||||
"nen":"ㄋㄣ","nie":"ㄋㄧㄝ","nin":"ㄋㄧㄣ","niu":"ㄋㄧㄡ","nou":"ㄋㄡ","nui":"ㄋㄨㄟ",
|
||||
"nun":"ㄋㄨㄣ","nuo":"ㄋㄨㄛ","nve":"ㄋㄩㄝ","pai":"ㄆㄞ","pan":"ㄆㄢ","pao":"ㄆㄠ",
|
||||
"pei":"ㄆㄟ","pen":"ㄆㄣ","pia":"ㄆㄧㄚ","pie":"ㄆㄧㄝ","pin":"ㄆㄧㄣ","pou":"ㄆㄡ",
|
||||
"qia":"ㄑㄧㄚ","qie":"ㄑㄧㄝ","qin":"ㄑㄧㄣ","qiu":"ㄑㄧㄡ","que":"ㄑㄩㄝ",
|
||||
"qun":"ㄑㄩㄣ","ran":"ㄖㄢ","rao":"ㄖㄠ","ren":"ㄖㄣ","rou":"ㄖㄡ","rui":"ㄖㄨㄟ",
|
||||
"run":"ㄖㄨㄣ","ruo":"ㄖㄨㄛ","sai":"ㄙㄞ","san":"ㄙㄢ","sao":"ㄙㄠ","sei":"ㄙㄟ",
|
||||
"sen":"ㄙㄣ","sha":"ㄕㄚ","she":"ㄕㄜ","shi":"ㄕ","shu":"ㄕㄨ","sou":"ㄙㄡ",
|
||||
"sui":"ㄙㄨㄟ","sun":"ㄙㄨㄣ","suo":"ㄙㄨㄛ","tai":"ㄊㄞ","tan":"ㄊㄢ","tao":"ㄊㄠ",
|
||||
"tie":"ㄊㄧㄝ","tou":"ㄊㄡ","tui":"ㄊㄨㄟ","tun":"ㄊㄨㄣ","tuo":"ㄊㄨㄛ",
|
||||
"wai":"ㄨㄞ","wan":"ㄨㄢ","wei":"ㄨㄟ","wen":"ㄨㄣ","xia":"ㄒㄧㄚ","xie":"ㄒㄧㄝ",
|
||||
"xin":"ㄒㄧㄣ","xiu":"ㄒㄧㄡ","xue":"ㄒㄩㄝ","xun":"ㄒㄩㄣ","yai":"ㄧㄞ",
|
||||
"yan":"ㄧㄢ","yao":"ㄧㄠ","yin":"ㄧㄣ","you":"ㄧㄡ","yue":"ㄩㄝ","yun":"ㄩㄣ",
|
||||
"zai":"ㄗㄞ","zan":"ㄗㄢ","zao":"ㄗㄠ","zei":"ㄗㄟ","zen":"ㄗㄣ","zha":"ㄓㄚ",
|
||||
"zhe":"ㄓㄜ","zhi":"ㄓ","zhu":"ㄓㄨ","zou":"ㄗㄡ","zui":"ㄗㄨㄟ","zun":"ㄗㄨㄣ",
|
||||
"zuo":"ㄗㄨㄛ"},
|
||||
"4":{"bang":"ㄅㄤ","beng":"ㄅㄥ","bian":"ㄅㄧㄢ","biao":"ㄅㄧㄠ","bing":"ㄅㄧㄥ",
|
||||
"cang":"ㄘㄤ","ceng":"ㄘㄥ","chai":"ㄔㄞ","chan":"ㄔㄢ","chao":"ㄔㄠ","chen":"ㄔㄣ",
|
||||
"chou":"ㄔㄡ","chua":"ㄔㄨㄚ","chui":"ㄔㄨㄟ","chun":"ㄔㄨㄣ","chuo":"ㄔㄨㄛ",
|
||||
"cong":"ㄘㄨㄥ","cuan":"ㄘㄨㄢ","dang":"ㄉㄤ","deng":"ㄉㄥ","dian":"ㄉㄧㄢ",
|
||||
"diao":"ㄉㄧㄠ","ding":"ㄉㄧㄥ","dong":"ㄉㄨㄥ","duan":"ㄉㄨㄢ","fang":"ㄈㄤ",
|
||||
"feng":"ㄈㄥ","fiao":"ㄈㄧㄠ","fong":"ㄈㄨㄥ","gang":"ㄍㄤ","geng":"ㄍㄥ",
|
||||
"giao":"ㄍㄧㄠ","gong":"ㄍㄨㄥ","guai":"ㄍㄨㄞ","guan":"ㄍㄨㄢ","hang":"ㄏㄤ",
|
||||
"heng":"ㄏㄥ","hong":"ㄏㄨㄥ","huai":"ㄏㄨㄞ","huan":"ㄏㄨㄢ","jian":"ㄐㄧㄢ",
|
||||
"jiao":"ㄐㄧㄠ","jing":"ㄐㄧㄥ","juan":"ㄐㄩㄢ","kang":"ㄎㄤ","keng":"ㄎㄥ",
|
||||
"kong":"ㄎㄨㄥ","kuai":"ㄎㄨㄞ","kuan":"ㄎㄨㄢ","lang":"ㄌㄤ","leng":"ㄌㄥ",
|
||||
"lian":"ㄌㄧㄢ","liao":"ㄌㄧㄠ","ling":"ㄌㄧㄥ","long":"ㄌㄨㄥ","luan":"ㄌㄨㄢ",
|
||||
"lvan":"ㄌㄩㄢ","mang":"ㄇㄤ","meng":"ㄇㄥ","mian":"ㄇㄧㄢ","miao":"ㄇㄧㄠ",
|
||||
"ming":"ㄇㄧㄥ","nang":"ㄋㄤ","neng":"ㄋㄥ","nian":"ㄋㄧㄢ","niao":"ㄋㄧㄠ",
|
||||
"ning":"ㄋㄧㄥ","nong":"ㄋㄨㄥ","nuan":"ㄋㄨㄢ","pang":"ㄆㄤ","peng":"ㄆㄥ",
|
||||
"pian":"ㄆㄧㄢ","piao":"ㄆㄧㄠ","ping":"ㄆㄧㄥ","qian":"ㄑㄧㄢ","qiao":"ㄑㄧㄠ",
|
||||
"qing":"ㄑㄧㄥ","quan":"ㄑㄩㄢ","rang":"ㄖㄤ","reng":"ㄖㄥ","rong":"ㄖㄨㄥ",
|
||||
"ruan":"ㄖㄨㄢ","sang":"ㄙㄤ","seng":"ㄙㄥ","shai":"ㄕㄞ","shan":"ㄕㄢ",
|
||||
"shao":"ㄕㄠ","shei":"ㄕㄟ","shen":"ㄕㄣ","shou":"ㄕㄡ","shua":"ㄕㄨㄚ",
|
||||
"shui":"ㄕㄨㄟ","shun":"ㄕㄨㄣ","shuo":"ㄕㄨㄛ","song":"ㄙㄨㄥ","suan":"ㄙㄨㄢ",
|
||||
"tang":"ㄊㄤ","teng":"ㄊㄥ","tian":"ㄊㄧㄢ","tiao":"ㄊㄧㄠ","ting":"ㄊㄧㄥ",
|
||||
"tong":"ㄊㄨㄥ","tuan":"ㄊㄨㄢ","wang":"ㄨㄤ","weng":"ㄨㄥ","xian":"ㄒㄧㄢ",
|
||||
"xiao":"ㄒㄧㄠ","xing":"ㄒㄧㄥ","xuan":"ㄒㄩㄢ","yang":"ㄧㄤ","ying":"ㄧㄥ",
|
||||
"yong":"ㄩㄥ","yuan":"ㄩㄢ","zang":"ㄗㄤ","zeng":"ㄗㄥ","zhai":"ㄓㄞ",
|
||||
"zhan":"ㄓㄢ","zhao":"ㄓㄠ","zhei":"ㄓㄟ","zhen":"ㄓㄣ","zhou":"ㄓㄡ",
|
||||
"zhua":"ㄓㄨㄚ","zhui":"ㄓㄨㄟ","zhun":"ㄓㄨㄣ","zhuo":"ㄓㄨㄛ",
|
||||
"zong":"ㄗㄨㄥ","zuan":"ㄗㄨㄢ"},
|
||||
"5":{"biang":"ㄅㄧㄤ","chang":"ㄔㄤ","cheng":"ㄔㄥ","chong":"ㄔㄨㄥ","chuai":"ㄔㄨㄞ",
|
||||
"chuan":"ㄔㄨㄢ","duang":"ㄉㄨㄤ","guang":"ㄍㄨㄤ","huang":"ㄏㄨㄤ","jiang":"ㄐㄧㄤ",
|
||||
"jiong":"ㄐㄩㄥ","kiang":"ㄎㄧㄤ","kuang":"ㄎㄨㄤ","liang":"ㄌㄧㄤ","niang":"ㄋㄧㄤ",
|
||||
"qiang":"ㄑㄧㄤ","qiong":"ㄑㄩㄥ","shang":"ㄕㄤ","sheng":"ㄕㄥ","shuai":"ㄕㄨㄞ",
|
||||
"shuan":"ㄕㄨㄢ","xiang":"ㄒㄧㄤ","xiong":"ㄒㄩㄥ","zhang":"ㄓㄤ","zheng":"ㄓㄥ",
|
||||
"zhong":"ㄓㄨㄥ","zhuai":"ㄓㄨㄞ","zhuan":"ㄓㄨㄢ"},
|
||||
"6":{"chuang":"ㄔㄨㄤ","shuang":"ㄕㄨㄤ","zhuang":"ㄓㄨㄤ"}
|
||||
}
|
||||
"""#
|
|
@ -7,13 +7,11 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Megrez
|
||||
import PinyinPhonaConverter
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMAssociates {
|
||||
public private(set) var filePath: String?
|
||||
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:]
|
||||
var rangeMap: [String: [(Range<String.Index>, Int)]] = [:] // Range 只可能是一整行,所以必須得有 index。
|
||||
var strData: String = ""
|
||||
|
||||
public var count: Int { rangeMap.count }
|
||||
|
@ -48,8 +46,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -93,28 +91,21 @@ extension LMAssembly {
|
|||
do {
|
||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
public func valuesFor(pair: Megrez.KeyValuePaired) -> [String] {
|
||||
var pairs: [String] = []
|
||||
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.toNGramKey] {
|
||||
for (netaRange, index) in arrRangeRecords {
|
||||
let availableResults = [rangeMap[pair.toNGramKey], rangeMap[pair.value]].compactMap { $0 }
|
||||
availableResults.forEach { arrRangeRecords in
|
||||
arrRangeRecords.forEach { netaRange, index in
|
||||
let neta = strData[netaRange].split(separator: " ")
|
||||
let theValue: String = .init(neta[index])
|
||||
pairs.append(theValue)
|
||||
}
|
||||
}
|
||||
if let arrRangeRecords: [(Range<String.Index>, Int)] = rangeMap[pair.value] {
|
||||
for (netaRange, index) in arrRangeRecords {
|
||||
let neta = strData[netaRange].split(separator: " ")
|
||||
let theValue: String = .init(neta[index])
|
||||
pairs.append(theValue)
|
||||
}
|
||||
}
|
||||
var set = Set<String>()
|
||||
return pairs.filter { set.insert($0).inserted }
|
||||
return pairs.deduplicated
|
||||
}
|
||||
|
||||
public func hasValuesFor(pair: Megrez.KeyValuePaired) -> Bool {
|
||||
|
@ -123,3 +114,17 @@ extension LMAssembly {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMAssociates {
|
||||
var dictRepresented: [String: [String]] {
|
||||
var result = [String: [String]]()
|
||||
rangeMap.forEach { key, arrRangeRecords in
|
||||
arrRangeRecords.forEach { netaRange, index in
|
||||
let neta = strData[netaRange].split(separator: " ")
|
||||
let theValue: String = .init(neta[index])
|
||||
result[key, default: []].append(theValue)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
import Foundation
|
||||
import LineReader
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
/// 磁帶模組,用來方便使用者自行擴充字根輸入法。
|
||||
|
@ -40,6 +39,7 @@ extension LMAssembly {
|
|||
public private(set) var areCandidateKeysShiftHeld: Bool = false
|
||||
public private(set) var supplyQuickResults: Bool = false
|
||||
public private(set) var supplyPartiallyMatchedResults: Bool = false
|
||||
public var candidateKeysValidator: (String) -> Bool = { _ in false }
|
||||
/// 計算頻率時要用到的東西 - NORM
|
||||
private var norm = 0.0
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ extension LMAssembly.LMCassette {
|
|||
// Post process.
|
||||
// 備註:因為 Package 層級嵌套的現狀,此處不太方便檢查是否需要篩掉 J / K 鍵。
|
||||
// 因此只能在其他地方做篩檢。
|
||||
if CandidateKey.validate(keys: selectionKeys) != nil { selectionKeys = "1234567890" }
|
||||
if !candidateKeysValidator(selectionKeys) { selectionKeys = "1234567890" }
|
||||
if !keysUsedInCharDef.intersection(selectionKeys.map(\.description)).isEmpty {
|
||||
areCandidateKeysShiftHeld = true
|
||||
}
|
||||
|
@ -204,10 +204,10 @@ extension LMAssembly.LMCassette {
|
|||
filePath = path
|
||||
return true
|
||||
} catch {
|
||||
vCLog("CIN Loading Failed: File Access Error.")
|
||||
vCLMLog("CIN Loading Failed: File Access Error.")
|
||||
}
|
||||
} else {
|
||||
vCLog("CIN Loading Failed: File Missing.")
|
||||
vCLMLog("CIN Loading Failed: File Missing.")
|
||||
}
|
||||
filePath = oldPath
|
||||
return false
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Megrez
|
||||
import PinyinPhonaConverter
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
/// 與之前的 LMCore 不同,LMCoreEX 不在辭典內記錄實體,而是記錄 range 範圍。
|
||||
|
@ -81,8 +79,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -133,7 +131,7 @@ extension LMAssembly {
|
|||
}
|
||||
try dataToWrite.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,7 +148,7 @@ extension LMAssembly {
|
|||
strDump += addline
|
||||
}
|
||||
}
|
||||
vCLog(strDump)
|
||||
vCLMLog(strDump)
|
||||
}
|
||||
|
||||
/// 根據給定的讀音索引鍵,來獲取資料庫辭典內的對應資料陣列的字串首尾範圍資料、據此自 strData 取得字串形式的資料、生成單元圖陣列。
|
||||
|
@ -186,3 +184,15 @@ extension LMAssembly {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMCoreEX {
|
||||
var dictRepresented: [String: [String]] {
|
||||
var result = [String: [String]]()
|
||||
rangeMap.forEach { key, arrValueRanges in
|
||||
result[key, default: []] = arrValueRanges.map { currentRange in
|
||||
strData[currentRange].description
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMPlainBopomofo {
|
||||
|
@ -22,8 +21,8 @@ extension LMAssembly {
|
|||
let rawJSON = try JSONDecoder().decode([String: [String: String]].self, from: rawData)
|
||||
dataMap = rawJSON
|
||||
} catch {
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when parsing raw JSON sequence data from vChewing LMAssembly.")
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when parsing raw JSON sequence data from vChewing LMAssembly.")
|
||||
dataMap = [:]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -518,7 +518,7 @@ let jsnEtenDosSequence = """
|
|||
"ㄍㄡ":{"S":"句沟勾钩枸泃篝缑构芶耩","T":"句溝勾鉤枸泃篝緱构芶耩"},
|
||||
"ㄍㄡˇ":{"S":"狗茍岣枸苟笱耇茩蚼","T":"狗茍岣枸苟笱耇茩蚼"},
|
||||
"ㄍㄡˋ":{"S":"够购垢构媾彀构诟遘觏冓姤雊傋瞉簼鞲唦","T":"夠購垢構媾彀搆詬遘覯冓姤雊傋瞉簼韝唦"},
|
||||
"ㄍㄢ":{"S":"乾甘干竿肝尴柑坩泔咁疳玕杆矸虷筸蜬鳱嵅","T":"乾甘干竿肝尷柑坩泔咁疳玕杆矸虷筸蜬鳱嵅"},
|
||||
"ㄍㄢ":{"S":"干甘干竿肝尴柑坩泔咁疳玕杆矸虷筸蜬鳱嵅","T":"乾甘干竿肝尷柑坩泔咁疳玕杆矸虷筸蜬鳱嵅"},
|
||||
"ㄍㄢˇ":{"S":"敢感赶杆橄秆澉皯盰赶","T":"敢感趕桿橄稈澉皯盰赶"},
|
||||
"ㄍㄢˋ":{"S":"干赣凎淦绀旰骭詌干赣涻簳嵅","T":"幹贛凎淦紺旰骭詌榦灨涻簳嵅"},
|
||||
"ㄍㄣ":{"S":"跟根","T":"跟根"},
|
||||
|
@ -843,7 +843,7 @@ let jsnEtenDosSequence = """
|
|||
"ㄓㄚˊ":{"S":"扎札扎闸炸霅铡哳札蚻譗蠿","T":"扎札紮閘炸霅鍘哳劄蚻譗蠿"},
|
||||
"ㄓㄚˋ":{"S":"榨栅炸诈乍榨蚱咋蜡吒溠砟醡鮓痄簎","T":"榨柵炸詐乍搾蚱咋蜡吒溠砟醡鮓痄簎"},
|
||||
"ㄓㄜ":{"S":"遮螫晢嫬","T":"遮螫晢嫬"},
|
||||
"ㄓㄜ˙":{"S":"著遮晢嫬","T":"著遮晢嫬"},
|
||||
"ㄓㄜ˙":{"S":"着遮晢嫬","T":"著遮晢嫬"},
|
||||
"ㄓㄜˇ":{"S":"者赭锗","T":"者赭鍺"},
|
||||
"ㄓㄜˊ":{"S":"折哲摺慑褶谪辄摘辙慑蜇磔乇讋晢鮿耴悊砓謺虴鸅讘瓋","T":"折哲摺懾褶謫輒摘轍慴蜇磔乇讋晢鮿耴悊砓謺虴鸅讘瓋"},
|
||||
"ㄓㄜˋ":{"S":"这浙蔗鹧柘宅檡烢蟅","T":"這浙蔗鷓柘宅檡烢蟅"},
|
||||
|
@ -851,9 +851,9 @@ let jsnEtenDosSequence = """
|
|||
"ㄓㄞˇ":{"S":"窄岝","T":"窄岝"},
|
||||
"ㄓㄞˊ":{"S":"宅翟","T":"宅翟"},
|
||||
"ㄓㄞˋ":{"S":"债寨祭责瘵砦","T":"債寨祭責瘵砦"},
|
||||
"ㄓㄠ":{"S":"朝招昭召著嘲钊駋鉊晁盄鍣妱","T":"朝招昭召著嘲釗駋鉊晁盄鍣妱"},
|
||||
"ㄓㄠ":{"S":"朝招昭召着嘲钊駋鉊晁盄鍣妱","T":"朝招昭召著嘲釗駋鉊晁盄鍣妱"},
|
||||
"ㄓㄠˇ":{"S":"找沼爪菬瑵","T":"找沼爪菬瑵"},
|
||||
"ㄓㄠˊ":{"S":"著","T":"著"},
|
||||
"ㄓㄠˊ":{"S":"着","T":"著"},
|
||||
"ㄓㄠˋ":{"S":"照赵召罩兆肇诏晁笊棹照雿狣棹箌鵫垗旐曌","T":"照趙召罩兆肇詔晁笊櫂炤雿狣棹箌鵫垗旐曌"},
|
||||
"ㄓㄡ":{"S":"周周州洲舟粥賙啁盩譸輈喌騆鸼洀淍銂珘徟輖侜婤","T":"周週州洲舟粥賙啁盩譸輈喌騆鵃洀淍銂珘徟輖侜婤"},
|
||||
"ㄓㄡˇ":{"S":"帚肘睭鯞","T":"帚肘睭鯞"},
|
||||
|
@ -878,7 +878,7 @@ let jsnEtenDosSequence = """
|
|||
"ㄓㄨㄚ":{"S":"抓挝髽檛","T":"抓撾髽檛"},
|
||||
"ㄓㄨㄚˇ":{"S":"爪","T":"爪"},
|
||||
"ㄓㄨㄛ":{"S":"捉桌涿棹穛","T":"捉桌涿棹穛"},
|
||||
"ㄓㄨㄛˊ":{"S":"卓茁浊拙濯酌灼著啄镯擢琢诼倬斫斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂","T":"卓茁濁拙濯酌灼著啄鐲擢琢諑倬斲斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂"},
|
||||
"ㄓㄨㄛˊ":{"S":"卓茁浊拙濯酌灼着啄镯擢琢诼倬斫斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂","T":"卓茁濁拙濯酌灼著啄鐲擢琢諑倬斲斮梲椓焯蝃踔鷟彴汋斫禚篧浞棳謶錣啅罬斀蠗圴剢灂"},
|
||||
"ㄓㄨㄞ":{"S":"拽","T":"拽"},
|
||||
"ㄓㄨㄞˇ":{"S":"跩","T":"跩"},
|
||||
"ㄓㄨㄞˋ":{"S":"拽","T":"拽"},
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Shared
|
||||
|
||||
extension LMAssembly {
|
||||
struct LMReplacements {
|
||||
public private(set) var filePath: String?
|
||||
|
@ -35,8 +33,8 @@ extension LMAssembly {
|
|||
replaceData(textData: rawStrData)
|
||||
} catch {
|
||||
filePath = oldPath
|
||||
vCLog("\(error)")
|
||||
vCLog("↑ Exception happened when reading data at: \(path).")
|
||||
vCLMLog("\(error)")
|
||||
vCLMLog("↑ Exception happened when reading data at: \(path).")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -72,7 +70,7 @@ extension LMAssembly {
|
|||
do {
|
||||
try strData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("Failed to save current database to: \(filePath)")
|
||||
vCLMLog("Failed to save current database to: \(filePath)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +79,7 @@ extension LMAssembly {
|
|||
for entry in rangeMap {
|
||||
strDump += strData[entry.value] + "\n"
|
||||
}
|
||||
vCLog(strDump)
|
||||
vCLMLog(strDump)
|
||||
}
|
||||
|
||||
public func valuesFor(key: String) -> String {
|
||||
|
@ -100,3 +98,13 @@ extension LMAssembly {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LMAssembly.LMReplacements {
|
||||
var dictRepresented: [String: String] {
|
||||
var result = [String: String]()
|
||||
rangeMap.forEach { key, valueRange in
|
||||
result[key] = strData[valueRange].description
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import Foundation
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
// MARK: - Public Types.
|
||||
|
||||
|
@ -202,18 +201,18 @@ extension LMAssembly.LMUserOverride {
|
|||
do {
|
||||
let nullData = "{}"
|
||||
guard let fileURL = fileURL ?? fileSaveLocationURL else {
|
||||
throw "given fileURL is invalid or nil."
|
||||
throw UOMError(rawValue: "given fileURL is invalid or nil.")
|
||||
}
|
||||
try nullData.write(to: fileURL, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
vCLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
|
||||
vCLMLog("UOM Error: Unable to clear the data in the UOM file. Details: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func saveData(toURL fileURL: URL? = nil) {
|
||||
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
|
||||
vCLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
|
||||
vCLMLog("UOM saveData() failed. At least the file Save URL is not set for the current UOM.")
|
||||
return
|
||||
}
|
||||
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
||||
|
@ -222,14 +221,14 @@ extension LMAssembly.LMUserOverride {
|
|||
guard let jsonData = try? encoder.encode(mutLRUMap) else { return }
|
||||
try jsonData.write(to: fileURL, options: .atomic)
|
||||
} catch {
|
||||
vCLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||
vCLMLog("UOM Error: Unable to save data, abort saving. Details: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func loadData(fromURL fileURL: URL? = nil) {
|
||||
guard let fileURL: URL = fileURL ?? fileSaveLocationURL else {
|
||||
vCLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
|
||||
vCLMLog("UOM loadData() failed. At least the file Load URL is not set for the current UOM.")
|
||||
return
|
||||
}
|
||||
// 此處不要使用 JSONSerialization,不然執行緒會炸掉。
|
||||
|
@ -238,13 +237,13 @@ extension LMAssembly.LMUserOverride {
|
|||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
if ["", "{}"].contains(String(data: data, encoding: .utf8)) { return }
|
||||
guard let jsonResult = try? decoder.decode([String: KeyObservationPair].self, from: data) else {
|
||||
vCLog("UOM Error: Read file content type invalid, abort loading.")
|
||||
vCLMLog("UOM Error: Read file content type invalid, abort loading.")
|
||||
return
|
||||
}
|
||||
mutLRUMap = jsonResult
|
||||
resetMRUList()
|
||||
} catch {
|
||||
vCLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
|
||||
vCLMLog("UOM Error: Unable to read file or parse the data, abort loading. Details: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +270,7 @@ extension LMAssembly.LMUserOverride {
|
|||
mutLRUMap.removeValue(forKey: mutLRUList[mutLRUList.endIndex - 1].key)
|
||||
mutLRUList.removeLast()
|
||||
}
|
||||
vCLog("UOM: Observation finished with new observation: \(key)")
|
||||
vCLMLog("UOM: Observation finished with new observation: \(key)")
|
||||
saveCallback?() ?? saveData()
|
||||
return
|
||||
}
|
||||
|
@ -282,7 +281,7 @@ extension LMAssembly.LMUserOverride {
|
|||
)
|
||||
mutLRUList.insert(theNeta, at: 0)
|
||||
mutLRUMap[key] = theNeta
|
||||
vCLog("UOM: Observation finished with existing observation: \(key)")
|
||||
vCLMLog("UOM: Observation finished with existing observation: \(key)")
|
||||
saveCallback?() ?? saveData()
|
||||
}
|
||||
}
|
||||
|
@ -400,3 +399,10 @@ extension LMAssembly.LMUserOverride {
|
|||
return result
|
||||
}
|
||||
}
|
||||
|
||||
struct UOMError: LocalizedError {
|
||||
var rawValue: String
|
||||
var errorDescription: String? {
|
||||
NSLocalizedString("rawValue", comment: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import SQLite3
|
||||
|
||||
public enum LMAssembly {
|
||||
|
@ -56,7 +55,7 @@ extension Array where Element == String {
|
|||
sqlite3_prepare_v2(ptrDB, strStmt, -1, &ptrStmt, nil) == SQLITE_OK && sqlite3_step(ptrStmt) == SQLITE_DONE
|
||||
}
|
||||
guard thisResult else {
|
||||
vCLog("SQL Query Error. Statement: \(strStmt)")
|
||||
vCLMLog("SQL Query Error. Statement: \(strStmt)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -83,3 +82,13 @@ func performStatementSansResult(_ handler: (inout OpaquePointer?) -> Void) {
|
|||
}
|
||||
handler(&ptrStmt)
|
||||
}
|
||||
|
||||
func vCLMLog(_ strPrint: StringLiteralType) {
|
||||
guard let toLog = UserDefaults.standard.object(forKey: "_DebugMode") as? Bool else {
|
||||
NSLog("vChewingDebug: %@", strPrint)
|
||||
return
|
||||
}
|
||||
if toLog {
|
||||
NSLog("vChewingDebug: %@", strPrint)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ let package = Package(
|
|||
.package(path: "../HangarRash_SwiftyCapsLockToggler"),
|
||||
.package(path: "../Jad_BookmarkManager"),
|
||||
.package(path: "../Qwertyyb_ShiftKeyUpChecker"),
|
||||
.package(path: "../vChewing_BrailleSputnik"),
|
||||
.package(path: "../vChewing_CandidateWindow"),
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
.package(path: "../vChewing_Hotenka"),
|
||||
.package(path: "../vChewing_IMKUtils"),
|
||||
.package(path: "../vChewing_KimoDataReader"),
|
||||
|
@ -38,9 +39,10 @@ let package = Package(
|
|||
.target(
|
||||
name: "MainAssembly",
|
||||
dependencies: [
|
||||
.product(name: "BrailleSputnik", package: "vChewing_BrailleSputnik"),
|
||||
.product(name: "BookmarkManager", package: "Jad_BookmarkManager"),
|
||||
.product(name: "CandidateWindow", package: "vChewing_CandidateWindow"),
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
.product(name: "FolderMonitor", package: "DanielGalasko_FolderMonitor"),
|
||||
.product(name: "Hotenka", package: "vChewing_Hotenka"),
|
||||
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftUI
|
||||
|
||||
public class CtlAboutUI: NSWindowController, NSWindowDelegate {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftUI
|
||||
|
||||
public struct VwrAboutUI {
|
||||
|
|
|
@ -69,6 +69,8 @@ public extension AppDelegate {
|
|||
|
||||
SpeechSputnik.shared.refreshStatus() // 根據現狀條件決定是否初期化語音引擎。
|
||||
|
||||
CandidateTextService.enableFinalSanityCheck()
|
||||
|
||||
// 一旦發現與使用者半衰模組的觀察行為有關的崩潰標記被開啟:
|
||||
// 如果有開啟 Debug 模式的話,就將既有的半衰記憶資料檔案更名+打上當時的時間戳。
|
||||
// 如果沒有開啟 Debug 模式的話,則將半衰記憶資料直接清空。
|
||||
|
@ -145,7 +147,7 @@ public extension AppDelegate {
|
|||
guard let currentMemorySizeInBytes = NSApplication.memoryFootprint else { return 0 }
|
||||
let currentMemorySize: Double = (Double(currentMemorySizeInBytes) / 1024 / 1024).rounded(toPlaces: 1)
|
||||
switch currentMemorySize {
|
||||
case 384...:
|
||||
case 1024...:
|
||||
vCLog("WARNING: EXCESSIVE MEMORY FOOTPRINT (\(currentMemorySize)MB).")
|
||||
let msgPackage = UNMutableNotificationContent()
|
||||
msgPackage.title = NSLocalizedString("vChewing", comment: "")
|
||||
|
@ -167,4 +169,10 @@ public extension AppDelegate {
|
|||
}
|
||||
return currentMemorySize
|
||||
}
|
||||
|
||||
// New About Window
|
||||
@IBAction func about(_: Any) {
|
||||
CtlAboutUI.show()
|
||||
NSApp.popup()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import BrailleSputnik
|
||||
import Foundation
|
||||
import Shared
|
||||
import Tekkon
|
||||
|
||||
public extension CandidateTextService {
|
||||
// MARK: - Final Sanity Check Implementation.
|
||||
|
||||
static func enableFinalSanityCheck() {
|
||||
finalSanityCheck = finalSanityCheckImplemented
|
||||
}
|
||||
|
||||
private static func finalSanityCheckImplemented(_ target: CandidateTextService) -> Bool {
|
||||
switch target.value {
|
||||
case .url: return true
|
||||
case let .selector(strSelector):
|
||||
guard target.candidateText != "%s" else { return true } // 防止誤傷到編輯器。
|
||||
switch strSelector {
|
||||
case "copyUnicodeMetadata:": return true
|
||||
case _ where strSelector.hasPrefix("copyRuby"),
|
||||
_ where strSelector.hasPrefix("copyBraille"),
|
||||
_ where strSelector.hasPrefix("copyInline"):
|
||||
return !target.reading.joined().isEmpty // 以便應對 [""] 的情況。
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Selector Methods, CandidatePairServicable, and the Coordinator.
|
||||
|
||||
var responseFromSelector: String? {
|
||||
switch value {
|
||||
case .url: return nil
|
||||
case let .selector(string):
|
||||
let passable = CandidatePairServicable(value: candidateText, reading: reading)
|
||||
return Coordinator().runTask(selectorName: string, candidate: passable)
|
||||
}
|
||||
}
|
||||
|
||||
@objcMembers class CandidatePairServicable: NSObject {
|
||||
public var value: String
|
||||
public var reading: [String]
|
||||
public init(value: String, reading: [String] = []) {
|
||||
self.value = value
|
||||
self.reading = reading
|
||||
}
|
||||
|
||||
public typealias SubPair = (key: String, value: String)
|
||||
|
||||
@nonobjc var smashed: [SubPair] {
|
||||
var pairs = [SubPair]()
|
||||
if value.count != reading.count {
|
||||
pairs.append((reading.joined(separator: " "), value))
|
||||
} else {
|
||||
value.enumerated().forEach { i, valChar in
|
||||
pairs.append((reading[i], valChar.description))
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
}
|
||||
|
||||
@objc class Coordinator: NSObject {
|
||||
private var result: String?
|
||||
|
||||
public func runTask(selectorName: String, candidate param: CandidatePairServicable) -> String? {
|
||||
guard !selectorName.isEmpty, !param.value.isEmpty else { return nil }
|
||||
guard responds(to: Selector(selectorName)) else { return nil }
|
||||
performSelector(onMainThread: Selector(selectorName), with: param, waitUntilDone: true)
|
||||
defer { result = nil }
|
||||
return result
|
||||
}
|
||||
|
||||
/// 生成 Unicode 統一碼碼位中繼資料。
|
||||
/// - Parameter param: 要處理的詞音配對物件。
|
||||
@objc func copyUnicodeMetadata(_ param: CandidatePairServicable) {
|
||||
var resultArray = [String]()
|
||||
param.value.forEach { char in
|
||||
resultArray.append("\(char) \(char.description.charDescriptions.first ?? "NULL")")
|
||||
}
|
||||
result = resultArray.joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// 生成 HTML Ruby (教科書注音)。
|
||||
/// - Parameter param: 要處理的詞音配對物件。
|
||||
@objc func copyRubyHTMLZhuyinTextbookStyle(_ param: CandidatePairServicable) {
|
||||
prepareTextBookZhuyinReadings(param)
|
||||
copyRubyHTMLCommon(param)
|
||||
}
|
||||
|
||||
/// 生成 HTML Ruby (教科書漢語拼音注音)。
|
||||
/// - Parameter param: 要處理的詞音配對物件。
|
||||
@objc func copyRubyHTMLHanyuPinyinTextbookStyle(_ param: CandidatePairServicable) {
|
||||
prepareTextBookPinyinReadings(param)
|
||||
copyRubyHTMLCommon(param)
|
||||
}
|
||||
|
||||
/// 生成內文讀音標注 (教科書注音)。
|
||||
/// - Parameter param: 要處理的詞音配對物件。
|
||||
@objc func copyInlineZhuyinAnnotationTextbookStyle(_ param: CandidatePairServicable) {
|
||||
prepareTextBookZhuyinReadings(param)
|
||||
copyInlineAnnotationCommon(param)
|
||||
}
|
||||
|
||||
/// 生成內文讀音標注 (教科書漢語拼音注音)。
|
||||
/// - Parameter param: 要處理的詞音配對物件。
|
||||
@objc func copyInlineHanyuPinyinAnnotationTextbookStyle(_ param: CandidatePairServicable) {
|
||||
prepareTextBookPinyinReadings(param)
|
||||
copyInlineAnnotationCommon(param)
|
||||
}
|
||||
|
||||
@objc func copyBraille1947(_ param: CandidatePairServicable) {
|
||||
result = BrailleSputnik(standard: .of1947).convertToBraille(smashedPairs: param.smashed)
|
||||
}
|
||||
|
||||
@objc func copyBraille2018(_ param: CandidatePairServicable) {
|
||||
result = BrailleSputnik(standard: .of2018).convertToBraille(smashedPairs: param.smashed)
|
||||
}
|
||||
|
||||
// MARK: Privates
|
||||
}
|
||||
}
|
||||
|
||||
private extension CandidateTextService.Coordinator {
|
||||
func copyInlineAnnotationCommon(_ param: CandidateTextService.CandidatePairServicable) {
|
||||
var composed = ""
|
||||
param.smashed.forEach { subPair in
|
||||
let subKey = subPair.key
|
||||
let subValue = subPair.value
|
||||
composed += subKey.contains("_") ? subValue : "\(subValue)(\(subKey))"
|
||||
}
|
||||
result = composed
|
||||
}
|
||||
|
||||
func copyRubyHTMLCommon(_ param: CandidateTextService.CandidatePairServicable) {
|
||||
var composed = ""
|
||||
param.smashed.forEach { subPair in
|
||||
let subKey = subPair.key
|
||||
let subValue = subPair.value
|
||||
composed += subKey.contains("_") ? subValue : "<ruby>\(subValue)<rp>(</rp><rt>\(subKey)</rt><rp>)</rp></ruby>"
|
||||
}
|
||||
result = composed
|
||||
}
|
||||
|
||||
func prepareTextBookZhuyinReadings(_ param: CandidateTextService.CandidatePairServicable) {
|
||||
let newReadings = param.reading.map { currentReading in
|
||||
if currentReading.contains("_") { return "_??" }
|
||||
return Tekkon.cnvPhonaToTextbookStyle(target: currentReading)
|
||||
}
|
||||
param.reading = newReadings
|
||||
}
|
||||
|
||||
func prepareTextBookPinyinReadings(_ param: CandidateTextService.CandidatePairServicable) {
|
||||
let newReadings = param.reading.map { currentReading in
|
||||
if currentReading.contains("_") { return "_??" }
|
||||
return Tekkon.cnvHanyuPinyinToTextbookStyle(
|
||||
targetJoined: Tekkon.cnvPhonaToHanyuPinyin(targetJoined: currentReading)
|
||||
)
|
||||
}
|
||||
param.reading = newReadings
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Hotenka
|
||||
import Shared
|
||||
|
||||
public enum ChineseConverter {
|
||||
public static let shared = HotenkaChineseConverter(
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
import AppKit
|
||||
import Foundation
|
||||
import Shared
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public class VwrClientListMgr: NSViewController {
|
||||
let windowWidth: CGFloat = 770
|
||||
let contentWidth: CGFloat = 750
|
||||
let buttonWidth: CGFloat = 150
|
||||
let tableHeight: CGFloat = 230
|
||||
|
||||
lazy var tblClients: NSTableView = .init()
|
||||
lazy var btnAddClient = NSButton("Add Client", target: self, action: #selector(btnAddClientClicked(_:)))
|
||||
|
@ -35,7 +35,7 @@ public class VwrClientListMgr: NSViewController {
|
|||
var body: NSView? {
|
||||
NSStackView.build(.vertical, insets: .new(all: 14)) {
|
||||
makeScrollableTable()
|
||||
.makeSimpleConstraint(.height, relation: .equal, value: 232)
|
||||
.makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
|
||||
NSStackView.build(.horizontal) {
|
||||
let descriptionWidth = contentWidth - buttonWidth - 20
|
||||
NSStackView.build(.vertical) {
|
||||
|
@ -59,6 +59,8 @@ public class VwrClientListMgr: NSViewController {
|
|||
scrollContainer.scrollerStyle = .legacy
|
||||
scrollContainer.autohidesScrollers = true
|
||||
scrollContainer.documentView = tblClients
|
||||
scrollContainer.hasVerticalScroller = true
|
||||
scrollContainer.hasHorizontalScroller = true
|
||||
if #available(macOS 11.0, *) {
|
||||
tblClients.style = .inset
|
||||
}
|
||||
|
@ -72,12 +74,12 @@ public class VwrClientListMgr: NSViewController {
|
|||
tblClients.autosaveTableColumns = false
|
||||
tblClients.backgroundColor = NSColor.controlBackgroundColor
|
||||
tblClients.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
|
||||
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: 230)
|
||||
tblClients.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
|
||||
tblClients.gridColor = NSColor.clear
|
||||
tblClients.intercellSpacing = CGSize(width: 17, height: 0)
|
||||
tblClients.rowHeight = 24
|
||||
tblClients.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
tblClients.registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
|
||||
tblClients.registerForDraggedTypes([.kUTTypeFileURL])
|
||||
tblClients.dataSource = self
|
||||
tblClients.action = #selector(onItemClicked(_:))
|
||||
tblClients.target = self
|
||||
|
@ -150,7 +152,7 @@ extension VwrClientListMgr {
|
|||
neta info: NSDraggingInfo, onError: @escaping () -> Void?, handler: (([URL]) -> Void)? = nil
|
||||
) {
|
||||
let board = info.draggingPasteboard
|
||||
let type = NSPasteboard.PasteboardType(rawValue: kUTTypeApplicationBundle as String)
|
||||
let type = NSPasteboard.PasteboardType.kUTTypeAppBundle
|
||||
let options: [NSPasteboard.ReadingOptionKey: Any] = [
|
||||
.urlReadingFileURLsOnly: true,
|
||||
.urlReadingContentsConformToTypes: [type],
|
||||
|
|
|
@ -200,7 +200,8 @@ public extension IMEState {
|
|||
case .ofCandidates where cursor != marker: return data.attributedStringMarking(for: session)
|
||||
case .ofCandidates where cursor == marker: break
|
||||
case .ofAssociates: return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where displayedText.isEmpty: return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where displayedText.isEmpty || node.containsCandidateServices:
|
||||
return data.attributedStringPlaceholder(for: session)
|
||||
case .ofSymbolTable where !displayedText.isEmpty: break
|
||||
default: break
|
||||
}
|
||||
|
|
|
@ -212,7 +212,7 @@ public extension IMEStateData {
|
|||
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
|
||||
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
|
||||
} else {
|
||||
subNeta = Tekkon.cnvPhonaToTextbookReading(target: subNeta)
|
||||
subNeta = Tekkon.cnvPhonaToTextbookStyle(target: subNeta)
|
||||
}
|
||||
}
|
||||
arrOutput.append(subNeta)
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
/// 該檔案乃輸入調度模組當中「用來規定在選字窗出現時的按鍵行為」的部分。
|
||||
|
||||
import CandidateWindow
|
||||
import CocoaExtension
|
||||
import InputMethodKit
|
||||
import Megrez
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
// MARK: - § 對選字狀態進行調度 (Handle Candidate State).
|
||||
|
@ -30,6 +30,47 @@ extension InputHandler {
|
|||
guard ctlCandidate.visible else { return false }
|
||||
let inputText = ignoringModifiers ? (input.inputTextIgnoringModifiers ?? input.text) : input.text
|
||||
let allowMovingCompositorCursor = state.type == .ofCandidates && !prefs.useSCPCTypingMode
|
||||
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex]
|
||||
|
||||
// MARK: 選字窗服務選單(Shift+?)。
|
||||
|
||||
var candidateTextServiceMenuRunning: Bool {
|
||||
state.node.containsCandidateServices && state.type == .ofSymbolTable
|
||||
}
|
||||
|
||||
serviceMenu: if prefs.useShiftQuestionToCallServiceMenu, input.commonKeyModifierFlags == .shift, input.text == "?" {
|
||||
if candidateTextServiceMenuRunning { break serviceMenu }
|
||||
let handled = handleServiceMenuInitiation(
|
||||
candidateText: highlightedCandidate.value,
|
||||
reading: highlightedCandidate.keyArray
|
||||
)
|
||||
if handled { return true }
|
||||
}
|
||||
|
||||
// MARK: 波浪符號鍵(選字窗服務選單 / 輔助翻頁 / 其他功能)。
|
||||
|
||||
if input.isSymbolMenuPhysicalKey {
|
||||
switch input.commonKeyModifierFlags {
|
||||
case .shift, [],
|
||||
.option where !candidateTextServiceMenuRunning:
|
||||
if !candidateTextServiceMenuRunning {
|
||||
let handled = handleServiceMenuInitiation(
|
||||
candidateText: highlightedCandidate.value,
|
||||
reading: highlightedCandidate.keyArray
|
||||
)
|
||||
if handled { return true }
|
||||
}
|
||||
var updated = true
|
||||
let reverseTrigger = input.isShiftHold || input.isOptionHold
|
||||
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
|
||||
if !updated { delegate.callError("66F3477B") }
|
||||
return true
|
||||
case .option where state.type == .ofSymbolTable:
|
||||
// 繞過內碼輸入模式,直接進入漢音鍵盤符號模式。
|
||||
return revolveTypingMethod(to: .haninKeyboardSymbol)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: 選字窗內使用熱鍵升權、降權、刪詞。
|
||||
|
||||
|
@ -101,7 +142,6 @@ extension InputHandler {
|
|||
delegate.switchState(IMEState.ofAbortion())
|
||||
return true
|
||||
}
|
||||
let highlightedCandidate = state.candidates[ctlCandidate.highlightedIndex] // 關聯詞語功能專用。
|
||||
var handleAssociates = !prefs.useSCPCTypingMode && prefs.associatedPhrasesEnabled // 關聯詞語功能專用。
|
||||
handleAssociates = handleAssociates && compositor.cursor == compositor.length // 關聯詞語功能專用。
|
||||
confirmHighlightedCandidate()
|
||||
|
@ -315,7 +355,7 @@ extension InputHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Flipping pages by using modified bracket keys (when they are not occupied).
|
||||
// MARK: Flipping pages by using modified bracket keys (when they are not occupied).
|
||||
|
||||
// Shift+Command+[] 被 Chrome 系瀏覽器佔用,所以改用 Ctrl。
|
||||
let ctrlCMD: Bool = input.commonKeyModifierFlags == [.control, .command]
|
||||
|
@ -333,24 +373,6 @@ extension InputHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Flipping pages by using symbol menu keys (when they are not occupied).
|
||||
|
||||
if input.isSymbolMenuPhysicalKey {
|
||||
switch input.commonKeyModifierFlags {
|
||||
case .shift, [],
|
||||
.option where state.type != .ofSymbolTable:
|
||||
var updated = true
|
||||
let reverseTrigger = input.isShiftHold || input.isOptionHold
|
||||
updated = reverseTrigger ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
|
||||
if !updated { delegate.callError("66F3477B") }
|
||||
return true
|
||||
case .option where state.type == .ofSymbolTable:
|
||||
// 繞過內碼輸入模式,直接進入漢音鍵盤符號模式。
|
||||
return revolveTypingMethod(to: .haninKeyboardSymbol)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
if state.type == .ofInputting { return false } // `%quick`
|
||||
|
||||
delegate.callError("172A0F81")
|
||||
|
|
|
@ -58,7 +58,7 @@ private extension InputHandler {
|
|||
|
||||
func narrateTheComposer(with maybeKey: String? = nil, when condition: Bool, allowDuplicates: Bool = true) {
|
||||
guard condition else { return }
|
||||
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
|
||||
let maybeKey = maybeKey ?? composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
|
||||
guard var keyToNarrate = maybeKey else { return }
|
||||
if composer.intonation == Tekkon.Phonabet(" ") { keyToNarrate.append("ˉ") }
|
||||
SpeechSputnik.shared.narrate(keyToNarrate, allowDuplicates: allowDuplicates)
|
||||
|
@ -119,7 +119,7 @@ private extension InputHandler {
|
|||
return handleEnter(input: input, readingOnly: true)
|
||||
}
|
||||
// 拿取用來進行索引檢索用的注音。這裡先不急著處理「僅有注音符號輸入」的情況。
|
||||
let maybeKey = composer.phonabetKeyForQuery(pronouncable: prefs.acceptLeadingIntonations)
|
||||
let maybeKey = composer.phonabetKeyForQuery(pronounceableOnly: prefs.acceptLeadingIntonations)
|
||||
guard let readingKey = maybeKey else { break ifComposeReading }
|
||||
// 向語言模型詢問是否有對應的記錄。
|
||||
if !currentLM.hasUnigramsFor(keyArray: [readingKey]) {
|
||||
|
@ -204,7 +204,7 @@ private extension InputHandler {
|
|||
/// 但這裡不處理陰平聲調。
|
||||
if keyConsumedByReading {
|
||||
// 此處將 strict 設為 false,以應對「僅有注音符號輸入」的情況。
|
||||
if composer.phonabetKeyForQuery(pronouncable: false) == nil {
|
||||
if composer.phonabetKeyForQuery(pronounceableOnly: false) == nil {
|
||||
// 將被空格鍵覆蓋掉的既有聲調塞入組字器。
|
||||
if !composer.isPinyinMode, input.isSpace,
|
||||
compositor.insertKey(existedIntonation.value)
|
||||
|
|
|
@ -393,75 +393,6 @@ extension InputHandler {
|
|||
return true
|
||||
}
|
||||
|
||||
// MARK: - Command+Enter 鍵的處理(注音文)
|
||||
|
||||
/// Command+Enter 鍵的處理(注音文)。
|
||||
/// - Parameter isShiftPressed: 有沒有同時摁著 Shift 鍵。
|
||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||
private func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
|
||||
var displayedText = compositor.keys.joined(separator: "\t")
|
||||
if compositor.isEmpty {
|
||||
displayedText = readingForDisplay
|
||||
}
|
||||
if !prefs.cassetteEnabled {
|
||||
if prefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||
if !compositor.isEmpty {
|
||||
var arrDisplayedTextElements = [String]()
|
||||
compositor.keys.forEach { key in
|
||||
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) // 恢復陰平標記
|
||||
}
|
||||
displayedText = arrDisplayedTextElements.joined(separator: "\t")
|
||||
}
|
||||
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) // 注音轉拼音
|
||||
}
|
||||
if prefs.showHanyuPinyinInCompositionBuffer {
|
||||
if compositor.isEmpty {
|
||||
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
|
||||
return displayedText
|
||||
}
|
||||
|
||||
// MARK: - Command+Option+Enter 鍵的處理(網頁 Ruby 注音文標記)
|
||||
|
||||
/// Command+Option+Enter 鍵的處理(網頁 Ruby 注音文標記)。
|
||||
/// - Parameter isShiftPressed: 有沒有同時摁著 Shift 鍵。摁了的話則只遞交讀音字串。
|
||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||
private func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
|
||||
var composed = ""
|
||||
|
||||
compositor.walkedNodes.smashedPairs.forEach { key, value in
|
||||
var key = key
|
||||
if !prefs.cassetteEnabled {
|
||||
key =
|
||||
prefs.inlineDumpPinyinInLieuOfZhuyin
|
||||
? Tekkon.restoreToneOneInPhona(target: key) // 恢復陰平標記
|
||||
: Tekkon.cnvPhonaToTextbookReading(target: key) // 恢復陰平標記
|
||||
|
||||
if prefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) // 注音轉拼音
|
||||
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 轉教科書式標調
|
||||
}
|
||||
}
|
||||
|
||||
key = key.replacingOccurrences(of: "\t", with: " ")
|
||||
|
||||
if isShiftPressed {
|
||||
if !composed.isEmpty { composed += " " }
|
||||
composed += key.contains("_") ? "??" : key
|
||||
return
|
||||
}
|
||||
|
||||
// 不要給標點符號等特殊元素加注音
|
||||
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
||||
}
|
||||
|
||||
return composed
|
||||
}
|
||||
|
||||
// MARK: - 處理 BackSpace (macOS Delete) 按鍵行為
|
||||
|
||||
/// 處理 BackSpace (macOS Delete) 按鍵行為。
|
||||
|
@ -967,6 +898,20 @@ extension InputHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - 處理選字窗服務選單 (Service Menu)
|
||||
|
||||
func handleServiceMenuInitiation(candidateText: String, reading: [String]) -> Bool {
|
||||
guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false }
|
||||
guard !candidateText.isEmpty else { return false }
|
||||
let rootNode = CandidateTextService.getCurrentServiceMenu(candidate: candidateText, reading: reading)
|
||||
guard let rootNode = rootNode else { return false }
|
||||
// 得在這裡先 commit buffer,不然會導致「在摁 ESC 離開符號選單時會重複輸入上一次的組字區的內容」的不當行為。
|
||||
let textToCommit = generateStateOfInputting(sansReading: true).displayedText
|
||||
delegate.switchState(IMEState.ofCommitting(textToCommit: textToCommit))
|
||||
delegate.switchState(IMEState.ofSymbolTable(node: rootNode))
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - 處理 Caps Lock 與英數輸入模式(Caps Lock and Alphanumerical mode)
|
||||
|
||||
/// 處理 CapsLock 與英數輸入模式。
|
||||
|
@ -1096,11 +1041,15 @@ extension InputHandler {
|
|||
let fullWidthResult = behaviorValue % 2 != 0 // 能被二整除的都是半形。
|
||||
triagePrefs: switch (behaviorValue, isConsideredEmptyForNow) {
|
||||
case (2, _), (3, _), (4, false), (5, false):
|
||||
currentLM.config.numPadFWHWStatus = fullWidthResult
|
||||
currentLM.setOptions { config in
|
||||
config.numPadFWHWStatus = fullWidthResult
|
||||
}
|
||||
if handlePunctuation("_NumPad_\(inputText)") { return true }
|
||||
default: break triagePrefs // 包括 case 0 & 1。
|
||||
}
|
||||
currentLM.config.numPadFWHWStatus = nil
|
||||
currentLM.setOptions { config in
|
||||
config.numPadFWHWStatus = nil
|
||||
}
|
||||
delegate.switchState(IMEState.ofEmpty())
|
||||
let charToCommit = inputText.applyingTransformFW2HW(reverse: fullWidthResult)
|
||||
delegate.switchState(IMEState.ofCommitting(textToCommit: charToCommit))
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import BrailleSputnik
|
||||
import Shared
|
||||
import Tekkon
|
||||
|
||||
/// 該檔案專門管理「用指定熱鍵遞交特殊的內容」的這一類函式。
|
||||
|
||||
extension InputHandler {
|
||||
// MARK: - (Shift+)Ctrl+Command+Enter 鍵的處理(注音文)
|
||||
|
||||
/// Command+Enter 鍵的處理(注音文)。
|
||||
/// - Parameter isShiftPressed: 有沒有同時摁著 Shift 鍵。
|
||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||
func commissionByCtrlCommandEnter(isShiftPressed: Bool = false) -> String {
|
||||
var displayedText = compositor.keys.joined(separator: "\t")
|
||||
if compositor.isEmpty {
|
||||
displayedText = readingForDisplay
|
||||
}
|
||||
if !prefs.cassetteEnabled {
|
||||
if prefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||
if !compositor.isEmpty {
|
||||
var arrDisplayedTextElements = [String]()
|
||||
compositor.keys.forEach { key in
|
||||
arrDisplayedTextElements.append(Tekkon.restoreToneOneInPhona(target: key)) // 恢復陰平標記
|
||||
}
|
||||
displayedText = arrDisplayedTextElements.joined(separator: "\t")
|
||||
}
|
||||
displayedText = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: displayedText) // 注音轉拼音
|
||||
}
|
||||
if prefs.showHanyuPinyinInCompositionBuffer {
|
||||
if compositor.isEmpty {
|
||||
displayedText = displayedText.replacingOccurrences(of: "1", with: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
displayedText = displayedText.replacingOccurrences(of: "\t", with: isShiftPressed ? "-" : " ")
|
||||
return displayedText
|
||||
}
|
||||
|
||||
// MARK: - (Shift+)Ctrl+Command+Option+Enter 鍵的處理(網頁 Ruby 注音文標記)
|
||||
|
||||
private enum CommitableMarkupType: Int {
|
||||
case bareKeys = -1
|
||||
case textWithBracketedAnnotations = 0
|
||||
case textWithHTMLRubyAnnotations = 1
|
||||
case braille1947 = 2
|
||||
case braille2018 = 3
|
||||
|
||||
static func match(rawValue: Int) -> Self {
|
||||
CommitableMarkupType(rawValue: rawValue) ?? .textWithBracketedAnnotations
|
||||
}
|
||||
|
||||
var brailleStandard: BrailleSputnik.BrailleStandard? {
|
||||
switch self {
|
||||
case .braille1947: return .of1947
|
||||
case .braille2018: return .of2018
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command+Option+Enter 鍵的處理(網頁 Ruby 注音文標記)。
|
||||
///
|
||||
/// 關於 prefs.specifyCmdOptCtrlEnterBehavior 的幾個參數作用:
|
||||
/// 1. 帶括弧的注音標記。
|
||||
/// 2. HTML Ruby 注音標記。
|
||||
/// 3. 國語點字 (1947)。
|
||||
/// 4. 國通盲文 (GF0019-2018)。
|
||||
/// - Parameter isShiftPressed: 有沒有同時摁著 Shift 鍵。摁了的話則只遞交讀音字串。
|
||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 SessionCtl 回報給 IMK。
|
||||
func commissionByCtrlOptionCommandEnter(isShiftPressed: Bool = false) -> String {
|
||||
var behavior = CommitableMarkupType.match(rawValue: prefs.specifyCmdOptCtrlEnterBehavior)
|
||||
if prefs.cassetteEnabled, behavior.brailleStandard != nil {
|
||||
behavior = .textWithBracketedAnnotations
|
||||
}
|
||||
if isShiftPressed { behavior = .bareKeys }
|
||||
guard let brailleStandard = behavior.brailleStandard else {
|
||||
return specifyTextMarkupToCommit(behavior: behavior)
|
||||
}
|
||||
let brailleProcessor = BrailleSputnik(standard: brailleStandard)
|
||||
return brailleProcessor.convertToBraille(
|
||||
smashedPairs: compositor.walkedNodes.smashedPairs,
|
||||
extraInsertion: (reading: composer.value, cursor: compositor.cursor)
|
||||
)
|
||||
}
|
||||
|
||||
private func specifyTextMarkupToCommit(behavior: CommitableMarkupType) -> String {
|
||||
var composed = ""
|
||||
compositor.walkedNodes.smashedPairs.forEach { key, value in
|
||||
var key = key
|
||||
if !prefs.cassetteEnabled {
|
||||
key =
|
||||
prefs.inlineDumpPinyinInLieuOfZhuyin
|
||||
? Tekkon.restoreToneOneInPhona(target: key) // 恢復陰平標記
|
||||
: Tekkon.cnvPhonaToTextbookStyle(target: key) // 恢復陰平標記
|
||||
|
||||
if prefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||
key = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: key) // 注音轉拼音
|
||||
key = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: key) // 轉教科書式標調
|
||||
}
|
||||
}
|
||||
key = key.replacingOccurrences(of: "\t", with: " ")
|
||||
switch behavior {
|
||||
case .bareKeys:
|
||||
if !composed.isEmpty { composed += " " }
|
||||
composed += key.contains("_") ? "??" : key
|
||||
case .textWithBracketedAnnotations:
|
||||
composed += key.contains("_") ? value : "\(value)(\(key))"
|
||||
case .textWithHTMLRubyAnnotations:
|
||||
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
||||
case .braille1947: break // 另案處理
|
||||
case .braille2018: break // 另案處理
|
||||
}
|
||||
}
|
||||
return composed
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
/// 該檔案乃輸入調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給輸入調度模組處理時、
|
||||
/// 輸入調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。
|
||||
|
||||
import CocoaExtension
|
||||
import IMKUtils
|
||||
import InputMethodKit
|
||||
import LangModelAssembly
|
||||
import Megrez
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) * Triage
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
// MARK: - Typing Method
|
||||
|
|
|
@ -65,6 +65,11 @@ public class LMMgr {
|
|||
/// 載入磁帶資料。
|
||||
/// - Remark: cassettePath() 會在輸入法停用磁帶時直接返回
|
||||
public static func loadCassetteData() {
|
||||
func validateCassetteCandidateKey(_ target: String) -> Bool {
|
||||
CandidateKey.validate(keys: target) == nil
|
||||
}
|
||||
|
||||
LMAssembly.LMInstantiator.setCassetCandidateKeyValidator(validateCassetteCandidateKey)
|
||||
LMAssembly.LMInstantiator.loadCassetteData(path: cassettePath())
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import CandidateWindow
|
||||
import Foundation
|
||||
import LangModelAssembly
|
||||
import Megrez
|
||||
import Shared
|
||||
|
||||
// MARK: - 使用者語彙類型定義
|
||||
|
@ -32,6 +34,10 @@ public extension LMMgr {
|
|||
!keyArray.isEmpty && keyArray.filter(\.isEmpty).isEmpty && !value.isEmpty
|
||||
}
|
||||
|
||||
var isSingleCharReadingPair: Bool {
|
||||
value.count == 1 && keyArray.count == 1 && keyArray.first?.first != "_"
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
descriptionCells.joined(separator: " ")
|
||||
}
|
||||
|
@ -190,3 +196,51 @@ public extension LMMgr {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Weight Suggestions.
|
||||
|
||||
public extension LMMgr.UserPhrase {
|
||||
mutating func updateWeight(basedOn action: CandidateContextMenuAction) {
|
||||
weight = suggestNextFreq(for: action)
|
||||
}
|
||||
|
||||
func suggestNextFreq(for action: CandidateContextMenuAction, extreme: Bool = false) -> Double? {
|
||||
var extremeFallbackResult: Double? {
|
||||
switch action {
|
||||
case .toBoost: return nil // 不填寫權重的話,預設權重是 0
|
||||
case .toNerf: return -114.514
|
||||
case .toFilter: return nil
|
||||
}
|
||||
}
|
||||
guard !extreme, isSingleCharReadingPair else { return extremeFallbackResult }
|
||||
let fetchedUnigrams = inputMode.langModel.unigramsFor(keyArray: keyArray)
|
||||
let currentWeight = weight ?? fetchedUnigrams.first { $0.value == value }?.score
|
||||
guard let currentWeight = currentWeight else { return extremeFallbackResult }
|
||||
let fetchedScores = fetchedUnigrams.map(\.score)
|
||||
var neighborValue: Double?
|
||||
switch action {
|
||||
case .toBoost:
|
||||
neighborValue = currentWeight.findNeighborValue(from: fetchedScores, greater: true)
|
||||
if let realNeighborValue = neighborValue {
|
||||
neighborValue = realNeighborValue + 0.000_001
|
||||
} else if let fetchedMax = fetchedScores.min(), currentWeight <= fetchedMax {
|
||||
neighborValue = Swift.min(0, currentWeight + 0.000_001)
|
||||
} else {
|
||||
// 理論上來講,這種情況不該出現。
|
||||
neighborValue = Swift.min(0, currentWeight + 1)
|
||||
}
|
||||
case .toNerf:
|
||||
neighborValue = currentWeight.findNeighborValue(from: fetchedScores, greater: false)
|
||||
if let realNeighborValue = neighborValue {
|
||||
neighborValue = realNeighborValue - 0.000_001
|
||||
} else if let fetchedMax = fetchedScores.max(), currentWeight >= fetchedMax {
|
||||
neighborValue = Swift.max(-114.514, currentWeight - 0.000_001)
|
||||
} else {
|
||||
// 理論上來講,這種情況不該出現。
|
||||
neighborValue = Swift.max(-114.514, currentWeight - 1)
|
||||
}
|
||||
case .toFilter: return nil
|
||||
}
|
||||
return neighborValue ?? extremeFallbackResult
|
||||
}
|
||||
}
|
||||
|
|
|
@ -345,3 +345,26 @@ public extension LMMgr {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 將當前輸入法的所有使用者辭典數據傾印成 JSON
|
||||
|
||||
// 威注音輸入法並非可永續的專案。用單個 JSON 檔案遷移資料的話,可方便其他程式開發者們實作相關功能。
|
||||
|
||||
public extension LMMgr {
|
||||
@discardableResult static func dumpUserDictDataToJSON(print: Bool = false, all: Bool) -> String? {
|
||||
var summarizedDict = [LMAssembly.UserDictionarySummarized]()
|
||||
Shared.InputMode.allCases.forEach { mode in
|
||||
guard mode != .imeModeNULL else { return }
|
||||
summarizedDict.append(mode.langModel.summarize(all: all))
|
||||
}
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
if #available(macOS 10.13, *) {
|
||||
encoder.outputFormatting.insert(.sortedKeys)
|
||||
}
|
||||
guard let data = try? encoder.encode(summarizedDict) else { return nil }
|
||||
guard let outputStr = String(data: data, encoding: .utf8) else { return nil }
|
||||
if print { Swift.print(outputStr) }
|
||||
return outputStr
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Shared
|
||||
|
||||
public extension PrefMgr {
|
||||
static let shared: PrefMgr = {
|
||||
let result = PrefMgr()
|
||||
result.assignDidSetActions()
|
||||
return result
|
||||
}()
|
||||
|
||||
private func assignDidSetActions() {
|
||||
didAskForSyncingLMPrefs = {
|
||||
if PrefMgr.shared.phraseReplacementEnabled {
|
||||
LMMgr.loadUserPhraseReplacement()
|
||||
}
|
||||
if PrefMgr.shared.associatedPhrasesEnabled {
|
||||
LMMgr.loadUserAssociatesData()
|
||||
}
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
didAskForRefreshingSpeechSputnik = {
|
||||
SpeechSputnik.shared.refreshStatus()
|
||||
}
|
||||
didAskForSyncingShiftKeyDetectorPrefs = {
|
||||
SessionCtl.theShiftKeyDetector.toggleWithLShift = PrefMgr.shared.togglingAlphanumericalModeWithLShift
|
||||
SessionCtl.theShiftKeyDetector.toggleWithRShift = PrefMgr.shared.togglingAlphanumericalModeWithRShift
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import AppKit
|
||||
import Carbon
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
public class SecurityAgentHelper {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
public class CtlServiceMenuEditor: NSWindowController {
|
||||
let viewController = VwrServiceMenuEditor()
|
||||
|
||||
public static var shared: CtlServiceMenuEditor?
|
||||
public init() {
|
||||
super.init(
|
||||
window: .init(
|
||||
contentRect: CGRect(x: 401, y: 295, width: 770, height: 335),
|
||||
styleMask: [.titled, .closable, .miniaturizable],
|
||||
backing: .buffered,
|
||||
defer: true
|
||||
)
|
||||
)
|
||||
viewController.windowController = self
|
||||
viewController.loadView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
public static func show() {
|
||||
if shared == nil {
|
||||
shared = CtlServiceMenuEditor()
|
||||
}
|
||||
guard let shared = shared, let sharedWindow = shared.window else { return }
|
||||
if !sharedWindow.isVisible {
|
||||
shared.windowDidLoad()
|
||||
}
|
||||
sharedWindow.setPosition(vertical: .center, horizontal: .right, padding: 20)
|
||||
sharedWindow.orderFrontRegardless() // 逼著視窗往最前方顯示
|
||||
sharedWindow.title = "Service Menu Editor".localized
|
||||
sharedWindow.level = .statusBar
|
||||
if #available(macOS 10.10, *) {
|
||||
sharedWindow.titlebarAppearsTransparent = true
|
||||
}
|
||||
shared.showWindow(shared)
|
||||
NSApp.popup()
|
||||
}
|
||||
|
||||
override public func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
let view = viewController.view
|
||||
window?.contentView = view
|
||||
if let window = window {
|
||||
var frame = window.frame
|
||||
frame.size = view.fittingSize
|
||||
window.setFrame(frame, display: true)
|
||||
}
|
||||
window?.setPosition(vertical: .center, horizontal: .right, padding: 20)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
public class VwrServiceMenuEditor: NSViewController {
|
||||
let windowWidth: CGFloat = 770
|
||||
let contentWidth: CGFloat = 750
|
||||
let tableHeight: CGFloat = 230
|
||||
|
||||
lazy var tblServices: NSTableView = .init()
|
||||
lazy var btnShowInstructions = NSButton("How to Fill", target: self, action: #selector(btnShowInstructionsClicked(_:)))
|
||||
lazy var btnAddService = NSFileDragRetrieverButton(
|
||||
"Add Service",
|
||||
target: self,
|
||||
action: #selector(btnAddServiceClicked(_:)),
|
||||
postDrag: handleDrag
|
||||
)
|
||||
lazy var btnRemoveService = NSButton("Remove Selected", target: self, action: #selector(btnRemoveServiceClicked(_:)))
|
||||
lazy var btnResetService = NSButton("Reset Default", target: self, action: #selector(btnResetServiceClicked(_:)))
|
||||
lazy var btnCopyAllToClipboard = NSButton("Copy All to Clipboard", target: self, action: #selector(btnCopyAllToClipboardClicked(_:)))
|
||||
lazy var tableColumn1Cell = NSTextFieldCell()
|
||||
lazy var tableColumn1 = NSTableColumn()
|
||||
lazy var tableColumn2Cell = NSTextFieldCell()
|
||||
lazy var tableColumn2 = NSTableColumn()
|
||||
|
||||
var windowController: NSWindowController?
|
||||
|
||||
public convenience init(windowController: NSWindowController? = nil) {
|
||||
self.init()
|
||||
self.windowController = windowController
|
||||
}
|
||||
|
||||
override public func loadView() {
|
||||
tblServices.reloadData()
|
||||
view = body ?? .init()
|
||||
(view as? NSStackView)?.alignment = .centerX
|
||||
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
|
||||
btnRemoveService.keyEquivalent = .init(NSEvent.SpecialKey.delete.unicodeScalar)
|
||||
}
|
||||
|
||||
var body: NSView? {
|
||||
NSStackView.build(.vertical, insets: .new(all: 14)) {
|
||||
NSStackView.build(.horizontal) {
|
||||
btnAddService
|
||||
btnRemoveService
|
||||
btnCopyAllToClipboard
|
||||
btnShowInstructions
|
||||
NSView()
|
||||
btnResetService
|
||||
}
|
||||
makeScrollableTable()
|
||||
.makeSimpleConstraint(.height, relation: .equal, value: tableHeight)
|
||||
NSStackView.build(.horizontal) {
|
||||
let descriptionWidth = contentWidth - 10
|
||||
NSStackView.build(.vertical) {
|
||||
let strDescription = "i18n:CandidateServiceMenuEditor.description"
|
||||
strDescription.makeNSLabel(descriptive: true, fixWidth: descriptionWidth)
|
||||
.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: descriptionWidth)
|
||||
NSView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeScrollableTable() -> NSScrollView {
|
||||
let scrollContainer = NSScrollView()
|
||||
scrollContainer.scrollerStyle = .legacy
|
||||
scrollContainer.autohidesScrollers = true
|
||||
scrollContainer.documentView = tblServices
|
||||
scrollContainer.hasVerticalScroller = true
|
||||
scrollContainer.hasHorizontalScroller = true
|
||||
if #available(macOS 11.0, *) {
|
||||
tblServices.style = .inset
|
||||
}
|
||||
tblServices.addTableColumn(tableColumn1)
|
||||
tblServices.addTableColumn(tableColumn2)
|
||||
// tblServices.headerView = nil
|
||||
tblServices.delegate = self
|
||||
tblServices.allowsExpansionToolTips = true
|
||||
tblServices.allowsMultipleSelection = true
|
||||
tblServices.autoresizingMask = [.width, .height]
|
||||
tblServices.autosaveTableColumns = false
|
||||
tblServices.backgroundColor = NSColor.controlBackgroundColor
|
||||
tblServices.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
|
||||
tblServices.frame = CGRect(x: 0, y: 0, width: 728, height: tableHeight)
|
||||
tblServices.gridColor = NSColor.clear
|
||||
tblServices.intercellSpacing = CGSize(width: 15, height: 0)
|
||||
tblServices.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
tblServices.registerForDraggedTypes([.kUTTypeData, .kUTTypeFileURL])
|
||||
tblServices.dataSource = self
|
||||
tblServices.target = self
|
||||
if #available(macOS 11.0, *) { tblServices.style = .inset }
|
||||
|
||||
tableColumn1.identifier = NSUserInterfaceItemIdentifier("colTitle")
|
||||
tableColumn1.headerCell.title = "i18n:CandidateServiceMenuEditor.table.field.MenuTitle".localized
|
||||
tableColumn1.maxWidth = 280
|
||||
tableColumn1.minWidth = 200
|
||||
tableColumn1.resizingMask = [.autoresizingMask, .userResizingMask]
|
||||
tableColumn1.width = 200
|
||||
tableColumn1.dataCell = tableColumn1Cell
|
||||
|
||||
tableColumn1Cell.font = NSFont.systemFont(ofSize: 13)
|
||||
tableColumn1Cell.isEditable = true
|
||||
tableColumn1Cell.isSelectable = true
|
||||
tableColumn1Cell.lineBreakMode = .byTruncatingTail
|
||||
tableColumn1Cell.stringValue = "Text Cell"
|
||||
tableColumn1Cell.textColor = NSColor.controlTextColor
|
||||
|
||||
tableColumn2.identifier = NSUserInterfaceItemIdentifier("colValue")
|
||||
tableColumn2.headerCell.title = "i18n:CandidateServiceMenuEditor.table.field.Value".localized
|
||||
tableColumn2.maxWidth = 1000
|
||||
tableColumn2.minWidth = 40
|
||||
tableColumn2.resizingMask = [.autoresizingMask, .userResizingMask]
|
||||
tableColumn2.width = 480
|
||||
tableColumn2.dataCell = tableColumn2Cell
|
||||
|
||||
tableColumn2Cell.backgroundColor = NSColor.controlBackgroundColor
|
||||
tableColumn2Cell.font = NSFont.systemFont(ofSize: 13)
|
||||
tableColumn2Cell.isEditable = true
|
||||
tableColumn2Cell.isSelectable = true
|
||||
tableColumn2Cell.lineBreakMode = .byTruncatingTail
|
||||
tableColumn2Cell.stringValue = "Text Cell"
|
||||
tableColumn2Cell.textColor = NSColor.controlTextColor
|
||||
|
||||
return scrollContainer
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UserDefaults Handlers.
|
||||
|
||||
public extension VwrServiceMenuEditor {
|
||||
static var servicesList: [CandidateTextService] {
|
||||
get {
|
||||
PrefMgr.shared.candidateServiceMenuContents.parseIntoCandidateTextServiceStack()
|
||||
}
|
||||
set {
|
||||
PrefMgr.shared.candidateServiceMenuContents = newValue.rawRepresentation
|
||||
}
|
||||
}
|
||||
|
||||
static func removeService(at index: Int) {
|
||||
guard index < Self.servicesList.count else { return }
|
||||
Self.servicesList.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Common Operation Methods.
|
||||
|
||||
extension VwrServiceMenuEditor {
|
||||
func refresh() {
|
||||
tblServices.reloadData()
|
||||
reassureButtonAvailability()
|
||||
}
|
||||
|
||||
func reassureButtonAvailability() {
|
||||
btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(
|
||||
tblServices.selectedRow)
|
||||
}
|
||||
|
||||
func handleDrag(_ givenURL: URL) {
|
||||
guard let string = try? String(contentsOf: givenURL) else { return }
|
||||
Self.servicesList.append(contentsOf: string.components(separatedBy: .newlines).parseIntoCandidateTextServiceStack())
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IBActions.
|
||||
|
||||
extension VwrServiceMenuEditor {
|
||||
@IBAction func btnShowInstructionsClicked(_: Any) {
|
||||
let strTitle = "How to Fill".localized
|
||||
let strFillGuide = "i18n:CandidateServiceMenuEditor.formatGuide".localized
|
||||
windowController?.window.callAlert(title: strTitle, text: strFillGuide)
|
||||
}
|
||||
|
||||
@IBAction func btnResetServiceClicked(_: Any) {
|
||||
PrefMgr.shared.candidateServiceMenuContents = PrefMgr.kDefaultCandidateServiceMenuItem
|
||||
tblServices.reloadData()
|
||||
}
|
||||
|
||||
@IBAction func btnCopyAllToClipboardClicked(_: Any) {
|
||||
var resultArrayLines = [String]()
|
||||
Self.servicesList.forEach { currentService in
|
||||
resultArrayLines.append("\(currentService.key)\t\(currentService.definedValue)")
|
||||
}
|
||||
let result = resultArrayLines.joined(separator: "\n").appending("\n")
|
||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||
NSPasteboard.general.setString(result, forType: .string)
|
||||
}
|
||||
|
||||
@IBAction func btnRemoveServiceClicked(_: Any) {
|
||||
guard let minIndexSelected = tblServices.selectedRowIndexes.min() else { return }
|
||||
if minIndexSelected >= Self.servicesList.count { return }
|
||||
if minIndexSelected < 0 { return }
|
||||
var isLastRow = false
|
||||
tblServices.selectedRowIndexes.sorted().reversed().forEach { index in
|
||||
isLastRow = {
|
||||
if Self.servicesList.count < 2 { return false }
|
||||
return minIndexSelected == Self.servicesList.count - 1
|
||||
}()
|
||||
if index < Self.servicesList.count {
|
||||
Self.removeService(at: index)
|
||||
}
|
||||
}
|
||||
if isLastRow {
|
||||
tblServices.selectRowIndexes(.init(arrayLiteral: minIndexSelected - 1), byExtendingSelection: false)
|
||||
}
|
||||
tblServices.reloadData()
|
||||
btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(minIndexSelected)
|
||||
}
|
||||
|
||||
@IBAction func btnAddServiceClicked(_: Any) {
|
||||
guard let window = windowController?.window else { return }
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString(
|
||||
"i18n:CandidateServiceMenuEditor.prompt", comment: ""
|
||||
)
|
||||
alert.informativeText = NSLocalizedString(
|
||||
"i18n:CandidateServiceMenuEditor.howToGetGuide", comment: ""
|
||||
)
|
||||
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
|
||||
|
||||
let maxFloat = Double(Float.greatestFiniteMagnitude)
|
||||
let scrollview = NSScrollView(frame: NSRect(x: 0, y: 0, width: 512, height: 200))
|
||||
let contentSize = scrollview.contentSize
|
||||
scrollview.borderType = .noBorder
|
||||
scrollview.hasVerticalScroller = true
|
||||
scrollview.hasHorizontalScroller = true
|
||||
scrollview.horizontalScroller?.scrollerStyle = .legacy
|
||||
scrollview.verticalScroller?.scrollerStyle = .legacy
|
||||
scrollview.autoresizingMask = [.width, .height]
|
||||
let theTextView = NSTextView(frame: NSRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height))
|
||||
scrollview.documentView = theTextView
|
||||
theTextView.minSize = NSSize(width: 0.0, height: contentSize.height)
|
||||
theTextView.maxSize = NSSize(width: maxFloat, height: maxFloat)
|
||||
theTextView.isVerticallyResizable = true
|
||||
theTextView.isHorizontallyResizable = false
|
||||
theTextView.autoresizingMask = .width
|
||||
theTextView.textContainer?.containerSize = NSSize(width: contentSize.width, height: maxFloat)
|
||||
theTextView.textContainer?.widthTracksTextView = true
|
||||
theTextView.enclosingScrollView?.hasHorizontalScroller = true
|
||||
theTextView.isHorizontallyResizable = true
|
||||
theTextView.autoresizingMask = [.width, .height]
|
||||
theTextView.textContainer?.containerSize = NSSize(width: maxFloat, height: maxFloat)
|
||||
theTextView.textContainer?.widthTracksTextView = false
|
||||
theTextView.toolTip = "i18n:CandidateServiceMenuEditor.formatGuide".localized
|
||||
|
||||
alert.accessoryView = scrollview
|
||||
alert.beginSheetModal(at: window) { result in
|
||||
switch result {
|
||||
case .alertFirstButtonReturn:
|
||||
let rawLines = theTextView.textContainer?.textView?.string.components(separatedBy: .newlines) ?? []
|
||||
self.tblServices.beginUpdates()
|
||||
Self.servicesList.append(contentsOf: rawLines.parseIntoCandidateTextServiceStack())
|
||||
self.tblServices.endUpdates()
|
||||
default: return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TableView Extensions.
|
||||
|
||||
extension VwrServiceMenuEditor: NSTableViewDelegate, NSTableViewDataSource {
|
||||
public func numberOfRows(in _: NSTableView) -> Int {
|
||||
Self.servicesList.count
|
||||
}
|
||||
|
||||
public func tableView(_: NSTableView, shouldEdit _: NSTableColumn?, row _: Int) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public func tableView(_: NSTableView, objectValueFor column: NSTableColumn?, row: Int) -> Any? {
|
||||
defer {
|
||||
self.btnRemoveService.isEnabled = (0 ..< Self.servicesList.count).contains(
|
||||
self.tblServices.selectedRow)
|
||||
}
|
||||
guard row < Self.servicesList.count else { return "" }
|
||||
if let column = column {
|
||||
let colName = column.identifier.rawValue
|
||||
switch colName {
|
||||
case "colTitle": return Self.servicesList[row].key
|
||||
case "colValue": return Self.servicesList[row].definedValue // TODO: 回頭這裡可能需要自訂。
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
return Self.servicesList[row]
|
||||
}
|
||||
|
||||
// MARK: Pasteboard Operations.
|
||||
|
||||
public func tableView(
|
||||
_: NSTableView, pasteboardWriterForRow row: Int
|
||||
) -> NSPasteboardWriting? {
|
||||
let pasteboard = NSPasteboardItem()
|
||||
pasteboard.setString(row.description, forType: .string)
|
||||
return pasteboard
|
||||
}
|
||||
|
||||
public func tableView(
|
||||
_: NSTableView,
|
||||
validateDrop _: NSDraggingInfo,
|
||||
proposedRow _: Int,
|
||||
proposedDropOperation _: NSTableView.DropOperation
|
||||
) -> NSDragOperation {
|
||||
.move
|
||||
}
|
||||
|
||||
public func tableView(
|
||||
_ tableView: NSTableView,
|
||||
acceptDrop info: NSDraggingInfo,
|
||||
row: Int,
|
||||
dropOperation _: NSTableView.DropOperation
|
||||
) -> Bool {
|
||||
var oldIndexes = [Int]()
|
||||
info.enumerateDraggingItems(
|
||||
options: [],
|
||||
for: tableView,
|
||||
classes: [NSPasteboardItem.self],
|
||||
searchOptions: [:]
|
||||
) { dragItem, _, _ in
|
||||
guard let pasteboardItem = dragItem.item as? NSPasteboardItem else { return }
|
||||
guard let index = Int(pasteboardItem.string(forType: .string) ?? "NULL"), index >= 0 else { return }
|
||||
oldIndexes.append(index)
|
||||
}
|
||||
|
||||
var oldIndexOffset = 0
|
||||
var newIndexOffset = 0
|
||||
|
||||
tableView.beginUpdates()
|
||||
for oldIndex in oldIndexes {
|
||||
if oldIndex < row {
|
||||
let contentToMove = Self.servicesList[oldIndex + oldIndexOffset]
|
||||
Self.servicesList.remove(at: oldIndex + oldIndexOffset)
|
||||
Self.servicesList.insert(contentToMove, at: row - 1)
|
||||
tableView.moveRow(at: oldIndex + oldIndexOffset, to: row - 1)
|
||||
oldIndexOffset -= 1
|
||||
} else {
|
||||
let contentToMove = Self.servicesList[oldIndex]
|
||||
Self.servicesList.remove(at: oldIndex)
|
||||
Self.servicesList.insert(contentToMove, at: row + newIndexOffset)
|
||||
tableView.moveRow(at: oldIndex, to: row + newIndexOffset)
|
||||
newIndexOffset += 1
|
||||
}
|
||||
}
|
||||
tableView.endUpdates()
|
||||
reassureButtonAvailability()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview.
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
#Preview(traits: .fixedLayout(width: 770, height: 335)) {
|
||||
VwrServiceMenuEditor()
|
||||
}
|
|
@ -7,10 +7,10 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import CandidateWindow
|
||||
import CocoaExtension
|
||||
import IMKUtils
|
||||
import InputMethodKit
|
||||
import NotifierUI
|
||||
import OSFrameworkImpl
|
||||
import PopupCompositionBuffer
|
||||
import Shared
|
||||
import ShiftKeyUpChecker
|
||||
|
|
|
@ -46,8 +46,11 @@ extension SessionCtl: InputHandlerDelegate {
|
|||
var userPhrase = LMMgr.UserPhrase(
|
||||
keyArray: kvPair.keyArray, value: kvPair.value, inputMode: inputMode
|
||||
)
|
||||
if Self.areWeNerfing { userPhrase.weight = -114.514 }
|
||||
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: addToFilter) {
|
||||
var action = CandidateContextMenuAction.toBoost
|
||||
if Self.areWeNerfing { action = .toNerf }
|
||||
if addToFilter { action = .toFilter }
|
||||
userPhrase.updateWeight(basedOn: action)
|
||||
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
|
||||
succeeded = false
|
||||
}
|
||||
if !succeeded { return false }
|
||||
|
@ -125,6 +128,8 @@ extension SessionCtl: CtlCandidateDelegate {
|
|||
return shortened ? theEmoji : "\(theEmoji) " + NSLocalizedString("Quick Candidates", comment: "")
|
||||
} else if PrefMgr.shared.cassetteEnabled {
|
||||
return shortened ? "📼" : "📼 " + NSLocalizedString("CIN Cassette Mode", comment: "")
|
||||
} else if state.type == .ofSymbolTable, state.node.containsCandidateServices {
|
||||
return shortened ? "🌎" : "🌎 " + NSLocalizedString("Service Menu", comment: "")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -201,6 +206,22 @@ extension SessionCtl: CtlCandidateDelegate {
|
|||
let node = state.node.members[index]
|
||||
if !node.members.isEmpty {
|
||||
switchState(IMEState.ofSymbolTable(node: node))
|
||||
} else if let serviceNode = node.asServiceMenuNode {
|
||||
switch serviceNode.service.value {
|
||||
case let .url(theURL):
|
||||
// 雖然 Safari 理論上是啟動速度最快的,但這裡還是尊重一下使用者各自電腦內的偏好設定好了。
|
||||
NSWorkspace.shared.open(theURL)
|
||||
case .selector:
|
||||
if let response = serviceNode.service.responseFromSelector {
|
||||
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||
NSPasteboard.general.setString(response, forType: .string)
|
||||
Notifier.notify(message: "i18n:candidateServiceMenu.selectorResponse.succeeded".localized)
|
||||
} else {
|
||||
callError("4DFDC487: Candidate Text Service Selector Responsiveness Failure.")
|
||||
Notifier.notify(message: "i18n:candidateServiceMenu.selectorResponse.failed".localized)
|
||||
}
|
||||
}
|
||||
switchState(IMEState.ofAbortion())
|
||||
} else {
|
||||
switchState(IMEState.ofCommitting(textToCommit: node.name))
|
||||
}
|
||||
|
@ -257,7 +278,7 @@ extension SessionCtl: CtlCandidateDelegate {
|
|||
var userPhrase = LMMgr.UserPhrase(
|
||||
keyArray: rawPair.keyArray, value: rawPair.value, inputMode: inputMode
|
||||
)
|
||||
if action == .toNerf { userPhrase.weight = -114.514 }
|
||||
userPhrase.updateWeight(basedOn: action)
|
||||
LMMgr.writeUserPhrasesAtOnce(userPhrase, areWeFiltering: action == .toFilter) {
|
||||
succeeded = false
|
||||
}
|
||||
|
|
|
@ -79,13 +79,12 @@ public extension SessionCtl {
|
|||
func showCandidates() {
|
||||
guard client() != nil else { return }
|
||||
updateVerticalTypingStatus()
|
||||
isVerticalCandidateWindow = (isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList)
|
||||
let isServiceMenu = state.type == .ofSymbolTable && state.node.containsCandidateServices
|
||||
isVerticalCandidateWindow = isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList
|
||||
isVerticalCandidateWindow = isVerticalCandidateWindow || isServiceMenu
|
||||
|
||||
/// 無論是田所選字窗還是 IMK 選字窗,在這裡都有必要重新初期化。
|
||||
let candidateLayout: NSUserInterfaceLayoutOrientation =
|
||||
((isVerticalTyping || !PrefMgr.shared.useHorizontalCandidateList)
|
||||
? .vertical
|
||||
: .horizontal)
|
||||
let candidateLayout: NSUserInterfaceLayoutOrientation = (isVerticalCandidateWindow ? .vertical : .horizontal)
|
||||
|
||||
let isInputtingWithCandidates = state.type == .ofInputting && state.isCandidateContainer
|
||||
/// 先取消既有的選字窗的內容顯示。否則可能會重複生成選字窗的 NSWindow()。
|
||||
|
@ -93,6 +92,8 @@ public extension SessionCtl {
|
|||
candidateUI = CtlCandidateTDK(candidateLayout)
|
||||
var singleLine = isVerticalTyping || PrefMgr.shared.candidateWindowShowOnlyOneLine
|
||||
singleLine = singleLine || isInputtingWithCandidates
|
||||
singleLine = singleLine || isServiceMenu
|
||||
|
||||
(candidateUI as? CtlCandidateTDK)?.maxLinesPerPage = singleLine ? 1 : 4
|
||||
|
||||
candidateUI?.candidateFont = Self.candidateFont(
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import CocoaExtension
|
||||
import IMKUtils
|
||||
import InputMethodKit
|
||||
import NotifierUI
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import SwiftyCapsLockToggler
|
||||
|
||||
|
@ -84,6 +84,7 @@ public extension SessionCtl {
|
|||
if PrefMgr.shared.shiftEisuToggleOffTogetherWithCapsLock, !isCapsLockTurnedOn, self?.isASCIIMode ?? false {
|
||||
self?.isASCIIMode.toggle()
|
||||
}
|
||||
self?.resetInputHandler()
|
||||
guard PrefMgr.shared.showNotificationsWhenTogglingCapsLock else { return }
|
||||
guard !PrefMgr.shared.bypassNonAppleCapsLockHandling else { return }
|
||||
let status = NSLocalizedString("NotificationSwitchRevolver", comment: "")
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import NotifierUI
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
|
||||
|
@ -129,6 +129,9 @@ extension SessionCtl {
|
|||
NSMenu.Item(verbatim: "Client Manager".localized.withEllipsis)?
|
||||
.act(#selector(showClientListMgr(_:)))
|
||||
.nulled(silentMode)
|
||||
NSMenu.Item(verbatim: "Service Menu Editor".localized.withEllipsis)?
|
||||
.act(#selector(showServiceMenuEditor(_:)))
|
||||
.alternated().nulled(silentMode)
|
||||
NSMenu.Item("Check for Updates…")?
|
||||
.act(#selector(checkForUpdate(_:)))
|
||||
.nulled(silentMode)
|
||||
|
@ -184,6 +187,11 @@ public extension SessionCtl {
|
|||
NSApp.popup()
|
||||
}
|
||||
|
||||
@objc func showServiceMenuEditor(_: Any? = nil) {
|
||||
CtlServiceMenuEditor.show()
|
||||
NSApp.popup()
|
||||
}
|
||||
|
||||
@objc func toggleCassetteMode(_: Any? = nil) {
|
||||
resetInputHandler(forceComposerCleanup: true)
|
||||
if !PrefMgr.shared.cassetteEnabled, !LMMgr.checkCassettePathValidity(PrefMgr.shared.cassettePath) {
|
||||
|
|
|
@ -33,7 +33,11 @@ public extension SettingsPanesCocoa {
|
|||
UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: innerContentWidth)
|
||||
UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: innerContentWidth)
|
||||
UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: innerContentWidth)
|
||||
UserDef.kSpecifyCmdOptCtrlEnterBehavior.render(fixWidth: innerContentWidth)
|
||||
}?.boxed()
|
||||
NSView()
|
||||
}
|
||||
NSTabView.TabPage(title: "B") {
|
||||
NSStackView.buildSection(width: innerContentWidth) {
|
||||
UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: innerContentWidth)
|
||||
UserDef.kNumPadCharInputBehavior.render(fixWidth: innerContentWidth)
|
||||
|
@ -44,7 +48,7 @@ public extension SettingsPanesCocoa {
|
|||
}?.boxed()
|
||||
NSView()
|
||||
}
|
||||
NSTabView.TabPage(title: "B") {
|
||||
NSTabView.TabPage(title: "C") {
|
||||
NSStackView.buildSection(width: innerContentWidth) {
|
||||
UserDef.kChooseCandidateUsingSpace.render(fixWidth: innerContentWidth)
|
||||
UserDef.kEscToCleanInputBuffer.render(fixWidth: innerContentWidth)
|
||||
|
@ -65,7 +69,7 @@ public extension SettingsPanesCocoa {
|
|||
}?.boxed()
|
||||
NSView()
|
||||
}
|
||||
NSTabView.TabPage(title: "C") {
|
||||
NSTabView.TabPage(title: "D") {
|
||||
NSStackView.buildSection(width: innerContentWidth) {
|
||||
UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: innerContentWidth)
|
||||
UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: innerContentWidth)
|
||||
|
|
|
@ -37,6 +37,7 @@ public extension SettingsPanesCocoa {
|
|||
}
|
||||
UserDef.kCandidateWindowShowOnlyOneLine.render(fixWidth: innerContentWidth)
|
||||
UserDef.kAlwaysExpandCandidateWindow.render(fixWidth: innerContentWidth)
|
||||
UserDef.kMinCellWidthForHorizontalMatrix.render(fixWidth: innerContentWidth)
|
||||
UserDef.kRespectClientAccentColor.render(fixWidth: innerContentWidth)
|
||||
}?.boxed()
|
||||
NSView()
|
||||
|
@ -54,10 +55,15 @@ public extension SettingsPanesCocoa {
|
|||
UserDef.kMoveCursorAfterSelectingCandidate.render(fixWidth: innerContentWidth)
|
||||
UserDef.kUseDynamicCandidateWindowOrigin.render(fixWidth: innerContentWidth)
|
||||
UserDef.kDodgeInvalidEdgeCandidateCursorPosition.render(fixWidth: innerContentWidth)
|
||||
UserDef.kUseShiftQuestionToCallServiceMenu
|
||||
.render(fixWidth: innerContentWidth) { renderable in
|
||||
renderable.currentControl?.target = self
|
||||
renderable.currentControl?.action = #selector(self.performCandidateKeysSanityCheck(_:))
|
||||
}
|
||||
UserDef.kUseJKtoMoveCompositorCursorInCandidateState
|
||||
.render(fixWidth: innerContentWidth) { renderable in
|
||||
renderable.currentControl?.target = self
|
||||
renderable.currentControl?.action = #selector(self.useJKToMoveBufferCursorDidSet(_:))
|
||||
renderable.currentControl?.action = #selector(self.performCandidateKeysSanityCheck(_:))
|
||||
}
|
||||
}?.boxed()
|
||||
NSView()
|
||||
|
@ -91,7 +97,7 @@ public extension SettingsPanesCocoa {
|
|||
window.callAlert(title: title.localized, text: explanation.localized)
|
||||
}
|
||||
|
||||
@IBAction func useJKToMoveBufferCursorDidSet(_: NSControl) {
|
||||
@IBAction func performCandidateKeysSanityCheck(_: NSControl) {
|
||||
// 利用該變數的 didSet 屬性自糾。
|
||||
PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
import AppKit
|
||||
import BookmarkManager
|
||||
import CocoaExtension
|
||||
import Foundation
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
|
||||
public extension SettingsPanesCocoa {
|
||||
|
|
|
@ -35,6 +35,9 @@ public struct VwrSettingsPaneBehavior: View {
|
|||
@AppStorage(wrappedValue: false, UserDef.kSpecifyShiftSpaceKeyBehavior.rawValue)
|
||||
private var specifyShiftSpaceKeyBehavior: Bool
|
||||
|
||||
@AppStorage(wrappedValue: 0, UserDef.kSpecifyCmdOptCtrlEnterBehavior.rawValue)
|
||||
private var specifyCmdOptCtrlEnterBehavior: Int
|
||||
|
||||
@AppStorage(wrappedValue: true, UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.rawValue)
|
||||
private var useSpaceToCommitHighlightedSCPCCandidate: Bool
|
||||
|
||||
|
@ -80,6 +83,7 @@ public struct VwrSettingsPaneBehavior: View {
|
|||
UserDef.kSpecifyShiftBackSpaceKeyBehavior.bind($specifyShiftBackSpaceKeyBehavior).render()
|
||||
UserDef.kSpecifyShiftTabKeyBehavior.bind($specifyShiftTabKeyBehavior).render()
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
UserDef.kSpecifyCmdOptCtrlEnterBehavior.bind($specifyCmdOptCtrlEnterBehavior).render()
|
||||
VStack(alignment: .leading) {
|
||||
UserDef.kSpecifyShiftSpaceKeyBehavior.bind($specifyShiftSpaceKeyBehavior).render()
|
||||
UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.bind($useSpaceToCommitHighlightedSCPCCandidate).render()
|
||||
|
|
|
@ -26,6 +26,9 @@ public struct VwrSettingsPaneCandidates: View {
|
|||
@AppStorage(wrappedValue: true, UserDef.kRespectClientAccentColor.rawValue)
|
||||
private var respectClientAccentColor: Bool
|
||||
|
||||
@AppStorage(wrappedValue: 0, UserDef.kMinCellWidthForHorizontalMatrix.rawValue)
|
||||
private var minCellWidthForHorizontalMatrix: Int
|
||||
|
||||
@AppStorage(wrappedValue: false, UserDef.kAlwaysExpandCandidateWindow.rawValue)
|
||||
private var alwaysExpandCandidateWindow: Bool
|
||||
|
||||
|
@ -41,6 +44,9 @@ public struct VwrSettingsPaneCandidates: View {
|
|||
@AppStorage(wrappedValue: false, UserDef.kUseJKtoMoveCompositorCursorInCandidateState.rawValue)
|
||||
private var useJKtoMoveCompositorCursorInCandidateState: Bool
|
||||
|
||||
@AppStorage(wrappedValue: true, UserDef.kUseShiftQuestionToCallServiceMenu.rawValue)
|
||||
public var useShiftQuestionToCallServiceMenu: Bool
|
||||
|
||||
@AppStorage(wrappedValue: true, UserDef.kMoveCursorAfterSelectingCandidate.rawValue)
|
||||
private var moveCursorAfterSelectingCandidate: Bool
|
||||
|
||||
|
@ -72,6 +78,12 @@ public struct VwrSettingsPaneCandidates: View {
|
|||
.disabled(useRearCursorMode)
|
||||
}
|
||||
UserDef.kDodgeInvalidEdgeCandidateCursorPosition.bind($dodgeInvalidEdgeCandidateCursorPosition).render()
|
||||
UserDef.kUseShiftQuestionToCallServiceMenu.bind(
|
||||
$useShiftQuestionToCallServiceMenu.didChange {
|
||||
// 利用該變數的 didSet 屬性自糾。
|
||||
PrefMgr.shared.candidateKeys = PrefMgr.shared.candidateKeys
|
||||
}
|
||||
).render()
|
||||
UserDef.kUseJKtoMoveCompositorCursorInCandidateState.bind(
|
||||
$useJKtoMoveCompositorCursorInCandidateState.didChange {
|
||||
// 利用該變數的 didSet 屬性自糾。
|
||||
|
@ -93,6 +105,8 @@ public struct VwrSettingsPaneCandidates: View {
|
|||
if !candidateWindowShowOnlyOneLine {
|
||||
UserDef.kAlwaysExpandCandidateWindow.bind($alwaysExpandCandidateWindow).render()
|
||||
.disabled(candidateWindowShowOnlyOneLine)
|
||||
UserDef.kMinCellWidthForHorizontalMatrix.bind($minCellWidthForHorizontalMatrix).render()
|
||||
.disabled(candidateWindowShowOnlyOneLine)
|
||||
}
|
||||
UserDef.kRespectClientAccentColor.bind($respectClientAccentColor).render()
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import BookmarkManager
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 13, *)
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import BookmarkManager
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@available(macOS 13, *)
|
||||
public struct VwrSettingsPaneDictionary: View {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import AppKit
|
||||
import AVFoundation
|
||||
import Shared
|
||||
|
||||
public class SpeechSputnik {
|
||||
public static var shared: SpeechSputnik = .init()
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import InputMethodKit
|
||||
import LangModelAssembly
|
||||
@testable import MainAssembly
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import XCTest
|
||||
|
||||
class CandidateServiceCoordinatorTests: XCTestCase {
|
||||
static let testDataMap: [String] = [
|
||||
#"Unicode Metadata: %s"# + "\t" + #"@SEL:copyUnicodeMetadata:"#,
|
||||
#"HTML Ruby Zhuyin: %s"# + "\t" + #"@SEL:copyRubyHTMLZhuyinTextbookStyle:"#,
|
||||
#"HTML Ruby Pinyin: %s"# + "\t" + #"@SEL:copyRubyHTMLHanyuPinyinTextbookStyle:"#,
|
||||
#"Zhuyin Annotation: %s"# + "\t" + #"@SEL:copyInlineZhuyinAnnotationTextbookStyle:"#,
|
||||
#"Pinyin Annotation: %s"# + "\t" + #"@SEL:copyInlineHanyuPinyinAnnotationTextbookStyle:"#,
|
||||
#"Braille 1947: %s"# + "\t" + #"@SEL:copyBraille1947:"#,
|
||||
#"Braille 2018: %s"# + "\t" + #"@SEL:copyBraille2018:"#,
|
||||
]
|
||||
|
||||
func testSelector_FinalSanityCheck() throws {
|
||||
var stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "胡桃", reading: [] // 故意使用空 Reading
|
||||
)
|
||||
let count1 = stacked.count
|
||||
print("Current Count before Sanity Check ON: \(stacked.count)")
|
||||
CandidateTextService.enableFinalSanityCheck()
|
||||
stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "胡桃", reading: [] // 故意使用空 Reading
|
||||
)
|
||||
let count2 = stacked.count
|
||||
print("Current Count after Sanity Check ON: \(stacked.count)")
|
||||
XCTAssertGreaterThan(count1, count2)
|
||||
}
|
||||
|
||||
func testSelector_UnicodeMetadata() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "胡桃", reading: ["ㄏㄨˊ", "ㄊㄠˊ"]
|
||||
)
|
||||
let theService = stacked[0]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "胡 U+80E1 CJK UNIFIED IDEOGRAPH-80E1\n桃 U+6843 CJK UNIFIED IDEOGRAPH-6843"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func testSelector_HTMLRubyZhuyinTextbookStyle() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "甜的", reading: ["ㄊㄧㄢˊ", "ㄉㄜ˙"]
|
||||
)
|
||||
let theService = stacked[1]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "<ruby>甜<rp>(</rp><rt>ㄊㄧㄢˊ</rt><rp>)</rp></ruby><ruby>的<rp>(</rp><rt>˙ㄉㄜ</rt><rp>)</rp></ruby>"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func testSelector_HTMLRubyPinyinTextbookStyle() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "鐵嘴", reading: ["ㄊㄧㄝˇ", "ㄗㄨㄟˇ"]
|
||||
)
|
||||
let theService = stacked[2]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "<ruby>鐵<rp>(</rp><rt>tiě</rt><rp>)</rp></ruby><ruby>嘴<rp>(</rp><rt>zuǐ</rt><rp>)</rp></ruby>"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func testSelector_InlineAnnotationZhuyinTextbookStyle() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "甜的", reading: ["ㄊㄧㄢˊ", "ㄉㄜ˙"]
|
||||
)
|
||||
let theService = stacked[3]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "甜(ㄊㄧㄢˊ)的(˙ㄉㄜ)"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func testSelector_InlineAnnotationTextbookStyle() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "鐵嘴", reading: ["ㄊㄧㄝˇ", "ㄗㄨㄟˇ"]
|
||||
)
|
||||
let theService = stacked[4]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "鐵(tiě)嘴(zuǐ)"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func testSelector_Braille1947() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "高科技公司的",
|
||||
reading: ["ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ", "ㄍㄨㄥ", "ㄙ", "ㄉㄜ˙"]
|
||||
)
|
||||
let theService = stacked[5]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "⠅⠩⠄⠇⠮⠄⠅⠡⠐⠅⠯⠄⠑⠄⠙⠮⠁"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func testSelector_Braille2018() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack(
|
||||
candidate: "高科技公司的",
|
||||
reading: ["ㄍㄠ", "ㄎㄜ", "ㄐㄧˋ", "ㄍㄨㄥ", "ㄙ", "ㄉㄜ˙"]
|
||||
)
|
||||
let theService = stacked[6]
|
||||
switch theService.value {
|
||||
case .url: break
|
||||
case .selector:
|
||||
let response = theService.responseFromSelector
|
||||
let expectedResponse = "⠛⠖⠁⠅⠢⠁⠛⠊⠆⠛⠲⠁⠎⠁⠙⠢"
|
||||
XCTAssertEqual(response, expectedResponse)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,10 +6,10 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import CocoaExtension
|
||||
import InputMethodKit
|
||||
import LangModelAssembly
|
||||
@testable import MainAssembly
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import XCTest
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import CocoaExtension
|
||||
import InputMethodKit
|
||||
import LangModelAssembly
|
||||
@testable import MainAssembly
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import XCTest
|
||||
|
||||
|
@ -151,4 +151,35 @@ extension MainAssemblyTests {
|
|||
XCTAssertEqual(resultText6, "濟公的年中獎金")
|
||||
vCTestLog("- 已成功證實「年終」的記憶不會對除了給定上下文以外的情形生效。")
|
||||
}
|
||||
|
||||
/// 測試 inputHandler.commissionByCtrlOptionCommandEnter()。
|
||||
func test105_InputHandler_MiscCommissionTest() throws {
|
||||
PrefMgr.shared.useSCPCTypingMode = false
|
||||
clearTestUOM()
|
||||
vCTestLog("正在測試 inputHandler.commissionByCtrlOptionCommandEnter()。")
|
||||
testSession.resetInputHandler(forceComposerCleanup: true)
|
||||
typeSentenceOrCandidates("el dk ru4ej/ n 2k7")
|
||||
guard let handler = testSession.inputHandler as? InputHandler else {
|
||||
XCTAssertThrowsError("testSession.handler is nil.")
|
||||
return
|
||||
}
|
||||
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 0
|
||||
var result = handler.commissionByCtrlOptionCommandEnter(isShiftPressed: true)
|
||||
XCTAssertEqual(result, "ㄍㄠ ㄎㄜ ㄐㄧˋ ㄍㄨㄥ ㄙ ˙ㄉㄜ")
|
||||
result = handler.commissionByCtrlOptionCommandEnter() // isShiftPressed 的參數預設是 false。
|
||||
XCTAssertEqual(result, "高(ㄍㄠ)科(ㄎㄜ)技(ㄐㄧˋ)公(ㄍㄨㄥ)司(ㄙ)的(˙ㄉㄜ)")
|
||||
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 1
|
||||
result = handler.commissionByCtrlOptionCommandEnter()
|
||||
let expectedRubyResult = """
|
||||
<ruby>高<rp>(</rp><rt>ㄍㄠ</rt><rp>)</rp></ruby><ruby>科<rp>(</rp><rt>ㄎㄜ</rt><rp>)</rp></ruby><ruby>技<rp>(</rp><rt>ㄐㄧˋ</rt><rp>)</rp></ruby><ruby>公<rp>(</rp><rt>ㄍㄨㄥ</rt><rp>)</rp></ruby><ruby>司<rp>(</rp><rt>ㄙ</rt><rp>)</rp></ruby><ruby>的<rp>(</rp><rt>˙ㄉㄜ</rt><rp>)</rp></ruby>
|
||||
"""
|
||||
XCTAssertEqual(result, expectedRubyResult)
|
||||
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 2
|
||||
result = handler.commissionByCtrlOptionCommandEnter()
|
||||
XCTAssertEqual(result, "⠅⠩⠄⠇⠮⠄⠅⠡⠐⠅⠯⠄⠑⠄⠙⠮⠁")
|
||||
PrefMgr.shared.specifyCmdOptCtrlEnterBehavior = 3
|
||||
result = handler.commissionByCtrlOptionCommandEnter()
|
||||
XCTAssertEqual(result, "⠛⠖⠁⠅⠢⠁⠛⠊⠆⠛⠲⠁⠎⠁⠙⠢")
|
||||
vCTestLog("成功完成測試 inputHandler.commissionByCtrlOptionCommandEnter()。")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,13 +13,13 @@ let package = Package(
|
|||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "NotifierUI",
|
||||
dependencies: [
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
|
||||
public class Notifier: NSWindowController {
|
||||
public static func notify(message: String) {
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "CocoaExtension",
|
||||
name: "OSFrameworkImpl",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "CocoaExtension",
|
||||
targets: ["CocoaExtension"]
|
||||
name: "OSFrameworkImpl",
|
||||
targets: ["OSFrameworkImpl"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
|
@ -17,7 +17,7 @@ let package = Package(
|
|||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CocoaExtension",
|
||||
name: "OSFrameworkImpl",
|
||||
dependencies: [
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
]
|
|
@ -1,4 +1,4 @@
|
|||
# CocoaExtension
|
||||
# OSFrameworkImpl
|
||||
|
||||
威注音輸入法針對 Cocoa 的一些功能擴充,使程式維護體驗更佳。
|
||||
|
|
@ -71,6 +71,18 @@ public extension NSAttributedString {
|
|||
|
||||
public extension NSString {
|
||||
var localized: String { NSLocalizedString(description, comment: "") }
|
||||
|
||||
@objc func getCharDescriptions(_: Any? = nil) -> [String] {
|
||||
(self as String).charDescriptions
|
||||
}
|
||||
|
||||
@objc func getCodePoints(_: Any? = nil) -> [String] {
|
||||
(self as String).codePoints
|
||||
}
|
||||
|
||||
@objc func getDescriptionAsCodePoints(_: Any? = nil) -> [String] {
|
||||
(self as String).describedAsCodePoints
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSRange Extension
|
||||
|
@ -128,15 +140,14 @@ public extension NSApplication {
|
|||
// MARK: - System Dark Mode Status Detector.
|
||||
|
||||
static var isDarkMode: Bool {
|
||||
if #unavailable(macOS 10.14) { return false }
|
||||
if #available(macOS 10.15, *) {
|
||||
let appearanceDescription = NSApp.effectiveAppearance.debugDescription
|
||||
.lowercased()
|
||||
return appearanceDescription.contains("dark")
|
||||
} else if let appleInterfaceStyle = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||
return appleInterfaceStyle.lowercased().contains("dark")
|
||||
// "NSApp" can be nil during SPM unit tests.
|
||||
// Therefore, the method dedicated for macOS 10.15 and later is not considered stable anymore.
|
||||
// Fortunately, the method for macOS 10.14 works well on later macOS releases.
|
||||
if #available(macOS 10.14, *), let strAIS = UserDefaults.current.string(forKey: "AppleInterfaceStyle") {
|
||||
return strAIS.lowercased().contains("dark")
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Tell whether this IME is running with Root privileges.
|
||||
|
@ -199,19 +210,6 @@ public extension NSApplication {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - String.applyingTransform
|
||||
|
||||
public extension String {
|
||||
func applyingTransformFW2HW(reverse: Bool) -> String {
|
||||
if #available(macOS 10.11, *) {
|
||||
return applyingTransform(.fullwidthToHalfwidth, reverse: reverse) ?? self
|
||||
}
|
||||
let theString = NSMutableString(string: self)
|
||||
CFStringTransform(theString, nil, kCFStringTransformFullwidthHalfwidth, reverse)
|
||||
return theString as String
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Check whether current date is the given date.
|
||||
|
||||
public extension Date {
|
||||
|
@ -327,3 +325,13 @@ public extension NSApplication {
|
|||
UserDefaults.standard.object(forKey: "AppleAccentColor") != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pasteboard Type Extension.
|
||||
|
||||
public extension NSPasteboard.PasteboardType {
|
||||
static let kUTTypeFileURL = Self(rawValue: "public.file-url") // import UniformTypeIdentifiers
|
||||
static let kUTTypeData = Self(rawValue: "public.data") // import UniformTypeIdentifiers
|
||||
static let kUTTypeAppBundle = Self(rawValue: "com.apple.application-bundle") // import UniformTypeIdentifiers
|
||||
static let kUTTypeUTF8PlainText = Self(rawValue: "public.utf8-plain-text")
|
||||
static let kNSFilenamesPboardType = Self(rawValue: "NSFilenamesPboardType")
|
||||
}
|
|
@ -460,12 +460,33 @@ public extension NSMenuItem {
|
|||
|
||||
var allowedTypes: [String] = ["txt"]
|
||||
|
||||
public init() {
|
||||
public convenience init(
|
||||
_ givenTitle: String? = nil,
|
||||
target: AnyObject? = nil,
|
||||
action: Selector? = nil,
|
||||
postDrag: ((URL) -> Void)? = nil
|
||||
) {
|
||||
self.init(
|
||||
verbatim: givenTitle?.localized,
|
||||
target: target,
|
||||
action: action,
|
||||
postDrag: postDrag
|
||||
)
|
||||
}
|
||||
|
||||
public init(
|
||||
verbatim givenTitle: String? = nil,
|
||||
target: AnyObject? = nil,
|
||||
action: Selector? = nil,
|
||||
postDrag: ((URL) -> Void)? = nil
|
||||
) {
|
||||
super.init(frame: .zero)
|
||||
bezelStyle = .rounded
|
||||
title = "DRAG FILE TO HERE"
|
||||
registerForDraggedTypes([.init(rawValue: kUTTypeFileURL as String)])
|
||||
target = self
|
||||
title = givenTitle ?? "DRAG FILE TO HERE"
|
||||
registerForDraggedTypes([.kUTTypeFileURL])
|
||||
self.target = target ?? self
|
||||
self.action = action
|
||||
postDragHandler = postDrag ?? postDragHandler
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -478,7 +499,7 @@ public extension NSMenuItem {
|
|||
|
||||
fileprivate func checkExtension(_ drag: NSDraggingInfo) -> Bool {
|
||||
guard let pasteboard = drag.draggingPasteboard.propertyList(
|
||||
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
|
||||
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
|
||||
) as? [String], let path = pasteboard.first else {
|
||||
return false
|
||||
}
|
||||
|
@ -494,7 +515,7 @@ public extension NSMenuItem {
|
|||
|
||||
override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
||||
guard let pasteboard = sender.draggingPasteboard.propertyList(
|
||||
forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")
|
||||
forType: NSPasteboard.PasteboardType.kNSFilenamesPboardType
|
||||
) as? [String], let path = pasteboard.first else {
|
||||
print("failure")
|
||||
return false
|
|
@ -20,15 +20,19 @@ public class SecureEventInputSputnik {
|
|||
}
|
||||
|
||||
public static func getIORegListResults() -> String? {
|
||||
// Don't generate results under any of the following situations:
|
||||
// - Hibernation / LoggedOut / SwitchedOut / ScreenSaver situations.
|
||||
guard NSWorkspace.activationFlags.isEmpty else { return nil }
|
||||
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
|
||||
defer { resultDictionaryCF = nil }
|
||||
/// Regarding the parameter in IORegistryGetRootEntry:
|
||||
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
|
||||
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
|
||||
let statusSucceeded = IORegistryEntryCreateCFProperties(
|
||||
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
|
||||
)
|
||||
let dict: CFMutableDictionary? = resultDictionaryCF?.takeRetainedValue()
|
||||
guard statusSucceeded == KERN_SUCCESS else { return nil }
|
||||
let dict = resultDictionaryCF?.takeRetainedValue()
|
||||
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
|
||||
return (dict.description)
|
||||
}
|
||||
|
@ -75,7 +79,7 @@ public extension NSWorkspace {
|
|||
|
||||
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
|
||||
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
|
||||
public static let sesssionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
|
||||
public static let sessionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
|
||||
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
|
@ -123,11 +127,11 @@ extension SecureEventInputSputnik {
|
|||
.store(in: &Self.combinePool)
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
|
||||
.sink { _ in NSWorkspace.activationFlags.insert(.sesssionSwitchedOut) }
|
||||
.sink { _ in NSWorkspace.activationFlags.insert(.sessionSwitchedOut) }
|
||||
.store(in: &Self.combinePool)
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
|
||||
.sink { _ in NSWorkspace.activationFlags.remove(.sesssionSwitchedOut) }
|
||||
.sink { _ in NSWorkspace.activationFlags.remove(.sessionSwitchedOut) }
|
||||
.store(in: &Self.combinePool)
|
||||
} else {
|
||||
Self.combinePoolCocoa.append(
|
||||
|
@ -169,13 +173,13 @@ extension SecureEventInputSputnik {
|
|||
Self.combinePoolCocoa.append(
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
|
||||
NSWorkspace.activationFlags.insert(.sesssionSwitchedOut)
|
||||
NSWorkspace.activationFlags.insert(.sessionSwitchedOut)
|
||||
}
|
||||
)
|
||||
Self.combinePoolCocoa.append(
|
||||
NSWorkspace.shared.notificationCenter
|
||||
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
|
||||
NSWorkspace.activationFlags.remove(.sesssionSwitchedOut)
|
||||
NSWorkspace.activationFlags.remove(.sessionSwitchedOut)
|
||||
}
|
||||
)
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import SwiftExtension
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Add "didChange" support to bindings.
|
|
@ -10,6 +10,7 @@ import AppKit
|
|||
import Combine
|
||||
import Foundation
|
||||
import LangModelAssembly
|
||||
import OSFrameworkImpl
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
import SwiftUI
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
// swift-tools-version:5.3
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "PinyinPhonaConverter",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "PinyinPhonaConverter",
|
||||
targets: ["PinyinPhonaConverter"]
|
||||
),
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "PinyinPhonaConverter",
|
||||
dependencies: []
|
||||
),
|
||||
]
|
||||
)
|
|
@ -1,92 +0,0 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
|
||||
public extension String {
|
||||
mutating func convertToPhonabets(newToneOne: String = "") {
|
||||
if isEmpty || contains("_") || !isNotPureAlphanumerical { return }
|
||||
for key in arrHanyuPinyinToPhonabets {
|
||||
self = replacingOccurrences(of: key.0, with: key.1)
|
||||
}
|
||||
self = replacingOccurrences(of: " ", with: newToneOne)
|
||||
}
|
||||
}
|
||||
|
||||
/// 檢測字串是否包含半形英數內容
|
||||
private extension String {
|
||||
var isNotPureAlphanumerical: Bool {
|
||||
let regex = ".*[^A-Za-z0-9].*"
|
||||
let testString = NSPredicate(format: "SELF MATCHES %@", regex)
|
||||
return testString.evaluate(with: self)
|
||||
}
|
||||
}
|
||||
|
||||
private let arrHanyuPinyinToPhonabets: [(String, String)] = [
|
||||
("chuang", "ㄔㄨㄤ"), ("shuang", "ㄕㄨㄤ"), ("zhuang", "ㄓㄨㄤ"), ("chang", "ㄔㄤ"), ("cheng", "ㄔㄥ"), ("chong", "ㄔㄨㄥ"),
|
||||
("chuai", "ㄔㄨㄞ"), ("chuan", "ㄔㄨㄢ"), ("guang", "ㄍㄨㄤ"), ("huang", "ㄏㄨㄤ"), ("jiang", "ㄐㄧㄤ"), ("jiong", "ㄐㄩㄥ"),
|
||||
("kiang", "ㄎㄧㄤ"), ("kuang", "ㄎㄨㄤ"), ("biang", "ㄅㄧㄤ"), ("duang", "ㄉㄨㄤ"), ("liang", "ㄌㄧㄤ"), ("niang", "ㄋㄧㄤ"),
|
||||
("qiang", "ㄑㄧㄤ"), ("qiong", "ㄑㄩㄥ"), ("shang", "ㄕㄤ"), ("sheng", "ㄕㄥ"), ("shuai", "ㄕㄨㄞ"), ("shuan", "ㄕㄨㄢ"),
|
||||
("xiang", "ㄒㄧㄤ"), ("xiong", "ㄒㄩㄥ"), ("zhang", "ㄓㄤ"), ("zheng", "ㄓㄥ"), ("zhong", "ㄓㄨㄥ"), ("zhuai", "ㄓㄨㄞ"),
|
||||
("zhuan", "ㄓㄨㄢ"), ("bang", "ㄅㄤ"), ("beng", "ㄅㄥ"), ("bian", "ㄅㄧㄢ"), ("biao", "ㄅㄧㄠ"), ("bing", "ㄅㄧㄥ"), ("cang", "ㄘㄤ"),
|
||||
("ceng", "ㄘㄥ"), ("chai", "ㄔㄞ"), ("chan", "ㄔㄢ"), ("chao", "ㄔㄠ"), ("chen", "ㄔㄣ"), ("chou", "ㄔㄡ"), ("chua", "ㄔㄨㄚ"),
|
||||
("chui", "ㄔㄨㄟ"), ("chun", "ㄔㄨㄣ"), ("chuo", "ㄔㄨㄛ"), ("cong", "ㄘㄨㄥ"), ("cuan", "ㄘㄨㄢ"), ("dang", "ㄉㄤ"), ("deng", "ㄉㄥ"),
|
||||
("dian", "ㄉㄧㄢ"), ("diao", "ㄉㄧㄠ"), ("ding", "ㄉㄧㄥ"), ("dong", "ㄉㄨㄥ"), ("duan", "ㄉㄨㄢ"), ("fang", "ㄈㄤ"), ("feng", "ㄈㄥ"),
|
||||
("fiao", "ㄈㄧㄠ"), ("fong", "ㄈㄨㄥ"), ("gang", "ㄍㄤ"), ("geng", "ㄍㄥ"), ("giao", "ㄍㄧㄠ"), ("gong", "ㄍㄨㄥ"), ("guai", "ㄍㄨㄞ"),
|
||||
("guan", "ㄍㄨㄢ"), ("hang", "ㄏㄤ"), ("heng", "ㄏㄥ"), ("hong", "ㄏㄨㄥ"), ("huai", "ㄏㄨㄞ"), ("huan", "ㄏㄨㄢ"), ("jian", "ㄐㄧㄢ"),
|
||||
("jiao", "ㄐㄧㄠ"), ("jing", "ㄐㄧㄥ"), ("juan", "ㄐㄩㄢ"), ("kang", "ㄎㄤ"), ("keng", "ㄎㄥ"), ("kong", "ㄎㄨㄥ"), ("kuai", "ㄎㄨㄞ"),
|
||||
("kuan", "ㄎㄨㄢ"), ("lang", "ㄌㄤ"), ("leng", "ㄌㄥ"), ("lian", "ㄌㄧㄢ"), ("liao", "ㄌㄧㄠ"), ("ling", "ㄌㄧㄥ"), ("long", "ㄌㄨㄥ"),
|
||||
("luan", "ㄌㄨㄢ"), ("lvan", "ㄌㄩㄢ"), ("mang", "ㄇㄤ"), ("meng", "ㄇㄥ"), ("mian", "ㄇㄧㄢ"), ("miao", "ㄇㄧㄠ"), ("ming", "ㄇㄧㄥ"),
|
||||
("nang", "ㄋㄤ"), ("neng", "ㄋㄥ"), ("nian", "ㄋㄧㄢ"), ("niao", "ㄋㄧㄠ"), ("ning", "ㄋㄧㄥ"), ("nong", "ㄋㄨㄥ"), ("nuan", "ㄋㄨㄢ"),
|
||||
("pang", "ㄆㄤ"), ("peng", "ㄆㄥ"), ("pian", "ㄆㄧㄢ"), ("piao", "ㄆㄧㄠ"), ("ping", "ㄆㄧㄥ"), ("qian", "ㄑㄧㄢ"), ("qiao", "ㄑㄧㄠ"),
|
||||
("qing", "ㄑㄧㄥ"), ("quan", "ㄑㄩㄢ"), ("rang", "ㄖㄤ"), ("reng", "ㄖㄥ"), ("rong", "ㄖㄨㄥ"), ("ruan", "ㄖㄨㄢ"), ("sang", "ㄙㄤ"),
|
||||
("seng", "ㄙㄥ"), ("shai", "ㄕㄞ"), ("shan", "ㄕㄢ"), ("shao", "ㄕㄠ"), ("shei", "ㄕㄟ"), ("shen", "ㄕㄣ"), ("shou", "ㄕㄡ"),
|
||||
("shua", "ㄕㄨㄚ"), ("shui", "ㄕㄨㄟ"), ("shun", "ㄕㄨㄣ"), ("shuo", "ㄕㄨㄛ"), ("song", "ㄙㄨㄥ"), ("suan", "ㄙㄨㄢ"), ("tang", "ㄊㄤ"),
|
||||
("teng", "ㄊㄥ"), ("tian", "ㄊㄧㄢ"), ("tiao", "ㄊㄧㄠ"), ("ting", "ㄊㄧㄥ"), ("tong", "ㄊㄨㄥ"), ("tuan", "ㄊㄨㄢ"), ("wang", "ㄨㄤ"),
|
||||
("weng", "ㄨㄥ"), ("xian", "ㄒㄧㄢ"), ("xiao", "ㄒㄧㄠ"), ("xing", "ㄒㄧㄥ"), ("xuan", "ㄒㄩㄢ"), ("yang", "ㄧㄤ"), ("ying", "ㄧㄥ"),
|
||||
("yong", "ㄩㄥ"), ("yuan", "ㄩㄢ"), ("zang", "ㄗㄤ"), ("zeng", "ㄗㄥ"), ("zhai", "ㄓㄞ"), ("zhan", "ㄓㄢ"), ("zhao", "ㄓㄠ"),
|
||||
("zhei", "ㄓㄟ"), ("zhen", "ㄓㄣ"), ("zhou", "ㄓㄡ"), ("zhua", "ㄓㄨㄚ"), ("zhui", "ㄓㄨㄟ"), ("zhun", "ㄓㄨㄣ"), ("zhuo", "ㄓㄨㄛ"),
|
||||
("zong", "ㄗㄨㄥ"), ("zuan", "ㄗㄨㄢ"), ("jun", "ㄐㄩㄣ"), ("ang", "ㄤ"), ("bai", "ㄅㄞ"), ("ban", "ㄅㄢ"), ("bao", "ㄅㄠ"),
|
||||
("bei", "ㄅㄟ"), ("ben", "ㄅㄣ"), ("bie", "ㄅㄧㄝ"), ("bin", "ㄅㄧㄣ"), ("cai", "ㄘㄞ"), ("can", "ㄘㄢ"), ("cao", "ㄘㄠ"),
|
||||
("cei", "ㄘㄟ"), ("cen", "ㄘㄣ"), ("cha", "ㄔㄚ"), ("che", "ㄔㄜ"), ("chi", "ㄔ"), ("chu", "ㄔㄨ"), ("cou", "ㄘㄡ"),
|
||||
("cui", "ㄘㄨㄟ"), ("cun", "ㄘㄨㄣ"), ("cuo", "ㄘㄨㄛ"), ("dai", "ㄉㄞ"), ("dan", "ㄉㄢ"), ("dao", "ㄉㄠ"), ("dei", "ㄉㄟ"),
|
||||
("den", "ㄉㄣ"), ("dia", "ㄉㄧㄚ"), ("die", "ㄉㄧㄝ"), ("diu", "ㄉㄧㄡ"), ("dou", "ㄉㄡ"), ("dui", "ㄉㄨㄟ"), ("dun", "ㄉㄨㄣ"),
|
||||
("duo", "ㄉㄨㄛ"), ("eng", "ㄥ"), ("fan", "ㄈㄢ"), ("fei", "ㄈㄟ"), ("fen", "ㄈㄣ"), ("fou", "ㄈㄡ"), ("gai", "ㄍㄞ"),
|
||||
("gan", "ㄍㄢ"), ("gao", "ㄍㄠ"), ("gei", "ㄍㄟ"), ("gin", "ㄍㄧㄣ"), ("gen", "ㄍㄣ"), ("gou", "ㄍㄡ"), ("gua", "ㄍㄨㄚ"),
|
||||
("gue", "ㄍㄨㄜ"), ("gui", "ㄍㄨㄟ"), ("gun", "ㄍㄨㄣ"), ("guo", "ㄍㄨㄛ"), ("hai", "ㄏㄞ"), ("han", "ㄏㄢ"), ("hao", "ㄏㄠ"),
|
||||
("hei", "ㄏㄟ"), ("hen", "ㄏㄣ"), ("hou", "ㄏㄡ"), ("hua", "ㄏㄨㄚ"), ("hui", "ㄏㄨㄟ"), ("hun", "ㄏㄨㄣ"), ("huo", "ㄏㄨㄛ"),
|
||||
("jia", "ㄐㄧㄚ"), ("jie", "ㄐㄧㄝ"), ("jin", "ㄐㄧㄣ"), ("jiu", "ㄐㄧㄡ"), ("jue", "ㄐㄩㄝ"), ("kai", "ㄎㄞ"), ("kan", "ㄎㄢ"),
|
||||
("kao", "ㄎㄠ"), ("ken", "ㄎㄣ"), ("kiu", "ㄎㄧㄡ"), ("kou", "ㄎㄡ"), ("kua", "ㄎㄨㄚ"), ("kui", "ㄎㄨㄟ"), ("kun", "ㄎㄨㄣ"),
|
||||
("kuo", "ㄎㄨㄛ"), ("lai", "ㄌㄞ"), ("lan", "ㄌㄢ"), ("lao", "ㄌㄠ"), ("lei", "ㄌㄟ"), ("lia", "ㄌㄧㄚ"), ("lie", "ㄌㄧㄝ"),
|
||||
("lin", "ㄌㄧㄣ"), ("liu", "ㄌㄧㄡ"), ("lou", "ㄌㄡ"), ("lun", "ㄌㄨㄣ"), ("luo", "ㄌㄨㄛ"), ("lve", "ㄌㄩㄝ"), ("mai", "ㄇㄞ"),
|
||||
("man", "ㄇㄢ"), ("mao", "ㄇㄠ"), ("mei", "ㄇㄟ"), ("men", "ㄇㄣ"), ("mie", "ㄇㄧㄝ"), ("min", "ㄇㄧㄣ"), ("miu", "ㄇㄧㄡ"),
|
||||
("mou", "ㄇㄡ"), ("nai", "ㄋㄞ"), ("nan", "ㄋㄢ"), ("nao", "ㄋㄠ"), ("nei", "ㄋㄟ"), ("nen", "ㄋㄣ"), ("nie", "ㄋㄧㄝ"),
|
||||
("nin", "ㄋㄧㄣ"), ("niu", "ㄋㄧㄡ"), ("nou", "ㄋㄡ"), ("nui", "ㄋㄨㄟ"), ("nun", "ㄋㄨㄣ"), ("nuo", "ㄋㄨㄛ"), ("nve", "ㄋㄩㄝ"),
|
||||
("pai", "ㄆㄞ"), ("pan", "ㄆㄢ"), ("pao", "ㄆㄠ"), ("pei", "ㄆㄟ"), ("pen", "ㄆㄣ"), ("pia", "ㄆㄧㄚ"), ("pie", "ㄆㄧㄝ"),
|
||||
("pin", "ㄆㄧㄣ"), ("pou", "ㄆㄡ"), ("qia", "ㄑㄧㄚ"), ("qie", "ㄑㄧㄝ"), ("qin", "ㄑㄧㄣ"), ("qiu", "ㄑㄧㄡ"), ("que", "ㄑㄩㄝ"),
|
||||
("qun", "ㄑㄩㄣ"), ("ran", "ㄖㄢ"), ("rao", "ㄖㄠ"), ("ren", "ㄖㄣ"), ("rou", "ㄖㄡ"), ("rui", "ㄖㄨㄟ"), ("run", "ㄖㄨㄣ"),
|
||||
("ruo", "ㄖㄨㄛ"), ("sai", "ㄙㄞ"), ("san", "ㄙㄢ"), ("sao", "ㄙㄠ"), ("sei", "ㄙㄟ"), ("sen", "ㄙㄣ"), ("sha", "ㄕㄚ"),
|
||||
("she", "ㄕㄜ"), ("shi", "ㄕ"), ("shu", "ㄕㄨ"), ("sou", "ㄙㄡ"), ("sui", "ㄙㄨㄟ"), ("sun", "ㄙㄨㄣ"), ("suo", "ㄙㄨㄛ"),
|
||||
("tai", "ㄊㄞ"), ("tan", "ㄊㄢ"), ("tao", "ㄊㄠ"), ("tie", "ㄊㄧㄝ"), ("tou", "ㄊㄡ"), ("tui", "ㄊㄨㄟ"), ("tun", "ㄊㄨㄣ"),
|
||||
("tuo", "ㄊㄨㄛ"), ("wai", "ㄨㄞ"), ("wan", "ㄨㄢ"), ("wei", "ㄨㄟ"), ("wen", "ㄨㄣ"), ("xia", "ㄒㄧㄚ"), ("xie", "ㄒㄧㄝ"),
|
||||
("xin", "ㄒㄧㄣ"), ("xiu", "ㄒㄧㄡ"), ("xue", "ㄒㄩㄝ"), ("xun", "ㄒㄩㄣ"), ("yai", "ㄧㄞ"), ("yan", "ㄧㄢ"), ("yao", "ㄧㄠ"),
|
||||
("yin", "ㄧㄣ"), ("you", "ㄧㄡ"), ("yue", "ㄩㄝ"), ("yun", "ㄩㄣ"), ("zai", "ㄗㄞ"), ("zan", "ㄗㄢ"), ("zao", "ㄗㄠ"),
|
||||
("zei", "ㄗㄟ"), ("zen", "ㄗㄣ"), ("zha", "ㄓㄚ"), ("zhe", "ㄓㄜ"), ("zhi", "ㄓ"), ("zhu", "ㄓㄨ"), ("zou", "ㄗㄡ"),
|
||||
("zui", "ㄗㄨㄟ"), ("zun", "ㄗㄨㄣ"), ("zuo", "ㄗㄨㄛ"), ("ai", "ㄞ"), ("an", "ㄢ"), ("ao", "ㄠ"), ("ba", "ㄅㄚ"), ("bi", "ㄅㄧ"),
|
||||
("bo", "ㄅㄛ"), ("bu", "ㄅㄨ"), ("ca", "ㄘㄚ"), ("ce", "ㄘㄜ"), ("ci", "ㄘ"), ("cu", "ㄘㄨ"), ("da", "ㄉㄚ"), ("de", "ㄉㄜ"),
|
||||
("di", "ㄉㄧ"), ("du", "ㄉㄨ"), ("eh", "ㄝ"), ("ei", "ㄟ"), ("en", "ㄣ"), ("er", "ㄦ"), ("fa", "ㄈㄚ"), ("fo", "ㄈㄛ"),
|
||||
("fu", "ㄈㄨ"), ("ga", "ㄍㄚ"), ("ge", "ㄍㄜ"), ("gi", "ㄍㄧ"), ("gu", "ㄍㄨ"), ("ha", "ㄏㄚ"), ("he", "ㄏㄜ"), ("hu", "ㄏㄨ"),
|
||||
("ji", "ㄐㄧ"), ("ju", "ㄐㄩ"), ("ka", "ㄎㄚ"), ("ke", "ㄎㄜ"), ("ku", "ㄎㄨ"), ("la", "ㄌㄚ"), ("le", "ㄌㄜ"), ("li", "ㄌㄧ"),
|
||||
("lo", "ㄌㄛ"), ("lu", "ㄌㄨ"), ("lv", "ㄌㄩ"), ("ma", "ㄇㄚ"), ("me", "ㄇㄜ"), ("mi", "ㄇㄧ"), ("mo", "ㄇㄛ"), ("mu", "ㄇㄨ"),
|
||||
("na", "ㄋㄚ"), ("ne", "ㄋㄜ"), ("ni", "ㄋㄧ"), ("nu", "ㄋㄨ"), ("nv", "ㄋㄩ"), ("ou", "ㄡ"), ("pa", "ㄆㄚ"), ("pi", "ㄆㄧ"),
|
||||
("po", "ㄆㄛ"), ("pu", "ㄆㄨ"), ("qi", "ㄑㄧ"), ("qu", "ㄑㄩ"), ("re", "ㄖㄜ"), ("ri", "ㄖ"), ("ru", "ㄖㄨ"), ("sa", "ㄙㄚ"),
|
||||
("se", "ㄙㄜ"), ("si", "ㄙ"), ("su", "ㄙㄨ"), ("ta", "ㄊㄚ"), ("te", "ㄊㄜ"), ("ti", "ㄊㄧ"), ("tu", "ㄊㄨ"), ("wa", "ㄨㄚ"),
|
||||
("wo", "ㄨㄛ"), ("wu", "ㄨ"), ("xi", "ㄒㄧ"), ("xu", "ㄒㄩ"), ("ya", "ㄧㄚ"), ("ye", "ㄧㄝ"), ("yi", "ㄧ"), ("yo", "ㄧㄛ"),
|
||||
("yu", "ㄩ"), ("za", "ㄗㄚ"), ("ze", "ㄗㄜ"), ("zi", "ㄗ"), ("zu", "ㄗㄨ"), ("a", "ㄚ"), ("e", "ㄜ"), ("o", "ㄛ"), ("q", "ㄑ"),
|
||||
("1", " "), ("2", "ˊ"), ("3", "ˇ"), ("4", "ˋ"), ("5", "˙"),
|
||||
]
|
|
@ -34,8 +34,15 @@ public class PopupCompositionBuffer: NSWindowController {
|
|||
|
||||
public func sync(accent: NSColor?, locale: String) {
|
||||
self.locale = locale
|
||||
self.accent = accent ?? themeColorCocoa
|
||||
window?.backgroundColor = adjustedThemeColor
|
||||
if let accent = accent {
|
||||
self.accent = (accent.alphaComponent == 1) ? accent.withAlphaComponent(Self.bgOpacity) : accent
|
||||
} else {
|
||||
self.accent = themeColorCocoa
|
||||
}
|
||||
let themeColor = adjustedThemeColor
|
||||
window?.backgroundColor = .clear
|
||||
window?.contentView?.layer?.backgroundColor = themeColor.cgColor
|
||||
window?.contentView?.layer?.borderColor = NSColor.white.withAlphaComponent(0.1).cgColor
|
||||
messageTextField.backgroundColor = .clear
|
||||
messageTextField.textColor = textColor
|
||||
}
|
||||
|
@ -44,12 +51,14 @@ public class PopupCompositionBuffer: NSWindowController {
|
|||
|
||||
private var locale: String = ""
|
||||
|
||||
private static let bgOpacity: CGFloat = 0.8
|
||||
|
||||
var themeColorCocoa: NSColor {
|
||||
switch locale {
|
||||
case "zh-Hans": return .init(red: 255 / 255, green: 64 / 255, blue: 53 / 255, alpha: 0.85)
|
||||
case "zh-Hant": return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: 0.85)
|
||||
case "ja": return .init(red: 167 / 255, green: 137 / 255, blue: 99 / 255, alpha: 0.85)
|
||||
default: return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: 0.85)
|
||||
case "zh-Hans": return .init(red: 255 / 255, green: 64 / 255, blue: 53 / 255, alpha: Self.bgOpacity)
|
||||
case "zh-Hant": return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: Self.bgOpacity)
|
||||
case "ja": return .init(red: 167 / 255, green: 137 / 255, blue: 99 / 255, alpha: Self.bgOpacity)
|
||||
default: return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: Self.bgOpacity)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,9 +75,8 @@ public class PopupCompositionBuffer: NSWindowController {
|
|||
)
|
||||
panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 1)
|
||||
panel.hasShadow = true
|
||||
panel.backgroundColor = NSColor.controlBackgroundColor
|
||||
panel.styleMask = .utilityWindow
|
||||
panel.isMovable = false
|
||||
panel.backgroundColor = .clear
|
||||
panel.isOpaque = false
|
||||
messageTextField = NSTextField()
|
||||
messageTextField.isEditable = false
|
||||
messageTextField.isSelectable = false
|
||||
|
@ -79,6 +87,15 @@ public class PopupCompositionBuffer: NSWindowController {
|
|||
messageTextField.font = .systemFont(ofSize: 18) // 不是最終值。
|
||||
panel.contentView?.addSubview(messageTextField)
|
||||
panel.contentView?.wantsLayer = true
|
||||
panel.contentView?.shadow = .init()
|
||||
panel.contentView?.shadow?.shadowBlurRadius = 6
|
||||
panel.contentView?.shadow?.shadowColor = .black
|
||||
panel.contentView?.shadow?.shadowOffset = .zero
|
||||
if let layer = panel.contentView?.layer {
|
||||
layer.cornerRadius = 9
|
||||
layer.borderWidth = 1
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
Self.currentWindow = panel
|
||||
super.init(window: panel)
|
||||
}
|
||||
|
@ -256,6 +273,6 @@ public class PopupCompositionBuffer: NSWindowController {
|
|||
}
|
||||
|
||||
private var adjustedThemeColor: NSColor {
|
||||
accent.blended(withFraction: NSApplication.isDarkMode ? 0.75 : 0.25, of: .black) ?? accent
|
||||
accent.blended(withFraction: NSApplication.isDarkMode ? 0.5 : 0.25, of: .black) ?? accent
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ let package = Package(
|
|||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../vChewing_CocoaExtension"),
|
||||
.package(path: "../vChewing_OSFrameworkImpl"),
|
||||
.package(path: "../vChewing_IMKUtils"),
|
||||
.package(path: "../vChewing_SwiftExtension"),
|
||||
],
|
||||
|
@ -21,10 +21,14 @@ let package = Package(
|
|||
.target(
|
||||
name: "Shared",
|
||||
dependencies: [
|
||||
.product(name: "CocoaExtension", package: "vChewing_CocoaExtension"),
|
||||
.product(name: "OSFrameworkImpl", package: "vChewing_OSFrameworkImpl"),
|
||||
.product(name: "IMKUtils", package: "vChewing_IMKUtils"),
|
||||
.product(name: "SwiftExtension", package: "vChewing_SwiftExtension"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "SharedTests",
|
||||
dependencies: ["Shared"]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
@objcMembers public class Broadcaster: NSObject {
|
||||
public static var shared = Broadcaster()
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
|
||||
public class CandidateNode {
|
||||
open class CandidateNode {
|
||||
public var name: String
|
||||
public var members: [CandidateNode]
|
||||
public var previous: CandidateNode?
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
public extension CandidateNode {
|
||||
convenience init(
|
||||
name: String, services: [CandidateTextService], previous: CandidateNode? = nil
|
||||
) {
|
||||
self.init(name: name, members: services.map(\.asCandidateNode), previous: previous)
|
||||
}
|
||||
|
||||
var asServiceMenuNode: ServiceMenuNode? {
|
||||
self as? ServiceMenuNode
|
||||
}
|
||||
|
||||
var containsCandidateServices: Bool {
|
||||
!members.compactMap(\.asServiceMenuNode).isEmpty
|
||||
}
|
||||
|
||||
class ServiceMenuNode: CandidateNode {
|
||||
public var service: CandidateTextService
|
||||
public init(
|
||||
name: String, service givenService: CandidateTextService, previous: CandidateNode? = nil
|
||||
) {
|
||||
service = givenService
|
||||
super.init(name: name, previous: previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension CandidateTextService {
|
||||
var asCandidateNode: CandidateNode.ServiceMenuNode {
|
||||
.init(name: menuTitle, service: self)
|
||||
}
|
||||
|
||||
static func getCurrentServiceMenu(
|
||||
fromMap map: [String]? = nil, candidate: String, reading: [String]
|
||||
) -> CandidateNode? {
|
||||
let fetchedRaw = map ?? PrefMgr().candidateServiceMenuContents
|
||||
let fetched = fetchedRaw.parseIntoCandidateTextServiceStack(candidate: candidate, reading: reading)
|
||||
return fetched.isEmpty ? nil : .init(name: candidate, services: fetched)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CandidateTextService: Codable {
|
||||
public enum ServiceValueType: Int {
|
||||
case url = 0
|
||||
case selector = 1
|
||||
}
|
||||
|
||||
public enum ServiceValue: Codable {
|
||||
case url(URL)
|
||||
case selector(String)
|
||||
}
|
||||
|
||||
public let key: String
|
||||
public let reading: [String]
|
||||
public let menuTitle: String
|
||||
public let definedValue: String
|
||||
public let value: ServiceValue
|
||||
public let candidateText: String
|
||||
|
||||
public static var finalSanityCheck: ((CandidateTextService) -> Bool)?
|
||||
|
||||
public init?(key: String, definedValue: String, param: String = #"%s"#, reading: [String] = []) {
|
||||
guard !key.isEmpty, !definedValue.isEmpty, definedValue.first != "#" else { return nil }
|
||||
candidateText = param
|
||||
self.key = key.replacingOccurrences(of: #"%s"#, with: param)
|
||||
self.reading = reading
|
||||
let rawKeyHasParam = self.key != key
|
||||
self.definedValue = definedValue.replacingOccurrences(of: #"%s"#, with: param)
|
||||
|
||||
// Handle Symbol Menu Title
|
||||
var newMenuTitle = self.key
|
||||
if param.count == 1, let strUTFCharCode = param.first?.codePoint, rawKeyHasParam {
|
||||
newMenuTitle = "\(self.key) (\(strUTFCharCode))"
|
||||
}
|
||||
menuTitle = newMenuTitle
|
||||
|
||||
// Start parsing rawValue
|
||||
var temporaryRawValue = definedValue
|
||||
var finalServiceValue: ServiceValue?
|
||||
let fetchedTypeHeader = temporaryRawValue.prefix(5)
|
||||
guard fetchedTypeHeader.count == 5 else { return nil }
|
||||
for _ in 0 ..< 5 {
|
||||
temporaryRawValue.removeFirst()
|
||||
}
|
||||
switch fetchedTypeHeader.uppercased() {
|
||||
case #"@SEL:"#:
|
||||
finalServiceValue = .selector(temporaryRawValue)
|
||||
case #"@WEB:"#, #"@URL:"#:
|
||||
let encodedParam = param.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
|
||||
guard let encodedParam = encodedParam else { return nil }
|
||||
let newURL = URL(string: temporaryRawValue.replacingOccurrences(of: #"%s"#, with: encodedParam))
|
||||
guard let newURL = newURL else { return nil }
|
||||
finalServiceValue = .url(newURL)
|
||||
default: return nil
|
||||
}
|
||||
guard let finalServiceValue = finalServiceValue else { return nil }
|
||||
value = finalServiceValue
|
||||
let finalSanityCheckResult = Self.finalSanityCheck?(self) ?? true
|
||||
if !finalSanityCheckResult { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
extension CandidateTextService: RawRepresentable {
|
||||
public init?(rawValue: String) {
|
||||
let cells = rawValue.components(separatedBy: "\t")
|
||||
guard cells.count == 2 else { return nil }
|
||||
self.init(key: cells[0], definedValue: cells[1])
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
"\(key)\t\(definedValue)"
|
||||
}
|
||||
|
||||
public init?(rawValue: String, param: String, reading: [String]) {
|
||||
let cells = rawValue.components(separatedBy: "\t")
|
||||
guard cells.count >= 2 else { return nil }
|
||||
self.init(key: cells[0], definedValue: cells[1], param: param, reading: reading)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
public extension Array where Element == CandidateTextService {
|
||||
var rawRepresentation: [String] {
|
||||
map(\.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element == String {
|
||||
func parseIntoCandidateTextServiceStack(
|
||||
candidate: String = #"%s"#, reading: [String] = []
|
||||
) -> [CandidateTextService] {
|
||||
compactMap { rawValue in
|
||||
CandidateTextService(rawValue: rawValue, param: candidate, reading: reading)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import AppKit
|
||||
import Carbon
|
||||
import Shared
|
||||
|
||||
// MARK: - Top-level Enums relating to Input Mode and Language Supports.
|
||||
|
||||
|
@ -52,7 +51,7 @@ public enum IMEApp {
|
|||
// MARK: - 輸入法的當前的簡繁體中文模式
|
||||
|
||||
public static var currentInputMode: Shared.InputMode {
|
||||
.init(rawValue: PrefMgr.shared.mostRecentInputMode) ?? .imeModeNULL
|
||||
.init(rawValue: PrefMgr().mostRecentInputMode) ?? .imeModeNULL
|
||||
}
|
||||
|
||||
/// 當前鍵盤是否是 JIS 佈局
|
||||
|
@ -62,9 +61,10 @@ public enum IMEApp {
|
|||
|
||||
/// Fart or Beep?
|
||||
public static func buzz() {
|
||||
if PrefMgr.shared.isDebugModeEnabled {
|
||||
NSSound.buzz(fart: !PrefMgr.shared.shouldNotFartInLieuOfBeep)
|
||||
} else if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
|
||||
let prefs = PrefMgr()
|
||||
if prefs.isDebugModeEnabled {
|
||||
NSSound.buzz(fart: !prefs.shouldNotFartInLieuOfBeep)
|
||||
} else if !prefs.shouldNotFartInLieuOfBeep {
|
||||
NSSound.buzz(fart: true)
|
||||
} else {
|
||||
NSSound.beep()
|
|
@ -6,14 +6,12 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import Shared
|
||||
import Foundation
|
||||
import SwiftExtension
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objcMembers public class PrefMgr: NSObject, PrefMgrProtocol {
|
||||
public static let shared = PrefMgr()
|
||||
public static let kDefaultCandidateKeys = "123456"
|
||||
public static let kDefaultBasicKeyboardLayout = "com.apple.keylayout.ZhuyinBopomofo"
|
||||
public static let kDefaultAlphanumericalKeyboardLayout = {
|
||||
|
@ -27,6 +25,33 @@ import SwiftExtension
|
|||
"com.valvesoftware.steam": true, "jp.naver.line.mac": true,
|
||||
]
|
||||
|
||||
public static let kDefaultCandidateServiceMenuItem: [String] = [
|
||||
#"Unicode Metadata: %s"# + "\t" + #"@SEL:copyUnicodeMetadata:"#,
|
||||
#"macOS Dict: %s"# + "\t" + #"@URL:dict://%s"#,
|
||||
#"Bing: %s"# + "\t" + #"@WEB:https://www.bing.com/search?q=%s"#,
|
||||
#"DuckDuckGo: %s"# + "\t" + #"@WEB:https://duckduckgo.com/?t=h_&q=%s"#,
|
||||
#"Ecosia: %s"# + "\t" + #"@WEB:https://www.ecosia.org/search?method=index&q=%s"#,
|
||||
#"Google: %s"# + "\t" + #"@WEB:https://www.google.com/search?q=%s"#,
|
||||
#"MoeDict: %s"# + "\t" + #"@WEB:https://www.moedict.tw/%s"#,
|
||||
#"Wikitonary: %s"# + "\t" + #"@WEB:https://zh.wiktionary.org/wiki/Special:Search?search=%s"#,
|
||||
#"Unihan: %s"# + "\t" + #"@WEB:https://www.unicode.org/cgi-bin/GetUnihanData.pl?codepoint=%s"#,
|
||||
#"Zi-Hi: %s"# + "\t" + #"@WEB:https://zi-hi.com/sp/uni/%s"#,
|
||||
#"HTML Ruby Zhuyin: %s"# + "\t" + #"@SEL:copyRubyHTMLZhuyinTextbookStyle:"#,
|
||||
#"HTML Ruby Pinyin: %s"# + "\t" + #"@SEL:copyRubyHTMLHanyuPinyinTextbookStyle:"#,
|
||||
#"Zhuyin Annotation: %s"# + "\t" + #"@SEL:copyInlineZhuyinAnnotationTextbookStyle:"#,
|
||||
#"Pinyin Annotation: %s"# + "\t" + #"@SEL:copyInlineHanyuPinyinAnnotationTextbookStyle:"#,
|
||||
#"Braille 1947: %s"# + "\t" + #"@SEL:copyBraille1947:"#,
|
||||
#"Braille 2018: %s"# + "\t" + #"@SEL:copyBraille2018:"#,
|
||||
#"Baidu: %s"# + "\t" + #"@WEB:https://www.baidu.com/s?wd=%s"#,
|
||||
#"BiliBili: %s"# + "\t" + #"@WEB:https://search.bilibili.com/all?keyword=%s"#,
|
||||
#"Genshin BiliWiki: %s"# + "\t" + #"@WEB:https://wiki.biligame.com/ys/%s"#,
|
||||
#"HSR BiliWiki: %s"# + "\t" + #"@WEB:https://wiki.biligame.com/sr/%s"#,
|
||||
]
|
||||
|
||||
public var didAskForSyncingLMPrefs: (() -> Void)?
|
||||
public var didAskForRefreshingSpeechSputnik: (() -> Void)?
|
||||
public var didAskForSyncingShiftKeyDetectorPrefs: (() -> Void)?
|
||||
|
||||
// MARK: - Settings (Tier 1)
|
||||
|
||||
@AppProperty(key: UserDef.kIsDebugModeEnabled.rawValue, defaultValue: false)
|
||||
|
@ -35,6 +60,9 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kFailureFlagForUOMObservation.rawValue, defaultValue: false)
|
||||
public dynamic var failureFlagForUOMObservation: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kCandidateServiceMenuContents.rawValue, defaultValue: kDefaultCandidateServiceMenuItem)
|
||||
public dynamic var candidateServiceMenuContents: [String]
|
||||
|
||||
@AppProperty(key: UserDef.kRespectClientAccentColor.rawValue, defaultValue: true)
|
||||
public dynamic var respectClientAccentColor: Bool
|
||||
|
||||
|
@ -106,6 +134,9 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kUseJKtoMoveCompositorCursorInCandidateState.rawValue, defaultValue: false)
|
||||
public var useJKtoMoveCompositorCursorInCandidateState: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kUseShiftQuestionToCallServiceMenu.rawValue, defaultValue: true)
|
||||
public var useShiftQuestionToCallServiceMenu: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kMoveCursorAfterSelectingCandidate.rawValue, defaultValue: true)
|
||||
public dynamic var moveCursorAfterSelectingCandidate: Bool
|
||||
|
||||
|
@ -118,6 +149,9 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kUseHorizontalCandidateList.rawValue, defaultValue: true)
|
||||
public dynamic var useHorizontalCandidateList: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kMinCellWidthForHorizontalMatrix.rawValue, defaultValue: 0)
|
||||
public dynamic var minCellWidthForHorizontalMatrix: Int
|
||||
|
||||
@AppProperty(key: UserDef.kChooseCandidateUsingSpace.rawValue, defaultValue: true)
|
||||
public dynamic var chooseCandidateUsingSpace: Bool
|
||||
|
||||
|
@ -135,9 +169,7 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kReadingNarrationCoverage.rawValue, defaultValue: 0)
|
||||
public dynamic var readingNarrationCoverage: Int {
|
||||
didSet {
|
||||
SpeechSputnik.shared.refreshStatus()
|
||||
}
|
||||
didSet { didAskForRefreshingSpeechSputnik?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kAlsoConfirmAssociatedCandidatesByEnter.rawValue, defaultValue: false)
|
||||
|
@ -160,16 +192,12 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kTogglingAlphanumericalModeWithLShift.rawValue, defaultValue: true)
|
||||
public dynamic var togglingAlphanumericalModeWithLShift: Bool {
|
||||
didSet {
|
||||
SessionCtl.theShiftKeyDetector.toggleWithLShift = togglingAlphanumericalModeWithLShift
|
||||
}
|
||||
didSet { didAskForSyncingShiftKeyDetectorPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kTogglingAlphanumericalModeWithRShift.rawValue, defaultValue: true)
|
||||
public dynamic var togglingAlphanumericalModeWithRShift: Bool {
|
||||
didSet {
|
||||
SessionCtl.theShiftKeyDetector.toggleWithRShift = togglingAlphanumericalModeWithRShift
|
||||
}
|
||||
didSet { didAskForSyncingShiftKeyDetectorPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kConsolidateContextOnCandidateSelection.rawValue, defaultValue: true)
|
||||
|
@ -244,23 +272,17 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kCNS11643Enabled.rawValue, defaultValue: false)
|
||||
public dynamic var cns11643Enabled: Bool {
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kSymbolInputEnabled.rawValue, defaultValue: true)
|
||||
public dynamic var symbolInputEnabled: Bool {
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kCassetteEnabled.rawValue, defaultValue: false)
|
||||
public dynamic var cassetteEnabled: Bool {
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kChineseConversionEnabled.rawValue, defaultValue: false)
|
||||
|
@ -319,6 +341,9 @@ import SwiftExtension
|
|||
@AppProperty(key: UserDef.kSpecifyShiftSpaceKeyBehavior.rawValue, defaultValue: false)
|
||||
public dynamic var specifyShiftSpaceKeyBehavior: Bool
|
||||
|
||||
@AppProperty(key: UserDef.kSpecifyCmdOptCtrlEnterBehavior.rawValue, defaultValue: 0)
|
||||
public dynamic var specifyCmdOptCtrlEnterBehavior: Int
|
||||
|
||||
// MARK: - Optional settings
|
||||
|
||||
@AppProperty(key: UserDef.kCandidateTextFontName.rawValue, defaultValue: "")
|
||||
|
@ -337,33 +362,17 @@ import SwiftExtension
|
|||
|
||||
@AppProperty(key: UserDef.kUseSCPCTypingMode.rawValue, defaultValue: false)
|
||||
public dynamic var useSCPCTypingMode: Bool {
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kPhraseReplacementEnabled.rawValue, defaultValue: false)
|
||||
public dynamic var phraseReplacementEnabled: Bool {
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
willSet {
|
||||
if newValue {
|
||||
LMMgr.loadUserPhraseReplacement()
|
||||
}
|
||||
}
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
}
|
||||
|
||||
@AppProperty(key: UserDef.kAssociatedPhrasesEnabled.rawValue, defaultValue: false)
|
||||
public dynamic var associatedPhrasesEnabled: Bool {
|
||||
didSet {
|
||||
LMMgr.syncLMPrefs()
|
||||
}
|
||||
willSet {
|
||||
if newValue {
|
||||
LMMgr.loadUserAssociatesData()
|
||||
}
|
||||
}
|
||||
didSet { didAskForSyncingLMPrefs?() }
|
||||
}
|
||||
|
||||
// MARK: - Keyboard HotKey Enable / Disable
|
|
@ -7,28 +7,16 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import InputMethodKit
|
||||
import Shared
|
||||
import SwiftExtension
|
||||
import SwiftUI
|
||||
|
||||
// MARK: ObservableProject.
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
extension PrefMgr: ObservableObject {}
|
||||
|
||||
extension PrefMgr {
|
||||
func sendObjWillChange() {
|
||||
if #available(macOS 10.15, *) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Guarded Method for Validating Candidate Keys.
|
||||
|
||||
public extension PrefMgr {
|
||||
func validate(candidateKeys: String) -> String? {
|
||||
let excluded = useJKtoMoveCompositorCursorInCandidateState ? "jk" : ""
|
||||
var excluded = ""
|
||||
if useJKtoMoveCompositorCursorInCandidateState { excluded.append("jk") }
|
||||
if useShiftQuestionToCallServiceMenu { excluded.append("?") }
|
||||
excluded.append(IMEApp.isKeyboardJIS ? "_" : "`~")
|
||||
return CandidateKey.validate(keys: candidateKeys, excluding: excluded)
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +68,9 @@ public extension PrefMgr {
|
|||
if ![0, 1, 2].contains(readingNarrationCoverage) {
|
||||
readingNarrationCoverage = 0
|
||||
}
|
||||
if ![0, 1, 2, 3].contains(specifyCmdOptCtrlEnterBehavior) {
|
||||
specifyCmdOptCtrlEnterBehavior = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +78,7 @@ public extension PrefMgr {
|
|||
|
||||
public extension PrefMgr {
|
||||
@discardableResult func dumpShellScriptBackup() -> String? {
|
||||
let mirror = Mirror(reflecting: PrefMgr.shared)
|
||||
let mirror = Mirror(reflecting: self)
|
||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return nil }
|
||||
let strDoubleDashLine = String(String(repeating: "=", count: 70))
|
||||
let consoleOutput = NSMutableString(string: "#!/bin/sh\n\n")
|
|
@ -7,7 +7,7 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import OSFrameworkImpl
|
||||
|
||||
// MARK: - InputSignalProtocol
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import Foundation
|
|||
public protocol PrefMgrProtocol {
|
||||
var isDebugModeEnabled: Bool { get set }
|
||||
var failureFlagForUOMObservation: Bool { get set }
|
||||
var candidateServiceMenuContents: [String] { get set }
|
||||
var respectClientAccentColor: Bool { get set }
|
||||
var securityHardenedCompositionBuffer: Bool { get set }
|
||||
var checkAbusersOfSecureEventInputAPI: Bool { get set }
|
||||
|
@ -31,10 +32,12 @@ public protocol PrefMgrProtocol {
|
|||
var shouldAutoReloadUserDataFiles: Bool { get set }
|
||||
var useRearCursorMode: Bool { get set }
|
||||
var useJKtoMoveCompositorCursorInCandidateState: Bool { get set }
|
||||
var useShiftQuestionToCallServiceMenu: Bool { get set }
|
||||
var moveCursorAfterSelectingCandidate: Bool { get set }
|
||||
var dodgeInvalidEdgeCandidateCursorPosition: Bool { get set }
|
||||
var useDynamicCandidateWindowOrigin: Bool { get set }
|
||||
var useHorizontalCandidateList: Bool { get set }
|
||||
var minCellWidthForHorizontalMatrix: Int { get set }
|
||||
var chooseCandidateUsingSpace: Bool { get set }
|
||||
var allowBoostingSingleKanjiAsUserPhrase: Bool { get set }
|
||||
var fetchSuggestionsFromUserOverrideModel: Bool { get set }
|
||||
|
@ -83,6 +86,7 @@ public protocol PrefMgrProtocol {
|
|||
var specifyShiftBackSpaceKeyBehavior: Int { get set }
|
||||
var specifyShiftTabKeyBehavior: Bool { get set }
|
||||
var specifyShiftSpaceKeyBehavior: Bool { get set }
|
||||
var specifyCmdOptCtrlEnterBehavior: Int { get set }
|
||||
var candidateTextFontName: String { get set }
|
||||
var candidateKeys: String { get set }
|
||||
var useSCPCTypingMode: Bool { get set }
|
||||
|
|
|
@ -274,12 +274,13 @@ public enum Shared {
|
|||
// MARK: - PEReloadEventObserver
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
public class PEReloadEventObserver: ObservableObject, Equatable {
|
||||
public class PEReloadEventObserver: NSObject, ObservableObject {
|
||||
public static let shared = PEReloadEventObserver()
|
||||
private var observation: NSKeyValueObservation?
|
||||
@Published public var id = UUID().uuidString
|
||||
|
||||
public init() {
|
||||
override public init() {
|
||||
super.init()
|
||||
observation = Broadcaster.shared.observe(\.eventForReloadingPhraseEditor, options: [.new]) { _, _ in
|
||||
self.touch()
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ public enum UserDef: String, CaseIterable, Identifiable {
|
|||
|
||||
case kIsDebugModeEnabled = "_DebugMode"
|
||||
case kFailureFlagForUOMObservation = "_FailureFlag_UOMObservation"
|
||||
case kCandidateServiceMenuContents = "CandidateServiceMenuContents"
|
||||
case kRespectClientAccentColor = "RespectClientAccentColor"
|
||||
case kSecurityHardenedCompositionBuffer = "SecurityHardenedCompositionBuffer"
|
||||
case kCheckAbusersOfSecureEventInputAPI = "CheckAbusersOfSecureEventInputAPI"
|
||||
|
@ -53,8 +54,10 @@ public enum UserDef: String, CaseIterable, Identifiable {
|
|||
case kShouldAutoReloadUserDataFiles = "ShouldAutoReloadUserDataFiles"
|
||||
case kUseRearCursorMode = "UseRearCursorMode"
|
||||
case kUseJKtoMoveCompositorCursorInCandidateState = "UseJKtoMoveCompositorCursorInCandidateState"
|
||||
case kUseShiftQuestionToCallServiceMenu = "UseShiftQuestionToCallServiceMenu"
|
||||
case kUseDynamicCandidateWindowOrigin = "UseDynamicCandidateWindowOrigin"
|
||||
case kUseHorizontalCandidateList = "UseHorizontalCandidateList"
|
||||
case kMinCellWidthForHorizontalMatrix = "MinCellWidthForHorizontalMatrix"
|
||||
case kChooseCandidateUsingSpace = "ChooseCandidateUsingSpace"
|
||||
case kCassetteEnabled = "CassetteEnabled"
|
||||
case kCNS11643Enabled = "CNS11643Enabled"
|
||||
|
@ -71,6 +74,7 @@ public enum UserDef: String, CaseIterable, Identifiable {
|
|||
case kSpecifyShiftBackSpaceKeyBehavior = "SpecifyShiftBackSpaceKeyBehavior"
|
||||
case kSpecifyShiftTabKeyBehavior = "SpecifyShiftTabKeyBehavior"
|
||||
case kSpecifyShiftSpaceKeyBehavior = "SpecifyShiftSpaceKeyBehavior"
|
||||
case kSpecifyCmdOptCtrlEnterBehavior = "SpecifyCmdOptCtrlEnterBehavior"
|
||||
case kAllowBoostingSingleKanjiAsUserPhrase = "AllowBoostingSingleKanjiAsUserPhrase"
|
||||
case kUseSCPCTypingMode = "UseSCPCTypingMode"
|
||||
case kMaxCandidateLength = "MaxCandidateLength"
|
||||
|
@ -157,6 +161,7 @@ public extension UserDef {
|
|||
switch self {
|
||||
case .kIsDebugModeEnabled: return .bool
|
||||
case .kFailureFlagForUOMObservation: return .bool
|
||||
case .kCandidateServiceMenuContents: return .dictionary
|
||||
case .kRespectClientAccentColor: return .bool
|
||||
case .kSecurityHardenedCompositionBuffer: return .bool
|
||||
case .kCheckAbusersOfSecureEventInputAPI: return .bool
|
||||
|
@ -177,8 +182,10 @@ public extension UserDef {
|
|||
case .kShouldAutoReloadUserDataFiles: return .bool
|
||||
case .kUseRearCursorMode: return .bool
|
||||
case .kUseJKtoMoveCompositorCursorInCandidateState: return .bool
|
||||
case .kUseShiftQuestionToCallServiceMenu: return .bool
|
||||
case .kUseDynamicCandidateWindowOrigin: return .bool
|
||||
case .kUseHorizontalCandidateList: return .bool
|
||||
case .kMinCellWidthForHorizontalMatrix: return .integer
|
||||
case .kChooseCandidateUsingSpace: return .bool
|
||||
case .kCassetteEnabled: return .bool
|
||||
case .kCNS11643Enabled: return .bool
|
||||
|
@ -195,6 +202,7 @@ public extension UserDef {
|
|||
case .kSpecifyShiftBackSpaceKeyBehavior: return .integer
|
||||
case .kSpecifyShiftTabKeyBehavior: return .bool
|
||||
case .kSpecifyShiftSpaceKeyBehavior: return .bool
|
||||
case .kSpecifyCmdOptCtrlEnterBehavior: return .integer
|
||||
case .kAllowBoostingSingleKanjiAsUserPhrase: return .bool
|
||||
case .kUseSCPCTypingMode: return .bool
|
||||
case .kMaxCandidateLength: return .integer
|
||||
|
@ -252,6 +260,7 @@ public extension UserDef {
|
|||
var metaData: MetaData? {
|
||||
switch self {
|
||||
case .kIsDebugModeEnabled: return .init(userDef: self, shortTitle: "Debug Mode")
|
||||
case .kCandidateServiceMenuContents: return nil
|
||||
case .kFailureFlagForUOMObservation: return nil
|
||||
case .kRespectClientAccentColor: return .init(
|
||||
userDef: self, shortTitle: "i18n:userdef.kRespectClientAccentColor.shortTitle",
|
||||
|
@ -339,6 +348,10 @@ public extension UserDef {
|
|||
shortTitle: "i18n:UserDef.kUseJKtoMoveCompositorCursorInCandidateState.shortTitle",
|
||||
description: "i18n:UserDef.kUseJKtoMoveCompositorCursorInCandidateState.description"
|
||||
)
|
||||
case .kUseShiftQuestionToCallServiceMenu: return .init(
|
||||
userDef: self,
|
||||
shortTitle: "i18n:UserDef.kUseShiftQuestionToCallServiceMenu.shortTitle"
|
||||
)
|
||||
case .kUseDynamicCandidateWindowOrigin: return .init(
|
||||
userDef: self, shortTitle: "Adjust candidate window location according to current node length"
|
||||
)
|
||||
|
@ -347,6 +360,13 @@ public extension UserDef {
|
|||
description: "Choose your preferred layout of the candidate window.",
|
||||
options: [0: "Vertical", 1: "Horizontal"]
|
||||
)
|
||||
case .kMinCellWidthForHorizontalMatrix: return .init(
|
||||
userDef: self, shortTitle: "i18n:userdef.kMinCellWidthForHorizontalMatrix.shortTitle",
|
||||
options: [
|
||||
0: "i18n:UserDef.kMinCellWidthForHorizontalMatrix.option.0",
|
||||
1: "i18n:UserDef.kMinCellWidthForHorizontalMatrix.option.1",
|
||||
]
|
||||
)
|
||||
case .kChooseCandidateUsingSpace: return .init(
|
||||
userDef: self, shortTitle: "Enable Space key for calling candidate window",
|
||||
description: "If disabled, this will insert space instead."
|
||||
|
@ -415,6 +435,16 @@ public extension UserDef {
|
|||
1: "Space to +revolve pages, Shift+Space to +revolve candidates",
|
||||
]
|
||||
)
|
||||
case .kSpecifyCmdOptCtrlEnterBehavior: return .init(
|
||||
userDef: self, shortTitle: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.shortTitle",
|
||||
description: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.description",
|
||||
options: [
|
||||
0: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.0",
|
||||
1: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.1",
|
||||
2: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.2",
|
||||
3: "i18n:UserDef.kSpecifyCmdOptCtrlEnterBehavior.option.3",
|
||||
]
|
||||
)
|
||||
case .kAllowBoostingSingleKanjiAsUserPhrase: return .init(
|
||||
userDef: self, shortTitle: "Allow boosting / excluding a candidate of single kanji when marking",
|
||||
description: "⚠︎ This may hinder the walking algorithm from giving appropriate results."
|
||||
|
@ -432,7 +462,8 @@ public extension UserDef {
|
|||
userDef: self, shortTitle: "Show Hanyu-Pinyin in the inline composition buffer"
|
||||
)
|
||||
case .kInlineDumpPinyinInLieuOfZhuyin: return .init(
|
||||
userDef: self, shortTitle: "Commit Hanyu-Pinyin instead on Ctrl(+Option)+Command+Enter"
|
||||
userDef: self, shortTitle: "Commit Hanyu-Pinyin instead on Ctrl(+Option)+Command+Enter",
|
||||
description: "i18n:UserDef.kInlineDumpPinyinInLieuOfZhuyin.description"
|
||||
)
|
||||
case .kFetchSuggestionsFromUserOverrideModel: return .init(
|
||||
userDef: self, shortTitle: "Applying typing suggestions from half-life user override model",
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
// 這其實是 UserDefRenderable 的另一個版本,但用的是 AppKit 而非 SwiftUI。
|
||||
|
||||
import AppKit
|
||||
import CocoaExtension
|
||||
import Foundation
|
||||
import IMKUtils
|
||||
import OSFrameworkImpl
|
||||
|
||||
public class UserDefRenderableCocoa: NSObject, Identifiable {
|
||||
public let def: UserDef
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
@testable import Shared
|
||||
import XCTest
|
||||
|
||||
final class SharedTests: XCTestCase {
|
||||
// MARK: - PrefMgr().dumpShellScriptBackup()
|
||||
|
||||
func testDumpedPrefs() throws {
|
||||
let prefs = PrefMgr()
|
||||
let fetched = prefs.dumpShellScriptBackup() ?? ""
|
||||
XCTAssertFalse(fetched.isEmpty)
|
||||
}
|
||||
|
||||
// MARK: - CandidateTextService (Basic Tests)
|
||||
|
||||
static let testDataMap: [String] = [
|
||||
#"Bing: %s"# + "\t" + #"@WEB:https://www.bing.com/search?q=%s"#,
|
||||
#"Ecosia: %s"# + "\t" + #"@WEB:https://www.ecosia.org/search?method=index&q=%s"#,
|
||||
]
|
||||
|
||||
func testDataRestoration() throws {
|
||||
let stacked = Self.testDataMap.parseIntoCandidateTextServiceStack()
|
||||
stacked.forEach { currentService in
|
||||
print(currentService)
|
||||
}
|
||||
XCTAssertEqual(stacked.rawRepresentation, Self.testDataMap)
|
||||
}
|
||||
|
||||
func testCandidateServiceMenuNode() throws {
|
||||
let rootNode = CandidateTextService.getCurrentServiceMenu(
|
||||
fromMap: Self.testDataMap,
|
||||
candidate: "🍰", reading: ["ㄉㄢˋ", "ㄍㄠ"]
|
||||
)
|
||||
guard let rootNode = rootNode else {
|
||||
XCTAssertThrowsError("Root Node Construction Failed.")
|
||||
return
|
||||
}
|
||||
print(rootNode.members.map(\.name))
|
||||
print(rootNode.members.compactMap(\.asServiceMenuNode?.service))
|
||||
}
|
||||
}
|
|
@ -6,8 +6,6 @@
|
|||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Bool Operators
|
||||
|
||||
public func |= (lhs: inout Bool, rhs: Bool) {
|
||||
|
@ -22,25 +20,11 @@ public func ^= (lhs: inout Bool, rhs: Bool) {
|
|||
lhs = lhs != rhs
|
||||
}
|
||||
|
||||
// MARK: - String.localized extension
|
||||
|
||||
public extension StringLiteralType {
|
||||
var localized: String { NSLocalizedString(description, comment: "") }
|
||||
}
|
||||
|
||||
// MARK: - Root Extensions
|
||||
// MARK: - Root Extensions (deduplicated)
|
||||
|
||||
// Extend the RangeReplaceableCollection to allow it clean duplicated characters.
|
||||
// Ref: https://stackoverflow.com/questions/25738817/
|
||||
public extension RangeReplaceableCollection where Element: Hashable {
|
||||
/// 使用 NSOrderedSet 處理 class 陣列的「去重複化」。
|
||||
var classDeduplicated: Self {
|
||||
NSOrderedSet(array: Array(self)).compactMap { $0 as? Element.Type } as? Self ?? self
|
||||
// 下述方法有 Bug 會在處理 KeyValuePaired 的時候崩掉,暫時停用。
|
||||
// var set = Set<Element>()
|
||||
// return filter { set.insert($0).inserted }
|
||||
}
|
||||
|
||||
/// 去重複化。
|
||||
/// - Remark: 該方法不適合用來處理 class,除非該 class 遵循 Identifiable 協定。
|
||||
var deduplicated: Self {
|
||||
|
@ -49,22 +33,6 @@ public extension RangeReplaceableCollection where Element: Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - String Tildes Expansion Extension
|
||||
|
||||
public extension String {
|
||||
var expandingTildeInPath: String {
|
||||
(self as NSString).expandingTildeInPath
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Localized Error Extension
|
||||
|
||||
extension String: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ensuring trailing slash of a string
|
||||
|
||||
public extension String {
|
||||
|
@ -77,21 +45,6 @@ public extension String {
|
|||
|
||||
// MARK: - CharCode printability check
|
||||
|
||||
// Ref: https://forums.swift.org/t/57085/5
|
||||
public extension UniChar {
|
||||
var isPrintable: Bool {
|
||||
guard Unicode.Scalar(UInt32(self)) != nil else {
|
||||
struct NotAWholeScalar: Error {}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var isPrintableASCII: Bool {
|
||||
(32 ... 126).contains(self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Unicode.Scalar {
|
||||
var isPrintableASCII: Bool {
|
||||
(32 ... 126).contains(value)
|
||||
|
@ -133,74 +86,27 @@ public extension Bool {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - User Defaults Storage
|
||||
|
||||
public extension UserDefaults {
|
||||
// 內部標記,看輸入法是否處於測試模式。
|
||||
static var pendingUnitTests = false
|
||||
|
||||
static var unitTests = UserDefaults(suiteName: "UnitTests")
|
||||
|
||||
static var current: UserDefaults {
|
||||
pendingUnitTests ? .unitTests ?? .standard : .standard
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property Wrapper
|
||||
|
||||
// Ref: https://www.avanderlee.com/swift/property-wrappers/
|
||||
|
||||
@propertyWrapper
|
||||
public struct AppProperty<Value> {
|
||||
public let key: String
|
||||
public let defaultValue: Value
|
||||
public var container: UserDefaults { .current }
|
||||
public init(key: String, defaultValue: Value) {
|
||||
self.key = key
|
||||
self.defaultValue = defaultValue
|
||||
if container.object(forKey: key) == nil {
|
||||
container.set(defaultValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get {
|
||||
container.object(forKey: key) as? Value ?? defaultValue
|
||||
}
|
||||
set {
|
||||
container.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 引入小數點位數控制函式
|
||||
|
||||
// Ref: https://stackoverflow.com/a/32581409/4162914
|
||||
public extension Double {
|
||||
func rounded(toPlaces places: Int) -> Double {
|
||||
let divisor = pow(10.0, Double(places))
|
||||
let divisor = 10.0.mathPowered(by: places)
|
||||
return (self * divisor).rounded() / divisor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String RegReplace Extension
|
||||
|
||||
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
|
||||
public extension String {
|
||||
mutating func regReplace(pattern: String, replaceWith: String = "") {
|
||||
do {
|
||||
let regex = try NSRegularExpression(
|
||||
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
|
||||
)
|
||||
let range = NSRange(startIndex..., in: self)
|
||||
self = regex.stringByReplacingMatches(
|
||||
in: self, options: [], range: range, withTemplate: replaceWith
|
||||
)
|
||||
} catch { return }
|
||||
public extension Double {
|
||||
func mathPowered(by operand: Int) -> Double {
|
||||
var target = self
|
||||
for _ in 0 ..< operand {
|
||||
target = target * target
|
||||
}
|
||||
return target
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String CharName Extension
|
||||
// MARK: - String CharName and CodePoint Extension
|
||||
|
||||
public extension String {
|
||||
var charDescriptions: [String] {
|
||||
|
@ -209,6 +115,25 @@ public extension String {
|
|||
return String(format: "U+%02X %@", $0.value, theName)
|
||||
}
|
||||
}
|
||||
|
||||
var codePoints: [String] {
|
||||
map(\.codePoint)
|
||||
}
|
||||
|
||||
var describedAsCodePoints: [String] {
|
||||
map {
|
||||
"\($0) (\($0.codePoint))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Character Codepoint
|
||||
|
||||
public extension Character {
|
||||
var codePoint: String {
|
||||
guard let value = unicodeScalars.first?.value else { return "U+NULL" }
|
||||
return String(format: "U+%02X", value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Ellipsis Extension
|
||||
|
@ -217,26 +142,6 @@ public extension String {
|
|||
var withEllipsis: String { self + "…" }
|
||||
}
|
||||
|
||||
// MARK: - Localized String Extension for Integers and Floats
|
||||
|
||||
public extension BinaryFloatingPoint {
|
||||
func i18n(loc: String) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: loc)
|
||||
formatter.numberStyle = .spellOut
|
||||
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
public extension BinaryInteger {
|
||||
func i18n(loc: String) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: loc)
|
||||
formatter.numberStyle = .spellOut
|
||||
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Index Revolver (only for Array)
|
||||
|
||||
// Further discussion: https://forums.swift.org/t/62847
|
||||
|
@ -264,31 +169,6 @@ public extension Int {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Parse String As Hex Literal
|
||||
|
||||
// Original author: Shiki Suen
|
||||
// Refactored by: Isaac Xen
|
||||
|
||||
public extension String {
|
||||
func parsedAsHexLiteral(encoding: CFStringEncodings? = nil) -> String? {
|
||||
guard !isEmpty else { return nil }
|
||||
var charBytes = [Int8]()
|
||||
var buffer: Int?
|
||||
compactMap(\.hexDigitValue).forEach { neta in
|
||||
if let validBuffer = buffer {
|
||||
charBytes.append(.init(bitPattern: UInt8(validBuffer << 4 + neta)))
|
||||
buffer = nil
|
||||
} else {
|
||||
buffer = neta
|
||||
}
|
||||
}
|
||||
let encodingUBE = CFStringBuiltInEncodings.UTF16BE.rawValue
|
||||
let encodingRAW = encoding.map { UInt32($0.rawValue) } ?? encodingUBE
|
||||
let result = CFStringCreateWithCString(nil, &charBytes, encodingRAW) as String?
|
||||
return result?.isEmpty ?? true ? nil : result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Overlap Checker (for two sets)
|
||||
|
||||
public extension Set where Element: Hashable {
|
||||
|
@ -321,32 +201,6 @@ public extension Array where Element: Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Version Comparer.
|
||||
|
||||
public extension String {
|
||||
/// ref: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
|
||||
func versionCompare(_ otherVersion: String) -> ComparisonResult {
|
||||
let versionDelimiter = "."
|
||||
|
||||
var versionComponents = components(separatedBy: versionDelimiter) // <1>
|
||||
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
|
||||
|
||||
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
|
||||
|
||||
// <3> Compare normally if the formats are the same.
|
||||
guard zeroDiff != 0 else { return compare(otherVersion, options: .numeric) }
|
||||
|
||||
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
|
||||
if zeroDiff > 0 {
|
||||
otherVersionComponents.append(contentsOf: zeros) // <5>
|
||||
} else {
|
||||
versionComponents.append(contentsOf: zeros)
|
||||
}
|
||||
return versionComponents.joined(separator: versionDelimiter)
|
||||
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array Builder.
|
||||
|
||||
@resultBuilder
|
||||
|
@ -379,3 +233,54 @@ public enum ArrayBuilder<OutputModel> {
|
|||
Array(components.joined())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extending Comparable to let it able to find its neighbor values in any collection.
|
||||
|
||||
public extension Comparable {
|
||||
func findNeighborValue(from givenSeq: any Collection<Self>, greater isGreater: Bool) -> Self? {
|
||||
let givenArray: [Self] = isGreater ? Array(givenSeq.sorted()) : Array(givenSeq.sorted().reversed())
|
||||
let givenMap: [Int: Self] = .init(uniqueKeysWithValues: Array(givenArray.enumerated()))
|
||||
var (startID, endID, returnableID) = (0, givenArray.count - 1, -1)
|
||||
func internalCompare(_ lhs: Self, _ rhs: Self) -> Bool { isGreater ? lhs <= rhs : lhs >= rhs }
|
||||
while let startObj = givenMap[startID], let endObj = givenMap[endID], internalCompare(startObj, endObj) {
|
||||
let midID = (startID + endID) / 2
|
||||
if let midObj = givenMap[midID], internalCompare(midObj, self) {
|
||||
startID = midID + 1
|
||||
} else {
|
||||
returnableID = midID
|
||||
endID = midID - 1
|
||||
}
|
||||
}
|
||||
return givenMap[returnableID]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String.applyingTransform
|
||||
|
||||
public extension String {
|
||||
/// This only works with ASCII chars for now.
|
||||
func applyingTransformFW2HW(reverse: Bool) -> String {
|
||||
var arr: [Character] = map { $0 }
|
||||
for i in 0 ..< arr.count {
|
||||
let oldChar = arr[i]
|
||||
guard oldChar.unicodeScalars.count == 1 else { continue }
|
||||
guard let oldCodePoint = oldChar.unicodeScalars.first?.value else { continue }
|
||||
if reverse {
|
||||
guard oldChar.isASCII else { continue }
|
||||
} else {
|
||||
guard oldCodePoint > 0xFEE0 || oldCodePoint == 0x3000 else { continue }
|
||||
}
|
||||
var newCodePoint: Int32 = reverse ? (Int32(oldCodePoint) + 0xFEE0) : (Int32(oldCodePoint) - 0xFEE0)
|
||||
checkSpace: switch (oldCodePoint, reverse) {
|
||||
case (0x3000, false): newCodePoint = 0x20
|
||||
case (0x20, true): newCodePoint = 0x3000
|
||||
default: break checkSpace
|
||||
}
|
||||
guard newCodePoint > 0 else { continue }
|
||||
guard let newScalar = Unicode.Scalar(UInt16(newCodePoint)) else { continue }
|
||||
let newChar = Character(newScalar)
|
||||
arr[i] = newChar
|
||||
}
|
||||
return String(arr)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||
// ====================
|
||||
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
|
||||
// ... with NTL restriction stating that:
|
||||
// No trademark license is granted to use the trade names, trademarks, service
|
||||
// marks, or product names of Contributor, except as required to fulfill notice
|
||||
// requirements defined in MIT License.
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - String.localized extension
|
||||
|
||||
public extension StringLiteralType {
|
||||
var localized: String { NSLocalizedString(description, comment: "") }
|
||||
}
|
||||
|
||||
// MARK: - Root Extensions (classDeduplicated)
|
||||
|
||||
// Extend the RangeReplaceableCollection to allow it clean duplicated characters.
|
||||
// Ref: https://stackoverflow.com/questions/25738817/
|
||||
public extension RangeReplaceableCollection where Element: Hashable {
|
||||
/// 使用 NSOrderedSet 處理 class 陣列的「去重複化」。
|
||||
var classDeduplicated: Self {
|
||||
NSOrderedSet(array: Array(self)).compactMap { $0 as? Element.Type } as? Self ?? self
|
||||
// 下述方法有 Bug 會在處理 KeyValuePaired 的時候崩掉,暫時停用。
|
||||
// var set = Set<Element>()
|
||||
// return filter { set.insert($0).inserted }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Tildes Expansion Extension
|
||||
|
||||
public extension String {
|
||||
var expandingTildeInPath: String {
|
||||
(self as NSString).expandingTildeInPath
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String Localized Error Extension
|
||||
|
||||
extension String: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CharCode printability check for UniChar (CoreFoundation)
|
||||
|
||||
// Ref: https://forums.swift.org/t/57085/5
|
||||
public extension UniChar {
|
||||
var isPrintable: Bool {
|
||||
guard Unicode.Scalar(UInt32(self)) != nil else {
|
||||
struct NotAWholeScalar: Error {}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var isPrintableASCII: Bool {
|
||||
(32 ... 126).contains(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Defaults Storage
|
||||
|
||||
public extension UserDefaults {
|
||||
// 內部標記,看輸入法是否處於測試模式。
|
||||
static var pendingUnitTests = false
|
||||
|
||||
static var unitTests = UserDefaults(suiteName: "UnitTests")
|
||||
|
||||
static var current: UserDefaults {
|
||||
pendingUnitTests ? .unitTests ?? .standard : .standard
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property Wrapper
|
||||
|
||||
// Ref: https://www.avanderlee.com/swift/property-wrappers/
|
||||
|
||||
@propertyWrapper
|
||||
public struct AppProperty<Value> {
|
||||
public let key: String
|
||||
public let defaultValue: Value
|
||||
public var container: UserDefaults { .current }
|
||||
public init(key: String, defaultValue: Value) {
|
||||
self.key = key
|
||||
self.defaultValue = defaultValue
|
||||
if container.object(forKey: key) == nil {
|
||||
container.set(defaultValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get {
|
||||
container.object(forKey: key) as? Value ?? defaultValue
|
||||
}
|
||||
set {
|
||||
container.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String RegReplace Extension
|
||||
|
||||
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
|
||||
public extension String {
|
||||
mutating func regReplace(pattern: String, replaceWith: String = "") {
|
||||
do {
|
||||
let regex = try NSRegularExpression(
|
||||
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
|
||||
)
|
||||
let range = NSRange(startIndex..., in: self)
|
||||
self = regex.stringByReplacingMatches(
|
||||
in: self, options: [], range: range, withTemplate: replaceWith
|
||||
)
|
||||
} catch { return }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Localized String Extension for Integers and Floats
|
||||
|
||||
public extension BinaryFloatingPoint {
|
||||
func i18n(loc: String) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: loc)
|
||||
formatter.numberStyle = .spellOut
|
||||
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
public extension BinaryInteger {
|
||||
func i18n(loc: String) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale(identifier: loc)
|
||||
formatter.numberStyle = .spellOut
|
||||
return formatter.string(from: NSDecimalNumber(string: "\(self)")) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Parse String As Hex Literal
|
||||
|
||||
// Original author: Shiki Suen
|
||||
// Refactored by: Isaac Xen
|
||||
|
||||
public extension String {
|
||||
func parsedAsHexLiteral(encoding: CFStringEncodings? = nil) -> String? {
|
||||
guard !isEmpty else { return nil }
|
||||
var charBytes = [Int8]()
|
||||
var buffer: Int?
|
||||
compactMap(\.hexDigitValue).forEach { neta in
|
||||
if let validBuffer = buffer {
|
||||
charBytes.append(.init(bitPattern: UInt8(validBuffer << 4 + neta)))
|
||||
buffer = nil
|
||||
} else {
|
||||
buffer = neta
|
||||
}
|
||||
}
|
||||
let encodingUBE = CFStringBuiltInEncodings.UTF16BE.rawValue
|
||||
let encodingRAW = encoding.map { UInt32($0.rawValue) } ?? encodingUBE
|
||||
let result = CFStringCreateWithCString(nil, &charBytes, encodingRAW) as String?
|
||||
return result?.isEmpty ?? true ? nil : result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Version Comparer.
|
||||
|
||||
public extension String {
|
||||
/// ref: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
|
||||
func versionCompare(_ otherVersion: String) -> ComparisonResult {
|
||||
let versionDelimiter = "."
|
||||
|
||||
var versionComponents = components(separatedBy: versionDelimiter) // <1>
|
||||
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
|
||||
|
||||
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
|
||||
|
||||
// <3> Compare normally if the formats are the same.
|
||||
guard zeroDiff != 0 else { return compare(otherVersion, options: .numeric) }
|
||||
|
||||
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
|
||||
if zeroDiff > 0 {
|
||||
otherVersionComponents.append(contentsOf: zeros) // <5>
|
||||
} else {
|
||||
versionComponents.append(contentsOf: zeros)
|
||||
}
|
||||
return versionComponents.joined(separator: versionDelimiter)
|
||||
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import PackageDescription
|
|||
let package = Package(
|
||||
name: "Tekkon",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
.macOS(.v10_11),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue