Starting from vChewing 2.6.2 SP3.

This commit is contained in:
ShikiSuen 2022-12-17 14:30:17 +08:00
parent ce6e8453e7
commit 498ddcc153
254 changed files with 40379 additions and 0 deletions

1
.clang-format Normal file
View File

@ -0,0 +1 @@
BasedOnStyle: Microsoft

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

@ -0,0 +1,56 @@
{
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"indentation" : {
"spaces" : 2
},
"indentConditionalCompilationBlocks" : true,
"indentSwitchCaseLabels" : true,
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : false,
"lineBreakBeforeEachGenericRequirement" : false,
"lineLength" : 120,
"maximumBlankLines" : 1,
"prioritizeKeepingFunctionOutputTogether" : false,
"respectsExistingLineBreaks" : true,
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLowerCamelCase" : true,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : false,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoBlockComments" : false,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : false,
"OrderedImports" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"UseEarlyExits" : false,
"UseLetInEveryBoundCaseVariable" : true,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : true,
"UseSynthesizedInitializer" : true,
"UseTripleSlashForDocumentationComments" : false,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
},
"tabWidth" : 8,
"version" : 1
}

39
.gitconfig_backup Normal file
View File

@ -0,0 +1,39 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = false
precomposeunicode = true
[submodule]
active = .
[remote "origin"]
url = https://gitee.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[submodule "Source/Data"]
url = https://gitee.com/vChewing/libvchewing-data
[branch "upd/dev"]
remote = origin
merge = refs/heads/upd/dev
[branch "bleed/1.5.x"]
remote = origin
merge = refs/heads/bleed/1.5.x
[remote "gitcode"]
url = https://gitcode.net/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/gitcode/*
[remote "gitlab"]
url = https://jihulab.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/gitlab/*
[remote "github"]
url = https://github.com/vChewing/vChewing-macOS/
fetch = +refs/heads/*:refs/remotes/github/*
[remote "all"]
url = https://gitee.com/vChewing/vChewing-macOS.git/
fetch = +refs/heads/*:refs/remotes/all/*
pushurl = https://gitee.com/vchewing/vChewing-macOS.git/
pushurl = https://gitcode.net/vChewing/vChewing-macOS.git/
pushurl = https://jihulab.com/vChewing/vChewing-macOS.git/
pushurl = https://github.com/vChewing/vChewing-macOS/

View File

@ -0,0 +1,44 @@
### **請在開新的工單之前閱讀《[故障提報與用儀器捉蟲](https://vchewing.github.io/BUGREPORT.html)》按照裡面的方法先電郵聯絡、或至少在開工單之後電郵知會之。因各種可能原因,威注音的主程式師可能無法隨時收取 GitHub 與 Gitee 的工單提醒訊息。**
### 請勿利用工單遞交**詞語新增請求**。所有「詞語新增請求」請一律使用電郵提報。
**摘要**
請簡單說明您遇到了什麼問題。這份表格雖然是中文的(我們相信注音輸入法的用戶應該都能看懂中文),但開發團隊可以使用中英文溝通,您也可以使用英文提報問題。
**快速分類**
請問您遇到的是:
- [ ] 輸入法崩潰或正在接受文字輸入的應用程式崩潰
- [ ] 輸入法叫不出來,或無法正確敲出中文
- [ ] 無法切換至特定功能的啟用/停用狀態
- [ ] 輸入法效能有問題(執行速度太慢/電腦發燙)
- [ ] 輸入法應用程式介面顯示不正常IMK 選字窗的應用介面美術瑕疵恕不受理,因為是 Apple 負責的)
- [ ] 輸入法亂放屁(請確保偏好設定內的廉恥模式的勾沒有被去掉)
- [ ] 其他
**問題發生步驟**
請問您是怎麼遇到這個問題的?像是:
1. 先執行某個接收文字的客體應用
2. 開始在某個區域打字
3. 切換到某種設定/模式…
**預期正常狀況**
您覺得這是不正常的狀況,那您覺得正常結果應該是…?
**螢幕截圖或螢幕錄製**
如果您能夠提供像螢幕截圖或螢幕錄製供大家參考,我們可以從畫面中,看出更多只從文字內容無法了解的線索。
**電腦環境**
請問您在怎樣的環境遇到這個問題?
- 威注音版本:(請填寫版本)
- 注音排列與基礎鍵盤佈局:(在偏好設定視窗的「鍵盤設定」內可以查詢到)
- macOS 版本請填寫版本與建置編號「13.0 (22A5321d)」)
- 在哪個應用程式中打字遇到問題:(應用程式名稱/版本)
- 電腦機種:(筆電或桌機,使用 Intel 或 Apple Silicon CPU
- 其他特殊設備:(請填寫是否有外接螢幕、特製鍵盤如非標準美式鍵盤等)
- 特殊設定:(是否原本正常,改了某個系統設定後就遇到問題)
**其他**
其他你覺得問題發生的疑點,或其他你想跟威注音開發團隊說的話。

52
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,52 @@
---
name: 功能問題提報
about: 請告訴我們您遇到什麼問題
title: "[功能問題提報]"
labels: "BUG / DEBUFF / REGRESSION"
assignees: ""
---
### **請在開新的工單之前閱讀《[故障提報與用儀器捉蟲](https://vchewing.github.io/BUGREPORT.html)》按照裡面的方法先電郵聯絡、或至少在開工單之後電郵知會之。因各種可能原因,威注音的主程式師可能無法隨時收取 GitHub 與 Gitee 的工單提醒訊息。**
**摘要**
請簡單說明您遇到了什麼問題。這份表格雖然是中文的(我們相信注音輸入法的用戶應該都能看懂中文),但開發團隊可以使用中英文溝通,您也可以使用英文提報問題。
**快速分類**
請問您遇到的是:
- [ ] 輸入法崩潰或正在接受文字輸入的應用程式崩潰
- [ ] 輸入法叫不出來,或無法正確敲出中文
- [ ] 無法切換至特定功能的啟用/停用狀態
- [ ] 輸入法效能有問題(執行速度太慢/電腦發燙)
- [ ] 輸入法應用程式介面顯示不正常IMK 選字窗的應用介面美術瑕疵恕不受理,因為是 Apple 負責的)
- [ ] 輸入法亂放屁(請確保偏好設定內的廉恥模式的勾沒有被去掉)
- [ ] 其他
**問題發生步驟**
請問您是怎麼遇到這個問題的?像是:
1. 先執行某個接收文字的客體應用
2. 開始在某個區域打字
3. 切換到某種設定/模式…
**預期正常狀況**
您覺得這是不正常的狀況,那您覺得正常結果應該是…?
**螢幕截圖或螢幕錄製**
如果您能夠提供像螢幕截圖或螢幕錄製供大家參考,我們可以從畫面中,看出更多只從文字內容無法了解的線索。
所以呢,這個問題也請答覆。但如果是威注音特有功能出現故障的話,那這個問題可以不用答覆。
**電腦環境**
請問您在怎樣的環境遇到這個問題?
- 威注音版本:(請填寫版本)
- 注音排列與基礎鍵盤佈局:(在偏好設定視窗的「鍵盤設定」內可以查詢到)
- macOS 版本請填寫版本與建置編號「13.0 (22A5321d)」)
- 在哪個應用程式中打字遇到問題:(應用程式名稱/版本)
- 電腦機種:(筆電或桌機,使用 Intel 或 Apple Silicon CPU
- 其他特殊設備:(請填寫是否有外接螢幕、特製鍵盤如非標準美式鍵盤等)
- 特殊設定:(是否原本正常,改了某個系統設定後就遇到問題)
**其他**
其他你覺得問題發生的疑點,或其他你想跟威注音開發團隊說的話。

View File

@ -0,0 +1,30 @@
---
name: 功能建議
about: 告訴我們您還有什麼沒被滿足的需求
title: "[功能建議]"
labels: "Q&A / DISCUSSION"
assignees: ""
---
**免責聲明**
威注音輸入法保留拒絕任何種類的功能建議的權利(本來就有)。
閱讀來自各位使用者的功能建議請求,是一回事;**但**威注音研發方怎樣利用這些建議請求背後的潛在資訊、來制定威注音接下來的研發方向(或可能棄用相關想法),則是**另一回事**。
**痛點**
請告訴我們為什麼目前威注音沒辦法解決您的問題。您可以講一個能讓讀者得以感同身受的小故事,比如:
- 我從 xx 年前開始,使用了 xx 輸入法,我相當依賴其中一項功能,但是威注音沒有...
- 一直以來我在輸入 xx 這類內容的時候,都很花時間,我想要有更有效率的辦法…
**功能說明**
請大致說明您想要的功能,最後會看起來會像是怎樣。當然最簡單的說明可能會像是「跟 xxx 輸入法一樣」,但請您諒解,不是什麼人都用過你之前用過的那套輸入法(更何況像 mac 版漢音輸入法那樣的必須買來不一定能買得來的老機種+灌上不一定能下載到的老系統才能重新體驗,成本很大),其他人想了解「跟 xxx 輸入法一樣」,首先還得找到那套輸入法,而且有些年代久遠的輸入法還找不到,這樣雙方的認知不同,會大幅降低溝通效率。
比方說,您想要某種符號表設計,或許可以提供我們一個草圖,或是其他你喜歡的輸入法的符號表的截圖。遇到某些更複雜的情境,您也可以考慮錄製螢幕畫面供大家參考。
**替代方案**
請問,您目前是用什麼方法,可以做到就算沒有這項功能,也可以達最後目的。或著,如果這項功能開發與其他功能衝突,您起碼可以接受什麼方案?
**其他**
其他你想跟威注音開發團隊說的話。

24
.github/ISSUE_TEMPLATE/phrase_report.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: 原廠辭典問題提報
about: 請告訴我們與原廠辭典有關的問題
title: "[原廠辭典問題提報]"
labels: "BUG / DEBUFF / REGRESSION"
assignees: ""
---
### **請在開新的工單之前閱讀《[故障提報與用儀器捉蟲](https://vchewing.github.io/BUGREPORT.html)》按照裡面的方法先電郵聯絡、或至少在開工單之後電郵知會之。因各種可能原因,威注音的主程式師可能無法隨時收取 GitHub 與 Gitee 的工單提醒訊息。**
### 請勿利用工單遞交**詞語新增請求**。所有「詞語新增請求」請一律使用電郵提報。
**分類**
- [ ] 錯字或錯誤讀音
- [ ] 缺字/詞
- [ ] 字詞權重(優先級)問題
- [ ] 其他
**說明**
請說明是哪個字寫錯或是標音錯誤,以及正確的寫法與念法。
**相關資料**
像是字典的連結等。

View File

@ -0,0 +1,20 @@
name: Build-with-macOS-12
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
name: Build (macOS 12)
runs-on: macOS-12
env:
GIT_SSL_NO_VERIFY: true
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
steps:
- uses: actions/checkout@v1
- name: Clean
run: make clean
- name: Build
run: git pull --all && git submodule sync; make update; make

230
.gitignore vendored Normal file
View File

@ -0,0 +1,230 @@
!**/[Pp]ackages/build/
!*.[Cc]ache/
!.axoCover/settings.json
$RECYCLE.BIN/
$tf/
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.HTMLClient/GeneratedArtifacts
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
*.app
*.appx
*.aps
*.azurePubxml
*.bim.layout
*.bim_*.settings
*.binlog
*.btm.cs
*.btp.cs
*.build.csdef
*.cab
*.cachefile
*.coverage
*.coveragexml
*.dbmdl
*.dbproj.schemaview
*.dmg
*.dotCover
*.DotSettings.user
*.e2e
*.GhostDoc.xml
*.gpState
*.ilk
*.iobj
*.ipdb
*.jfm
*.jmconfig
*.ldf
*.lnk
*.log
*.mdf
*.meta
*.mm.*
*.mode1v3
*.msi
*.msix
*.msm
*.msp
*.ncb
*.ndf
*.nuget.props
*.nuget.targets
*.nupkg
*.nvuser
*.obj
*.odx.cs
*.opendb
*.opensdf
*.opt
*.pbxuser
*.pch
*.pdb
*.pfx
*.pgc
*.pgd
*.pidb
*.plg
*.psess
*.publishproj
*.publishsettings
*.pubxml
*.pyc
*.rdl.data
*.rptproj.bak
*.rptproj.rsuser
*.rsp
*.sap
*.sbr
*.scc
*.sdf
*.sln.docstates
*.sln.iml
*.stackdump
*.suo
*.svclog
*.tlb
*.tlh
*.tli
*.tmp
*.tmp_proj
*.tm_build_errors
*.tss
*.user
*.userosscache
*.userprefs
*.usertasks
*.vbw
*.VC.db
*.VC.VC.opendb
*.VisualState.xml
*.vsp
*.vspscc
*.vspx
*.vssscc
*.xsd.cs
*.[Cc]ache
*.[Pp]ublish.xml
*.[Rr]e[Ss]harper
*_h.h
*_i.c
*_p.c
*_wpftmp.csproj
*~
.*crunch*.local.xml
.apdisk
.AppleDB
.AppleDesktop
.AppleDouble
.axoCover/*
.build
.builds
.com.apple.timemachine.donotpresent
.cr/personal
.DocumentRevisions-V100
.DS_Store
.fake/
.fseventsd
.idea
.idea/
.JustCode
.localhistory/
.LSOverride
.mfractor/
.ntvs_analysis.dat
.paket/paket.exe
.sass-cache/
.Spotlight-V100
.swiftpm
.TemporaryItems
.Trashes
.VolumeIcon.icns
.vs/
.vscode
._*
aclocal.m4
AppPackages/
artifacts/
ASALocalRun/
autom4te.cache/
AutoTest.Net/
Backup*/
BenchmarkDotNet.Artifacts/
bld/
build
BundleArtifacts/
ClientBin/
config.make
config.status
Credits.rtf
csx/
dlldata.c
DocProject/buildhelp/
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/*.HxC
DocProject/Help/*.HxT
DocProject/Help/html
DocProject/Help/Html2
ecf/
ehthumbs.db
ehthumbs_vista.db
FakesAssemblies/
Generated\ Files/
Generated_Code/
Icon
Installer/PKGRoot/
install-sh
ipch/
Makefile.in
nCrunchTemp_*
Network Trash Folder
node_modules/
OpenCover/
orleans.codegen.cs
Package.StoreAssociation.xml
paket-files/
project.fragment.lock.json
project.lock.json
project.xcworkspace
publish/
PublishScripts/
rcf/
ServiceFabricBackup/
Source/Data/*
StyleCopReport.xml
tarballs/
Temporary Items
test-results/
TestResult.xml
Thumbs.db
UpgradeLog*.htm
UpgradeLog*.XML
x64/
x86/
xcuserdata
[Bb]in/
[Bb]uild
[Bb]uild[Ll]og.*
[Dd]ebug/
[Dd]ebugPS/
[Dd]ebugPublic/
[Dd]esktop.ini
[Ee]xpress/
[Ll]og/
[Oo]bj/
[Rr]elease/
[Rr]eleasePS/
[Rr]eleases/
[Tt]est[Rr]esult*/
_Chutzpah*
_NCrunch_*
_pkginfo.txt
_Pvt_Extensions
_ReSharper*/
_TeamCity*
_UpgradeReport_Files/
__pycache__/
~$*
DataCompiler/dataCompiler.exe

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "Source/Data"]
path = Source/Data
url = https://gitee.com/vchewing/libvchewing-data

38
AUTHORS Normal file
View File

@ -0,0 +1,38 @@
$ Main contributors and volunteers of this repository (vChewing for macOS):
- Shiki Suen // Main developer of vChewing for macOS, Megrez language engine, Hotenka Chinese Converter, and Tekkon syllable composer engine.
- Hiraku Wang // Technical reinforcement in Cocoa during the Object-Cpp dev period of this project.
- Isaac Xen // Technical reinforcement in Swift: SFX Module (NSSound ver.) and StringView Ranges Extension.
$ 3rd-Party Modules Used:
- ShiftKeyUpChecker: (c) 2019 and onwards Qwertyyb (March Yang) (MIT License).
- LineReader: (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
- SwiftUI Preferences UI Framework: (c) 2018 and onwards Sindre Sorhus (MIT License).
- SwiftUI VDKComboBox: (c) 2022 and onwards Bryan Jones (CC BY-SA 4.0)
- Note that Hotenka Chinese Converter is rewritten by Shiki Suen (using Swift) from Nick Chen's Objective-C library "NCChineseConverter" (MIT License).
$ Contributors and volunteers of the upstream repo, having no responsibility in discussing anything in the current repo:
- Zonble Yang:
- McBopomofo for macOS 2.x architect.
- Voltaire candidate window MK2 (massively modified as MK3 in vChewing by Shiki Suen).
- Notifier window.
- App-style installer (only preserved for developer purposes).
- mgrPrefs (userdefaults manager).
- Mengjuei Hsieh:
- McBopomofo for macOS 1.x main developer and architect.
- The original C++ version of the User Override Module.
- Shiki Suen is trying to rewrite this module in Swift (and CSharp) with further development.
Although there is no Lukhnos's codes left in the current repository, we still credit him for his previous work:
- Lukhnos Liu:
- Developer of Gramambular 2 language engine (removed since vChewing 1.5.4).
- Shiki Suen's Megrez engine (MIT License) is basically a Swift-rewritten version of Gramambular 2 with further development.
- Developer of Mandarin syllable composer (removed since vChewing 1.5.7).
- Shiki Suen's Tekkon engine is made from scratch and has no relationship to Mandarin syllable composer.
$ Special thanks to:
- All supporters from Cocoaheads Taipei and Mobile01 community, etc.

72
BuildVersionSpecifier.swift Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env swift
// Copyright (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 Cocoa
extension String {
fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do {
let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
)
let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith
)
} catch { return }
}
}
var verMarket: String = "1.0.0"
var verBuild: String = "1000"
var strXcodeProjContent: String = ""
var dirXcodeProjectFile = "./vChewing.xcodeproj/project.pbxproj"
var dirPackageProjectFile = "./vChewing.pkgproj"
var dirUpdateInfoPlist = "./Update-Info.plist"
var theDictionary: NSDictionary?
if CommandLine.arguments.count == 3 {
verMarket = CommandLine.arguments[1]
verBuild = CommandLine.arguments[2]
// Xcode project file version update.
do {
strXcodeProjContent += try String(contentsOfFile: dirXcodeProjectFile, encoding: .utf8)
} catch {
NSLog(" - Exception happened when reading raw phrases data.")
}
strXcodeProjContent.regReplace(
pattern: #"CURRENT_PROJECT_VERSION = .*$"#, replaceWith: "CURRENT_PROJECT_VERSION = " + verBuild + ";"
)
strXcodeProjContent.regReplace(
pattern: #"MARKETING_VERSION = .*$"#, replaceWith: "MARKETING_VERSION = " + verMarket + ";"
)
do {
try strXcodeProjContent.write(to: URL(fileURLWithPath: dirXcodeProjectFile), atomically: false, encoding: .utf8)
} catch {
NSLog(" -: Error on writing strings to file: \(error)")
}
NSLog(" - Xcode 專案版本資訊更新完成:\(verMarket) \(verBuild)")
// Packages project file version update.
theDictionary = NSDictionary(contentsOfFile: dirPackageProjectFile)
theDictionary?.setValue(verMarket, forKeyPath: "PACKAGES.PACKAGE_SETTINGS.VERSION")
theDictionary?.write(toFile: dirPackageProjectFile, atomically: true)
NSLog(" - Packages 專案版本資訊更新完成:\(verMarket) \(verBuild)")
// Update notification project file version update.
theDictionary = NSDictionary(contentsOfFile: dirUpdateInfoPlist)
theDictionary?.setValue(verBuild, forKeyPath: "CFBundleVersion")
theDictionary?.setValue(verMarket, forKeyPath: "CFBundleShortVersionString")
theDictionary?.write(toFile: dirUpdateInfoPlist, atomically: true)
NSLog(" - 更新用通知 plist 版本資訊更新完成:\(verMarket) \(verBuild)")
}

5
CHANGELOG Normal file
View File

@ -0,0 +1,5 @@
https://gitee.com/vchewing/vChewing-macOS/wikis/sort_id=5401886
Please read the CHANGELOG through the wiki link above since it will be a disaster to use Git to manage it together with other files in this repo.
因內容管理上的便利性需求,請改洽上述網址檢視歷代發行說明。

50
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,50 @@
# 威注音輸入法研發參與相關說明
威注音輸入法歡迎有熱心的志願者們參與。
威注音目前的 codebase 更能代表一個先進的 macOS 輸入法雛形專案的形態。目前的 dev 分支全都是清一色的 Swift codebase一目了然方便他人參與比某些其它開源品牌旗下的專案更具程式方面的生命力。為什麼這樣講呢那些傳統開源品牌的專案主要使用 C++ 這門不太友好的語言Mandarin 模組現在對我而言仍舊是天書,一大堆針對記憶體指針的操作完全看不懂。搞不清楚在這一層之上的功能邏輯的話,就無法制定 Swift 版的 coding 策略),這也是我這次用 Swift 重寫了語言模型引擎與注音拼音並擊處理引擎、來換掉 Gramambular 與 OVMandarin 的原因(也是為後來者行方便)。
## 問題提報:
因技術原因,請默認本人無法接收來自任何「需要本人使用虛擬專用網路方可使用」的途徑的輸入法問題提報。
下述問題途徑可以用來提報與輸入法有關的問題:
- 大陸用戶請洽 Gitee 倉庫的工單區https://gitee.com/vChewing/vChewing-macOS/issues
- 台澎金馬及海外用戶請洽 GitLab China 倉庫的工單區可發起非公開工單https://jihulab.com/vChewing/vChewing-macOS/-/issues/
- 如果台澎金馬用戶無法註冊 Gitee 的話,請使用 GitLab China 的工單區。
- 還可以使用電郵理論上最快shikisuen◎yeah●net
### 我想在小麥注音的基礎上開發新功能,該怎麼開始?
首先,您得有 Swift 基礎、對設計模式(策略模式與狀態模式)與演算法都有一定的了解。無論是比較上層的使用體驗,還是比較底層的演算法,威注音都僅使用 Swift。
威注音倉庫內可能會在未來不久新增與程式架構有關的百科說明文章,但對鐵恨注拼引擎與天權星語彙引擎的架構說明則會擇日放入對應的倉庫的百科內。然而,書寫這些百科,需要花費時間精力。威注音相信專案內已有的針對函式的中文註解應該已經足夠了。
## 參與說明:
為了不讓參與者們浪費各自的熱情,特設此文以說明該專案目前最需要協助的地方。
(暫無)
除了上述各項以外的貢獻,除非特邀、或者有足夠的說服理由與吸引力(比如語法錯誤或更好的重構方法等),否則敝專案可能會無視或者拒絕。
請注意不要浪費自己的時間精力與感情,一定要在自己動工之前打個招呼。
如果您對威注音的產品功能發展另有所期的話,威注音雖不接受相關的爭論,但您可以自行建立分流專案。只需要遵守 MIT-NTL 協議、不沿用威注音的品牌名稱即可。
## 格式規範:
該專案對源碼格式有規範:
- Swift: 採 [Apple 官方 Swift-Format](https://github.com/apple/swift-format),且施加如下例外修改項目:
- `"indentSwitchCaseLabels" : true,`
- `"lineLength" : 120,`
- `"NoBlockComments" : false,`
- `"OnlyOneTrailingClosureArgument" : false,` // SwiftUI 相容
- `"UseTripleSlashForDocumentationComments" : false,`
- `"DontRepeatTypeInStaticProperties" : false,`
之前,為了節省檔案體積,曾經對 Swift 檔案改採 1-Tab 縮進。然而,這會導致 Gitee 等線上 git 專案管理網站內的顯示變成 8-Space 縮進。於是,該專案對 Swift 檔案又改回了 2-Spaces 縮進。
$ EOF.

25
COPYING Normal file
View File

@ -0,0 +1,25 @@
The vChewing Input Method is derived from OpenVanilla McBopomofo project under:
SPDX-License-Identifier: MIT
The vChewing Input Method is provided under:
SPDX-License-Identifier: MIT
with one extra requirements (turning it into MIT-NTL license):
No trademark license is granted to use the trade names, trademarks,
service marks, or product names of Contributor, except as required
to fulfill notice requirements above.
Being under the terms of the MIT-NTL License only, according with:
./LICENSES.txt
Please see:
./AUTHORS
for more details.
All contributions to the vChewing Input Method are subject to ./AUTHORS file.

25
CREDITS Normal file
View File

@ -0,0 +1,25 @@
The vChewing Input Method is derived from OpenVanilla McBopomofo project under:
SPDX-License-Identifier: MIT
The vChewing Input Method is provided under:
SPDX-License-Identifier: MIT
with one extra requirements (turning it into MIT-NTL license):
No trademark license is granted to use the trade names, trademarks,
service marks, or product names of Contributor, except as required
to fulfill notice requirements above.
Being under the terms of the MIT-NTL License only, according with:
./LICENSES.txt
Please see:
./AUTHORS
for more details.
All contributions to the vChewing Input Method are subject to ./AUTHORS file.

View File

@ -0,0 +1,828 @@
#!/usr/bin/env swift
// (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: -
extension String {
fileprivate mutating func regReplace(pattern: String, replaceWith: String = "") {
// Ref: https://stackoverflow.com/a/40993403/4162914 && https://stackoverflow.com/a/71291137/4162914
do {
let regex = try NSRegularExpression(
pattern: pattern, options: [.caseInsensitive, .anchorsMatchLines]
)
let range = NSRange(startIndex..., in: self)
self = regex.stringByReplacingMatches(
in: self, options: [], range: range, withTemplate: replaceWith
)
} catch { return }
}
}
// MARK: - StringView Ranges Extension (by Isaac Xen)
extension String {
fileprivate func ranges(splitBy separator: Element) -> [Range<String.Index>] {
var startIndex = startIndex
return split(separator: separator).reduce(into: []) { ranges, substring in
_ = range(of: substring, range: startIndex..<endIndex).map { range in
ranges.append(range)
startIndex = range.upperBound
}
}
}
}
// MARK: -
// Ref: https://stackoverflow.com/a/32581409/4162914
extension Double {
fileprivate func rounded(toPlaces places: Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
}
// MARK: -
// Ref: https://stackoverflow.com/a/41581695/4162914
precedencegroup ExponentiationPrecedence {
associativity: right
higherThan: MultiplicationPrecedence
}
infix operator **: ExponentiationPrecedence
func ** (_ base: Double, _ exp: Double) -> Double {
pow(base, exp)
}
// MARK: -
struct Unigram: CustomStringConvertible {
var key: String = ""
var value: String = ""
var score: Double = -1.0
var count: Int = 0
var description: String {
"(\(key), \(value), \(score))"
}
}
// MARK: - plist
func cnvPhonabetToASCII(_ incoming: String) -> String {
let dicPhonabet2ASCII = [
"": "b", "": "p", "": "m", "": "f", "": "d", "": "t", "": "n", "": "l", "": "g", "": "k", "": "h",
"": "j", "": "q", "": "x", "": "Z", "": "C", "": "S", "": "r", "": "z", "": "c", "": "s", "": "i",
"": "u", "": "v", "": "a", "": "o", "": "e", "": "E", "": "B", "": "P", "": "M", "": "F", "": "D",
"": "T", "": "N", "": "L", "": "R", "ˊ": "2", "ˇ": "3", "ˋ": "4", "˙": "5",
]
var strOutput = incoming
if !strOutput.contains("_") {
for Unigram in dicPhonabet2ASCII {
strOutput = strOutput.replacingOccurrences(of: Unigram.key, with: Unigram.value)
}
}
return strOutput
}
// MARK: -
private let urlCurrentFolder = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
private let urlCHSforCustom: String = "./components/chs/phrases-custom-chs.txt"
private let urlCHSforTABE: String = "./components/chs/phrases-tabe-chs.txt"
private let urlCHSforMOE: String = "./components/chs/phrases-moe-chs.txt"
private let urlCHSforVCHEW: String = "./components/chs/phrases-vchewing-chs.txt"
private let urlCHTforCustom: String = "./components/cht/phrases-custom-cht.txt"
private let urlCHTforTABE: String = "./components/cht/phrases-tabe-cht.txt"
private let urlCHTforMOE: String = "./components/cht/phrases-moe-cht.txt"
private let urlCHTforVCHEW: String = "./components/cht/phrases-vchewing-cht.txt"
private let urlKanjiCore: String = "./components/common/char-kanji-core.txt"
private let urlMiscBPMF: String = "./components/common/char-misc-bpmf.txt"
private let urlMiscNonKanji: String = "./components/common/char-misc-nonkanji.txt"
private let urlPunctuation: String = "./components/common/data-punctuations.txt"
private let urlSymbols: String = "./components/common/data-symbols.txt"
private let urlZhuyinwen: String = "./components/common/data-zhuyinwen.txt"
private let urlCNS: String = "./components/common/char-kanji-cns.txt"
private let urlPlistSymbols: String = "./data-symbols.plist"
private let urlPlistZhuyinwen: String = "./data-zhuyinwen.plist"
private let urlPlistCNS: String = "./data-cns.plist"
private let urlOutputCHS: String = "./data-chs.txt"
private let urlPlistCHS: String = "./data-chs.plist"
private let urlOutputCHT: String = "./data-cht.txt"
private let urlPlistCHT: String = "./data-cht.plist"
// MARK: -
func rawDictForPhrases(isCHS: Bool) -> [Unigram] {
var arrUnigramRAW: [Unigram] = []
var strRAW = ""
let urlCustom: String = isCHS ? urlCHSforCustom : urlCHTforCustom
let urlTABE: String = isCHS ? urlCHSforTABE : urlCHTforTABE
let urlMOE: String = isCHS ? urlCHSforMOE : urlCHTforMOE
let urlVCHEW: String = isCHS ? urlCHSforVCHEW : urlCHTforVCHEW
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
//
do {
strRAW += try String(contentsOfFile: urlCustom, encoding: .utf8)
strRAW += "\n"
strRAW += try String(contentsOfFile: urlTABE, encoding: .utf8)
strRAW += "\n"
strRAW += try String(contentsOfFile: urlMOE, encoding: .utf8)
strRAW += "\n"
strRAW += try String(contentsOfFile: urlVCHEW, encoding: .utf8)
} catch {
NSLog(" - Exception happened when reading raw phrases data.")
return []
}
//
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
// CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space
// ASCII
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
//
let arrData = Array(
NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
for lineData in arrData {
//
let arrLineData = lineData.components(separatedBy: " ")
var varLineDataProcessed = ""
var count = 0
for currentCell in arrLineData {
count += 1
if count < 3 {
varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-"
} else {
varLineDataProcessed += currentCell
}
}
// Unigram
let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 //
var phone = ""
var phrase = ""
var occurrence = 0
for cell in arrCells {
count += 1
switch count {
case 1: phrase = cell
case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0
default: break
}
}
if phrase != "" { //
arrUnigramRAW += [
Unigram(
key: phone, value: phrase, score: 0.0,
count: occurrence
)
]
}
}
NSLog(" - \(i18n): 成功生成詞語語料辭典(權重待計算)。")
return arrUnigramRAW
}
// MARK: -
func rawDictForKanjis(isCHS: Bool) -> [Unigram] {
var arrUnigramRAW: [Unigram] = []
var strRAW = ""
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
//
do {
strRAW += try String(contentsOfFile: urlKanjiCore, encoding: .utf8)
} catch {
NSLog(" - Exception happened when reading raw core kanji data.")
return []
}
//
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
// CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space
// ASCII
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
//
let arrData = Array(
NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
var varLineData = ""
for lineData in arrData {
// 1,2,4 1,3,4
let varLineDataPre = lineData.components(separatedBy: " ").prefix(isCHS ? 2 : 1)
.joined(
separator: "\t")
let varLineDataPost = lineData.components(separatedBy: " ").suffix(isCHS ? 1 : 2)
.joined(
separator: "\t")
varLineData = varLineDataPre + "\t" + varLineDataPost
let arrLineData = varLineData.components(separatedBy: " ")
var varLineDataProcessed = ""
var count = 0
for currentCell in arrLineData {
count += 1
if count < 3 {
varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-"
} else {
varLineDataProcessed += currentCell
}
}
// Unigram
let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 //
var phone = ""
var phrase = ""
var occurrence = 0
for cell in arrCells {
count += 1
switch count {
case 1: phrase = cell
case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0
default: break
}
}
if phrase != "" { //
arrUnigramRAW += [
Unigram(
key: phone, value: phrase, score: 0.0,
count: occurrence
)
]
}
}
NSLog(" - \(i18n): 成功生成單字語料辭典(權重待計算)。")
return arrUnigramRAW
}
// MARK: -
func rawDictForNonKanjis(isCHS: Bool) -> [Unigram] {
var arrUnigramRAW: [Unigram] = []
var strRAW = ""
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
//
do {
strRAW += try String(contentsOfFile: urlMiscBPMF, encoding: .utf8)
strRAW += "\n"
strRAW += try String(contentsOfFile: urlMiscNonKanji, encoding: .utf8)
} catch {
NSLog(" - Exception happened when reading raw core kanji data.")
return []
}
//
strRAW = strRAW.replacingOccurrences(of: " #MACOS", with: "") // macOS
// CJKWhiteSpace (\x{3000}) to ASCII Space
// NonBreakWhiteSpace (\x{A0}) to ASCII Space
// Tab to ASCII Space
// ASCII
strRAW.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: " ")
strRAW.regReplace(pattern: #"(^ | $)"#, replaceWith: "") //
strRAW.regReplace(pattern: #"(\f+|\r+|\n+)+"#, replaceWith: "\n") // CR & Form Feed to LF,
strRAW.regReplace(pattern: #"^(#.*|.*#WIN32.*)$"#, replaceWith: "") // #+ WIN32
//
let arrData = Array(
NSOrderedSet(array: strRAW.components(separatedBy: "\n")).array as! [String])
var varLineData = ""
for lineData in arrData {
varLineData = lineData
//
varLineData = varLineData.components(separatedBy: " ").prefix(3).joined(
separator: "\t") //
let arrLineData = varLineData.components(separatedBy: " ")
var varLineDataProcessed = ""
var count = 0
for currentCell in arrLineData {
count += 1
if count < 3 {
varLineDataProcessed += currentCell + "\t"
} else if count < arrLineData.count {
varLineDataProcessed += currentCell + "-"
} else {
varLineDataProcessed += currentCell
}
}
// Unigram
let arrCells: [String] = varLineDataProcessed.components(separatedBy: "\t")
count = 0 //
var phone = ""
var phrase = ""
var occurrence = 0
for cell in arrCells {
count += 1
switch count {
case 1: phrase = cell
case 3: phone = cell
case 2: occurrence = Int(cell) ?? 0
default: break
}
}
if phrase != "" { //
arrUnigramRAW += [
Unigram(
key: phone, value: phrase, score: 0.0,
count: occurrence
)
]
}
}
NSLog(" - \(i18n): 成功生成非漢字語料辭典(權重待計算)。")
return arrUnigramRAW
}
func weightAndSort(_ arrStructUncalculated: [Unigram], isCHS: Bool) -> [Unigram] {
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
var arrStructCalculated: [Unigram] = []
let fscale = 2.7
var norm = 0.0
for unigram in arrStructUncalculated {
if unigram.count >= 0 {
norm += fscale ** (Double(unigram.value.count) / 3.0 - 1.0)
* Double(unigram.count)
}
}
// norm norm
//
// 1 0 0.5
for unigram in arrStructUncalculated {
var weight: Double = 0
switch unigram.count {
case -2: //
weight = -13
case -1: //
weight = -13
case 0: //
weight = log10(
fscale ** (Double(unigram.value.count) / 3.0 - 1.0) * 0.25 / norm)
default:
weight = log10(
fscale ** (Double(unigram.value.count) / 3.0 - 1.0)
* Double(unigram.count) / norm) // Credit: MJHsieh.
}
let weightRounded: Double = weight.rounded(toPlaces: 3) //
arrStructCalculated += [
Unigram(
key: unigram.key, value: unigram.value, score: weightRounded,
count: unigram.count
)
]
}
NSLog(" - \(i18n): 成功計算權重。")
// ==========================================
//
let arrStructSorted: [Unigram] = arrStructCalculated.sorted(by: { lhs, rhs -> Bool in
(lhs.key, rhs.count) < (rhs.key, lhs.count)
})
NSLog(" - \(i18n): 排序整理完畢,準備編譯要寫入的檔案內容。")
return arrStructSorted
}
func fileOutput(isCHS: Bool) {
let i18n: String = isCHS ? "簡體中文" : "繁體中文"
var strPunctuation = ""
var rangeMap: [String: [Data]] = [:]
let pathOutput = urlCurrentFolder.appendingPathComponent(
isCHS ? urlOutputCHS : urlOutputCHT)
let plistURL = urlCurrentFolder.appendingPathComponent(
isCHS ? urlPlistCHS : urlPlistCHT)
var strPrintLine = ""
//
do {
strPunctuation = try String(contentsOfFile: urlPunctuation, encoding: .utf8).replacingOccurrences(
of: "\t", with: " "
)
strPrintLine += try String(contentsOfFile: urlPunctuation, encoding: .utf8).replacingOccurrences(
of: "\t", with: " "
)
} catch {
NSLog(" - \(i18n): Exception happened when reading raw punctuation data.")
}
NSLog(" - \(i18n): 成功插入標點符號與西文字母數據txt")
//
strPunctuation.ranges(splitBy: "\n").forEach {
let neta = strPunctuation[$0].split(separator: " ")
let line = String(strPunctuation[$0])
if neta.count >= 2 {
let theKey = String(neta[0])
let theValue = String(neta[1])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
rangeMap[cnvPhonabetToASCII(theKey), default: []].append(theValue.data(using: .utf8)!)
}
}
}
var arrStructUnified: [Unigram] = []
arrStructUnified += rawDictForKanjis(isCHS: isCHS)
arrStructUnified += rawDictForNonKanjis(isCHS: isCHS)
arrStructUnified += rawDictForPhrases(isCHS: isCHS)
//
arrStructUnified = weightAndSort(arrStructUnified, isCHS: isCHS)
//
NSLog(" - \(i18n): 執行資料重複性檢查,會在之後再給出對應的檢查結果。")
var setAlreadyInserted = Set<String>()
var arrFoundedDuplications = [String]()
//
NSLog(" - \(i18n): 執行資料健康狀況檢查。")
print(healthCheck(arrStructUnified))
for unigram in arrStructUnified {
if setAlreadyInserted.contains(unigram.value + "\t" + unigram.key) {
arrFoundedDuplications.append(unigram.value + "\t" + unigram.key)
} else {
setAlreadyInserted.insert(unigram.value + "\t" + unigram.key)
}
let theKey = unigram.key
let theValue = (String(unigram.score) + " " + unigram.value)
rangeMap[cnvPhonabetToASCII(theKey), default: []].append(theValue.data(using: .utf8)!)
strPrintLine +=
unigram.key + " " + unigram.value + " " + String(unigram.score)
+ "\n"
}
NSLog(" - \(i18n): 要寫入檔案的 txt 內容編譯完畢。")
do {
try strPrintLine.write(to: pathOutput, atomically: false, encoding: .utf8)
let plistData = try PropertyListSerialization.data(fromPropertyList: rangeMap, format: .binary, options: 0)
try plistData.write(to: plistURL)
} catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
}
NSLog(" - \(i18n): 寫入完成。")
if !arrFoundedDuplications.isEmpty {
NSLog(" - \(i18n): 尋得下述重複項目,請務必手動排查:")
print("-------------------")
print(arrFoundedDuplications.joined(separator: "\n"))
}
print("===================")
}
func commonFileOutput() {
let i18n = "語言中性"
var strSymbols = ""
var strZhuyinwen = ""
var strCNS = ""
var mapSymbols: [String: [Data]] = [:]
var mapZhuyinwen: [String: [Data]] = [:]
var mapCNS: [String: [Data]] = [:]
//
do {
strSymbols = try String(contentsOfFile: urlSymbols, encoding: .utf8).replacingOccurrences(of: "\t", with: " ")
strZhuyinwen = try String(contentsOfFile: urlZhuyinwen, encoding: .utf8).replacingOccurrences(of: "\t", with: " ")
strCNS = try String(contentsOfFile: urlCNS, encoding: .utf8).replacingOccurrences(of: "\t", with: " ")
} catch {
NSLog(" - \(i18n): Exception happened when reading raw punctuation data.")
}
NSLog(" - \(i18n): 成功取得標點符號與西文字母原始資料plist")
//
strSymbols.ranges(splitBy: "\n").forEach {
let neta = strSymbols[$0].split(separator: " ")
let line = String(strSymbols[$0])
if neta.count >= 2 {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
mapSymbols[cnvPhonabetToASCII(theKey), default: []].append(theValue.data(using: .utf8)!)
}
}
}
strZhuyinwen.ranges(splitBy: "\n").forEach {
let neta = strZhuyinwen[$0].split(separator: " ")
let line = String(strZhuyinwen[$0])
if neta.count >= 2 {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
mapZhuyinwen[cnvPhonabetToASCII(theKey), default: []].append(theValue.data(using: .utf8)!)
}
}
}
strCNS.ranges(splitBy: "\n").forEach {
let neta = strCNS[$0].split(separator: " ")
let line = String(strCNS[$0])
if neta.count >= 2 {
let theKey = String(neta[1])
let theValue = String(neta[0])
if !neta[0].isEmpty, !neta[1].isEmpty, line.first != "#" {
mapCNS[cnvPhonabetToASCII(theKey), default: []].append(theValue.data(using: .utf8)!)
}
}
}
NSLog(" - \(i18n): 要寫入檔案的內容編譯完畢。")
do {
try PropertyListSerialization.data(fromPropertyList: mapSymbols, format: .binary, options: 0).write(
to: URL(fileURLWithPath: urlPlistSymbols))
try PropertyListSerialization.data(fromPropertyList: mapZhuyinwen, format: .binary, options: 0).write(
to: URL(fileURLWithPath: urlPlistZhuyinwen))
try PropertyListSerialization.data(fromPropertyList: mapCNS, format: .binary, options: 0).write(
to: URL(fileURLWithPath: urlPlistCNS))
} catch {
NSLog(" - \(i18n): Error on writing strings to file: \(error)")
}
NSLog(" - \(i18n): 寫入完成。")
}
// MARK: -
func main() {
NSLog("// 準備編譯符號表情ㄅ文語料檔案。")
commonFileOutput()
NSLog("// 準備編譯繁體中文核心語料檔案。")
fileOutput(isCHS: false)
NSLog("// 準備編譯簡體中文核心語料檔案。")
fileOutput(isCHS: true)
}
main()
// MARK: -
func healthCheck(_ data: [Unigram]) -> String {
var result = ""
var unigramMonoChar = [String: Unigram]()
var valueToScore = [String: Double]()
let unigramMonoCharCounter = data.filter { $0.score > -14 && $0.key.split(separator: "-").count == 1 }.count
let unigramPolyCharCounter = data.filter { $0.score > -14 && $0.key.split(separator: "-").count > 1 }.count
// -10
for neta in data.filter({ $0.score > -14 }) {
valueToScore[neta.value] = max(neta.score, valueToScore[neta.value] ?? -14)
let theKeySliceArr = neta.key.split(separator: "-")
guard let theKey = theKeySliceArr.first, theKeySliceArr.count == 1 else { continue }
if unigramMonoChar.keys.contains(String(theKey)), let theRecord = unigramMonoChar[String(theKey)] {
if neta.score > theRecord.score { unigramMonoChar[String(theKey)] = neta }
} else {
unigramMonoChar[String(theKey)] = neta
}
}
var faulty = [Unigram]()
var indifferents: [(String, String, Double, [Unigram], Double)] = []
var insufficients: [(String, String, Double, [Unigram], Double)] = []
var competingUnigrams = [(String, Double, String, Double)]()
for neta in data.filter({ $0.key.split(separator: "-").count >= 2 && $0.score > -14 }) {
var competants = [Unigram]()
var tscore: Double = 0
var bad = false
for x in neta.key.split(separator: "-") {
if !unigramMonoChar.keys.contains(String(x)) {
bad = true
break
}
guard let u = unigramMonoChar[String(x)] else { continue }
tscore += u.score
competants.append(u)
}
if bad {
faulty.append(neta)
continue
}
if tscore >= neta.score {
let instance = (neta.key, neta.value, neta.score, competants, neta.score - tscore)
let valueJoined = String(competants.map(\.value).joined(separator: ""))
if neta.value == valueJoined {
indifferents.append(instance)
} else {
if valueToScore.keys.contains(valueJoined), neta.value != valueJoined {
if let valueJoinedScore = valueToScore[valueJoined], neta.score < valueJoinedScore {
competingUnigrams.append((neta.value, neta.score, valueJoined, valueJoinedScore))
}
}
insufficients.append(instance)
}
}
}
insufficients = insufficients.sorted(by: { lhs, rhs -> Bool in
(lhs.2) > (rhs.2)
})
competingUnigrams = competingUnigrams.sorted(by: { lhs, rhs -> Bool in
(lhs.1 - lhs.3) > (rhs.1 - rhs.3)
})
let separator: String = {
var result = ""
for _ in 0..<72 { result += "-" }
return result
}()
func printl(_ input: String) {
result += input + "\n"
}
printl(separator)
printl("持單個字符的有效單元圖數量:\(unigramMonoCharCounter)")
printl("持多個字符的有效單元圖數量:\(unigramPolyCharCounter)")
printl(separator)
printl("總結一下那些容易被單個漢字的字頻干擾輸入的詞組單元圖:")
printl("因干擾組件和字詞本身完全重疊、而不需要處理的單元圖的數量:\(indifferents.count)")
printl(
"\(insufficients.count) 個複字單元圖被自身成分讀音對應的其它單字單元圖奪權,約佔全部有效單元圖的 \(insufficients.count / unigramPolyCharCounter * 100)%"
)
printl("\n其中有:")
var insufficientsMap = [Int: [(String, String, Double, [Unigram], Double)]]()
for x in 2...10 {
insufficientsMap[x] = insufficients.filter { $0.0.split(separator: "-").count == x }
}
printl(" \(insufficientsMap[2]?.count ?? 0) 個有效雙字單元圖")
printl(" \(insufficientsMap[3]?.count ?? 0) 個有效三字單元圖")
printl(" \(insufficientsMap[4]?.count ?? 0) 個有效四字單元圖")
printl(" \(insufficientsMap[5]?.count ?? 0) 個有效五字單元圖")
printl(" \(insufficientsMap[6]?.count ?? 0) 個有效六字單元圖")
printl(" \(insufficientsMap[7]?.count ?? 0) 個有效七字單元圖")
printl(" \(insufficientsMap[8]?.count ?? 0) 個有效八字單元圖")
printl(" \(insufficientsMap[9]?.count ?? 0) 個有效九字單元圖")
printl(" \(insufficientsMap[10]?.count ?? 0) 個有效十字單元圖")
if let insufficientsMap2 = insufficientsMap[2], !insufficientsMap2.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效雙字單元圖")
for (i, content) in insufficientsMap2.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap3 = insufficientsMap[3], !insufficientsMap3.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效三字單元圖")
for (i, content) in insufficientsMap3.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap4 = insufficientsMap[4], !insufficientsMap4.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效四字單元圖")
for (i, content) in insufficientsMap4.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap5 = insufficientsMap[5], !insufficientsMap5.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效五字單元圖")
for (i, content) in insufficientsMap5.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap6 = insufficientsMap[6], !insufficientsMap6.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效六字單元圖")
for (i, content) in insufficientsMap6.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap7 = insufficientsMap[7], !insufficientsMap7.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效七字單元圖")
for (i, content) in insufficientsMap7.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap8 = insufficientsMap[8], !insufficientsMap8.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效八字單元圖")
for (i, content) in insufficientsMap8.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap9 = insufficientsMap[9], !insufficientsMap9.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效九字單元圖")
for (i, content) in insufficientsMap9.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if let insufficientsMap10 = insufficientsMap[10], !insufficientsMap10.isEmpty {
printl(separator)
printl("前二十五個被奪權的有效十字單元圖")
for (i, content) in insufficientsMap10.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += content.1 + ","
contentToPrint += String(content.2) + ","
contentToPrint += "[" + content.3.map(\.description).joined(separator: ",") + "]" + ","
contentToPrint += String(content.4) + "}"
printl(contentToPrint)
}
}
if !competingUnigrams.isEmpty {
printl(separator)
printl("也發現有 \(competingUnigrams.count) 個複字單元圖被某些由高頻單字組成的複字單元圖奪權的情況,")
printl("例如(前二十五例):")
for (i, content) in competingUnigrams.enumerated() {
if i == 25 { break }
var contentToPrint = "{"
contentToPrint += content.0 + ","
contentToPrint += String(content.1) + ","
contentToPrint += content.2 + ","
contentToPrint += String(content.3) + "}"
printl(contentToPrint)
}
}
if !faulty.isEmpty {
printl(separator)
printl("下述單元圖用到了漢字核心表當中尚未收錄的讀音,可能無法正常輸入:")
for content in faulty {
printl(content.description)
}
}
result += "\n"
return result
}

78
FAQ.md Normal file
View File

@ -0,0 +1,78 @@
# 常見問題解答
## 問題回報後的處理
### 我提的問題或需求或者 PR怎麼一直都沒有人處理
1. 不是所有的問題或需求都活該得到積極響應。與上游不同,敝專案會在某些問題/需求因自身特性而無法響應之的情況下做出對應的說明、以節約雙方的時間與感情。
2. 雖然威注音專案的研發時間投入比上游高出幾個數量級,但也只是偶然。請勿將這種日常視為理所當然,誰都會有因為私事而突然變忙的時候。
3. 有時,自己的痛苦,就是源於自己的無能。自己覺得對方不去做的話,「自己學、自己做」可能還真的就是最快的方式,因為反正對方可能到死都不會去做、就是要跟你鬧情緒。「軟體與程式碼是開源的,但專案是私人的」。早一點做出最壞的預期的話,就早一點意識到:自己的情緒管理控制權應該在自己的手中、且管理的方法就是讓自己變得比對方更強。你不需要獲得對方的承認,因為你的勝利在於你比對方創造了更多被公認的有利於社會的價值。
## 使用相關
### 1. 輸入法選單內的輸入法是灰色的,無法選中。
1. macOS 系統終端機內的「加密的鍵盤輸入」選項被啟用了。請點選「終端機」選單關閉「加密的鍵盤輸入」選項威注音就會正常出現在輸入法選單上。英文語系的使用者可以從「Terminal -> Secure Keyboard Entry」選單關閉該選項。包括 iTerms 在內的某些副廠終端機模擬軟體也會有類似的功能,請自行排查。
2. 你可能需要重新登入,特別是你在對威注音輸入法進行升級或者降級安裝的情況下。
### 2. 輸入法選單內出現多個相同的威注音輸入法副本。
請立刻重新登入。如果問題仍舊持續存在的話,請用終端機執行:
```bash
sudo "/Library/Input Methods/vChewing.app/Contents/MacOS/vChewing" uninstall
```
這道命令可以將任何安裝到錯誤位置的威注音輸入法移除掉。在這道命令之後,請再次執行最新的威注音的安裝程式。
威注音輸入法僅該被安裝到使用者目錄內的「~/Library/Input Methods/」目錄下,這也有助於將威注音輸入法安裝到受公司資安策略管控的電腦上。
### 3. 怎樣用非常規手段安裝部署威注音?
本次以 pkg 的形式發行安裝程式,方便資安管理業者們藉由終端機進行部署。
終端部署可以用這道指令:
```bash
installer -pkg ~/Downloads/vChewing-macOS-?.?.?-unsigned.pkg -target CurrentUserHomeDirectory
```
由於只會安裝至使用者目錄內,所以同一台電腦不同使用者需要分別安裝一遍。
下述終端命令亦可使下載來的程式從 macOS 門衛檢查隔離區內取出來:
```bash
xattr -dr com.apple.quarantine ~/Downloads/vChewing-macOS-?.?.?-unsigned.pkg
xattr -dr com.apple.quarantine ~/Downloads/vChewingInstaller.app
```
另請注意 macOS 10 & 11 的所有系統版本均有一處行為故障pkg 安裝包指定僅裝在使用者目錄下的話,**在 macOS 10 & 11
內執行時,仍舊會往總根目錄下安裝,除非您手動點「更改安裝位置」再將那唯一的「安裝只供我使用」再點一遍才可以**。
### 4. 有打算支援 Homebrew 等安裝途徑嗎?
這樣會增大維護成本,所以不會再考慮。
### 5. 選字窗位置不對欸。
這往往是您在敲字時使用的應用程式,並沒有正確地將正確的游標位置告知 IMK、導致輸入法無法得知相關的資訊使然。您在某個應用程式中敲字輸入游標的位置到底在哪裡一開始只有那個應用程式知道然後那個應用程式必須把正確的位置告知輸入法輸入法才知道應該在什麼位置顯示像是選字窗這樣的使用者介面元件。某些應用完全沒有認真處理與 macOS 的 IMK 框架有關的內容,所以就通知了輸入法一個奇怪的位置資訊,有時候座標根本就在螢幕大小之外。威注音在使用的 Voltaire MK3 與上游使用的 Voltaire MK2 的判斷方法相同:如果某個應用程式將奇怪的位置(不在任何一個螢幕的範圍內)告知給 IMK那麼輸入法就會想辦法把選字窗擺在螢幕範圍內最接近的位置。比方說如果是 y 軸超過了螢幕尺寸,就會改在螢幕的最上方顯示。
### 6. 自訂使用者語彙資料該怎麼管理?
輸入法選單內有相關的選項、允許您開啟使用者語彙資料目錄來自行備份。您也可以藉由輸入法偏好設定來修改這個目錄的位置。修改該目錄的位置的行為並不會自動遷移這些資料。
出於研發負擔與使用者體驗管理方面的疑慮,威注音輸入法不打算內建基於 git 的使用者語彙資料版本管理備份功能。如果您有相關的產品需求的話,完全可以自己寫 bash 腳本搭配系統內建的 cron 功能來使用。
## 技術相關:
### 威注音會使用多少系統資源?
此部分以威注音 v1.7.4 版來說明。
在 2018 Intel Mac Mini 以及 2020 年的末代 Intel MacBook Pro 13-inch 機種內使用 Xcode 對威注音做側寫的過程中,我們得到的測試結果是:大約會佔用 110-120 MB 左右的記憶體,平時也大概是這個數。值得注意的是,如果使用者語彙內容體積有所增長的話,對應的記憶體開銷也會有相應的上漲。
為什麼會比上游多出幾乎接近 10 倍呢?因為 Swift 對 string 資料的處理就是這樣不經濟。這還是在經過優化處理之後的結果。(威注音在 1.5.4 版內部測試的時候,語言模組體系剛剛 Swift 化,記憶體開銷是 700MB。我們能得出的對 Swift 而言的最佳處理方案就是使用 plist 格式的原廠語彙資料,這樣能夠兼顧十年前的舊機種的 CPU 算力。假如只照顧最近三年以來的機種的話,還可以換用另一種算法、來將記憶體開銷縮減至 80MB 以內。
得益於威注音的純 Swift 化,平常處理每個按鍵事件都可以在毫秒級別的時間完成。
$ EOF.

404
Installer/AppDelegate.swift Normal file
View File

@ -0,0 +1,404 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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 Cocoa
import InputMethodKit
private let kTargetBin = "vChewing"
private let kTargetType = "app"
private let kTargetBundle = "vChewing.app"
private let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app"
private let realHomeDir = URL(
fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil
)
private let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods")
private let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents)
private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS")
.appendingPathComponent(kTargetBin)
private let kDestinationPartial = urlDestinationPartial.path
private let kTargetPartialPath = urlTargetPartial.path
private let kTargetFullBinPartialPath = urlTargetFullBinPartial.path
private let kTranslocationRemovalTickInterval: TimeInterval = 0.5
private let kTranslocationRemovalDeadline: TimeInterval = 60.0
@NSApplicationMain
@objc(AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet private var installButton: NSButton!
@IBOutlet private var cancelButton: NSButton!
@IBOutlet private var progressSheet: NSWindow!
@IBOutlet private var progressIndicator: NSProgressIndicator!
@IBOutlet private var appVersionLabel: NSTextField!
@IBOutlet private var appCopyrightLabel: NSTextField!
@IBOutlet private var appEULAContent: NSTextView!
private var archiveUtil: ArchiveUtil?
private var installingVersion = ""
private var upgrading = false
private var translocationRemovalStartTime: Date?
private var currentVersionNumber: Int = 0
let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app")
var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
guard let components = Bundle(url: imeURLInstalled)?.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) }
}
func runAlertPanel(title: String, message: String, buttonTitle: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
func applicationDidFinishLaunching(_: Notification) {
guard
let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
as? String,
let window = window,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
else {
return
}
self.installingVersion = installingVersion
archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle)
_ = archiveUtil?.validateIfNotarizedArchiveExists()
cancelButton.nextKeyView = installButton
installButton.nextKeyView = cancelButton
if let cell = installButton.cell as? NSButtonCell {
window.defaultButtonCell = cell
}
if let copyrightLabel = Bundle.main.localizedInfoDictionary?["NSHumanReadableCopyright"]
as? String
{
appCopyrightLabel.stringValue = copyrightLabel
}
if let eulaContent = Bundle.main.localizedInfoDictionary?["CFEULAContent"] as? String {
appEULAContent.string = eulaContent
}
appVersionLabel.stringValue = "\(versionString) Build \(installingVersion)"
window.title = "\(window.title) (v\(versionString), Build \(installingVersion))"
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
window.titlebarAppearsTransparent = true
if FileManager.default.fileExists(
atPath: kTargetPartialPath)
{
let currentBundle = Bundle(path: kTargetPartialPath)
let shortVersion =
currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion =
currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0
if shortVersion != nil, let currentVersion = currentVersion,
currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending
{
upgrading = true
}
}
if upgrading {
installButton.title = NSLocalizedString("Upgrade", comment: "")
}
window.center()
window.orderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func agreeAndInstallAction(_: AnyObject) {
cancelButton.isEnabled = false
installButton.isEnabled = false
removeThenInstallInputMethod()
}
@objc func timerTick(_ timer: Timer) {
guard let window = window else { return }
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date())
if elapsed >= kTranslocationRemovalDeadline {
timer.invalidate()
window.endSheet(progressSheet, returnCode: .cancel)
} else if isAppBundleTranslocated(atPath: kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0
timer.invalidate()
window.endSheet(progressSheet, returnCode: .continue)
}
}
func removeThenInstallInputMethod() {
// if !FileManager.default.fileExists(atPath: kTargetPartialPath) {
// installInputMethod(
// previousExists: false, previousVersionNotFullyDeactivatedWarning: false
// )
// return
// }
guard let window = window else { return }
let shouldWaitForTranslocationRemoval =
isAppBundleTranslocated(atPath: kTargetPartialPath)
&& window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:)))
//
do {
let sourceDir = kDestinationPartial
let fileManager = FileManager.default
let fileURLString = sourceDir + "/" + kTargetBundle
let fileURL = URL(fileURLWithPath: fileURLString)
//
if fileManager.fileExists(atPath: fileURLString) {
//
try fileManager.trashItem(at: fileURL, resultingItemURL: nil)
} else {
NSLog("File does not exist")
}
} catch let error as NSError {
NSLog("An error took place: \(error)")
}
let killTask = Process()
killTask.launchPath = "/usr/bin/killall"
killTask.arguments = ["-9", kTargetBin]
killTask.launch()
killTask.waitUntilExit()
if shouldWaitForTranslocationRemoval {
progressIndicator.startAnimation(self)
window.beginSheet(progressSheet) { returnCode in
DispatchQueue.main.async {
if returnCode == .continue {
self.installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: false
)
} else {
self.installInputMethod(
previousExists: true,
previousVersionNotFullyDeactivatedWarning: true
)
}
}
}
translocationRemovalStartTime = Date()
Timer.scheduledTimer(
timeInterval: kTranslocationRemovalTickInterval, target: self,
selector: #selector(timerTick(_:)), userInfo: nil, repeats: true
)
} else {
installInputMethod(
previousExists: false, previousVersionNotFullyDeactivatedWarning: false
)
}
}
func installInputMethod(
previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool
) {
guard
let targetBundle = archiveUtil?.unzipNotarizedArchive()
?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType)
else {
return
}
let cpTask = Process()
cpTask.launchPath = "/bin/cp"
print(kDestinationPartial)
cpTask.arguments = [
"-R", targetBundle, kDestinationPartial,
]
cpTask.launch()
cpTask.waitUntilExit()
if cpTask.terminationStatus != 0 {
runAlertPanel(
title: NSLocalizedString("Install Failed", comment: ""),
message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""),
buttonTitle: NSLocalizedString("Cancel", comment: "")
)
endAppWithDelay()
}
_ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)")
guard let theBundle = Bundle(url: imeURLInstalled),
let imeIdentifier = theBundle.bundleIdentifier
else {
endAppWithDelay()
return
}
let imeBundleURL = theBundle.bundleURL
if allRegisteredInstancesOfThisInputMethod.isEmpty {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).")
let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr)
if !status {
let message = String(
format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: ""
),
imeIdentifier
)
runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "")
)
endAppWithDelay()
return
}
if allRegisteredInstancesOfThisInputMethod.isEmpty {
let message = String(
format: NSLocalizedString(
"Cannot find input source %@ after registration.", comment: ""
),
imeIdentifier
)
runAlertPanel(
title: NSLocalizedString("Fatal Error", comment: ""), message: message,
buttonTitle: NSLocalizedString("Abort", comment: "")
)
}
}
var isMacOS12OrAbove = false
if #available(macOS 12.0, *) {
NSLog("macOS 12 or later detected.")
isMacOS12OrAbove = true
} else {
NSLog("Installer runs with the pre-macOS 12 flow.")
}
// Unconditionally enable the IME on macOS 12.0+,
// as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not*
// enabled in the user's current set of IMEs (which means the IME does not show up in
// the user's input menu).
var mainInputSourceEnabled = false
allRegisteredInstancesOfThisInputMethod.forEach {
if $0.activate() {
NSLog("Input method enabled: \(imeIdentifier)")
} else {
NSLog("Failed to enable input method: \(imeIdentifier)")
}
mainInputSourceEnabled = $0.isActivated
}
// Alert Panel
let ntfPostInstall = NSAlert()
if warning {
ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "")
ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
} else {
if !mainInputSourceEnabled, !isMacOS12OrAbove {
ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "")
ntfPostInstall.informativeText = NSLocalizedString(
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.",
comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
} else {
ntfPostInstall.messageText = NSLocalizedString(
"Installation Successful", comment: ""
)
ntfPostInstall.informativeText = NSLocalizedString(
"vChewing is ready to use.", comment: ""
)
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
}
}
ntfPostInstall.beginSheetModal(for: window!) { _ in
self.endAppWithDelay()
}
}
func endAppWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
NSApp.terminate(self)
}
}
@IBAction func cancelAction(_: AnyObject) {
NSApp.terminate(self)
}
func windowWillClose(_: Notification) {
NSApp.terminate(self)
}
private func shell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
} else {
task.launchPath = "/bin/zsh"
}
task.standardInput = nil
if #available(macOS 10.13, *) {
try task.run()
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
// Determines if an app is translocated by Gatekeeper to a randomized path.
// See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/
// Originally written by Zonble Yang in Objective-C (MIT License).
// Swiftified by: Rob Mayoff. Ref: https://forums.swift.org/t/58719/5
func isAppBundleTranslocated(atPath bundlePath: String) -> Bool {
var entryCount = getfsstat(nil, 0, 0)
var entries: [statfs] = .init(repeating: .init(), count: Int(entryCount))
let absPath = bundlePath.cString(using: .utf8)
entryCount = getfsstat(&entries, entryCount * Int32(MemoryLayout<statfs>.stride), MNT_NOWAIT)
for entry in entries.prefix(Int(entryCount)) {
let isMatch = withUnsafeBytes(of: entry.f_mntfromname) { mntFromName in
strcmp(absPath, mntFromName.baseAddress) == 0
}
if isMatch {
var stat = statfs()
let rc = statfs(absPath, &stat)
return rc == 0
}
}
return false
}
}

119
Installer/ArchiveUtil.swift Normal file
View File

@ -0,0 +1,119 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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 Cocoa
struct ArchiveUtil {
var appName: String
var targetAppBundleName: String
init(appName: String, targetAppBundleName: String) {
self.appName = appName
self.targetAppBundleName = targetAppBundleName
}
// Returns YES if (1) a zip file under
// Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if
// Resources/$_invalidAppBundleName does not exist.
func validateIfNotarizedArchiveExists() -> Bool {
guard let resourePath = Bundle.main.resourcePath,
let notarizedArchivesPath = notarizedArchivesPath,
let notarizedArchive = notarizedArchive,
let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(
atPath: notarizedArchivesPath)
else {
return false
}
let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName)
let count = notarizedArchivesContent.count
let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive)
let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath)
if !notarizedArchivesContent.isEmpty {
// count > 0!isEmpty滿
if count != 1 || !notarizedArchiveExists || devModeAppBundleExists {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Internal Error"
alert.informativeText =
"devMode installer, expected archive name: \(notarizedArchive), "
+ "archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)"
alert.addButton(withTitle: "Terminate")
alert.runModal()
NSApp.terminate(nil)
} else {
return true
}
}
if !devModeAppBundleExists {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Internal Error"
alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)"
alert.addButton(withTitle: "Terminate")
alert.runModal()
NSApp.terminate(nil)
}
return false
}
func unzipNotarizedArchive() -> String? {
if !validateIfNotarizedArchiveExists() {
return nil
}
guard let notarizedArchive = notarizedArchive,
let resourcePath = Bundle.main.resourcePath
else {
return nil
}
let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(
UUID().uuidString)
let arguments: [String] = [notarizedArchive, "-d", tempFilePath]
let unzipTask = Process()
unzipTask.launchPath = "/usr/bin/unzip"
unzipTask.currentDirectoryPath = resourcePath
unzipTask.arguments = arguments
unzipTask.launch()
unzipTask.waitUntilExit()
assert(unzipTask.terminationStatus == 0, "Must successfully unzipped")
let result = (tempFilePath as NSString).appendingPathComponent(targetAppBundleName)
assert(
FileManager.default.fileExists(atPath: result),
"App bundle must be unzipped at \(result)."
)
return result
}
private var notarizedArchivesPath: String? {
guard let resourePath = Bundle.main.resourcePath else {
return nil
}
let notarizedArchivesPath = (resourePath as NSString).appendingPathComponent(
"NotarizedArchives")
return notarizedArchivesPath
}
private var notarizedArchive: String? {
guard let notarizedArchivesPath = notarizedArchivesPath,
let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String]
as? String
else {
return nil
}
let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip"
let notarizedArchive = (notarizedArchivesPath as NSString).appendingPathComponent(
notarizedArchiveBasename)
return notarizedArchive
}
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleSignature</key>
<string>MBIN</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFEULAContent</key>
<string>License texts used in the customized about window.</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSHasLocalizedDisplayName</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
<key>NSHumanReadableCopyright</key>
<string>© 2021-2022 vChewing Project.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

BIN
Installer/InstallerBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1 @@
Place the notarized archive here for producing the release installer.

View File

@ -0,0 +1,366 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21223" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21223"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="494" id="495"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu title="AMainMenu" systemMenu="main" id="29">
<items>
<menuItem title="vChewing Installer" id="56">
<menu key="submenu" title="vChewing Installer" systemMenu="apple" id="57">
<items>
<menuItem title="About vChewing Installer" id="58">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="236">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Services" id="131">
<menu key="submenu" title="Services" systemMenu="services" id="130"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="144">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Hide vChewing Installer" keyEquivalent="h" id="134">
<connections>
<action selector="hide:" target="-1" id="367"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="145">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="368"/>
</connections>
</menuItem>
<menuItem title="Show All" id="150">
<connections>
<action selector="unhideAllApplications:" target="-1" id="370"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="149">
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</menuItem>
<menuItem title="Quit vChewing Installer" keyEquivalent="q" id="136">
<connections>
<action selector="terminate:" target="-3" id="449"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="83"/>
<menuItem title="Edit" id="LJX-Bb-mhU">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="6QY-kP-PQQ">
<items>
<menuItem title="Undo" keyEquivalent="z" id="5tq-5G-Yoy">
<connections>
<action selector="undo:" target="-1" id="P9n-jj-WpM"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="GRe-Pk-1EX">
<connections>
<action selector="redo:" target="-1" id="cbT-AB-slM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="cYt-uT-CAh"/>
<menuItem title="Cut" keyEquivalent="x" id="zAh-7y-AvL">
<connections>
<action selector="cut:" target="-1" id="arZ-EA-CgM"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="WoU-zb-uKy">
<connections>
<action selector="copy:" target="-1" id="0JC-Jc-0Xl"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="Fid-E7-Ykc">
<connections>
<action selector="paste:" target="-1" id="fVk-V0-Sbq"/>
</connections>
</menuItem>
<menuItem title="Delete" id="Ier-IT-JZa">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="X7x-wD-fWC"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="ZsT-7a-SE6">
<connections>
<action selector="selectAll:" target="-1" id="iwd-aI-lml"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="139" y="154"/>
</menu>
<window title="vChewing Installer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="371">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
<rect key="contentRect" x="335" y="390" width="533" height="457"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<value key="minSize" type="size" width="533" height="457"/>
<value key="maxSize" type="size" width="533" height="457"/>
<view key="contentView" id="372">
<rect key="frame" x="0.0" y="0.0" width="533" height="457"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="575">
<rect key="frame" x="378" y="101" width="147" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="133" id="S4F-Qe-xuk"/>
</constraints>
<buttonCell key="cell" type="push" title="I Accept" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="576">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" size="13" name="Tahoma-Bold"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="agreeAndInstallAction:" target="494" id="708"/>
</connections>
</button>
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" findBarPosition="belowContent" translatesAutoresizingMaskIntoConstraints="NO" id="YCR-wo-M5a">
<rect key="frame" x="91" y="166" width="427" height="190"/>
<clipView key="contentView" drawsBackground="NO" id="NrY-FL-PVu" userLabel="appEULAContentClip">
<rect key="frame" x="1" y="1" width="425" height="188"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView editable="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" findStyle="bar" smartInsertDelete="YES" id="47J-tO-8TZ" userLabel="appEULAContent">
<rect key="frame" x="0.0" y="-2" width="425" height="188"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="425" height="188"/>
<size key="maxSize" width="427" height="10000000"/>
<attributedString key="textStorage">
<fragment content="Placeholder for EULA Texts.">
<attributes>
<color key="NSColor" name="textColor" catalog="System" colorSpace="catalog"/>
<font key="NSFont" metaFont="systemLight" size="11"/>
<paragraphStyle key="NSParagraphStyle" alignment="natural" lineBreakMode="wordWrapping" baseWritingDirection="natural" tighteningFactorForTruncation="0.0"/>
</attributes>
</fragment>
</attributedString>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="YOZ-MC-EF2">
<rect key="frame" x="-100" y="-100" width="240" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" controlSize="mini" horizontal="NO" id="E5B-3B-faV">
<rect key="frame" x="412" y="1" width="14" height="188"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ir5-sQ-sJc">
<rect key="frame" x="89" y="442" width="130" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="vChewing for macOS" id="GNc-8S-1VG" userLabel="appNameLabel">
<font key="font" size="12" name="Tahoma-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bzR-Oa-BZa">
<rect key="frame" x="89" y="426" width="263" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Derived from OpenVanilla McBopopmofo Project." id="QYf-Nf-hoi">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="03l-rN-zf9">
<rect key="frame" x="89" y="410" width="297" height="15"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="293" id="v2b-OK-WGD"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="Placeholder for showing copyright information." id="eo3-TK-0rB" userLabel="appCopyrightLabel">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XLb-mv-73s">
<rect key="frame" x="89" y="387" width="431" height="15"/>
<textFieldCell key="cell" title="Placeholder for detailed credits." id="VW8-s5-Wpn">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Yyh-Nw-Sba">
<rect key="frame" x="15" y="137" width="503" height="5"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="k5O-zZ-gQY">
<rect key="frame" x="89" y="364" width="431" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="MIT-NTL License:" id="AVS-ih-FXM">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="miu-08-dZk">
<rect key="frame" x="13" y="41" width="360" height="90"/>
<constraints>
<constraint firstAttribute="width" constant="356" id="pu3-zr-hJy"/>
</constraints>
<textFieldCell key="cell" id="Q9M-ni-kUM">
<font key="font" size="12" name="Tahoma"/>
<string key="title">DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView translatesAutoresizingMaskIntoConstraints="NO" id="Ked-gt-bjE">
<rect key="frame" x="15" y="147" width="63" height="310"/>
<constraints>
<constraint firstAttribute="width" constant="63" id="fgC-vo-Ho8"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="AboutBanner" id="akk-zO-Abm"/>
</imageView>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="z1m-8k-Z63">
<rect key="frame" x="218" y="442" width="126" height="15"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="122" id="yKq-Fv-W1J"/>
</constraints>
<textFieldCell key="cell" lineBreakMode="clipping" title="version_placeholder" id="JRP-At-H9q" userLabel="appVersionLabel">
<font key="font" size="12" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nul-TQ-gOI">
<rect key="frame" x="89" y="148" width="431" height="15"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="By installing the software, you must accept the terms above." id="mf8-6e-z7X">
<font key="font" size="12" name="Tahoma-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="592">
<rect key="frame" x="378" y="74" width="147" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="593">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" size="13" name="Tahoma"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancelAction:" target="494" id="707"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="575" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="miu-08-dZk" secondAttribute="trailing" constant="8" symbolic="YES" id="18X-Qf-xUC"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="top" secondItem="Ked-gt-bjE" secondAttribute="bottom" constant="7" id="1Hx-8o-xpF"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="top" secondItem="nul-TQ-gOI" secondAttribute="bottom" constant="8" symbolic="YES" id="1Mz-Yp-lqA"/>
<constraint firstItem="575" firstAttribute="leading" secondItem="592" secondAttribute="leading" id="2Kf-DA-DXH"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="leading" secondItem="372" secondAttribute="leading" constant="15" id="2ne-pt-ddK"/>
<constraint firstItem="03l-rN-zf9" firstAttribute="leading" secondItem="XLb-mv-73s" secondAttribute="leading" id="6Mv-X8-W55"/>
<constraint firstItem="nul-TQ-gOI" firstAttribute="trailing" secondItem="Yyh-Nw-Sba" secondAttribute="trailing" id="6yu-Wm-g26"/>
<constraint firstItem="592" firstAttribute="top" secondItem="575" secondAttribute="bottom" constant="7" id="7Q7-30-gTO"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bzR-Oa-BZa" secondAttribute="trailing" constant="20" symbolic="YES" id="8td-FZ-tnM"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="baseline" secondItem="z1m-8k-Z63" secondAttribute="baseline" id="9AX-QJ-G9U"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="leading" secondItem="Ked-gt-bjE" secondAttribute="trailing" constant="13" id="Brw-UI-0WK"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="trailing" secondItem="YCR-wo-M5a" secondAttribute="trailing" id="DfC-Ke-tb5"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="leading" secondItem="YCR-wo-M5a" secondAttribute="leading" id="FIo-Op-SV8"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="leading" secondItem="miu-08-dZk" secondAttribute="leading" id="H4v-4O-xZY"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="trailing" secondItem="nul-TQ-gOI" secondAttribute="trailing" id="HX6-hi-PJs"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="z1m-8k-Z63" secondAttribute="trailing" constant="20" symbolic="YES" id="Hg9-8d-O7s"/>
<constraint firstItem="z1m-8k-Z63" firstAttribute="leading" secondItem="ir5-sQ-sJc" secondAttribute="trailing" constant="3" id="LSc-gD-CbY"/>
<constraint firstItem="575" firstAttribute="top" secondItem="Yyh-Nw-Sba" secondAttribute="bottom" constant="11" id="Nw2-bH-vTF"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="03l-rN-zf9" secondAttribute="trailing" constant="20" symbolic="YES" id="PRC-Y1-rIz"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="leading" secondItem="Yyh-Nw-Sba" secondAttribute="leading" id="SKi-gn-JeS"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="trailing" secondItem="k5O-zZ-gQY" secondAttribute="trailing" id="VOo-Q9-rki"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="top" secondItem="372" secondAttribute="top" id="WPX-gk-uqh"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="top" secondItem="03l-rN-zf9" secondAttribute="bottom" constant="8" symbolic="YES" id="bJX-f1-1PU"/>
<constraint firstItem="Ked-gt-bjE" firstAttribute="top" secondItem="ir5-sQ-sJc" secondAttribute="top" id="caT-rm-xEa"/>
<constraint firstItem="bzR-Oa-BZa" firstAttribute="top" secondItem="ir5-sQ-sJc" secondAttribute="bottom" constant="1" id="dyJ-9C-f56"/>
<constraint firstItem="bzR-Oa-BZa" firstAttribute="leading" secondItem="03l-rN-zf9" secondAttribute="leading" id="etY-2E-2Sa"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="leading" secondItem="nul-TQ-gOI" secondAttribute="leading" id="fl0-wm-8Pa"/>
<constraint firstItem="miu-08-dZk" firstAttribute="top" secondItem="Yyh-Nw-Sba" secondAttribute="bottom" constant="8" symbolic="YES" id="lY7-Se-lpo"/>
<constraint firstItem="XLb-mv-73s" firstAttribute="leading" secondItem="k5O-zZ-gQY" secondAttribute="leading" id="qzW-qc-9yQ"/>
<constraint firstItem="575" firstAttribute="trailing" secondItem="592" secondAttribute="trailing" id="sIp-L2-QLj"/>
<constraint firstItem="575" firstAttribute="trailing" secondItem="Yyh-Nw-Sba" secondAttribute="trailing" id="tap-mY-bvB"/>
<constraint firstItem="nul-TQ-gOI" firstAttribute="top" secondItem="YCR-wo-M5a" secondAttribute="bottom" constant="3" id="tqc-zq-Egb"/>
<constraint firstItem="YCR-wo-M5a" firstAttribute="top" secondItem="k5O-zZ-gQY" secondAttribute="bottom" constant="8" symbolic="YES" id="u3L-Nh-ELP"/>
<constraint firstItem="k5O-zZ-gQY" firstAttribute="top" secondItem="XLb-mv-73s" secondAttribute="bottom" constant="8" symbolic="YES" id="uxg-3X-XtF"/>
<constraint firstItem="ir5-sQ-sJc" firstAttribute="leading" secondItem="bzR-Oa-BZa" secondAttribute="leading" id="vkK-Vc-WOf"/>
<constraint firstItem="Yyh-Nw-Sba" firstAttribute="centerX" secondItem="372" secondAttribute="centerX" id="xqG-pX-j0d"/>
<constraint firstItem="03l-rN-zf9" firstAttribute="top" secondItem="bzR-Oa-BZa" secondAttribute="bottom" constant="1" id="yKI-MD-nsC"/>
</constraints>
</view>
<connections>
<outlet property="delegate" destination="494" id="706"/>
</connections>
<point key="canvasLocation" x="-306.5" y="-230"/>
</window>
<customObject id="494" customClass="AppDelegate">
<connections>
<outlet property="appCopyrightLabel" destination="03l-rN-zf9" id="XS5-cZ-k9H"/>
<outlet property="appEULAContent" destination="47J-tO-8TZ" id="kRU-X2-8kX"/>
<outlet property="appVersionLabel" destination="z1m-8k-Z63" id="75X-uy-0Iz"/>
<outlet property="cancelButton" destination="592" id="710"/>
<outlet property="installButton" destination="575" id="709"/>
<outlet property="progressIndicator" destination="deb-uT-yNv" id="Cpk-6Z-0rj"/>
<outlet property="progressSheet" destination="gHl-Hx-eQn" id="gD4-XO-YO1"/>
<outlet property="window" destination="371" id="532"/>
</connections>
</customObject>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gHl-Hx-eQn">
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="480" height="180"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="wAe-c8-Vh9">
<rect key="frame" x="0.0" y="0.0" width="480" height="180"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="1" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="deb-uT-yNv">
<rect key="frame" x="20" y="67" width="440" height="20"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VDL-Yq-heb">
<rect key="frame" x="18" y="94" width="444" height="17"/>
<constraints>
<constraint firstAttribute="height" constant="17" id="MLj-KG-mL8"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Stopping the old version. This may take up to one minute…" id="nTo-dx-qfZ">
<font key="font" size="13" name="Tahoma"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="VDL-Yq-heb" firstAttribute="trailing" secondItem="deb-uT-yNv" secondAttribute="trailing" id="DCe-Xh-ee1"/>
<constraint firstItem="deb-uT-yNv" firstAttribute="top" secondItem="VDL-Yq-heb" secondAttribute="bottom" constant="8" symbolic="YES" id="HUE-gU-UFS"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="top" secondItem="wAe-c8-Vh9" secondAttribute="top" constant="69" id="IwI-63-e9H"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="leading" secondItem="deb-uT-yNv" secondAttribute="leading" id="UUz-sT-D9I"/>
<constraint firstItem="VDL-Yq-heb" firstAttribute="leading" secondItem="wAe-c8-Vh9" secondAttribute="leading" constant="20" symbolic="YES" id="Vgg-bw-6wt"/>
<constraint firstAttribute="trailing" secondItem="VDL-Yq-heb" secondAttribute="trailing" constant="20" symbolic="YES" id="ft0-oZ-8HD"/>
</constraints>
</view>
<point key="canvasLocation" x="529" y="-282"/>
</window>
<customObject id="420" customClass="NSFontManager"/>
</objects>
<resources>
<image name="AboutBanner" width="63" height="310"/>
</resources>
</document>

View File

@ -0,0 +1,5 @@
/* Localized versions of Info.plist keys */
CFBundleName = "vChewing Installer";
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
CFEULAContent = "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\n1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\n2. No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements above.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n";

View File

@ -0,0 +1,19 @@
"vChewing Input Method" = "vChewing Input Method";
"Upgrade" = "Accept & Upgrade";
"Cancel" = "Cancel";
"Cannot activate the input method." = "Cannot activate the input method.";
"Cannot copy the file to the destination." = "Cannot copy the file to the destination.";
"Install Failed" = "Install Failed";
"Installation Successful" = "Installation Successful";
"OK" = "OK";
"vChewing is ready to use." = "vChewing is ready to use.";
"Stopping the old version. This may take up to one minute…" = "Stopping the old version. This may take up to one minute…";
"Attention" = "Attention";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.";
"Fatal Error" = "Fatal Error";
"Abort" = "Abort";
"Cannot register input source %@ at %@." = "Cannot register input source %@ at %@.";
"Cannot find input source %@ after registration." = "Cannot find input source %@ after registration.";
"Warning" = "Warning";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.";
"Continue" = "Continue";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "vChewing Installer";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "vChewing Installer";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "About vChewing Installer";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "File";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "Services";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "Services";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "Hide vChewing Installer";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "Quit vChewing Installer";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "Hide Others";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "Show All";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "vChewing Installer";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "I Accept";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "Cancel";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "MIT-NTL License:";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.";
/* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "Derived from OpenVanilla McBopopmofo Project.";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen.";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "Window";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "By installing the software, you must accept the terms above.";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "Stopping the old version. This may take up to one minute…";

View File

@ -0,0 +1,5 @@
/* Localized versions of Info.plist keys */
CFBundleName = "威注音入力 実装用アプリ";
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
CFEULAContent = "以下に定める条件に従い、本ソフトウェアおよび関連文書のファイル(以下「ソフトウェア」)の複製を取得するすべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複写、変更、結合、掲載、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供する相手に同じことを許可する権利も無制限に含まれます。\n\nイ上記の著作権表示および本許諾表示を、ソフトウェアのすべての複製または重要な部分に記載するものとします。\n\nロ上記の通知要件を満たすために必要な場合を除き、コントリビューターの商号、商標、サービスマーク、または製品名を使用するための商標ライセンスは付与されていません。\n\nソフトウェアは「現状のまま」で、明示であるか暗黙であるかを問わず、何らの保証もなく提供されます。ここでいう保証とは、商品性、特定の目的への適合性、および権利非侵害についての保証も含みますが、それに限定されるものではありません。\n作者または著作権者は、契約行為、不法行為、またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの使用またはその他の扱いによって生じる一切の請求、損害、その他の義務について何らの責任も負わないものとします。";

View File

@ -0,0 +1,19 @@
"vChewing Input Method" = "威注音入力アプリ";
"Upgrade" = "承認と更新";
"Cancel" = "取消";
"Cannot activate the input method." = "入力アプリ、起動失敗。";
"Cannot copy the file to the destination." = "目標へファイルのコピーできません。";
"Install Failed" = "実装失敗。";
"Installation Successful" = "実装完了";
"OK" = "うむ";
"vChewing is ready to use." = "威注音入力、利用準備完了。";
"Stopping the old version. This may take up to one minute…" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";
"Attention" = "ご注意";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "威注音入力の更新は実装完了しましたが、うまく作動できるために、このパソコンの再起動および再ログインが必要だと恐れ入ります。";
"Fatal Error" = "致命錯乱";
"Abort" = "中止";
"Cannot register input source %@ at %@." = "「%2$@」で入力アプリ「\"%1$@\"」の実装は失敗しました。";
"Cannot find input source %@ after registration." = "登録済みですが「%@」は見つけませんでした。";
"Warning" = "お知らせ";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "入力アプリの自動起動はうまく出来なかったかもしれません。ご自分で「システム環境設定→キーボード→入力ソース」で起動してください。";
"Continue" = "続行";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音入力 実装用アプリ";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音入力 実装用アプリ";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "威注音入力 実装用アプリについて…";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "ファイル";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "サービス";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "サービス";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "全ウィンドウ隠す";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "威注音入力 実装用アプリ を終了";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "他のアプリのウィンドウを隠す";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "隠したウィンドウを全部表示する";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音入力 実装用アプリ";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "承認する";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消";
/* Class = "NSTextFieldCell"; title = "3-Clause BSD License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "MIT商標不許可ライセンス (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
"JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免責事項vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。";
/* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "OpenVanilla 小麦注音プロジェクトから派生。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "macOS 版威注音の開発Shiki Suen, Isaac Xen, Hiraku Wang, など。\n威注音語彙データの維持Shiki Suen。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
"eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "Window";
/* Class = "NSTextFieldCell"; title = "By installing the software, you must accept the terms above."; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "このアプリを実装するために、上記の条約を承認すべきである。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "古いバージョンを強制停止中。1分かかると恐れ入りますが……";

View File

@ -0,0 +1,5 @@
/* Localized versions of Info.plist keys */
CFBundleName = "威注音安装程式";
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
CFEULAContent = "软件之著作权利人依此麻理授权条款,将其对于软件之著作权利授权释出,只须使用者践履以下二项麻理授权条款叙明之义务性规定,其即享有对此软件程式及其相关说明文档自由不受限制地进行利用之权利,范围包括「使用、重制、修改、合并、出版、散布、再授权、及贩售程式重制作品」等诸多方面之应用,而散布程式之人、更可将上述权利传递予其后收受程式之后手,倘若其后收受程式之人亦服膺以下二项麻理授权条款之义务性规定,则其对程式亦享有与前手运用范围相同之同一权利。\n\n甲、散布此一软件程式者须将本条款其上之「著作权声明」及以下之「免责声明」内嵌于软件程式及其重制作品之实体之中。\n\n乙、敝授权合约不提供对「贡献者」之商品名称、商标、服务标志或产品名称之商标许可除非用以满足履行上文所述义务之必要。\n\n因麻理软件程式之授权模式乃是无偿提供是以在现行法律之架构下可以主张合理之免除担保责任。麻理软件之著作权人或任何之后续散布者对于其所散布之麻理软件程式皆不负任何形式上实质上之担保责任明示亦或隐喻、商业利用性亦或特定目之使用性这些均不在保障之列。利用麻理软件程式之所有风险均由使用者自行担负。假如所使用之麻理程式发生缺陷性问题使用者需自行担负修正、改正及必要之服务支出。麻理软件程式之著作权人不负任何形式上实质上之担保责任无论任何一般之、特殊之、偶发之、因果关系式之损害或是麻理软件程式之不适用性均须由使用者自行负担。\n";

View File

@ -0,0 +1,19 @@
"vChewing Input Method" = "威注音输入法";
"Upgrade" = "接受并升级";
"Cancel" = "取消";
"Cannot activate the input method." = "无法启用输入法。";
"Cannot copy the file to the destination." = "无法将输入法拷贝至目的地。";
"Install Failed" = "安装失败";
"Installation Successful" = "安装成功";
"OK" = "确定";
"vChewing is ready to use." = "威注音输入法安装成功";
"Stopping the old version. This may take up to one minute…" = "正在试图结束正在运行的旧版输入法,大概需要一分钟…";
"Attention" = "请注意";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安装完成,但建议您登出或重新开机,以便顺利使用新版。";
"Fatal Error" = "安装错误";
"Abort" = "放弃安装";
"Cannot register input source %@ at %@." = "无法从档案位置 %2$@ 安装输入法 \"%1$@\"。";
"Cannot find input source %@ after registration." = "在注册完输入法 \"%@\" 仍然无法找到输入法。";
"Warning" = "安装不完整";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "输入法已经安装好,但可能没有完全启用。请从「系统偏好设定」 > 「键盘」 > 「输入方式」分页加入输入法。";
"Continue" = "继续";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音安装程式";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音安装程式";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "关于威注音安装程式";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "档案";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "服务";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "服务";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "隐藏威注音安装程式";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "结束威注音安装程式";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "隐藏其他程式";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "显示所有程式";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音输入法安装程式";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "我接受";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消安装";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "麻理去商标授权合约 (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。";
/* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "该专案由 OpenVanilla 小麦注音专案衍生而来。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "威注音 macOS 程式研发Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音词库维护Shiki Suen。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "视窗";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "若要安装该软件,请接受上述条款。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "等待旧版完全停用,大约需要一分钟…";

View File

@ -0,0 +1,5 @@
/* Localized versions of Info.plist keys */
CFBundleName = "威注音安裝程式";
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
CFEULAContent = "軟體之著作權利人依此麻理授權條款,將其對於軟體之著作權利授權釋出,只須使用者踐履以下二項麻理授權條款敘明之義務性規定,其即享有對此軟體程式及其相關說明文檔自由不受限制地進行利用之權利,範圍包括「使用、重製、修改、合併、出版、散布、再授權、及販售程式重製作品」等諸多方面之應用,而散布程式之人、更可將上述權利傳遞予其後收受程式之後手,倘若其後收受程式之人亦服膺以下二項麻理授權條款之義務性規定,則其對程式亦享有與前手運用範圍相同之同一權利。\n\n甲、散布此一軟體程式者須將本條款其上之「著作權聲明」及以下之「免責聲明」內嵌於軟體程式及其重製作品之實體之中。\n\n乙、敝授權合約不提供對「貢獻者」之商品名稱、商標、服務標誌或產品名稱之商標許可除非用以滿足履行上文所述義務之必要。\n\n因麻理軟體程式之授權模式乃是無償提供是以在現行法律之架構下可以主張合理之免除擔保責任。麻理軟體之著作權人或任何之後續散布者對於其所散布之麻理軟體程式皆不負任何形式上實質上之擔保責任明示亦或隱喻、商業利用性亦或特定目之使用性這些均不在保障之列。利用麻理軟體程式之所有風險均由使用者自行擔負。假如所使用之麻理程式發生缺陷性問題使用者需自行擔負修正、改正及必要之服務支出。麻理軟體程式之著作權人不負任何形式上實質上之擔保責任無論任何一般之、特殊之、偶發之、因果關係式之損害或是麻理軟體程式之不適用性均須由使用者自行負擔。\n";

View File

@ -0,0 +1,19 @@
"vChewing Input Method" = "威注音輸入法";
"Upgrade" = "接受並升級";
"Cancel" = "取消";
"Cannot activate the input method." = "無法啟用輸入法。";
"Cannot copy the file to the destination." = "無法將輸入法拷貝至目的地。";
"Install Failed" = "安裝失敗";
"Installation Successful" = "安裝成功";
"OK" = "確定";
"vChewing is ready to use." = "威注音輸入法安裝成功";
"Stopping the old version. This may take up to one minute…" = "正在試圖結束正在運行的舊版輸入法,大概需要一分鐘…";
"Attention" = "請注意";
"vChewing is upgraded, but please log out or reboot for the new version to be fully functional." = "vChewing 安裝完成,但建議您登出或重新開機,以便順利使用新版。";
"Fatal Error" = "安裝錯誤";
"Abort" = "放棄安裝";
"Cannot register input source %@ at %@." = "無法從檔案位置 %2$@ 安裝輸入法 \"%1$@\"。";
"Cannot find input source %@ after registration." = "在註冊完輸入法 \"%@\" 仍然無法找到輸入法。";
"Warning" = "安裝不完整";
"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources." = "輸入法已經安裝好,但可能沒有完全啟用。請從「系統偏好設定」 > 「鍵盤」 > 「輸入方式」分頁加入輸入法。";
"Continue" = "繼續";

View File

@ -0,0 +1,72 @@
/* Class = "NSMenu"; title = "AMainMenu"; ObjectID = "29"; */
"29.title" = "AMainMenu";
/* Class = "NSMenuItem"; title = "vChewing Installer"; ObjectID = "56"; */
"56.title" = "威注音安裝程式";
/* Class = "NSMenu"; title = "vChewing Installer"; ObjectID = "57"; */
"57.title" = "威注音安裝程式";
/* Class = "NSMenuItem"; title = "About vChewing Installer"; ObjectID = "58"; */
"58.title" = "關於威注音安裝程式";
/* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */
"83.title" = "檔案";
/* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */
"130.title" = "服務";
/* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */
"131.title" = "服務";
/* Class = "NSMenuItem"; title = "Hide vChewing Installer"; ObjectID = "134"; */
"134.title" = "隱藏威注音安裝程式";
/* Class = "NSMenuItem"; title = "Quit vChewing Installer"; ObjectID = "136"; */
"136.title" = "結束威注音安裝程式";
/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */
"145.title" = "隱藏其他程式";
/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */
"150.title" = "顯示所有程式";
/* Class = "NSWindow"; title = "vChewing Installer"; ObjectID = "371"; */
"371.title" = "威注音輸入法安裝程式";
/* Class = "NSButtonCell"; title = "I Accept"; ObjectID = "576"; */
"576.title" = "我接受";
/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "593"; */
"593.title" = "取消安裝";
/* Class = "NSTextFieldCell"; title = "MIT-NTL License:"; ObjectID = "AVS-ih-FXM"; */
"AVS-ih-FXM.title" = "麻理去商標授權合約 (MIT-NTL License):";
/* Class = "NSTextFieldCell"; title = "vChewing for macOS"; ObjectID = "GNc-8S-1VG"; */
"GNc-8S-1VG.title" = "vChewing for macOS";
/* Class = "NSTextFieldCell"; title = "version_placeholder"; ObjectID = "JRP-At-H9q"; */
// "JRP-At-H9q.title" = "version_placeholder";
/* Class = "NSTextFieldCell"; title = "DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database."; ObjectID = "Q9M-ni-kUM"; */
"Q9M-ni-kUM.title" = "免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。";
/* Class = "NSTextFieldCell"; title = "Derived from OpenVanilla McBopopmofo Project."; ObjectID = "QYf-Nf-hoi"; */
"QYf-Nf-hoi.title" = "該專案由 OpenVanilla 小麥注音專案衍生而來。";
/* Class = "NSTextFieldCell"; title = "vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.\nvChewing Phrase Database Maintained by Shiki Suen."; ObjectID = "VW8-s5-Wpn"; */
"VW8-s5-Wpn.title" = "威注音 macOS 程式研發Shiki Suen, Isaac Xen, Hiraku Wang, 等。\n威注音詞庫維護Shiki Suen。";
/* Class = "NSTextFieldCell"; title = "Placeholder for showing copyright information."; ObjectID = "eo3-TK-0rB"; */
// "eo3-TK-0rB.title" = "Placeholder for showing copyright information.";
/* Class = "NSWindow"; title = "Window"; ObjectID = "gHl-Hx-eQn"; */
"gHl-Hx-eQn.title" = "視窗";
/* Class = "NSTextFieldCell"; title = "By installing the software, click the \"I Accept\" to the terms above:"; ObjectID = "mf8-6e-z7X"; */
"mf8-6e-z7X.title" = "若要安裝該軟體,請接受上述條款。";
/* Class = "NSTextFieldCell"; title = "Stopping the old version. This may take up to one minute…"; ObjectID = "nTo-dx-qfZ"; */
"nTo-dx-qfZ.title" = "等待舊版完全停用,大約需要一分鐘…";

View File

@ -0,0 +1,22 @@
#!/bin/sh
TARGET='vChewing'
login_user=$(/usr/bin/stat -f%Su /dev/console)
OS_Version=$(sw_vers -productVersion)
if [[ ${OS_Version} < 12.0.0 ]]; then
# Copy the wrongfully installed contents to the right location:
cp -r /Library/Input\ Methods/"${TARGET}".app /Users/"${login_user}"/Library/Input\ Methods/ || true
cp -r /Library/Keyboard\ Layouts/"${TARGET}"* /Users/"${login_user}"/Library/Keyboard\ Layouts/ || true
# Clean the wrongfully installed contents:
chown "${login_user}" /Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app || true
chown "${login_user}" /Users/"${login_user}"/Library/Keyboard\ Layouts/"${TARGET}"* || true
sleep 1
rm -rf /Library/Input\ Methods/"${TARGET}".app || true
rm -rf /Library/Keyboard\ Layouts/"${TARGET}"* || true
sleep 1
fi
# Finally, register the input method:
/Users/"${login_user}"/Library/Input\ Methods/"${TARGET}".app/Contents/MacOS/"${TARGET}" install --all || true

View File

@ -0,0 +1,21 @@
#!/bin/sh
killall vChewing || true
if [ "${login_user}" = root ]; then
rm -rf /Library/Input\ Methods/vChewing.app || true
rm -rf /Library/Keyboard\ Layouts/vChewingKeyLayout.bundle || true
rm -rf /Library/Keyboard\ Layouts/vChewing\ Dachen.keylayout || true
rm -rf /Library/Keyboard\ Layouts/vChewing\ ETen.keylayout || true
rm -rf /Library/Keyboard\ Layouts/vChewing\ FakeSeigyou.keylayout || true
rm -rf /Library/Keyboard\ Layouts/vChewing\ IBM.keylayout || true
rm -rf /Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout || true
fi
rm -rf ~/Library/Input\ Methods/vChewing.app
rm -rf ~/Library/Keyboard\ Layouts/vChewingKeyLayout.bundle
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ Dachen.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ ETen.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ FakeSeigyou.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ IBM.keylayout
rm -rf ~/Library/Keyboard\ Layouts/vChewing\ MiTAC.keylayout

View File

@ -0,0 +1,11 @@
注意事项:
一、macOS 10.x-11.x 系统有 Bug、令该安装程式无法自动将安装目标设为当前使用者资料夹。如果您在 macOS 12 Monterey 之前的系统安装该输入法的话,请务必「手动」将安装目的地设为当前使用者资料夹。否则,当您今后(在升级系统之后)升级输入法的时候,可能会出现各种混乱情况。下述 sudo 指令会将任何安装到错误位置的档案全部移除:
sudo bash /Users/$(stat -f%Su /dev/console)/Library/Input\ Methods/vChewing.app/Contents/Resources/fixinstall.sh
二、安装完毕之后,如果输入法无法正常使用的话,请重新登入即可。
三、终端部署指令(不限作业系统版本):
installer -pkg ~/Downloads/vChewing-macOS-?.?.?-unsigned.pkg -target CurrentUserHomeDirectory
顺颂时祺
威注音输入法

View File

@ -0,0 +1,11 @@
注意事項:
一、macOS 10.x-11.x 系統有 Bug、令該安裝程式無法自動將安裝目標設為當前使用者資料夾。如果您在 macOS 12 Monterey 之前的系統安裝該輸入法的話,請務必「手動」將安裝目的地設為當前使用者資料夾。否則,當您今後(在升級系統之後)升級輸入法的時候,可能會出現各種混亂情況。下述 sudo 指令會將任何安裝到錯誤位置的檔案全部移除:
sudo bash /Users/$(stat -f%Su /dev/console)/Library/Input\ Methods/vChewing.app/Contents/Resources/fixinstall.sh
二、安裝完畢之後,如果輸入法無法正常使用的話,請重新登入即可。
三、終端部署指令(不限作業系統版本):
installer -pkg ~/Downloads/vChewing-macOS-?.?.?-unsigned.pkg -target CurrentUserHomeDirectory
順頌時祺
威注音輸入法

View File

@ -0,0 +1,11 @@
Notice:
1. Due to a bug in macOS 10.x and 11.x, if you are trying to install this input method on macOS releases earlier than macOS 12 Monterey, PLEASE manually choose the install target to the user folder. Otherwise, there will be problems when you are trying to install this input method to later versions when your OS gets upgraded to macOS 12 Monterey or later. The following terminal command can solve such probelems by removing all incorrectly-installed files (must use sudo):
sudo bash /Users/$(stat -f%Su /dev/console)/Library/Input\ Methods/vChewing.app/Contents/Resources/fixinstall.sh
2. Feel free to logout and re-login if the input method doesn't work after installation.
3. IT maintainers can also use this terminal deploy command, regardless the macOS version:
installer -pkg ~/Downloads/vChewing-macOS-?.?.?-unsigned.pkg -target CurrentUserHomeDirectory
Warm Regards,
vChewing Input Method

View File

@ -0,0 +1,11 @@
ご注意:
 macOS 12 Monterey 以前の OSmacOS 10.x-11.xのバグのため、macOS 10.x-11.x でインストールする場合、この入力アプリ必ずご自分でユーザーフォルダをインストール先と設定してください。然もないと、いずれ macOS 12 にアップデートし、この入力アプリのもっと新しいバージョンをインストールする時に、予測できない支障が生ずる恐れがあります。下記のターミナル指令を(必ず下記のまま sudo で)実行すれば、この様な支障を解決することができます:
sudo bash /Users/$(stat -f%Su /dev/console)/Library/Input\ Methods/vChewing.app/Contents/Resources/fixinstall.sh
● そして、インストール直後、入力アプリがうまく使えない場合、再ログインすれば済ませることです。
 あと、IT 担当者は、Terminal で実装したい場合、OS バージョンを問わずに、下記の指令をご参考ください:
installer -pkg ~/Downloads/vChewing-macOS-?.?.?-unsigned.pkg -target CurrentUserHomeDirectory
よろしくお願いいたします。
威注音入力アプリ

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>org.atelierInmu.vChewing.keyLayouts</string>
<key>CFBundleName</key>
<string>vChewingKeyLayout</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>KLInfo_vChewing Dachen</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISInputSourceID</key>
<string>org.atelierInmu.vChewing.keyLayouts.vchewingdachen</string>
<key>TISIntendedLanguage</key>
<string>zh-Hanb</string>
</dict>
<key>KLInfo_vChewing ETen</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISInputSourceID</key>
<string>org.atelierInmu.vChewing.keyLayouts.vchewingeten</string>
<key>TISIntendedLanguage</key>
<string>zh-Hanb</string>
</dict>
<key>KLInfo_vChewing FakeSeigyou</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISInputSourceID</key>
<string>org.atelierInmu.vChewing.keyLayouts.vchewingfakeseigyou</string>
<key>TISIntendedLanguage</key>
<string>zh-Hanb</string>
</dict>
<key>KLInfo_vChewing IBM</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISInputSourceID</key>
<string>org.atelierInmu.vChewing.keyLayouts.vchewingibm</string>
<key>TISIntendedLanguage</key>
<string>zh-Hanb</string>
</dict>
<key>KLInfo_vChewing MiTAC</key>
<dict>
<key>TICapsLockLanguageSwitchCapable</key>
<false/>
<key>TISInputSourceID</key>
<string>org.atelierInmu.vChewing.keyLayouts.vchewingmitac</string>
<key>TISIntendedLanguage</key>
<string>zh-Hanb</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildVersion</key>
<string>22M2</string>
<key>ProjectName</key>
<string>vChewingKeyLayout</string>
<key>SourceVersion</key>
<string></string>
</dict>
</plist>

50
LICENSE-CHS.txt Normal file
View File

@ -0,0 +1,50 @@
免责声明:威注音专案对小麦注音官方专案内赠的小麦注音原版词库内容不负任何责任。威注音输入法专用的威注音官方词库不包含任何「会在法理上妨碍威注音在全球传播」的「与地缘政治及政治意识形态有关的」内容。威注音专案与 OpenVanilla 专案之间无合作关系、无隶属关系。
vChewing macOS: MIT-NTL License 麻理(去商标)授权合约
© 2021-2022 vChewing Project.
威注音 macOS 程式研发Shiki Suen, Isaac Xen, Hiraku Wang, 等。
威注音词库维护Shiki Suen。
软件之著作权利人依此麻理授权条款,将其对于软件之著作权利授权释出,只须使用者践履以下二项麻理授权条款叙明之义务性规定,其即享有对此软件程式及其相关说明文档自由不受限制地进行利用之权利,范围包括「使用、重制、修改、合并、出版、散布、再授权、及贩售程式重制作品」等诸多方面之应用,而散布程式之人、更可将上述权利传递予其后收受程式之后手,倘若其后收受程式之人亦服膺以下二项麻理授权条款之义务性规定,则其对程式亦享有与前手运用范围相同之同一权利。
甲、散布此一软件程式者,须将本条款其上之「著作权声明」及以下之「免责声明」内嵌于软件程式及其重制作品之实体之中。
乙、敝授权合约不提供对「贡献者」之商品名称、商标、服务标志或产品名称之商标许可,除非用以满足履行上文所述义务之必要。
因麻理软件程式之授权模式乃是无偿提供,是以在现行法律之架构下可以主张合理之免除担保责任。麻理软件之著作权人或任何之后续散布者,对于其所散布之麻理软件程式皆不负任何形式上实质上之担保责任,明示亦或隐喻、商业利用性亦或特定目之使用性,这些均不在保障之列。利用麻理软件程式之所有风险均由使用者自行担负。假如所使用之麻理程式发生缺陷性问题,使用者需自行担负修正、改正及必要之服务支出。麻理软件程式之著作权人不负任何形式上实质上之担保责任,无论任何一般之、特殊之、偶发之、因果关系式之损害,或是麻理软件程式之不适用性,均须由使用者自行负担。
$ 在本专案内用到了下述第三方模组:
- ShiftKeyUpChecker: (c) 2019 and onwards Qwertyyb (March Yang) (MIT License).
- LineReader: (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
- SwiftUI Preferences UI Framework: (c) 2018 and onwards Sindre Sorhus (MIT License).
- SwiftUI VDKComboBox: (c) 2022 and onwards Bryan Jones (CC BY-SA 4.0)
- Note that Hotenka Chinese Converter is rewritten by Shiki Suen (using Swift) from Nick Chen's Objective-C library "NCChineseConverter" (MIT License).
########################################################
## UPSTREAM LICENSE PROVIDED BELOW INTACT FOR REFERENCE
## https://github.com/openvanilla/McBopomofo/blob/5b319f491142c1170a3607e28ab4e91c22861998/LICENSE.txt
########################################################
MIT License
Copyright (c) 2011-2022 Mengjuei Hsieh et al.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
LICENSE-CHT.txt Normal file
View File

@ -0,0 +1,50 @@
免責聲明:威注音專案對小麥注音官方專案內贈的小麥注音原版詞庫內容不負任何責任。威注音輸入法專用的威注音官方詞庫不包含任何「會在法理上妨礙威注音在全球傳播」的「與地緣政治及政治意識形態有關的」內容。威註音專案與 OpenVanilla 專案之間無合作關係、無隸屬關係。
vChewing macOS: MIT-NTL License 麻理(去商標)授權合約
© 2021-2022 vChewing Project.
威注音 macOS 程式研發Shiki Suen, Isaac Xen, Hiraku Wang, 等。
威注音詞庫維護Shiki Suen。
軟體之著作權利人依此麻理授權條款,將其對於軟體之著作權利授權釋出,只須使用者踐履以下二項麻理授權條款敘明之義務性規定,其即享有對此軟體程式及其相關說明文檔自由不受限制地進行利用之權利,範圍包括「使用、重製、修改、合併、出版、散布、再授權、及販售程式重製作品」等諸多方面之應用,而散布程式之人、更可將上述權利傳遞予其後收受程式之後手,倘若其後收受程式之人亦服膺以下二項麻理授權條款之義務性規定,則其對程式亦享有與前手運用範圍相同之同一權利。
甲、散布此一軟體程式者,須將本條款其上之「著作權聲明」及以下之「免責聲明」內嵌於軟體程式及其重製作品之實體之中。
乙、敝授權合約不提供對「貢獻者」之商品名稱、商標、服務標誌或產品名稱之商標許可,除非用以滿足履行上文所述義務之必要。
因麻理軟體程式之授權模式乃是無償提供,是以在現行法律之架構下可以主張合理之免除擔保責任。麻理軟體之著作權人或任何之後續散布者,對於其所散布之麻理軟體程式皆不負任何形式上實質上之擔保責任,明示亦或隱喻、商業利用性亦或特定目之使用性,這些均不在保障之列。利用麻理軟體程式之所有風險均由使用者自行擔負。假如所使用之麻理程式發生缺陷性問題,使用者需自行擔負修正、改正及必要之服務支出。麻理軟體程式之著作權人不負任何形式上實質上之擔保責任,無論任何一般之、特殊之、偶發之、因果關係式之損害,或是麻理軟體程式之不適用性,均須由使用者自行負擔。
$ 在該專案內用到了下述第三方:
- ShiftKeyUpChecker: (c) 2019 and onwards Qwertyyb (March Yang) (MIT License).
- LineReader: (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
- SwiftUI Preferences UI Framework: (c) 2018 and onwards Sindre Sorhus (MIT License).
- SwiftUI VDKComboBox: (c) 2022 and onwards Bryan Jones (CC BY-SA 4.0)
- Note that Hotenka Chinese Converter is rewritten by Shiki Suen (using Swift) from Nick Chen's Objective-C library "NCChineseConverter" (MIT License).
########################################################
## UPSTREAM LICENSE PROVIDED BELOW INTACT FOR REFERENCE
## https://github.com/openvanilla/McBopomofo/blob/5b319f491142c1170a3607e28ab4e91c22861998/LICENSE.txt
########################################################
MIT License
Copyright (c) 2011-2022 Mengjuei Hsieh et al.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
LICENSE-JPN.txt Normal file
View File

@ -0,0 +1,50 @@
免責事項vChewing Project は、OpenVanilla と協力関係や提携関係にあるわけではなく、OpenVanilla が小麦注音プロジェクトに同梱した辞書データについて、vChewing Project は一切責任負い兼ねる。特定な地政学的・観念形態的な内容は、vChewing アプリの世界的な普及に妨害する恐れがあるため、vChewing 公式辞書データに不収録。
vChewing macOS: MIT商標不許可ライセンス (MIT-NTL License)
© 2021-2022 vChewing Project.
macOS 版威注音の開発Shiki Suen, Isaac Xen, Hiraku Wang, など。
威注音語彙データの維持Shiki Suen。
以下に定める条件に従い、本ソフトウェアおよび関連文書のファイル(以下「ソフトウェア」)の複製を取得するすべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複写、変更、結合、掲載、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供する相手に同じことを許可する権利も無制限に含まれます。
イ)上記の著作権表示および本許諾表示を、ソフトウェアのすべての複製または重要な部分に記載するものとします。
ロ)上記の通知要件を満たすために必要な場合を除き、コントリビューターの商号、商標、サービスマーク、または製品名を使用するための商標ライセンスは付与されていません。
ソフトウェアは「現状のまま」で、明示であるか暗黙であるかを問わず、何らの保証もなく提供されます。ここでいう保証とは、商品性、特定の目的への適合性、および権利非侵害についての保証も含みますが、それに限定されるものではありません。 作者または著作権者は、契約行為、不法行為、またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの使用またはその他の扱いによって生じる一切の請求、損害、その他の義務について何らの責任も負わないものとします。
$ 他の用いたモジュール:
- ShiftKeyUpChecker: (c) 2019 and onwards Qwertyyb (March Yang) (MIT License).
- LineReader: (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
- SwiftUI Preferences UI Framework: (c) 2018 and onwards Sindre Sorhus (MIT License).
- SwiftUI VDKComboBox: (c) 2022 and onwards Bryan Jones (CC BY-SA 4.0)
- Note that Hotenka Chinese Converter is rewritten by Shiki Suen (using Swift) from Nick Chen's Objective-C library "NCChineseConverter" (MIT License).
########################################################
## UPSTREAM LICENSE PROVIDED BELOW INTACT FOR REFERENCE
## https://github.com/openvanilla/McBopomofo/blob/5b319f491142c1170a3607e28ab4e91c22861998/LICENSE.txt
########################################################
MIT License
Copyright (c) 2011-2022 Mengjuei Hsieh et al.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

50
LICENSE.txt Normal file
View File

@ -0,0 +1,50 @@
DISCLAIMER: The vChewing project, having no relationship of cooperation or affiliation with the OpenVanilla project, is not responsible for the phrase database shipped in the original McBopomofo project. Certain geopolitical and ideological contents, which are potentially harmful to the global spread of this software, are not included in vChewing official phrase database.
vChewing macOS: MIT-NTL License
© 2021-2022 vChewing Project.
vChewing macOS Development: Shiki Suen, Isaac Xen, Hiraku Wang, etc.
vChewing Phrase Database Maintained by Shiki Suen.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2. No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
$ 3rd-Party Modules Used:
- ShiftKeyUpChecker: (c) 2019 and onwards Qwertyyb (March Yang) (MIT License).
- LineReader: (c) 2019 and onwards Robert Muckle-Jones (Apache 2.0 License).
- SwiftUI Preferences UI Framework: (c) 2018 and onwards Sindre Sorhus (MIT License).
- SwiftUI VDKComboBox: (c) 2022 and onwards Bryan Jones (CC BY-SA 4.0)
- Note that Hotenka Chinese Converter is rewritten by Shiki Suen (using Swift) from Nick Chen's Objective-C library "NCChineseConverter" (MIT License).
########################################################
## UPSTREAM LICENSE PROVIDED BELOW INTACT FOR REFERENCE
## https://github.com/openvanilla/McBopomofo/blob/5b319f491142c1170a3607e28ab4e91c22861998/LICENSE.txt
########################################################
MIT License
Copyright (c) 2011-2022 Mengjuei Hsieh et al.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
Makefile Normal file
View File

@ -0,0 +1,66 @@
+.PHONY: all
all: release
install: install-release
update:
@git restore Source/Data/
git submodule update --init --recursive --remote --force
ifdef ARCHS
BUILD_SETTINGS += ARCHS="$(ARCHS)"
BUILD_SETTINGS += ONLY_ACTIVE_ARCH=NO
endif
release:
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) build
debug:
xcodebuild -project vChewing.xcodeproj -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) build
DSTROOT = /Library/Input Methods
VC_APP_ROOT = $(DSTROOT)/vChewing.app
.PHONY: clang-format lint batchfix format clang-format-swift clang-format-cpp
format: batchfix clang-format lint
clang-format:
@git ls-files --exclude-standard | grep -E '\.swift$$' | xargs swift-format format --in-place --configuration ./.clang-format-swift.json --parallel
@git ls-files --exclude-standard | grep -E '\.swift$$' | xargs swift-format lint --configuration ./.clang-format-swift.json --parallel
lint:
@git ls-files --exclude-standard | grep -E '\.swift$$' | xargs swift-format lint --configuration ./.clang-format-swift.json --parallel
batchfix:
@git ls-files --exclude-standard | grep -E '\.swift$$' | swiftlint --fix --autocorrect
advanced-lint:
@swiftformat --swiftversion 5.5 --indent 2 ./
.PHONY: permission-check install-debug install-release
permission-check:
[ -w "$(DSTROOT)" ] && [ -w "$(VC_APP_ROOT)" ] || sudo chown -R ${USER} "$(DSTROOT)"
install-debug: permission-check
open Build/Products/Debug/vChewingInstaller.app
install-release: permission-check
open Build/Products/Release/vChewingInstaller.app
.PHONY: clean
clean:
xcodebuild -scheme vChewingInstaller -configuration Debug $(BUILD_SETTINGS) clean
xcodebuild -scheme vChewingInstaller -configuration Release $(BUILD_SETTINGS) clean
make clean --file=./Source/Data/Makefile || true
.PHONY: gc
gc:
git reflog expire --expire=now --all && git gc --prune=now --aggressive
.PHONY: test
test:
xcodebuild -project vChewing.xcodeproj -scheme vChewing -configuration Debug test

129
README-CHS.md Normal file
View File

@ -0,0 +1,129 @@
语言:*简体中文* | [繁體中文](./README.md)
仅以此 README 纪念祁建华 (CHIEN-HUA CHI, 1921-2001)。
---
有关该仓库及该输入法的最新资讯请洽产品主页https://vchewing.github.io/
因不可控原因,该仓库只能保证在 Gitee 有最新的内容可用:
- 下载https://gitee.com/vchewing/vChewing-macOS/releases
- 源码仓库https://gitee.com/vchewing/vChewing-macOS
# vChewing 威注音输入法
威注音输入法基于小麦注音二次开发,是**原生简体中文、原生繁体中文注音输入法**
- 威注音是业界现阶段支持注音排列种类数量与输入用拼音种类数量最多的注音输入法。
- 受威注音自家的铁恨注音并击引擎加持。
- 威注音的原厂词库内不存在任何可以妨碍该输入法在世界上任何地方传播的内容。
- 相比中州韵(鼠须管)而言,威注音能够做到真正的大千声韵并击。
- 拥有拼音并击模式,不懂注音的人群也可以受益于该输入法所带来的稳定的平均输入速度。
- 相比小鹤双拼等双拼方案而言,威注音双手声韵分工明确、且重码率只有双拼的五分之一。
- 威注音对陆规审音完全相容:不熟悉台澎金马审音的大陆用户不会遇到与汉字读音有关的不便。
- 反之亦然。
>威注音有很多特色功能。在此仅列举部分:
>- 支持 macOS 屏幕模拟键盘(仅传统大千与传统倚天布局)。
>- 可以将自己打的繁体中文自动转成日本 JIS 新字体来输出(包括基础的字词转换)、也可以转成康熙繁体来输出。
>- 简繁体中文语料库彼此分离,彻底杜绝任何繁简转换过程可能造成的失误。
>- 支持近年的全字库汉字输入。
>- 可以自动整理使用者语汇档案格式、自订联想词。
>- ……
威注音分支专案及威注音词库由孙志贵Shiki Suen维护其内容属于可在 Gitee 公开展示的合法内容。小麦注音官方原始仓库内的词库的内容均与孙志贵无关。
P.S.: 威注音输入法的 Shift 按键监测功能仅借由对 NSEvent 讯号资料流的上下文关系的观测来实现,仅接触借由 macOS 系统内建的 InputMethodKit 当中的 IMKServer 传来的 NSEvent 讯号资料流、而无须监听系统全局键盘事件,也无须向使用者申请用以达成这类「可能会引发资安疑虑」的行为所需的辅助权限,更不会将您的电脑内的任何资料传出去(本来就是这样,且自威注音 2.3.0 版引入的 Sandbox 特性更杜绝了这种可能性。请放心使用。Shift 中英模式切换功能要求至少 macOS 10.15 Catalina 才可以用。
## 系统需求
编译用系统需求:
- 至少 macOS 12 Monterey & Xcode 13.4.1。
- 原因Swift 封包管理支援与 Swift 5.5 所需,且仓库内包含了需要 Xcode 13.4.1 才能正常编译的内容App 型安装程式)。
- 我们已经没有条件测试比 Xcode 13.4.1 更老的环境了。硬要在这个环境下编译的话,可能需要额外安装[新版 Swift](https://www.swift.org/download/) 才可以。
- 请使用正式发行版 Xcode且最小子版本号越高越好因为 Bug 相对而言最少)。
- 如果是某个大版本的 Xcode 的 Release Candidate 版本的话,我们可能会对此做相容性测试。
编译出的成品对应系统需求:
- 至少 macOS El Capitan 10.11.5,否则无法处理 Unicode 8.0 的汉字。即便如此,仍需手动升级苹方至至少 macOS 10.12 开始随赠的版本、以支持 Unicode 8.0 的通用规范汉字表用字(全字库没有「𫫇」字)。
- 保留该系统支持的原因:非 Unibody 机种的 MacBook Pro 支持的最后一版 macOS 就是 El Capitan。
- **推荐最低系统版本**macOS 10.12 Sierra对 Unicode 8.0 开始的《通用规范汉字表》汉字有原生的苹方支持。
- 同时建议**系统运存应至少 4GB**。威注音输入法占用运存约 115MB 左右简繁双模式、75MB左右单模式供参考。
- 请务必使用 SSD 硬盘,否则可能会影响每次开机之后输入法首次载入的速度。从 10.10 Yosemite 开始macOS 就已经是针对机械硬盘负优化的操作系统了。
- 注:能装 macOS 10.13 High Sierra 就不要去碰 macOS 10.12 Sierra 这个半成品。
- 关于全字库支持,因下述事实而在理论上很难做到最完美:
- 很可惜 GB18030-2005 并没有官方提供的逐字读音对照表,所以目前才用了全字库。然而全字库并不等于完美。
- 有条件者可以安装全字库字型与花园明朝,否则全字库等高万国码码位汉字恐无法在输入法的选字窗内完整显示。
- 全字库汉字显示支持会受到具体系统版本对万国码版本的支持的限制。
- 有些全字库汉字一开始会依赖万国码的私人造字区,且在之后被新版本万国码所支持。
## 编译流程
安装 Xcode 之后,请先配置 Xcode 允许其直接构建在专案所在的资料夹下的 build 资料夹内。步骤:
```
「Xcode」->「Preferences...」->「Locations」
「File」->「Project/WorkspaceSettings...」->「Advanced」
选「Custom」->「Relative to Workspace」即可。不选的话make 的过程会出错。
```
在终端机内定位到威注音的克隆本地专案的本地仓库的目录之后,执行 `make update` 以获取最新词库。
接下来就是直接开 Xcode 专案Product -> Scheme 选「vChewingInstaller」编译即可。
> 之前说「在成功之后执行 `make` 即可编译、再执行 `make install` 可以触发威注音的安装程式」,这对新版威注音而言**当且仅当**使用纯 Swift 编译脚本工序时方可使用。目前的 libvchewing-data 模组已经针对 macOS 版威注音实装了纯 Swift 词库编译脚本。
第一次安装完,日后源码或词库有任何修改,只要重覆上述流程,再次安装威注音即可。
要注意的是 macOS 可能会限制同一次 login session 能终结同一个输入法的执行进程的次数(安装程式透过 kill input method process 来让新版的输入法生效)。如果安装若干次后,发现程式修改的结果并没有出现、或甚至输入法已无法再选用,只需要登出目前的 macOS 系统帐号、再重新登入即可。
补记: 该输入法是在 2021 年 11 月初「28ae7deb4092f067539cff600397292e66a5dd56」这一版小麦注音编译的基础上完成的。因为在清洗词库的时候清洗了全部的 git commit 历史,所以无法自动从小麦注音官方仓库上游继承任何改动,只能手动同步任何在此之后的程式修正。最近一次同步参照是上游主仓库的 2.2.2 版、以及 zonble 的分支「5cb6819e132a02bbcba77dbf083ada418750dab7」。
## 应用授权
威注音专案目前仅用到小麦注音的下述程式组件MIT License
- 仅供研发人员调试方便而使用的 App 版安装程式 (by Zonble Yang),不对公众使用。
- Voltaire MK2 选字窗、飘云通知视窗 (by Zonble Yang),有大幅度修改。
威注音专案目前还用到如下的来自 Lukhnos Liu 的算法:
- 半衰记忆模组 MK2被 Shiki Suen 用 Swift 重写。
- 基于 Gramambular 2 组字引擎的算法、被 Shiki Suen 用 Swift 重写(详见 [Megrez 组字引擎](https://github.com/vChewing/Megrez))。
威注音输入法 macOS 版以 MIT-NTL License 授权释出 (与 MIT 相容):© 2021-2022 vChewing 专案。
- 威注音输入法 macOS 版程式维护Shiki Suen。特别感谢 Isaac Xen 与 Hiraku Wong 等人的技术协力。
- 铁恨注音并击处理引擎Shiki Suen (MIT-NTL License)。
- 天权星语汇处理引擎Shiki Suen (MIT-NTL License)。
- 威注音词库由 Shiki Suen 维护,以 3-Clause BSD License 授权释出。其中的词频数据[由 NAER 授权用于非商业用途](https://twitter.com/ShikiSuen/status/1479329302713831424)。
使用者可自由使用、散播本软件,惟散播时必须完整保留版权声明及软件授权、且「一旦经过修改便不可以再继续使用威注音的产品名称」。换言之,这条相对上游 MIT 而言新增的规定就是:你 Fork 可以,但 Fork 成单独发行的产品名称时就必须修改产品名称。这条新增规定对 OpenVanilla 与威注音双方都有益,免得各自的旗号被盗版下载贩子等挪用做意外用途。
## 资料来源
原厂词库主要词语资料来源:
- 《重编国语辞典修订本 2015》的六字以内的词语资料 (CC BY-ND 3.0)。
- 《CNS11643中文标准交换码全字库(简称全字库)》 (OGDv1 License)。
- LibTaBE (by Pai-Hsiang Hsiao under 3-Clause BSD License)。
- [《新加坡华语资料库》](https://www.languagecouncils.sg/mandarin/ch/learning-resources/singaporean-mandarin-database)。
- 原始词频资料取自 NAER有经过换算处理与按需调整。
- 威注音并未使用由 LibTaBE 内建的来自 Sinica 语料库的词频资料。
- 威注音语汇库作者自行维护新增的词语资料,包括:
- 尽可能所有字词的陆规审音与齐铁恨广播读音。
- 中国大陆常用资讯电子术语等常用语,以确保简体中文母语者在使用输入法时不会受到审音差异的困扰。
- 其他使用者建议收录的资料。
## 参与研发时的注意事项
欢迎参与威注音的研发。论及相关细则,请洽该仓库内的「[CONTRIBUTING.md](./CONTRIBUTING.md)」档案、以及《[常见问题解答](./FAQ.md)》。
敝专案采用了《[贡献者品行准则承约书 v2.1](./code-of-conduct.md)》。考虑到上游链接给出的中文版翻译与英文原文严重不符合的情况(会出现因执法与被执法双方的认知偏差导致的矛盾,非常容易变成敌我矛盾),敝专案使用了自行翻译的版本、且新增了一些能促进双方共识的注解。
$ EOF.

129
README.md Normal file
View File

@ -0,0 +1,129 @@
語言:[简体中文](./README-CHS.md) | *繁體中文*
僅以此 README 紀念祁建華 (CHIEN-HUA CHI, 1921-2001)。
---
有關該倉庫及該輸入法的最新資訊請洽產品主頁https://vchewing.github.io/
因不可控原因,該倉庫只能保證在 Gitee 有最新的內容可用:
- 下載https://gitee.com/vchewing/vChewing-macOS/releases
- 程式碼倉庫https://gitee.com/vchewing/vChewing-macOS
# vChewing 威注音輸入法
威注音輸入法基於小麥注音二次開發,是**原生簡體中文、原生繁體中文注音輸入法**
- 威注音是業界現階段支援注音排列種類數量與輸入用拼音種類數量最多的注音輸入法。
- 受威注音自家的鐵恨注音並擊引擎加持。
- 威注音的原廠詞庫內不存在任何可以妨礙該輸入法在世界上任何地方傳播的內容。
- 相比中州韻(鼠須管)而言,威注音能夠做到真正的大千聲韻並擊。
- 擁有拼音並擊模式,不懂注音的人群也可以受益於該輸入法所帶來的穩定的平均輸入速度。
- 相比小鶴雙拼等雙拼方案而言,威注音雙手聲韻分工明確、且重碼率只有雙拼的五分之一。
- 威注音對陸規審音完全相容:不熟悉台澎金馬審音的大陸用戶不會遇到與漢字讀音有關的不便。
- 反之亦然。
>威注音有很多特色功能。在此僅列舉部分:
>- 支援 macOS 螢幕模擬鍵盤(僅傳統大千與傳統倚天佈局)。
>- 可以將自己打的繁體中文自動轉成日本 JIS 新字體來輸出(包括基礎的字詞轉換)、也可以轉成康熙繁體來輸出。
>- 簡繁體中文語料庫彼此分離,徹底杜絕任何繁簡轉換過程可能造成的失誤。
>- 支援近年的全字庫漢字輸入。
>- 可以自動整理使用者語彙檔案格式、自訂聯想詞。
>- ……
威注音分支專案及威注音詞庫由孫志貴Shiki Suen維護其內容屬於可在 Gitee 公開展示的合法內容。小麥注音官方原始倉庫內的詞庫的內容均與孫志貴無關。
P.S.: 威注音輸入法的 Shift 按鍵監測功能僅藉由對 NSEvent 訊號資料流的上下文關係的觀測來實現,僅接觸藉由 macOS 系統內建的 InputMethodKit 當中的 IMKServer 傳來的 NSEvent 訊號資料流、而無須監聽系統全局鍵盤事件,也無須向使用者申請用以達成這類「可能會引發資安疑慮」的行為所需的輔助權限,更不會將您的電腦內的任何資料傳出去(本來就是這樣,且自威注音 2.3.0 版引入的 Sandbox 特性更杜絕了這種可能性。請放心使用。Shift 中英模式切換功能要求至少 macOS 10.15 Catalina 才可以用。
## 系統需求
建置用系統需求:
- 至少 macOS 12 Monterey & Xcode 13.4.1。
- 原因Swift 封包管理支援與 Swift 5.5 所需,且倉庫內包含了需要 Xcode 13.4.1 才能正常編譯的內容App 型安裝程式)。
- 我們已經沒有條件測試比 Xcode 13.4.1 更老的環境了。硬要在這個環境下編譯的話,可能需要額外安裝[新版 Swift](https://www.swift.org/download/) 才可以。
- 請使用正式發行版 Xcode且最小子版本號越高越好因為 Bug 相對而言最少)。
- 如果是某個大版本的 Xcode 的 Release Candidate 版本的話,我們可能會對此做相容性測試。
編譯出的成品對應系統需求:
- 至少 macOS El Capitan 10.11.5,否則無法處理 Unicode 8.0 的漢字。即便如此,仍需手動升級蘋方至至少 macOS 10.12 開始隨贈的版本、以支援 Unicode 8.0 的通用規範漢字表用字(全字庫沒有「𫫇」字)。
- 保留該系統支援的原因:非 Unibody 機種的 MacBook Pro 支援的最後一版 macOS 就是 El Capitan。
- **推薦最低系統版本**macOS 10.12 Sierra對 Unicode 8.0 開始的《通用規範漢字表》漢字有原生的蘋方支援。
- 同時建議**系統記憶體應至少 4GB**。威注音輸入法佔用記憶體約 115MB 左右簡繁雙模式、75MB左右單模式供參考。
- 請務必使用 SSD 硬碟,否則可能會影響每次開機之後輸入法首次載入的速度。從 10.10 Yosemite 開始macOS 就已經是針對機械硬碟負優化的作業系統了。
- 注:能裝 macOS 10.13 High Sierra 就不要去碰 macOS 10.12 Sierra 這個半成品。
- 關於全字庫支援,因下述事實而在理論上很難做到最完美:
- 很可惜 GB18030-2005 並沒有官方提供的逐字讀音對照表,所以目前才用了全字庫。然而全字庫並不等於完美。
- 有條件者可以安裝全字庫字型與花園明朝,否則全字庫等高萬國碼碼位漢字恐無法在輸入法的選字窗內完整顯示。
- 全字庫漢字顯示支援會受到具體系統版本對萬國碼版本的支援的限制。
- 有些全字庫漢字一開始會依賴萬國碼的私人造字區,且在之後被新版本萬國碼所支援。
## 建置流程
安裝 Xcode 之後,請先配置 Xcode 允許其直接構建在專案所在的資料夾下的 build 資料夾內。步驟:
```
「Xcode」->「Preferences...」->「Locations」
「File」->「Project/WorkspaceSettings...」->「Advanced」
選「Custom」->「Relative to Workspace」即可。不選的話make 的過程會出錯。
```
在終端機內定位到威注音的克隆本地專案的本地倉庫的目錄之後,執行 `make update` 以獲取最新詞庫。
接下來就是直接開 Xcode 專案Product -> Scheme 選「vChewingInstaller」編譯即可。
> 之前說「在成功之後執行 `make` 即可組建、再執行 `make install` 可以觸發威注音的安裝程式」,這對新版威注音而言**當且僅當**使用純 Swift 編譯腳本工序時方可使用。目前的 libvchewing-data 模組已經針對 macOS 版威注音實裝了純 Swift 詞庫編譯腳本。
第一次安裝完,日後程式碼或詞庫有任何修改,只要重覆上述流程,再次安裝威注音即可。
要注意的是 macOS 可能會限制同一次 login session 能終結同一個輸入法的執行進程的次數(安裝程式透過 kill input method process 來讓新版的輸入法生效)。如果安裝若干次後,發現程式修改的結果並沒有出現、或甚至輸入法已無法再選用,只需要登出目前的 macOS 系統帳號、再重新登入即可。
補記: 該輸入法是在 2021 年 11 月初「28ae7deb4092f067539cff600397292e66a5dd56」這一版小麥注音建置的基礎上完成的。因為在清洗詞庫的時候清洗了全部的 git commit 歷史,所以無法自動從小麥注音官方倉庫上游繼承任何改動,只能手動同步任何在此之後的程式修正。最近一次同步參照是上游主倉庫的 2.2.2 版、以及 zonble 的分支「5cb6819e132a02bbcba77dbf083ada418750dab7」。
## 應用授權
威注音專案目前僅用到小麥注音的下述程式組件MIT License
- 僅供研發人員調試方便而使用的 App 版安裝程式 (by Zonble Yang),不對公眾使用。
- Voltaire MK2 選字窗、飄雲通知視窗 (by Zonble Yang),有大幅度修改。
威注音專案目前還用到如下的來自 Lukhnos Liu 的算法:
- 半衰記憶模組 MK2被 Shiki Suen 用 Swift 重寫。
- 基於 Gramambular 2 組字引擎的算法、被 Shiki Suen 用 Swift 重寫(詳見 [Megrez 組字引擎](https://github.com/vChewing/Megrez))。
威注音輸入法 macOS 版以 MIT-NTL License 授權釋出 (與 MIT 相容):© 2021-2022 vChewing 專案。
- 威注音輸入法 macOS 版程式維護Shiki Suen。特別感謝 Isaac Xen 與 Hiraku Wong 等人的技術協力。
- 鐵恨注音並擊處理引擎Shiki Suen (MIT-NTL License)。
- 天權星語彙處理引擎Shiki Suen (MIT-NTL License)。
- 威注音詞庫由 Shiki Suen 維護,以 3-Clause BSD License 授權釋出。其中的詞頻數據[由 NAER 授權用於非商業用途](https://twitter.com/ShikiSuen/status/1479329302713831424)。
使用者可自由使用、散播本軟體,惟散播時必須完整保留版權聲明及軟體授權、且「一旦經過修改便不可以再繼續使用威注音的產品名稱」。換言之,這條相對上游 MIT 而言新增的規定就是:你 Fork 可以,但 Fork 成單獨發行的產品名稱時就必須修改產品名稱。這條新增規定對 OpenVanilla 與威注音雙方都有益,免得各自的旗號被盜版下載販子等挪用做意外用途。
## 資料來源
原廠詞庫主要詞語資料來源:
- 《重編國語辭典修訂本 2015》的六字以內的詞語資料 (CC BY-ND 3.0)。
- 《CNS11643中文標準交換碼全字庫(簡稱全字庫)》 (OGDv1 License)。
- LibTaBE (by Pai-Hsiang Hsiao under 3-Clause BSD License)。
- [《新加坡華語資料庫》](https://www.languagecouncils.sg/mandarin/ch/learning-resources/singaporean-mandarin-database)。
- 原始詞頻資料取自 NAER有經過換算處理與按需調整。
- 威注音並未使用由 LibTaBE 內建的來自 Sinica 語料庫的詞頻資料。
- 威注音語彙庫作者自行維護新增的詞語資料,包括:
- 盡可能所有字詞的陸規審音與齊鐵恨廣播讀音。
- 中國大陸常用資訊電子術語等常用語,以確保簡體中文母語者在使用輸入法時不會受到審音差異的困擾。
- 其他使用者建議收錄的資料。
## 參與研發時的注意事項
歡迎參與威注音的研發。論及相關細則,請洽該倉庫內的「[CONTRIBUTING.md](./CONTRIBUTING.md)」檔案、以及《[常見問題解答](./FAQ.md)》。
敝專案採用了《[貢獻者品行準則承約書 v2.1](./code-of-conduct.md)》。考慮到上游鏈接給出的中文版翻譯與英文原文嚴重不符合的情況(會出現因執法與被執法雙方的認知偏差導致的矛盾,非常容易變成敵我矛盾),敝專案使用了自行翻譯的版本、且新增了一些能促進雙方共識的註解。
$ EOF.

View File

@ -0,0 +1,58 @@
// (c) 2018 Daniel Galasko
// Ref: https://medium.com/over-engineering/monitoring-a-folder-for-changes-in-ios-dc3f8614f902
import Foundation
class FolderMonitor {
// MARK: Properties
/// A file descriptor for the monitored directory.
private var monitoredFolderFileDescriptor: CInt = -1
/// A dispatch queue used for sending file changes in the directory.
private let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent)
/// A dispatch source to monitor a file descriptor created from the directory.
private var folderMonitorSource: DispatchSourceFileSystemObject?
/// URL for the directory being monitored.
let url: URL
var folderDidChange: (() -> Void)?
// MARK: Initializers
init(url: URL) {
self.url = url
}
// MARK: Monitoring
/// Listen for changes to the directory (if we are not already).
func startMonitoring() {
guard folderMonitorSource == nil, monitoredFolderFileDescriptor == -1 else {
return
}
// Open the directory referenced by URL for monitoring only.
monitoredFolderFileDescriptor = open(url.path, O_EVTONLY)
// Define a dispatch source monitoring the directory for additions, deletions, and renamings.
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: monitoredFolderFileDescriptor, eventMask: .write, queue: folderMonitorQueue
)
// Define the block to call when a file change is detected.
folderMonitorSource?.setEventHandler { [weak self] in
self?.folderDidChange?()
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
folderMonitorSource?.setCancelHandler { [weak self] in
guard let strongSelf = self else { return }
close(strongSelf.monitoredFolderFileDescriptor)
strongSelf.monitoredFolderFileDescriptor = -1
strongSelf.folderMonitorSource = nil
}
// Start monitoring the directory via the source.
folderMonitorSource?.resume()
}
/// Stop listening for changes to the directory, if the source has been created.
func stopMonitoring() {
folderMonitorSource?.cancel()
}
}

View File

@ -0,0 +1,113 @@
// (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 enum ChineseConverter {
public static let shared = HotenkaChineseConverter(plistDir: mgrLangModel.getBundleDataPath("convdict"))
private static var punctuationConversionTable: [(String, String)] = [
("", ""), ("", ""), ("", ""), ("", ""), ("", ""), ("", ""), ("", ""), ("", ""),
("", "︿"), ("", ""), ("", ""), ("", ""), ("", ""), ("", ""), ("", ""), ("", ""),
("", ""), ("", ""), ("", ""), ("", ""),
]
///
/// 使使
/// - Parameters:
/// - target:
/// - convert:
public static func hardenVerticalPunctuations(target: inout String, convert: Bool = false) {
guard convert else { return }
for neta in ChineseConverter.punctuationConversionTable {
target = target.replacingOccurrences(of: neta.0, with: neta.1)
}
}
// JIS
private static func processKanjiRepeatSymbol(target: inout String) {
guard !target.isEmpty else { return }
var arr = target.charComponents
for (i, char) in arr.enumerated() {
if i == 0 { continue }
if char == target.charComponents[i - 1] {
arr[i] = ""
}
}
target = arr.joined()
}
///
private static let currencyNumeralDictTable: [String: (String, String, String, String)] = [
"": ("", "", "", ""), "": ("", "", "", ""), "": ("", "", "", ""),
"": ("", "", "", ""), "": ("", "", "", ""), "": ("", "", "", ""),
"": ("", "", "", ""), "": ("", "", "", ""), "": ("", "", "", ""),
"": ("", "", "", ""), "": ("", "", "", ""), "": ("", "", "", ""),
"": ("", "", "", ""), "": ("", "", "", ""),
]
///
/// - Parameter target:
public static func ensureCurrencyNumerals(target: inout String) {
if !mgrPrefs.currencyNumeralsEnabled { return }
for key in currencyNumeralDictTable.keys {
guard let result = currencyNumeralDictTable[key] else { continue }
if IME.currentInputMode == InputMode.imeModeCHS {
target = target.replacingOccurrences(of: key, with: result.3) // Simplified Chinese
continue
}
switch (mgrPrefs.chineseConversionEnabled, mgrPrefs.shiftJISShinjitaiOutputEnabled) {
case (false, true), (true, true): target = target.replacingOccurrences(of: key, with: result.2) // JIS
case (true, false): target = target.replacingOccurrences(of: key, with: result.0) // KangXi
default: target = target.replacingOccurrences(of: key, with: result.1) // Contemporary
}
}
}
private static let tableMappingArabicNumeralsToChinese: [String: String] = [
"0": "", "1": "", "2": "", "3": "", "4": "", "5": "", "6": "", "7": "", "8": "", "9": "",
]
///
/// - Parameter target:
public static func convertArabicNumeralsToChinese(target: String) -> String {
var target = target
for key in tableMappingArabicNumeralsToChinese.keys {
guard let result = tableMappingArabicNumeralsToChinese[key] else { continue }
target = target.replacingOccurrences(of: key, with: result)
}
return target
}
/// CrossConvert.
///
/// - Parameter string: Text in Original Script.
/// - Returns: Text converted to Different Script.
public static func crossConvert(_ string: String) -> String? {
switch IME.currentInputMode {
case InputMode.imeModeCHS:
return shared.convert(string, to: .zhHantTW)
case InputMode.imeModeCHT:
return shared.convert(string, to: .zhHansCN)
default:
return string
}
}
public static func cnvTradToKangXi(_ strObj: String) -> String {
shared.convert(strObj, to: .zhHantKX)
}
public static func cnvTradToJIS(_ strObj: String) -> String {
//
let strObj = cnvTradToKangXi(strObj)
var result = shared.convert(strObj, to: .zhHansJP)
processKanjiRepeatSymbol(target: &result)
return result
}
}

View File

@ -0,0 +1,192 @@
// Swiftified by (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// Rebranded from (c) Nick Chen's Obj-C library "NCChineseConverter" (MIT 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
#if os(Linux)
import Glibc
#else
import Darwin
#endif
public enum DictType {
case zhHantTW
case zhHantHK
case zhHansSG
case zhHansJP
case zhHantKX
case zhHansCN
}
public class HotenkaChineseConverter {
private(set) var dict: [String: [String: String]]
private var dictFiles: [String: [String]]
public init(plistDir: String) {
dictFiles = .init()
do {
let rawData = try Data(contentsOf: URL(fileURLWithPath: plistDir))
let rawPlist: [String: [String: String]] =
try PropertyListSerialization.propertyList(from: rawData, format: nil) as? [String: [String: String]] ?? .init()
dict = rawPlist
} catch {
NSLog("// Exception happened when reading dict plist at: \(plistDir).")
dict = .init()
}
}
public init(dictDir: String) {
dictFiles = [
"zh2TW": [String](),
"zh2HK": [String](),
"zh2SG": [String](),
"zh2JP": [String](),
"zh2KX": [String](),
"zh2CN": [String](),
]
dict = [
"zh2TW": [String: String](),
"zh2HK": [String: String](),
"zh2SG": [String: String](),
"zh2JP": [String: String](),
"zh2KX": [String: String](),
"zh2CN": [String: String](),
]
let enumerator = FileManager.default.enumerator(atPath: dictDir)
guard let filePaths = enumerator?.allObjects as? [String] else { return }
let arrFiles = filePaths.filter { $0.contains(".txt") }.compactMap { URL(string: dictDir + $0) }
for theURL in arrFiles {
let fullFilename = theURL.lastPathComponent
let mainFilename = fullFilename.substring(to: fullFilename.range(of: ".").lowerBound)
if var neta = dictFiles[mainFilename] {
neta.append(theURL.path)
dictFiles[mainFilename] = neta
} else {
dictFiles[mainFilename] = [theURL.path]
}
}
for dictType in dictFiles.keys {
guard let arrFiles = dictFiles[dictType] else { continue }
if arrFiles.count <= 0 {
continue
}
for filePath in arrFiles {
if !FileManager.default.fileExists(atPath: filePath) {
continue
}
do {
let arrLines = try String(contentsOfFile: filePath, encoding: .utf8).split(separator: "\n")
for line in arrLines {
let arrWords = line.split(separator: "\t")
if arrWords.count == 2 {
if var theSubDict = dict[dictType] {
theSubDict[String(arrWords[0])] = String(arrWords[1])
dict[dictType] = theSubDict
} else {
dict[dictType] = .init()
}
}
}
} catch {
continue
}
}
}
sleep(1)
}
// MARK: - Public Methods
func convert(_ target: String, to dictType: DictType) -> String {
var dictTypeKey: String
switch dictType {
case .zhHantTW:
dictTypeKey = "zh2TW"
case .zhHantHK:
dictTypeKey = "zh2HK"
case .zhHansSG:
dictTypeKey = "zh2SG"
case .zhHansJP:
dictTypeKey = "zh2JP"
case .zhHantKX:
dictTypeKey = "zh2KX"
case .zhHansCN:
dictTypeKey = "zh2CN"
}
var result = ""
guard let useDict = dict[dictTypeKey] else { return target }
var i = 0
while i < (target.count) {
let max = (target.count) - i
var j: Int
j = max
innerloop: while j > 0 {
let start = target.index(target.startIndex, offsetBy: i)
let end = target.index(target.startIndex, offsetBy: i + j)
guard let useDictSubStr = useDict[String(target[start..<end])] else {
j -= 1
continue
}
result = result + useDictSubStr
break innerloop
}
if j == 0 {
let start = target.index(target.startIndex, offsetBy: i)
let end = target.index(target.startIndex, offsetBy: i + 1)
result = result + String(target[start..<end])
i += 1
} else {
i += j
}
}
return result
}
}
// MARK: - String extensions
extension String {
fileprivate func range(of str: String) -> Range<Int> {
var start = -1
withCString { bytes in
str.withCString { sbytes in
start = strstr(bytes, sbytes) - UnsafeMutablePointer<Int8>(mutating: bytes)
}
}
return start < 0 ? 0..<0 : start..<start + str.utf8.count
}
fileprivate func substring(to index: Int) -> String {
var out = self
withCString { bytes in
let bytes = UnsafeMutablePointer<Int8>(mutating: bytes)
bytes[index] = 0
out = String(cString: bytes)
}
return out
}
fileprivate func substring(from index: Int) -> String {
var out = self
withCString { bytes in
out = String(cString: bytes + index)
}
return out
}
}

Binary file not shown.

View File

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

View File

@ -0,0 +1,193 @@
// (c) 2021 and onwards Fuziki (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// Ref: https://qiita.com/fuziki/items/b31055a69330a3ce55a5
// Modified by The vChewing Project in order to use it with AppKit.
import Cocoa
import SwiftUI
@available(macOS 10.15, *)
public struct VText: NSViewRepresentable {
public var text: String?
public func makeNSView(context _: Context) -> NSAttributedTextView {
let nsView = NSAttributedTextView()
nsView.direction = .vertical
nsView.text = text
return nsView
}
public func updateNSView(_ nsView: NSAttributedTextView, context _: Context) {
nsView.text = text
}
}
@available(macOS 10.15, *)
public struct HText: NSViewRepresentable {
public var text: String?
public func makeNSView(context _: Context) -> NSAttributedTextView {
let nsView = NSAttributedTextView()
nsView.direction = .horizontal
nsView.text = text
return nsView
}
public func updateNSView(_ nsView: NSAttributedTextView, context _: Context) {
nsView.text = text
}
}
public class NSAttributedTextView: NSView {
public enum writingDirection: String {
case horizontal
case vertical
case verticalReversed
}
public var direction: writingDirection = .horizontal
public var fontSize: CGFloat = NSFont.systemFontSize {
didSet {
attributes[.font] = NSFont.systemFont(ofSize: fontSize)
}
}
public var textColor: NSColor = .textColor {
didSet {
attributes[.foregroundColor] = textColor
}
}
public func attributedStringValue(areaCalculation: Bool = false) -> NSAttributedString {
var newAttributes = attributes
let isVertical: Bool = !(direction == .horizontal)
newAttributes[.verticalGlyphForm] = isVertical
let newStyle: NSMutableParagraphStyle = newAttributes[.paragraphStyle] as! NSMutableParagraphStyle
if #available(macOS 10.13, *) {
newStyle.lineSpacing = isVertical ? (fontSize / -2) : fontSize * 0.1
newStyle.maximumLineHeight = fontSize * 1.1
newStyle.minimumLineHeight = fontSize * 1.1
}
newAttributes[.paragraphStyle] = newStyle
var text: String = text ?? text ?? ""
if areaCalculation {
text = text.replacingOccurrences(
of: "[^\n]",
with: "",
options: .regularExpression,
range: text.range(of: text)
)
}
let attributedText = NSMutableAttributedString(string: text, attributes: newAttributes)
return attributedText
}
public var backgroundColor: NSColor = .controlBackgroundColor
public var attributes: [NSAttributedString.Key: Any] = [
.verticalGlyphForm: true,
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
.foregroundColor: NSColor.textColor,
.paragraphStyle: {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
return paragraphStyle
}(),
]
public var text: String? { didSet { ctFrame = nil } }
private var ctFrame: CTFrame?
private(set) var currentRect: NSRect?
@discardableResult public func shrinkFrame() -> NSRect {
let attrString: NSAttributedString = {
switch direction {
case .horizontal: return attributedStringValue()
default: return attributedStringValue(areaCalculation: true)
}
}()
var rect = attrString.boundingRect(
with: NSSize(width: 1600.0, height: 1600.0),
options: [.usesLineFragmentOrigin, .usesFontLeading, .usesDeviceMetrics]
)
rect.size.height *= 1.03
rect.size.height = max(rect.size.height, NSFont.systemFontSize * 1.1)
rect.size.height = ceil(rect.size.height)
rect.size.width *= 1.03
rect.size.width = max(rect.size.width, NSFont.systemFontSize * 1.05)
rect.size.width = ceil(rect.size.width)
if direction != .horizontal {
rect = .init(x: rect.minX, y: rect.minY, width: rect.height, height: rect.width)
}
return rect
}
override public func draw(_ rect: CGRect) {
let context = NSGraphicsContext.current?.cgContext
guard let context = context else { return }
let setter = CTFramesetterCreateWithAttributedString(attributedStringValue())
let path = CGPath(rect: rect, transform: nil)
let theCTFrameProgression: CTFrameProgression = {
switch direction {
case .horizontal: return CTFrameProgression.topToBottom
case .vertical: return CTFrameProgression.rightToLeft
case .verticalReversed: return CTFrameProgression.leftToRight
}
}()
let frameAttrs: CFDictionary =
[
kCTFrameProgressionAttributeName: theCTFrameProgression.rawValue
] as CFDictionary
let newFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), path, frameAttrs)
ctFrame = newFrame
backgroundColor.setFill()
let bgPath: NSBezierPath = .init(roundedRect: rect, xRadius: 0, yRadius: 0)
bgPath.fill()
currentRect = rect
CTFrameDraw(newFrame, context)
}
}
public class NSAttributedTooltipTextView: NSAttributedTextView {
override public func attributedStringValue(areaCalculation: Bool = false) -> NSAttributedString {
var newAttributes = attributes
let isVertical: Bool = !(direction == .horizontal)
newAttributes[.verticalGlyphForm] = isVertical
let newStyle: NSMutableParagraphStyle = newAttributes[.paragraphStyle] as! NSMutableParagraphStyle
if #available(macOS 10.13, *) {
newStyle.lineSpacing = isVertical ? (fontSize / -2) : fontSize * 0.1
newStyle.maximumLineHeight = fontSize * 1.1
newStyle.minimumLineHeight = fontSize * 1.1
}
newAttributes[.paragraphStyle] = newStyle
var text: String = text ?? text ?? ""
if !(direction == .horizontal) {
text = text.replacingOccurrences(of: "˙", with: "")
text = text.replacingOccurrences(of: "\u{A0}", with: " ")
text = text.replacingOccurrences(of: "+", with: "")
text = text.replacingOccurrences(of: "Shift", with: "")
text = text.replacingOccurrences(of: "Control", with: "")
text = text.replacingOccurrences(of: "Enter", with: "")
text = text.replacingOccurrences(of: "Command", with: "")
text = text.replacingOccurrences(of: "Delete", with: "")
text = text.replacingOccurrences(of: "BackSpace", with: "")
text = text.replacingOccurrences(of: "SHIFT", with: "")
text = text.replacingOccurrences(of: "CONTROL", with: "")
text = text.replacingOccurrences(of: "ENTER", with: "")
text = text.replacingOccurrences(of: "COMMAND", with: "")
text = text.replacingOccurrences(of: "DELETE", with: "")
text = text.replacingOccurrences(of: "BACKSPACE", with: "")
}
if areaCalculation {
text = text.replacingOccurrences(
of: "[^\n]",
with: "",
options: .regularExpression,
range: text.range(of: text)
)
}
let attributedText = NSMutableAttributedString(string: text, attributes: newAttributes)
return attributedText
}
}

View File

@ -0,0 +1,59 @@
// (c) 2022 and onwards Qwertyyb (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Cocoa
extension Date {
static func - (lhs: Date, rhs: Date) -> TimeInterval {
lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate
}
}
class ShiftKeyUpChecker {
init() {}
private static var checkModifier: NSEvent.ModifierFlags { NSEvent.ModifierFlags.shift }
private static var checkKeyCode: [UInt16] {
mgrPrefs.togglingAlphanumericalModeWithLShift
? [KeyCode.kShift.rawValue, KeyCode.kRightShift.rawValue]
: [KeyCode.kRightShift.rawValue]
}
private static let delayInterval = 0.3
private static var lastTime: Date = .init()
private static func checkModifierKeyUp(event: NSEvent) -> Bool {
if event.type == .flagsChanged,
event.modifierFlags.intersection(.deviceIndependentFlagsMask) == .init(rawValue: 0),
Date() - lastTime <= delayInterval
{
// modifier keyup event
lastTime = Date(timeInterval: -3600 * 4, since: Date())
return true
}
return false
}
private static func checkModifierKeyDown(event: NSEvent) -> Bool {
let isLeftShift = event.modifierFlags.rawValue & UInt(NX_DEVICELSHIFTKEYMASK) != 0
let isRightShift = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0
print("isLeftShift: \(isLeftShift), isRightShift: \(isRightShift)")
let isKeyDown =
event.type == .flagsChanged
&& checkModifier.contains(event.modifierFlags.intersection(.deviceIndependentFlagsMask))
&& ShiftKeyUpChecker.checkKeyCode.contains(event.keyCode)
if isKeyDown {
// modifier keydown event
lastTime = Date()
} else {
lastTime = Date(timeInterval: -3600 * 4, since: Date())
}
return false
}
// To confirm that the shift key is "pressed-and-released".
public static func check(_ event: NSEvent) -> Bool {
ShiftKeyUpChecker.checkModifierKeyUp(event: event) || ShiftKeyUpChecker.checkModifierKeyDown(event: event)
}
}

View File

@ -0,0 +1,101 @@
//
// Ref: https://stackoverflow.com/a/61695824
// License: https://creativecommons.org/licenses/by-sa/4.0/
//
import Cocoa
class BookmarkManager {
static let shared = BookmarkManager()
// Save bookmark for URL. Use this inside the NSOpenPanel `begin` closure
func saveBookmark(for url: URL) {
guard let bookmarkDic = getBookmarkData(url: url),
let bookmarkURL = getBookmarkURL()
else {
IME.prtDebugIntel("Error getting data or bookmarkURL")
return
}
if #available(macOS 10.13, *) {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: bookmarkDic, requiringSecureCoding: false)
try data.write(to: bookmarkURL)
IME.prtDebugIntel("Did save data to url")
} catch {
IME.prtDebugIntel("Couldn't save bookmarks")
}
}
}
// Load bookmarks when your app launch for example
func loadBookmarks() {
guard let url = getBookmarkURL() else {
return
}
if fileExists(url) {
do {
let fileData = try Data(contentsOf: url)
if let fileBookmarks = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(fileData) as! [URL: Data]? {
for bookmark in fileBookmarks {
restoreBookmark(key: bookmark.key, value: bookmark.value)
}
}
} catch {
IME.prtDebugIntel("Couldn't load bookmarks")
}
}
}
private func restoreBookmark(key: URL, value: Data) {
let restoredUrl: URL?
var isStale = false
IME.prtDebugIntel("Restoring \(key)")
do {
restoredUrl = try URL(
resolvingBookmarkData: value, options: NSURL.BookmarkResolutionOptions.withSecurityScope, relativeTo: nil,
bookmarkDataIsStale: &isStale
)
} catch {
IME.prtDebugIntel("Error restoring bookmarks")
restoredUrl = nil
}
if let url = restoredUrl {
if isStale {
IME.prtDebugIntel("URL is stale")
} else {
if !url.startAccessingSecurityScopedResource() {
IME.prtDebugIntel("Couldn't access: \(url.path)")
}
}
}
}
private func getBookmarkData(url: URL) -> [URL: Data]? {
let data = try? url.bookmarkData(
options: NSURL.BookmarkCreationOptions.withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil
)
if let data = data {
return [url: data]
}
return nil
}
private func getBookmarkURL() -> URL? {
let urls = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
if let appSupportURL = urls.last {
let url = appSupportURL.appendingPathComponent("Bookmarks.dict")
return url
}
return nil
}
private func fileExists(_ url: URL) -> Bool {
var isDir = ObjCBool(false)
let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
return exists
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Cocoa
extension Preferences {
public struct PaneIdentifier: Hashable, RawRepresentable, Codable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
}
public protocol PreferencePane: NSViewController {
var preferencePaneIdentifier: Preferences.PaneIdentifier { get }
var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get }
}
extension PreferencePane {
public var toolbarItemIdentifier: NSToolbarItem.Identifier {
preferencePaneIdentifier.toolbarItemIdentifier
}
public var toolbarItemIcon: NSImage { .empty }
}
extension Preferences.PaneIdentifier {
public init(_ rawValue: String) {
self.init(rawValue: rawValue)
}
public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) {
self.init(rawValue: itemIdentifier.rawValue)
}
public var toolbarItemIdentifier: NSToolbarItem.Identifier {
NSToolbarItem.Identifier(rawValue)
}
}

View File

@ -0,0 +1,6 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
/// The namespace for this package.
public enum Preferences {}

View File

@ -0,0 +1,12 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Cocoa
extension Preferences {
public enum Style {
case toolbarItems
case segmentedControl
}
}

View File

@ -0,0 +1,19 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Cocoa
protocol PreferencesStyleController: AnyObject {
var delegate: PreferencesStyleControllerDelegate? { get set }
var isKeepingWindowCentered: Bool { get }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem?
func selectTab(index: Int)
}
protocol PreferencesStyleControllerDelegate: AnyObject {
func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool)
func activateTab(index: Int, animated: Bool)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,61 @@
// (c) 2018 and onwards Sindre Sorhus (MIT License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
import Cocoa
final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController {
let toolbar: NSToolbar
let centerToolbarItems: Bool
let preferencePanes: [PreferencePane]
var isKeepingWindowCentered: Bool { centerToolbarItems }
weak var delegate: PreferencesStyleControllerDelegate?
init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) {
self.preferencePanes = preferencePanes
self.toolbar = toolbar
self.centerToolbarItems = centerToolbarItems
}
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
var toolbarItemIdentifiers = [NSToolbarItem.Identifier]()
if centerToolbarItems {
toolbarItemIdentifiers.append(.flexibleSpace)
}
for preferencePane in preferencePanes {
toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier)
}
if centerToolbarItems {
toolbarItemIdentifiers.append(.flexibleSpace)
}
return toolbarItemIdentifiers
}
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? {
guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
preconditionFailure()
}
let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier)
toolbarItem.label = preference.preferencePaneTitle
toolbarItem.image = preference.toolbarItemIcon
toolbarItem.target = self
toolbarItem.action = #selector(toolbarItemSelected)
return toolbarItem
}
@IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
delegate?.activateTab(
preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
animated: true
)
}
func selectTab(index: Int) {
toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier
}
}

View File

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

View File

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

1
Source/Data Submodule

@ -0,0 +1 @@
Subproject commit 9642b1adc0404184655e03f3e884334f5d2bef3a

View File

@ -0,0 +1,10 @@
#import <InputMethodKit/InputMethodKit.h>
@interface IMKCandidates(vChewing) {}
- (unsigned long long)windowLevel;
- (void)setWindowLevel:(unsigned long long)level;
- (BOOL)handleKeyboardEvent:(NSEvent *)event;
- (void)setFontSize:(double)fontSize;
@end

1
Source/Modules/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cmake-build-debug

View File

@ -0,0 +1,148 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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 Cocoa
import InputMethodKit
@objc(AppDelegate)
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
private func reloadOnFolderChangeHappens() {
// 100ms 使使
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
if mgrPrefs.shouldAutoReloadUserDataFiles {
IME.initLangModels(userOnly: true)
}
}
}
// let vChewingKeyLayoutBundle = Bundle.init(path: URL(fileURLWithPath: Bundle.main.resourcePath ?? "").appendingPathComponent("vChewingKeyLayout.bundle").path)
@IBOutlet var window: NSWindow?
private var ctlClientListMgrInstance: ctlClientListMgr?
private var ctlPrefWindowInstance: ctlPrefWindow?
private var ctlAboutWindowInstance: ctlAboutWindow? // New About Window
public var folderMonitor = FolderMonitor(
url: URL(fileURLWithPath: mgrLangModel.dataFolderPath(isDefaultFolder: false))
)
private var currentAlertType: String = ""
func userNotificationCenter(_: NSUserNotificationCenter, shouldPresent _: NSUserNotification) -> Bool {
true
}
func applicationDidFinishLaunching(_: Notification) {
NSUserNotificationCenter.default.delegate = self
// 使
if mgrPrefs.failureFlagForUOMObservation {
mgrLangModel.clearUserOverrideModelData(.imeModeCHS)
mgrLangModel.clearUserOverrideModelData(.imeModeCHT)
mgrPrefs.failureFlagForUOMObservation = false
let userNotification = NSUserNotification()
userNotification.title = NSLocalizedString("vChewing", comment: "")
userNotification.informativeText =
"\(NSLocalizedString("vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability.", comment: ""))"
userNotification.soundName = NSUserNotificationDefaultSoundName
NSUserNotificationCenter.default.deliver(userNotification)
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
IME.initLangModels(userOnly: false)
self.folderMonitor.folderDidChange = { [weak self] in
self?.reloadOnFolderChangeHappens()
}
if mgrLangModel.userDataFolderExists {
self.folderMonitor.startMonitoring()
}
}
mgrPrefs.fixOddPreferences()
mgrPrefs.setMissingDefaults()
// 使
if mgrPrefs.checkUpdateAutomatically {
UpdateSputnik.shared.checkForUpdate()
}
}
func updateDirectoryMonitorPath() {
folderMonitor.stopMonitoring()
folderMonitor = FolderMonitor(
url: URL(fileURLWithPath: mgrLangModel.dataFolderPath(isDefaultFolder: false))
)
folderMonitor.folderDidChange = { [weak self] in
self?.reloadOnFolderChangeHappens()
}
if mgrLangModel.userDataFolderExists {
folderMonitor.startMonitoring()
}
}
func showClientListMgr() {
if ctlClientListMgrInstance == nil {
ctlClientListMgrInstance = ctlClientListMgr.init(windowNibName: "frmClientListMgr")
}
ctlClientListMgrInstance?.window?.center()
ctlClientListMgrInstance?.window?.orderFrontRegardless() //
ctlClientListMgrInstance?.window?.level = .statusBar
ctlClientListMgrInstance?.window?.titlebarAppearsTransparent = true
NSApp.setActivationPolicy(.accessory)
}
func showPreferences() {
if ctlPrefWindowInstance == nil {
ctlPrefWindowInstance = ctlPrefWindow.init(windowNibName: "frmPrefWindow")
}
ctlPrefWindowInstance?.window?.center()
ctlPrefWindowInstance?.window?.orderFrontRegardless() //
ctlPrefWindowInstance?.window?.level = .statusBar
ctlPrefWindowInstance?.window?.titlebarAppearsTransparent = true
NSApp.setActivationPolicy(.accessory)
}
// New About Window
func showAbout() {
if ctlAboutWindowInstance == nil {
ctlAboutWindowInstance = ctlAboutWindow.init(windowNibName: "frmAboutWindow")
}
ctlAboutWindowInstance?.window?.center()
ctlAboutWindowInstance?.window?.orderFrontRegardless() //
ctlAboutWindowInstance?.window?.level = .statusBar
ctlAboutWindowInstance?.window?.titlebarAppearsTransparent = true
NSApp.setActivationPolicy(.accessory)
}
func selfUninstall() {
currentAlertType = "Uninstall"
let content = String(
format: NSLocalizedString(
"This will remove vChewing Input Method from this user account, requiring your confirmation.",
comment: ""
))
let alert = NSAlert()
alert.messageText = NSLocalizedString("Uninstallation", comment: "")
alert.informativeText = content
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Not Now", comment: ""))
let result = alert.runModal()
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
NSWorkspace.shared.openFile(
mgrLangModel.dataFolderPath(isDefaultFolder: true), withApplication: "Finder"
)
IME.uninstall(isSudo: false, selfKill: true)
}
NSApp.setActivationPolicy(.accessory)
}
// New About Window
@IBAction func about(_: Any) {
(NSApp.delegate as? AppDelegate)?.showAbout()
NSApplication.shared.activate(ignoringOtherApps: true)
}
}

View File

@ -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.
enum AppleKeyboardConverter {
static var isDynamicBasicKeyboardLayoutEnabled: Bool {
IMKHelper.arrDynamicBasicKeyLayouts.contains(mgrPrefs.basicKeyboardLayout)
}
static func cnvStringApple2ABC(_ strProcessed: String) -> String {
var strProcessed = strProcessed
if isDynamicBasicKeyboardLayoutEnabled {
// Apple
switch mgrPrefs.basicKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo":
if strProcessed.count == 1, Character(strProcessed).isLowercase, Character(strProcessed).isASCII {
strProcessed = strProcessed.uppercased()
}
case "com.apple.keylayout.ZhuyinEten":
switch strProcessed {
case "": strProcessed = "A"
case "": strProcessed = "B"
case "": strProcessed = "C"
case "": strProcessed = "D"
case "": strProcessed = "E"
case "": strProcessed = "F"
case "": strProcessed = "G"
case "": strProcessed = "H"
case "": strProcessed = "I"
case "": strProcessed = "J"
case "": strProcessed = "K"
case "": strProcessed = "L"
case "": strProcessed = "M"
case "": strProcessed = "N"
case "": strProcessed = "O"
case "": strProcessed = "P"
case "": strProcessed = "Q"
case "": strProcessed = "R"
case "": strProcessed = "S"
case "": strProcessed = "T"
case "": strProcessed = "U"
case "": strProcessed = "V"
case "": strProcessed = "W"
case "": strProcessed = "X"
case "": strProcessed = "Y"
case "": strProcessed = "Z"
default: break
}
default: break
}
//
switch strProcessed {
case "": strProcessed = ","
case "": strProcessed = "-"
case "": strProcessed = "."
case "": strProcessed = "/"
case "": strProcessed = "0"
case "": strProcessed = "1"
case "": strProcessed = "2"
case "ˇ": strProcessed = "3"
case "ˋ": strProcessed = "4"
case "": strProcessed = "5"
case "ˊ": strProcessed = "6"
case "˙": strProcessed = "7"
case "": strProcessed = "8"
case "": strProcessed = "9"
case "": strProcessed = ";"
case "": strProcessed = "a"
case "": strProcessed = "b"
case "": strProcessed = "c"
case "": strProcessed = "d"
case "": strProcessed = "e"
case "": strProcessed = "f"
case "": strProcessed = "g"
case "": strProcessed = "h"
case "": strProcessed = "i"
case "": strProcessed = "j"
case "": strProcessed = "k"
case "": strProcessed = "l"
case "": strProcessed = "m"
case "": strProcessed = "n"
case "": strProcessed = "o"
case "": strProcessed = "p"
case "": strProcessed = "q"
case "": strProcessed = "r"
case "": strProcessed = "s"
case "": strProcessed = "t"
case "": strProcessed = "u"
case "": strProcessed = "v"
case "": strProcessed = "w"
case "": strProcessed = "x"
case "": strProcessed = "y"
case "": strProcessed = "z"
default: break
}
//
switch strProcessed {
case "": strProcessed = "\\"
case "": strProcessed = "["
case "": strProcessed = "]"
case "": strProcessed = "{"
case "": strProcessed = "}"
case "": strProcessed = "<"
case "": strProcessed = ">"
default: break
}
// SHIFT
switch strProcessed {
case "": strProcessed = "!"
case "": strProcessed = "@"
case "": strProcessed = "#"
case "": strProcessed = "$"
case "": strProcessed = "%"
case "︿": strProcessed = "^"
case "": strProcessed = "&"
case "": strProcessed = "*"
case "": strProcessed = "("
case "": strProcessed = ")"
default: break
}
// Alt
if strProcessed == "" { strProcessed = "-" }
// Apple
if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
switch strProcessed {
case "_": strProcessed = "_"
case "": strProcessed = ":"
case "": strProcessed = "?"
case "": strProcessed = "+"
case "": strProcessed = "|"
default: break
}
}
}
return strProcessed
}
}

View File

@ -0,0 +1,209 @@
// (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
// enum
public enum StateType: String {
case ofDeactivated = "Deactivated"
case ofEmpty = "Empty"
case ofAbortion = "Abortion" // Empty
case ofCommitting = "Committing"
case ofAssociates = "Associates"
case ofNotEmpty = "NotEmpty"
case ofInputting = "Inputting"
case ofMarking = "Marking"
case ofCandidates = "Candidates"
case ofSymbolTable = "SymbolTable"
}
// IMEState
public protocol IMEStateProtocol {
var type: StateType { get }
var data: StateData { get }
var candidates: [(String, String)] { get }
var hasComposition: Bool { get }
var isCandidateContainer: Bool { get }
var displayedText: String { get }
var textToCommit: String { get set }
var tooltip: String { get set }
var attributedString: NSAttributedString { get }
var convertedToInputting: IMEState { get }
var isFilterable: Bool { get }
var isMarkedLengthValid: Bool { get }
var node: SymbolNode { get set }
}
/// ctlInputMethod
///
/// Finite State Machine/
/// 使
/// 使
///
///
/// IMEState
/// 使
///
///
///
/// IMEState
/// IMEState.ofMarking IMEState.ofInputting
///
/// (Constructor)
///
///
///
/// - .Deactivated: 使使
/// - .AssociatedPhrases:
/// 西 .NotEmpty
/// - .Empty: 使
///
/// - .Abortion: Empty
/// .Empty()
/// - .Committing:
/// - .NotEmpty:
/// - .Inputting: 使Compositor
/// - .Marking: 使
///
/// - .ChoosingCandidate: 使
/// - .SymbolTable:
public struct IMEState: IMEStateProtocol {
public var type: StateType = .ofEmpty
public var data: StateData = .init()
public var node: SymbolNode = .init("")
init(_ data: StateData = .init(), type: StateType = .ofEmpty) {
self.data = data
self.type = type
}
init(_ data: StateData = .init(), type: StateType = .ofEmpty, node: SymbolNode) {
self.data = data
self.type = type
self.node = node
self.data.candidates = { node.children?.map(\.title) ?? [String]() }().map { ("", $0) }
}
}
// MARK: -
extension IMEState {
public static func ofDeactivated() -> IMEState { .init(type: .ofDeactivated) }
public static func ofEmpty() -> IMEState { .init(type: .ofEmpty) }
public static func ofAbortion() -> IMEState { .init(type: .ofAbortion) }
public static func ofCommitting(textToCommit: String) -> IMEState {
var result = IMEState(type: .ofCommitting)
result.data.textToCommit = textToCommit
ChineseConverter.ensureCurrencyNumerals(target: &result.data.textToCommit)
return result
}
public static func ofAssociates(candidates: [(String, String)]) -> IMEState {
var result = IMEState(type: .ofAssociates)
result.data.candidates = candidates
return result
}
public static func ofNotEmpty(displayTextSegments: [String], cursor: Int) -> IMEState {
var result = IMEState(type: .ofNotEmpty)
// displayTextSegments
result.data.displayTextSegments = displayTextSegments
result.data.cursor = cursor
result.data.marker = cursor
return result
}
public static func ofInputting(displayTextSegments: [String], cursor: Int) -> IMEState {
var result = IMEState.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor)
result.type = .ofInputting
return result
}
public static func ofMarking(
displayTextSegments: [String], markedReadings: [String], cursor: Int, marker: Int
)
-> IMEState
{
var result = IMEState.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor)
result.type = .ofMarking
result.data.marker = marker
result.data.markedReadings = markedReadings
StateData.Marking.updateParameters(&result.data)
return result
}
public static func ofCandidates(candidates: [(String, String)], displayTextSegments: [String], cursor: Int)
-> IMEState
{
var result = IMEState.ofNotEmpty(displayTextSegments: displayTextSegments, cursor: cursor)
result.type = .ofCandidates
result.data.candidates = candidates
return result
}
public static func ofSymbolTable(node: SymbolNode) -> IMEState {
var result = IMEState(type: .ofNotEmpty, node: node)
result.type = .ofSymbolTable
return result
}
}
// MARK: -
extension IMEState {
public var isFilterable: Bool { data.isFilterable }
public var isMarkedLengthValid: Bool { data.isMarkedLengthValid }
public var candidates: [(String, String)] { data.candidates }
public var convertedToInputting: IMEState {
if type == .ofInputting { return self }
var result = IMEState.ofInputting(displayTextSegments: data.displayTextSegments, cursor: data.cursor)
result.tooltip = data.tooltipBackupForInputting
return result
}
public var textToCommit: String {
get {
data.textToCommit
}
set {
data.textToCommit = newValue
}
}
public var tooltip: String {
get {
data.tooltip
}
set {
data.tooltip = newValue
}
}
public var attributedString: NSAttributedString {
switch type {
case .ofMarking: return data.attributedStringMarking
case .ofAssociates, .ofSymbolTable: return data.attributedStringPlaceholder
default: return data.attributedStringNormal
}
}
public var hasComposition: Bool {
switch type {
case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true
default: return false
}
}
public var isCandidateContainer: Bool {
switch type {
case .ofCandidates, .ofAssociates, .ofSymbolTable: return true
default: return false
}
}
public var displayedText: String { data.displayedText }
}

View File

@ -0,0 +1,252 @@
// (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 StateData {
var displayedText: String = ""
var displayedTextConverted: String {
///
var result = IME.kanjiConversionIfRequired(displayedText)
if result.utf16.count != displayedText.utf16.count
|| result.count != displayedText.count
{
result = displayedText
}
return result
}
// MARK: Cursor & Marker & Range for UTF8
var cursor: Int = 0 {
didSet {
cursor = min(max(cursor, 0), displayedText.count)
}
}
var marker: Int = 0 {
didSet {
marker = min(max(marker, 0), displayedText.count)
}
}
var markedRange: Range<Int> {
min(cursor, marker)..<max(cursor, marker)
}
// MARK: Cursor & Marker & Range for UTF16 (Read-Only)
/// IMK UTF8 emoji
/// Swift.utf16NSString.length()
///
var u16Cursor: Int {
displayedText.charComponents[0..<cursor].joined().utf16.count
}
var u16Marker: Int {
displayedText.charComponents[0..<marker].joined().utf16.count
}
var u16MarkedRange: Range<Int> {
min(u16Cursor, u16Marker)..<max(u16Cursor, u16Marker)
}
// MARK: Other data for non-empty states.
var markedTargetExists: Bool = false
var displayTextSegments = [String]() {
didSet {
displayedText = displayTextSegments.joined()
}
}
var reading: String = ""
var markedReadings = [String]()
var candidates = [(String, String)]()
var textToCommit: String = ""
var tooltip: String = ""
var tooltipBackupForInputting: String = ""
var attributedStringPlaceholder: NSAttributedString = .init(
string: " ",
attributes: [
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0,
]
)
var isFilterable: Bool {
markedTargetExists ? isMarkedLengthValid : false
}
var isMarkedLengthValid: Bool {
mgrPrefs.allowedMarkLengthRange.contains(markedRange.count)
}
var tooltipColorState: ctlTooltip.ColorStates = .normal
var attributedStringNormal: NSAttributedString {
///
/// JIS
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
var newBegin = 0
for (i, neta) in displayTextSegments.enumerated() {
attributedString.setAttributes(
[
/// .thick
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: i,
], range: NSRange(location: newBegin, length: neta.utf16.count)
)
newBegin += neta.utf16.count
}
return attributedString
}
var attributedStringMarking: NSAttributedString {
///
/// JIS
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
let end = u16MarkedRange.upperBound
attributedString.setAttributes(
[
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0,
], range: NSRange(location: 0, length: u16MarkedRange.lowerBound)
)
attributedString.setAttributes(
[
.underlineStyle: NSUnderlineStyle.thick.rawValue,
.markedClauseSegment: 1,
],
range: NSRange(
location: u16MarkedRange.lowerBound,
length: u16MarkedRange.upperBound - u16MarkedRange.lowerBound
)
)
attributedString.setAttributes(
[
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 2,
],
range: NSRange(
location: end,
length: displayedTextConverted.utf16.count - end
)
)
return attributedString
}
}
// MARK: - IMEState
extension StateData {
var chkIfUserPhraseExists: Bool {
let text = displayedText.charComponents[markedRange].joined()
let joined = markedReadings.joined(separator: "-")
return mgrLangModel.checkIfUserPhraseExist(
userPhrase: text, mode: IME.currentInputMode, key: joined
)
}
var userPhrase: String {
let text = displayedText.charComponents[markedRange].joined()
let joined = markedReadings.joined(separator: "-")
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
return "\(text) \(joined)\(nerfedScore)"
}
var userPhraseConverted: String {
let text =
ChineseConverter.crossConvert(displayedText.charComponents[markedRange].joined()) ?? ""
let joined = markedReadings.joined(separator: "-")
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)"
}
enum Marking {
private static func generateReadingThread(_ data: StateData) -> String {
var arrOutput = [String]()
for neta in data.markedReadings {
var neta = neta
if neta.isEmpty { continue }
if neta.contains("_") {
arrOutput.append("??")
continue
}
if mgrPrefs.showHanyuPinyinInCompositionBuffer,
mgrPrefs.alwaysShowTooltipTextsHorizontally || !ctlInputMethod.isVerticalTypingSituation
{
// ->->調
neta = Tekkon.restoreToneOneInZhuyinKey(target: neta)
neta = Tekkon.cnvPhonaToHanyuPinyin(target: neta)
neta = Tekkon.cnvHanyuPinyinToTextbookStyle(target: neta)
} else {
neta = Tekkon.cnvZhuyinChainToTextbookReading(target: neta)
}
arrOutput.append(neta)
}
return arrOutput.joined(separator: "\u{A0}")
}
///
/// - Parameter data:
public static func updateParameters(_ data: inout StateData) {
var tooltipGenerated: String {
if mgrPrefs.phraseReplacementEnabled {
data.tooltipColorState = .warning
return NSLocalizedString(
"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: ""
)
}
if data.markedRange.isEmpty {
return ""
}
let text = data.displayedText.charComponents[data.markedRange].joined()
if data.markedRange.count < mgrPrefs.allowedMarkLengthRange.lowerBound {
data.tooltipColorState = .denialInsufficiency
return String(
format: NSLocalizedString(
"\"%@\" length must ≥ 2 for a user phrase.", comment: ""
) + "\n" + generateReadingThread(data), text
)
} else if data.markedRange.count > mgrPrefs.allowedMarkLengthRange.upperBound {
data.tooltipColorState = .denialOverflow
return String(
format: NSLocalizedString(
"\"%@\" length should ≤ %d for a user phrase.", comment: ""
) + "\n" + generateReadingThread(data), text, mgrPrefs.allowedMarkLengthRange.upperBound
)
}
let joined = data.markedReadings.joined(separator: "-")
let exist = mgrLangModel.checkIfUserPhraseExist(
userPhrase: text, mode: IME.currentInputMode, key: joined
)
if exist {
data.markedTargetExists = exist
data.tooltipColorState = .prompt
return String(
format: NSLocalizedString(
"\"%@\" already exists:\n ENTER to boost, SHIFT+COMMAND+ENTER to nerf, \n BackSpace or Delete key to exclude.",
comment: ""
) + "\n" + generateReadingThread(data), text
)
}
data.tooltipColorState = .normal
return String(
format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n"
+ generateReadingThread(data),
text
)
}
data.tooltip = tooltipGenerated
}
}
}

View File

@ -0,0 +1,489 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// ====================
// 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.
/// 調
/// Megrez Tekkon
/// composer compositor
import Foundation
// MARK: - (Delegate).
/// KeyHandler
protocol KeyHandlerDelegate {
var clientBundleIdentifier: String { get }
var isVerticalTyping: Bool { get }
func ctlCandidate() -> ctlCandidateProtocol
func keyHandler(
_: KeyHandler, didSelectCandidateAt index: Int,
ctlCandidate controller: ctlCandidateProtocol
)
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
-> Bool
}
// MARK: - (Kernel).
/// KeyHandler 調
public class KeyHandler {
///
let kEpsilon: Double = 0.000001
///
var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty }
var composer: Tekkon.Composer = .init() //
var compositor: Megrez.Compositor //
var currentLM: vChewing.LMInstantiator = .init() //
var currentUOM: vChewing.LMUserOverride = .init() //
/// (ctlInputMethod)便
var delegate: KeyHandlerDelegate?
/// InputMode
/// IME UserPrefs
var inputMode: InputMode = IME.currentInputMode {
willSet {
//
let isCHS: Bool = (newValue == InputMode.imeModeCHS)
/// Prefs IME
IME.currentInputMode = newValue
mgrPrefs.mostRecentInputMode = IME.currentInputMode.rawValue
///
currentLM = isCHS ? mgrLangModel.lmCHS : mgrLangModel.lmCHT
currentUOM = isCHS ? mgrLangModel.uomCHS : mgrLangModel.uomCHT
///
syncBaseLMPrefs()
///
///
ensureCompositor()
ensureParser()
}
}
///
public init() {
///
Megrez.Compositor.maxSpanLength = mgrPrefs.maxCandidateLength
/// ensureCompositor()
compositor = Megrez.Compositor(with: currentLM, separator: "-")
///
ensureParser()
/// inputMode
/// defer willSet
defer { inputMode = IME.currentInputMode }
}
func clear() {
composer.clear()
compositor.clear()
}
// MARK: - Functions dealing with Megrez.
///
/// - Returns:
func currentMarkedRange() -> Range<Int> {
min(compositor.cursor, compositor.marker)..<max(compositor.cursor, compositor.marker)
}
///
func isCursorCuttingChar(isMarker: Bool = false) -> Bool {
let index = isMarker ? compositor.marker : compositor.cursor
var isBound = (index == compositor.walkedNodes.contextRange(ofGivenCursor: index).lowerBound)
if index == compositor.width { isBound = true }
let rawResult = compositor.walkedNodes.findNode(at: index)?.isReadingMismatched ?? false
return !isBound && rawResult
}
/// Megrez 使便
///
/// 使 Node Crossing
var actualCandidateCursor: Int {
compositor.cursor
- ((compositor.cursor == compositor.width || !mgrPrefs.useRearCursorMode) && compositor.cursor > 0 ? 1 : 0)
}
///
///
/// Viterbi
///
///
func walk() {
compositor.walk()
// GraphViz
if mgrPrefs.isDebugModeEnabled {
let result = compositor.dumpDOT
let appSupportPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending(
"/vChewing-visualization.dot")
do {
try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8)
} catch {
IME.prtDebugIntel("Failed from writing dumpDOT results.")
}
}
}
///
/// - Parameter key:
/// - Returns:
/// nil
func buildAssociatePhraseArray(withPair pair: Megrez.Compositor.KeyValuePaired) -> [(String, String)] {
var arrResult: [(String, String)] = []
if currentLM.hasAssociatedPhrasesFor(pair: pair) {
arrResult = currentLM.associatedPhrasesFor(pair: pair).map { ("", $0) }
}
return arrResult
}
///
///
/// 使
/// **macOS **
/// OV Bug
///
///
/// - Remark:
///
///
///
/// v1.9.3 SP2 Bug v1.9.4
/// v2.0.2
/// - Parameter theCandidate:
func consolidateCursorContext(with theCandidate: Megrez.Compositor.KeyValuePaired) {
var grid = compositor
var frontBoundaryEX = actualCandidateCursor + 1
var rearBoundaryEX = actualCandidateCursor
var debugIntelToPrint = ""
if grid.overrideCandidate(theCandidate, at: actualCandidateCursor) {
grid.walk()
let range = grid.walkedNodes.contextRange(ofGivenCursor: actualCandidateCursor)
rearBoundaryEX = range.lowerBound
frontBoundaryEX = range.upperBound
debugIntelToPrint.append("EX: \(rearBoundaryEX)..<\(frontBoundaryEX), ")
}
let range = compositor.walkedNodes.contextRange(ofGivenCursor: actualCandidateCursor)
var rearBoundary = min(range.lowerBound, rearBoundaryEX)
var frontBoundary = max(range.upperBound, frontBoundaryEX)
debugIntelToPrint.append("INI: \(rearBoundary)..<\(frontBoundary), ")
let cursorBackup = compositor.cursor
while compositor.cursor > rearBoundary { compositor.jumpCursorBySpan(to: .rear) }
rearBoundary = min(compositor.cursor, rearBoundary)
compositor.cursor = cursorBackup //
while compositor.cursor < frontBoundary { compositor.jumpCursorBySpan(to: .front) }
frontBoundary = min(max(compositor.cursor, frontBoundary), compositor.width)
compositor.cursor = cursorBackup //
debugIntelToPrint.append("FIN: \(rearBoundary)..<\(frontBoundary)")
IME.prtDebugIntel(debugIntelToPrint)
//
var nodeIndices = [Int]() //
var position = rearBoundary //
while position < frontBoundary {
guard let regionIndex = compositor.cursorRegionMap[position] else {
position += 1
continue
}
if !nodeIndices.contains(regionIndex) {
nodeIndices.append(regionIndex) //
guard compositor.walkedNodes.count > regionIndex else { break } //
let currentNode = compositor.walkedNodes[regionIndex]
guard currentNode.keyArray.count == currentNode.value.count else {
compositor.overrideCandidate(currentNode.currentPair, at: position)
position += currentNode.keyArray.count
continue
}
let values = currentNode.currentPair.value.charComponents
for (subPosition, key) in currentNode.keyArray.enumerated() {
guard values.count > subPosition else { break } //
let thePair = Megrez.Compositor.KeyValuePaired(
key: key, value: values[subPosition]
)
compositor.overrideCandidate(thePair, at: position)
position += 1
}
continue
}
position += 1
}
}
///
///
/// - Parameters:
/// - value:
/// - respectCursorPushing: true
/// - consolidate:
func fixNode(candidate: (String, String), respectCursorPushing: Bool = true, preConsolidate: Bool = false) {
let theCandidate: Megrez.Compositor.KeyValuePaired = .init(key: candidate.0, value: candidate.1)
///
if preConsolidate { consolidateCursorContext(with: theCandidate) }
//
if !compositor.overrideCandidate(theCandidate, at: actualCandidateCursor) { return }
let previousWalk = compositor.walkedNodes
//
walk()
let currentWalk = compositor.walkedNodes
// 使
var accumulatedCursor = 0
let currentNode = currentWalk.findNode(at: actualCandidateCursor, target: &accumulatedCursor)
guard let currentNode = currentNode else { return }
if currentNode.currentUnigram.score > -12, mgrPrefs.fetchSuggestionsFromUserOverrideModel {
IME.prtDebugIntel("UOM: Start Observation.")
// 使
//
// AppDelegate
mgrPrefs.failureFlagForUOMObservation = true
//
//
currentUOM.performObservation(
walkedBefore: previousWalk, walkedAfter: currentWalk, cursor: actualCandidateCursor,
timestamp: Date().timeIntervalSince1970, saveCallback: { mgrLangModel.saveUserOverrideModelData() }
)
//
mgrPrefs.failureFlagForUOMObservation = false
}
///
if mgrPrefs.moveCursorAfterSelectingCandidate, respectCursorPushing {
// compositor.cursor = accumulatedCursor
compositor.jumpCursorBySpan(to: .front)
}
}
///
func getCandidatesArray(fixOrder: Bool = true) -> [(String, String)] {
/// 使 nodesCrossing macOS
/// nodeCrossing
var arrCandidates: [Megrez.Compositor.KeyValuePaired] = {
switch mgrPrefs.useRearCursorMode {
case false:
return compositor.fetchCandidates(at: actualCandidateCursor, filter: .endAt)
case true:
return compositor.fetchCandidates(at: actualCandidateCursor, filter: .beginAt)
}
}()
/// nodes
///
///
if arrCandidates.isEmpty { return .init() }
// 調
if !mgrPrefs.fetchSuggestionsFromUserOverrideModel || mgrPrefs.useSCPCTypingMode || fixOrder {
return arrCandidates.map { ($0.key, $0.value) }
}
let arrSuggestedUnigrams: [(String, Megrez.Unigram)] = fetchSuggestionsFromUOM(apply: false)
let arrSuggestedCandidates: [Megrez.Compositor.KeyValuePaired] = arrSuggestedUnigrams.map {
Megrez.Compositor.KeyValuePaired(key: $0.0, value: $0.1.value)
}
arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates
arrCandidates = arrCandidates.deduplicate
arrCandidates = arrCandidates.stableSort { $0.key.split(separator: "-").count > $1.key.split(separator: "-").count }
return arrCandidates.map { ($0.key, $0.value) }
}
///
@discardableResult func fetchSuggestionsFromUOM(apply: Bool) -> [(String, Megrez.Unigram)] {
var arrResult = [(String, Megrez.Unigram)]()
///
if mgrPrefs.useSCPCTypingMode { return arrResult }
///
if !mgrPrefs.fetchSuggestionsFromUserOverrideModel { return arrResult }
///
let suggestion = currentUOM.fetchSuggestion(
currentWalk: compositor.walkedNodes, cursor: actualCandidateCursor, timestamp: Date().timeIntervalSince1970
)
arrResult.append(contentsOf: suggestion.candidates)
if apply {
///
if !suggestion.isEmpty, let newestSuggestedCandidate = suggestion.candidates.last {
let overrideBehavior: Megrez.Compositor.Node.OverrideType =
suggestion.forceHighScoreOverride ? .withHighScore : .withTopUnigramScore
let suggestedPair: Megrez.Compositor.KeyValuePaired = .init(
key: newestSuggestedCandidate.0, value: newestSuggestedCandidate.1.value
)
IME.prtDebugIntel(
"UOM: Suggestion retrieved, overriding the node score of the selected candidate: \(suggestedPair.toNGramKey)")
if !compositor.overrideCandidate(suggestedPair, at: actualCandidateCursor, overrideType: overrideBehavior) {
compositor.overrideCandidateLiteral(
newestSuggestedCandidate.1.value, at: actualCandidateCursor, overrideType: overrideBehavior
)
}
walk()
}
}
arrResult = arrResult.stableSort { $0.1.score > $1.1.score }
return arrResult
}
// MARK: - Extracted methods and functions (Tekkon).
/// _
var currentMandarinParser: String {
mgrPrefs.mandarinParserName + "_"
}
///
func ensureParser() {
switch mgrPrefs.mandarinParser {
case MandarinParser.ofStandard.rawValue:
composer.ensureParser(arrange: .ofDachen)
case MandarinParser.ofDachen26.rawValue:
composer.ensureParser(arrange: .ofDachen26)
case MandarinParser.ofETen.rawValue:
composer.ensureParser(arrange: .ofETen)
case MandarinParser.ofHsu.rawValue:
composer.ensureParser(arrange: .ofHsu)
case MandarinParser.ofETen26.rawValue:
composer.ensureParser(arrange: .ofETen26)
case MandarinParser.ofIBM.rawValue:
composer.ensureParser(arrange: .ofIBM)
case MandarinParser.ofMiTAC.rawValue:
composer.ensureParser(arrange: .ofMiTAC)
case MandarinParser.ofFakeSeigyou.rawValue:
composer.ensureParser(arrange: .ofFakeSeigyou)
case MandarinParser.ofSeigyou.rawValue:
composer.ensureParser(arrange: .ofSeigyou)
case MandarinParser.ofStarlight.rawValue:
composer.ensureParser(arrange: .ofStarlight)
case MandarinParser.ofHanyuPinyin.rawValue:
composer.ensureParser(arrange: .ofHanyuPinyin)
case MandarinParser.ofSecondaryPinyin.rawValue:
composer.ensureParser(arrange: .ofSecondaryPinyin)
case MandarinParser.ofYalePinyin.rawValue:
composer.ensureParser(arrange: .ofYalePinyin)
case MandarinParser.ofHualuoPinyin.rawValue:
composer.ensureParser(arrange: .ofHualuoPinyin)
case MandarinParser.ofUniversalPinyin.rawValue:
composer.ensureParser(arrange: .ofUniversalPinyin)
default:
composer.ensureParser(arrange: .ofDachen)
mgrPrefs.mandarinParser = MandarinParser.ofStandard.rawValue
}
composer.clear()
composer.phonabetCombinationCorrectionEnabled = mgrPrefs.autoCorrectReadingCombination
}
///
/// 調調
var previousParsableReading: (String, String, Bool)? {
if compositor.cursor == 0 { return nil }
let cursorPrevious = max(compositor.cursor - 1, 0)
let rawData = compositor.keys[cursorPrevious]
let components = rawData.charComponents
var hasIntonation = false
for neta in components {
if !Tekkon.allowedPhonabets.contains(neta) || neta == " " { return nil }
if Tekkon.allowedIntonations.contains(neta) { hasIntonation = true }
}
if hasIntonation, components.count == 1 { return nil } // 調
let rawDataSansIntonation = hasIntonation ? components.dropLast(1).joined() : rawData
return (rawData, rawDataSansIntonation, hasIntonation)
}
/// 調
/// - Parameter input:
/// - Returns: 調
func isIntonationKey(_ input: InputSignalProtocol) -> Bool {
var theComposer = composer //
theComposer.clear() //
theComposer.receiveKey(fromString: input.text)
return theComposer.hasToneMarker(withNothingElse: true)
}
// MARK: - Extracted methods and functions (Megrez).
///
func syncBaseLMPrefs() {
currentLM.isPhraseReplacementEnabled = mgrPrefs.phraseReplacementEnabled
currentLM.isCNSEnabled = mgrPrefs.cns11643Enabled
currentLM.isSymbolEnabled = mgrPrefs.symbolInputEnabled
}
/// 使
func ensureCompositor() {
// 西
compositor = Megrez.Compositor(with: currentLM, separator: "-")
}
///
/// - Parameter input:
/// - Returns:
func generatePunctuationNamePrefix(withKeyCondition input: InputSignalProtocol) -> String {
if mgrPrefs.halfWidthPunctuationEnabled {
return "_half_punctuation_"
}
switch (input.isControlHold, input.isOptionHold) {
case (true, true): return "_alt_ctrl_punctuation_"
case (true, false): return "_ctrl_punctuation_"
case (false, true): return "_alt_punctuation_"
case (false, false): return "_punctuation_"
}
}
}
// MARK: - Components for Popup Composition Buffer (PCB) Window.
///
/// - Remark: IMKTextInput mgrPrefs
private let compositorWidthLimit = 20
extension KeyHandler {
///
///
///
///
/// 使
///
var commitOverflownComposition: String {
guard !compositor.walkedNodes.isEmpty,
compositor.width > compositorWidthLimit,
let identifier = delegate?.clientBundleIdentifier,
mgrPrefs.clientsIMKTextInputIncapable.contains(identifier)
else {
return ""
}
// Steam Client Identifier
var textToCommit = ""
while compositor.width > compositorWidthLimit {
var delta = compositor.width - compositorWidthLimit
let node = compositor.walkedNodes[0]
if node.isReadingMismatched {
delta = node.keyArray.count
textToCommit += node.currentPair.value
} else {
delta = min(delta, node.keyArray.count)
textToCommit += node.currentPair.value.charComponents[0..<delta].joined()
}
let newCursor = max(compositor.cursor - delta, 0)
compositor.cursor = 0
if !node.isReadingMismatched {
consolidateCursorContext(with: node.currentPair)
}
// Bigram
for _ in 0..<delta {
compositor.dropKey(direction: .front)
}
compositor.cursor = newCursor
walk()
}
return textToCommit
}
}

View File

@ -0,0 +1,329 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// ====================
// 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: - § 調 (Handle Candidate State).
extension KeyHandler {
///
/// - Parameters:
/// - input:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: IMK
func handleCandidate(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard var ctlCandidateCurrent = delegate?.ctlCandidate() else {
IME.prtDebugIntel("06661F6E")
errorCallback()
return true
}
// MARK: (Cancel Candidate)
let cancelCandidateKey =
input.isBackSpace || input.isEsc || input.isDelete
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
if cancelCandidateKey {
if state.type == .ofAssociates
|| mgrPrefs.useSCPCTypingMode
|| compositor.isEmpty
{
//
//
// 使 BackSpace
// compositor.isEmpty
stateCallback(IMEState.ofAbortion())
} else {
stateCallback(buildInputtingState)
}
if state.type == .ofSymbolTable, let nodePrevious = state.node.previous, let _ = nodePrevious.children {
stateCallback(IMEState.ofSymbolTable(node: nodePrevious))
}
return true
}
// MARK: Enter
if input.isEnter {
if state.type == .ofAssociates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
stateCallback(IMEState.ofAbortion())
return true
}
delegate?.keyHandler(
self,
didSelectCandidateAt: ctlCandidateCurrent.selectedCandidateIndex,
ctlCandidate: ctlCandidateCurrent
)
return true
}
// MARK: Tab
if input.isTab {
let updated: Bool =
mgrPrefs.specifyShiftTabKeyBehavior
? (input.isShiftHold
? ctlCandidateCurrent.showPreviousPage()
: ctlCandidateCurrent.showNextPage())
: (input.isShiftHold
? ctlCandidateCurrent.highlightPreviousCandidate()
: ctlCandidateCurrent.highlightNextCandidate())
if !updated {
IME.prtDebugIntel("9B691919")
errorCallback()
}
return true
}
// MARK: Space
if input.isSpace {
let updated: Bool =
mgrPrefs.specifyShiftSpaceKeyBehavior
? (input.isShiftHold
? ctlCandidateCurrent.highlightNextCandidate()
: ctlCandidateCurrent.showNextPage())
: (input.isShiftHold
? ctlCandidateCurrent.showNextPage()
: ctlCandidateCurrent.highlightNextCandidate())
if !updated {
IME.prtDebugIntel("A11C781F")
errorCallback()
}
return true
}
// MARK: PgDn
if input.isPageDown {
let updated: Bool = ctlCandidateCurrent.showNextPage()
if !updated {
IME.prtDebugIntel("9B691919")
errorCallback()
}
return true
}
// MARK: PgUp
if input.isPageUp {
let updated: Bool = ctlCandidateCurrent.showPreviousPage()
if !updated {
IME.prtDebugIntel("9569955D")
errorCallback()
}
return true
}
// MARK: Left Arrow
if input.isLeft {
switch ctlCandidateCurrent.currentLayout {
case .horizontal:
if !ctlCandidateCurrent.highlightPreviousCandidate() {
IME.prtDebugIntel("1145148D")
errorCallback()
}
case .vertical:
if !ctlCandidateCurrent.showPreviousPage() {
IME.prtDebugIntel("1919810D")
errorCallback()
}
}
return true
}
// MARK: Right Arrow
if input.isRight {
switch ctlCandidateCurrent.currentLayout {
case .horizontal:
if !ctlCandidateCurrent.highlightNextCandidate() {
IME.prtDebugIntel("9B65138D")
errorCallback()
}
case .vertical:
if !ctlCandidateCurrent.showNextPage() {
IME.prtDebugIntel("9244908D")
errorCallback()
}
}
return true
}
// MARK: Up Arrow
if input.isUp {
switch ctlCandidateCurrent.currentLayout {
case .horizontal:
if !ctlCandidateCurrent.showPreviousPage() {
IME.prtDebugIntel("9B614524")
errorCallback()
}
case .vertical:
if !ctlCandidateCurrent.highlightPreviousCandidate() {
IME.prtDebugIntel("ASD9908D")
errorCallback()
}
}
return true
}
// MARK: Down Arrow
if input.isDown {
switch ctlCandidateCurrent.currentLayout {
case .horizontal:
if !ctlCandidateCurrent.showNextPage() {
IME.prtDebugIntel("92B990DD")
errorCallback()
}
case .vertical:
if !ctlCandidateCurrent.highlightNextCandidate() {
IME.prtDebugIntel("6B99908D")
errorCallback()
}
}
return true
}
// MARK: Home Key
if input.isHome {
if ctlCandidateCurrent.selectedCandidateIndex == 0 {
IME.prtDebugIntel("9B6EDE8D")
errorCallback()
} else {
ctlCandidateCurrent.selectedCandidateIndex = 0
}
return true
}
// MARK: End Key
if state.candidates.isEmpty {
return false
} else { // count > 0!isEmpty滿
if input.isEnd {
if ctlCandidateCurrent.selectedCandidateIndex == state.candidates.count - 1 {
IME.prtDebugIntel("9B69AAAD")
errorCallback()
} else {
ctlCandidateCurrent.selectedCandidateIndex = state.candidates.count - 1
}
return true
}
}
// MARK: (Associated Phrases)
if state.type == .ofAssociates {
if !input.isShiftHold { return false }
}
var index: Int = NSNotFound
let match: String =
(state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text
for j in 0..<ctlCandidateCurrent.keyLabels.count {
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
if match.compare(label.key, options: .caseInsensitive, range: nil, locale: .current) == .orderedSame {
index = j
break
}
}
if index != NSNotFound {
let candidateIndex = ctlCandidateCurrent.candidateIndexAtKeyLabelIndex(index)
if candidateIndex != Int.max {
delegate?.keyHandler(
self, didSelectCandidateAt: candidateIndex, ctlCandidate: ctlCandidateCurrent
)
return true
}
}
if state.type == .ofAssociates { return false }
// MARK: (SCPC Mode Processing)
if mgrPrefs.useSCPCTypingMode {
///
/// - /
/// -
let punctuationNamePrefix: String = generatePunctuationNamePrefix(withKeyCondition: input)
let parser = currentMandarinParser
let arrCustomPunctuations: [String] = [
punctuationNamePrefix, parser, input.text,
]
let customPunctuation: String = arrCustomPunctuations.joined(separator: "")
///
let arrPunctuations: [String] = [
punctuationNamePrefix, input.text,
]
let punctuation: String = arrPunctuations.joined(separator: "")
var shouldAutoSelectCandidate: Bool =
composer.inputValidityCheck(key: input.charCode) || currentLM.hasUnigramsFor(key: customPunctuation)
|| currentLM.hasUnigramsFor(key: punctuation)
if !shouldAutoSelectCandidate, input.isUpperCaseASCIILetterKey {
let letter = "_letter_\(input.text)"
if currentLM.hasUnigramsFor(key: letter) { shouldAutoSelectCandidate = true }
}
if shouldAutoSelectCandidate {
let candidateIndex = ctlCandidateCurrent.candidateIndexAtKeyLabelIndex(0)
if candidateIndex != Int.max {
delegate?.keyHandler(
self,
didSelectCandidateAt: candidateIndex,
ctlCandidate: ctlCandidateCurrent
)
stateCallback(IMEState.ofAbortion())
return handle(
input: input, state: IMEState.ofEmpty(), stateCallback: stateCallback, errorCallback: errorCallback
)
}
return true
}
}
// MARK: - Flipping pages by using symbol menu keys (when they are not occupied).
if input.isSymbolMenuPhysicalKey {
let updated: Bool =
input.isShiftHold ? ctlCandidateCurrent.showPreviousPage() : ctlCandidateCurrent.showNextPage()
if !updated {
IME.prtDebugIntel("66F3477B")
errorCallback()
}
return true
}
IME.prtDebugIntel("172A0F81")
errorCallback()
return true
}
}

View File

@ -0,0 +1,164 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// ====================
// 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.
/// KeyHandler.HandleInput()
extension KeyHandler {
/// KeyHandler.HandleInput()
/// - Parameters:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: IMK
func handleComposition(
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool? {
// MARK: (Handle BPMF Keys)
var keyConsumedByReading = false
let skipPhoneticHandling =
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|| input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold
// inputValidityCheck() charCode UniChar
// keyConsumedByReading
// composer.receiveKey() String UniChar
if !skipPhoneticHandling && composer.inputValidityCheck(key: input.charCode) {
// macOS 調
//
proc: if [0, 1].contains(mgrPrefs.specifyIntonationKeyBehavior), composer.isEmpty, !input.isSpace {
// prevReading 調調
guard let prevReading = previousParsableReading, isIntonationKey(input) else { break proc }
var theComposer = composer
prevReading.0.charComponents.forEach { theComposer.receiveKey(fromPhonabet: $0) }
// 調調
let oldIntonation: Tekkon.Phonabet = theComposer.intonation
theComposer.receiveKey(fromString: input.text)
if theComposer.intonation == oldIntonation, mgrPrefs.specifyIntonationKeyBehavior == 1 { break proc }
theComposer.intonation.clear()
//
let temporaryReadingKey = theComposer.getComposition()
if currentLM.hasUnigramsFor(key: theComposer.getComposition()) {
compositor.dropKey(direction: .rear)
walk() // Walk walk
composer = theComposer
// buildInputtingState調 buildInputtingState
} else {
IME.prtDebugIntel("4B0DD2D4語彙庫內無「\(temporaryReadingKey)」的匹配記錄,放棄覆寫游標身後的內容。")
errorCallback()
return true
}
}
composer.receiveKey(fromString: input.text)
keyConsumedByReading = true
// 調 updateClientdisplayedText() return true
// 調
if !composer.hasToneMarker() {
stateCallback(buildInputtingState)
return true
}
}
var composeReading = composer.hasToneMarker() && composer.inputValidityCheck(key: input.charCode) //
// Enter Space _composer
// |=
composeReading = composeReading || (!composer.isEmpty && (input.isSpace || input.isEnter))
if composeReading {
if input.isSpace, !composer.hasToneMarker() {
// 調
// 使 OVMandarin調
composer.receiveKey(fromString: " ")
}
let readingKey = composer.getComposition() //
//
//
if !currentLM.hasUnigramsFor(key: readingKey) {
IME.prtDebugIntel("B49C0979語彙庫內無「\(readingKey)」的匹配記錄。")
errorCallback()
if mgrPrefs.keepReadingUponCompositionError {
composer.intonation.clear() // 調
stateCallback(buildInputtingState)
return true
}
composer.clear()
//
switch compositor.isEmpty {
case false: stateCallback(buildInputtingState)
case true:
stateCallback(IMEState.ofAbortion())
}
return true // IMK
}
//
compositor.insertKey(readingKey)
//
walk()
// App
let textToCommit = commitOverflownComposition
//
fetchSuggestionsFromUOM(apply: true)
//
composer.clear()
// updateClientdisplayedText()
var inputting = buildInputtingState
inputting.textToCommit = textToCommit
stateCallback(inputting)
///
if mgrPrefs.useSCPCTypingMode {
let candidateState: IMEState = buildCandidate(
state: inputting,
isTypingVertical: input.isTypingVertical
)
if candidateState.candidates.count == 1, let firstCandidate = candidateState.candidates.first {
let reading: String = firstCandidate.0
let text: String = firstCandidate.1
stateCallback(IMEState.ofCommitting(textToCommit: text))
if !mgrPrefs.associatedPhrasesEnabled {
stateCallback(IMEState.ofEmpty())
} else {
let associatedPhrases =
buildAssociatePhraseState(
withPair: .init(key: reading, value: text)
)
stateCallback(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases)
}
} else {
stateCallback(candidateState)
}
}
// ctlInputMethod IMK
return true
}
/// 調
if keyConsumedByReading {
// updateClientdisplayedText()
stateCallback(buildInputtingState)
return true
}
return nil
}
}

View File

@ -0,0 +1,431 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// ====================
// 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.
/// 調 IMK 調
/// 調
import Foundation
// MARK: - § 調 (Handle Input with States)
extension KeyHandler {
///
/// - Parameters:
/// - input:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: IMK
func handle(
input: InputSignalProtocol,
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
// inputTest
guard !input.text.isEmpty else { return false }
let inputText: String = input.text
var state = state //
// Megrez
if input.isInvalid {
// .Empty(IgnoringPreviousState) .Deactivated
// .Abortion.Empty
if state.type == .ofEmpty || state.type == .ofDeactivated {
return false
}
IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.")
errorCallback()
stateCallback(state)
return true
}
//
let isFunctionKey: Bool =
input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey)
if state.type != .ofAssociates, !state.hasComposition, !state.isCandidateContainer, isFunctionKey {
return false
}
// MARK: Caps Lock processing.
/// Caps Lock
/// Shift Chromium
/// IMK Shift 使
/// Caps Lock
if input.isBackSpace || input.isEnter
|| input.isCursorClockLeft || input.isCursorClockRight
|| input.isCursorForward || input.isCursorBackward
{
// BackSpace
} else if input.isCapsLockOn || input.isASCIIModeInput {
//
stateCallback(IMEState.ofEmpty())
// Shift
if (input.isUpperCaseASCIILetterKey && input.isASCIIModeInput)
|| (input.isCapsLockOn && input.isShiftHold)
{
return false
}
/// ASCII 使insertText:replacementRange:
/// ASCII
if input.isASCII, !input.charCode.isPrintableASCII {
return false
}
// macOS bug
var charToCommit = inputText.lowercased()
if "".contains(charToCommit), input.isSymbolMenuPhysicalKey,
AppleKeyboardConverter.isDynamicBasicKeyboardLayoutEnabled
{
charToCommit = "`"
}
//
stateCallback(IMEState.ofCommitting(textToCommit: charToCommit))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: (Numeric Pad Processing)
// isNumericPadKey KeyCode
// 使 Cocoa flags
//
if input.isNumericPadKey {
if !(state.type == .ofCandidates || state.type == .ofAssociates
|| state.type == .ofSymbolTable)
{
stateCallback(IMEState.ofEmpty())
stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased()))
stateCallback(IMEState.ofEmpty())
return true
}
}
// MARK: (Handle Candidates)
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
return handleCandidate(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
)
}
// MARK: (Handle Associated Phrases)
if state.type == .ofAssociates {
if handleCandidate(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
) {
return true
} else {
stateCallback(IMEState.ofEmpty())
}
}
// MARK: 便使() (Handle Marking)
if state.type == .ofMarking {
if handleMarkingState(
state, input: input, stateCallback: stateCallback,
errorCallback: errorCallback
) {
return true
}
state = state.convertedToInputting
stateCallback(state)
}
// MARK: (Handle BPMF Keys)
if let compositionHandled = handleComposition(
input: input, stateCallback: stateCallback, errorCallback: errorCallback
) {
return compositionHandled
}
// MARK: (Calling candidate window using Up / Down or PageUp / PageDn.)
if state.hasComposition, composer.isEmpty, !input.isOptionHold,
input.isCursorClockLeft || input.isCursorClockRight || input.isSpace
|| input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior)
{
if input.isSpace {
/// Space
if !mgrPrefs.chooseCandidateUsingSpace {
if compositor.cursor >= compositor.length {
let displayedText = state.displayedText
if !displayedText.isEmpty {
stateCallback(IMEState.ofCommitting(textToCommit: displayedText))
}
stateCallback(IMEState.ofCommitting(textToCommit: " "))
stateCallback(IMEState.ofEmpty())
} else if currentLM.hasUnigramsFor(key: " ") {
compositor.insertKey(" ")
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = buildInputtingState
inputting.textToCommit = textToCommit
stateCallback(inputting)
}
return true
} else if input.isShiftHold { // Tab Shift+Command+Space /
return handleInlineCandidateRotation(
state: state, reverseModifier: input.isCommandHold, stateCallback: stateCallback,
errorCallback: errorCallback
)
}
}
stateCallback(buildCandidate(state: state))
return true
}
// MARK: -
// MARK: Esc
if input.isEsc { return handleEsc(state: state, stateCallback: stateCallback) }
// MARK: Tab
if input.isTab {
return handleInlineCandidateRotation(
state: state, reverseModifier: input.isShiftHold, stateCallback: stateCallback, errorCallback: errorCallback
)
}
// MARK: Cursor backward
if input.isCursorBackward {
return handleBackward(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
)
}
// MARK: Cursor forward
if input.isCursorForward {
return handleForward(
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
)
}
// MARK: Home
if input.isHome {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: End
if input.isEnd {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: Ctrl+PgLf or Shift+PgLf
if (input.isControlHold || input.isShiftHold) && (input.isOptionHold && input.isLeft) {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: Ctrl+PgRt or Shift+PgRt
if (input.isControlHold || input.isShiftHold) && (input.isOptionHold && input.isRight) {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: Clock-Left & Clock-Right
if input.isCursorClockLeft || input.isCursorClockRight {
if input.isOptionHold, state.type == .ofInputting {
if input.isCursorClockRight {
return handleInlineCandidateRotation(
state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback
)
}
if input.isCursorClockLeft {
return handleInlineCandidateRotation(
state: state, reverseModifier: true, stateCallback: stateCallback, errorCallback: errorCallback
)
}
}
return handleClockKey(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: BackSpace
if input.isBackSpace {
return handleBackSpace(state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: Delete
if input.isDelete {
return handleDelete(state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback)
}
// MARK: Enter
if input.isEnter {
return (input.isCommandHold && input.isControlHold)
? (input.isOptionHold
? handleCtrlOptionCommandEnter(state: state, stateCallback: stateCallback)
: handleCtrlCommandEnter(state: state, stateCallback: stateCallback))
: handleEnter(state: state, stateCallback: stateCallback)
}
// MARK: -
// MARK: Punctuation list
if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold, state.type != .ofDeactivated {
if input.isOptionHold {
if currentLM.hasUnigramsFor(key: "_punctuation_list") {
if composer.isEmpty {
compositor.insertKey("_punctuation_list")
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = buildInputtingState
inputting.textToCommit = textToCommit
stateCallback(inputting)
stateCallback(buildCandidate(state: inputting))
} else { //
IME.prtDebugIntel("17446655")
errorCallback()
}
return true
}
} else {
// commit buffer ESC
// Enter 使 commit buffer
// bool _ =
_ = handleEnter(state: state, stateCallback: stateCallback)
stateCallback(IMEState.ofSymbolTable(node: SymbolNode.root))
return true
}
}
// MARK: / (FW / HW Arabic Numbers Input)
if state.type == .ofEmpty {
if input.isMainAreaNumKey, input.isShiftHold, input.isOptionHold, !input.isControlHold, !input.isCommandHold {
// NOTE: macOS 10.11 El Capitan CFStringTransform StringTransform:
// https://developer.apple.com/documentation/foundation/stringtransform
guard let stringRAW = input.mainAreaNumKeyChar else { return false }
let string = NSMutableString(string: stringRAW)
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true)
stateCallback(
IMEState.ofCommitting(textToCommit: mgrPrefs.halfWidthPunctuationEnabled ? stringRAW : string as String)
)
stateCallback(IMEState.ofEmpty())
return true
}
}
// MARK: Punctuation
///
/// - /
/// -
let punctuationNamePrefix: String = generatePunctuationNamePrefix(withKeyCondition: input)
let parser = currentMandarinParser
let arrCustomPunctuations: [String] = [punctuationNamePrefix, parser, input.text]
let customPunctuation: String = arrCustomPunctuations.joined(separator: "")
if handlePunctuation(
customPunctuation,
state: state,
usingVerticalTyping: input.isTypingVertical,
stateCallback: stateCallback,
errorCallback: errorCallback
) {
return true
}
///
let arrPunctuations: [String] = [punctuationNamePrefix, input.text]
let punctuation: String = arrPunctuations.joined(separator: "")
if handlePunctuation(
punctuation,
state: state,
usingVerticalTyping: input.isTypingVertical,
stateCallback: stateCallback,
errorCallback: errorCallback
) {
return true
}
// MARK: / (Full-Width / Half-Width Space)
/// 使
if state.type == .ofEmpty {
if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold {
stateCallback(IMEState.ofCommitting(textToCommit: input.isShiftHold ? " " : " "))
stateCallback(IMEState.ofEmpty())
return true
}
}
// MARK: Shift+ (Shift+Letter Processing)
if input.isUpperCaseASCIILetterKey, !input.isCommandHold, !input.isControlHold {
if input.isShiftHold { // isOptionHold
switch mgrPrefs.upperCaseLetterKeyBehavior {
case 1:
stateCallback(IMEState.ofEmpty())
stateCallback(IMEState.ofCommitting(textToCommit: inputText.lowercased()))
stateCallback(IMEState.ofEmpty())
return true
case 2:
stateCallback(IMEState.ofEmpty())
stateCallback(IMEState.ofCommitting(textToCommit: inputText.uppercased()))
stateCallback(IMEState.ofEmpty())
return true
default: // case 0
let letter = "_letter_\(inputText)"
if handlePunctuation(
letter,
state: state,
usingVerticalTyping: input.isTypingVertical,
stateCallback: stateCallback,
errorCallback: errorCallback
) {
return true
}
}
}
}
// MARK: - (Still Nothing)
/// ctlInputMethod
///
/// F1-F12
/// 便
if state.hasComposition || !composer.isEmpty {
IME.prtDebugIntel(
"Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
IME.prtDebugIntel("A9BFF20E")
errorCallback()
stateCallback(state)
return true
}
return false
}
}

View File

@ -0,0 +1,862 @@
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// Refactored from the ObjCpp-version of this class by:
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// ====================
// 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: - § 調 (Functions Interact With States).
extension KeyHandler {
// MARK: - State Building
///
var buildInputtingState: IMEState {
/// (Update the composing buffer)
/// NSAttributeString
var displayTextSegments: [String] = compositor.walkedNodes.values.map {
guard let delegate = delegate, delegate.isVerticalTyping else { return $0 }
guard mgrPrefs.hardenVerticalPunctuations else { return $0 }
var neta = $0
ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping)
return neta
}
var cursor = convertCursorForDisplay(compositor.cursor)
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer)
if !reading.isEmpty {
var newDisplayTextSegments = [String]()
var temporaryNode = ""
var charCounter = 0
for node in displayTextSegments {
for char in node {
if charCounter == cursor {
newDisplayTextSegments.append(temporaryNode)
temporaryNode = ""
newDisplayTextSegments.append(reading)
}
temporaryNode += String(char)
charCounter += 1
}
newDisplayTextSegments.append(temporaryNode)
temporaryNode = ""
}
if newDisplayTextSegments == displayTextSegments { newDisplayTextSegments.append(reading) }
displayTextSegments = newDisplayTextSegments
cursor += reading.count
}
/// 使
return IMEState.ofInputting(displayTextSegments: displayTextSegments, cursor: cursor)
}
///
func convertCursorForDisplay(_ rawCursor: Int) -> Int {
var composedStringCursorIndex = 0
var readingCursorIndex = 0
for theNode in compositor.walkedNodes {
let strNodeValue = theNode.value
///
/// NodeAnchorspanningLength
///
let spanningLength: Int = theNode.keyArray.count
if readingCursorIndex + spanningLength <= rawCursor {
composedStringCursorIndex += strNodeValue.count
readingCursorIndex += spanningLength
continue
}
if !theNode.isReadingMismatched {
for _ in 0..<strNodeValue.count {
guard readingCursorIndex < rawCursor else { continue }
composedStringCursorIndex += 1
readingCursorIndex += 1
}
continue
}
guard readingCursorIndex < rawCursor else { continue }
composedStringCursorIndex += strNodeValue.count
readingCursorIndex += spanningLength
readingCursorIndex = min(readingCursorIndex, rawCursor)
}
return composedStringCursorIndex
}
// MARK: -
///
/// - Parameters:
/// - currentState:
/// - isTypingVertical:
/// - Returns:
func buildCandidate(
state currentState: IMEStateProtocol,
isTypingVertical _: Bool = false
) -> IMEState {
IMEState.ofCandidates(
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
displayTextSegments: compositor.walkedNodes.values,
cursor: currentState.data.cursor
)
}
// MARK: -
///
///
/// buildAssociatePhraseStateWithKey
/// 使
/// Core buildAssociatePhraseArray
/// String Swift
/// nil
///
///
/// - Parameters:
/// - key:
/// - Returns:
func buildAssociatePhraseState(
withPair pair: Megrez.Compositor.KeyValuePaired
) -> IMEState {
//  Xcode
IMEState.ofAssociates(
candidates: buildAssociatePhraseArray(withPair: pair))
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleMarkingState(
_ state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
if input.isEsc {
stateCallback(buildInputtingState)
return true
}
//
if input.isControlHold, input.isCommandHold, input.isEnter {
IME.prtDebugIntel("1198E3E5")
errorCallback()
return true
}
// Enter
if input.isEnter {
if let keyHandlerDelegate = delegate {
//
if input.isShiftHold, input.isCommandHold, !state.isFilterable {
IME.prtDebugIntel("2EAC1F7A")
errorCallback()
return true
}
if !state.isMarkedLengthValid {
IME.prtDebugIntel("9AAFAC00")
errorCallback()
return true
}
if !keyHandlerDelegate.keyHandler(self, didRequestWriteUserPhraseWith: state, addToFilter: false) {
IME.prtDebugIntel("5B69CC8D")
errorCallback()
return true
}
}
stateCallback(buildInputtingState)
return true
}
// BackSpace & Delete
if input.isBackSpace || input.isDelete {
if let keyHandlerDelegate = delegate {
if !state.isFilterable {
IME.prtDebugIntel("1F88B191")
errorCallback()
return true
}
if !keyHandlerDelegate.keyHandler(self, didRequestWriteUserPhraseWith: state, addToFilter: true) {
IME.prtDebugIntel("68D3C6C8")
errorCallback()
return true
}
}
stateCallback(buildInputtingState)
return true
}
// Shift + Left
if input.isCursorBackward, input.isShiftHold {
if compositor.marker > 0 {
compositor.marker -= 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: state.data.displayTextSegments,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
IME.prtDebugIntel("1149908D")
errorCallback()
stateCallback(state)
}
return true
}
// Shift + Right
if input.isCursorForward, input.isShiftHold {
if compositor.marker < compositor.width {
compositor.marker += 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: state.data.displayTextSegments,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
} else {
IME.prtDebugIntel("9B51408D")
errorCallback()
stateCallback(state)
}
return true
}
return false
}
// MARK: -
///
/// - Parameters:
/// - customPunctuation:
/// - state:
/// - isTypingVertical:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handlePunctuation(
_ customPunctuation: String,
state: IMEStateProtocol,
usingVerticalTyping isTypingVertical: Bool,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
if !currentLM.hasUnigramsFor(key: customPunctuation) {
return false
}
guard composer.isEmpty else {
//
IME.prtDebugIntel("A9B69908D")
errorCallback()
stateCallback(state)
return true
}
compositor.insertKey(customPunctuation)
walk()
// App
let textToCommit = commitOverflownComposition
var inputting = buildInputtingState
inputting.textToCommit = textToCommit
stateCallback(inputting)
//
guard mgrPrefs.useSCPCTypingMode, composer.isEmpty else { return true }
let candidateState = buildCandidate(
state: inputting,
isTypingVertical: isTypingVertical
)
if candidateState.candidates.count == 1 {
clear() // candidateState
if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty {
stateCallback(IMEState.ofCommitting(textToCommit: candidateToCommit.1))
stateCallback(IMEState.ofEmpty())
} else {
stateCallback(candidateState)
}
} else {
stateCallback(candidateState)
}
return true
}
// MARK: - Enter
/// Enter
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: ctlInputMethod IMK
func handleEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
stateCallback(IMEState.ofCommitting(textToCommit: state.displayedText))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: - Command+Enter
/// Command+Enter
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: ctlInputMethod IMK
func handleCtrlCommandEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
var displayedText = compositor.keys.joined(separator: "-")
if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin {
displayedText = Tekkon.restoreToneOneInZhuyinKey(target: displayedText) //
displayedText = Tekkon.cnvPhonaToHanyuPinyin(target: displayedText) //
}
if let delegate = delegate, !delegate.clientBundleIdentifier.contains("vChewingPhraseEditor") {
displayedText = displayedText.replacingOccurrences(of: "-", with: " ")
}
stateCallback(IMEState.ofCommitting(textToCommit: displayedText))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: - Command+Option+Enter Ruby
/// Command+Option+Enter Ruby
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: ctlInputMethod IMK
func handleCtrlOptionCommandEnter(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
var composed = ""
for node in compositor.walkedNodes {
var key = node.key
if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin {
key = Tekkon.restoreToneOneInZhuyinKey(target: key) //
key = Tekkon.cnvPhonaToHanyuPinyin(target: key) //
key = Tekkon.cnvHanyuPinyinToTextbookStyle(target: key) // 調
key = key.replacingOccurrences(of: "-", with: " ")
} else {
key = Tekkon.cnvZhuyinChainToTextbookReading(target: key, newSeparator: " ")
}
let value = node.value
//
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
}
stateCallback(IMEState.ofCommitting(textToCommit: composed))
stateCallback(IMEState.ofEmpty())
return true
}
// MARK: - BackSpace (macOS Delete)
/// BackSpace (macOS Delete)
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleBackSpace(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
// macOS Shift+BackSpace
switch mgrPrefs.specifyShiftBackSpaceKeyBehavior {
case 0:
guard input.isShiftHold, composer.isEmpty else { break }
guard let prevReading = previousParsableReading else { break }
// prevReading 調調
compositor.dropKey(direction: .rear)
walk() // Walk walk
prevReading.1.charComponents.forEach { composer.receiveKey(fromPhonabet: $0) }
stateCallback(buildInputtingState)
return true
case 1:
stateCallback(IMEState.ofAbortion())
return true
default: break
}
if input.isShiftHold, input.isOptionHold {
stateCallback(IMEState.ofAbortion())
return true
}
if composer.hasToneMarker(withNothingElse: true) {
composer.clear()
} else if composer.isEmpty {
if compositor.cursor > 0 {
compositor.dropKey(direction: .rear)
walk()
} else {
IME.prtDebugIntel("9D69908D")
errorCallback()
stateCallback(state)
return true
}
} else {
composer.doBackSpace()
}
switch composer.isEmpty && compositor.isEmpty {
case false: stateCallback(buildInputtingState)
case true:
stateCallback(IMEState.ofAbortion())
}
return true
}
// MARK: - PC Delete (macOS Fn+BackSpace)
/// PC Delete (macOS Fn+BackSpace)
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleDelete(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if input.isShiftHold {
stateCallback(IMEState.ofAbortion())
return true
}
if compositor.cursor == compositor.length, composer.isEmpty {
IME.prtDebugIntel("9B69938D")
errorCallback()
stateCallback(state)
return true
}
if composer.isEmpty {
compositor.dropKey(direction: .front)
walk()
} else {
composer.clear()
}
let inputting = buildInputtingState
// count > 0!isEmpty滿
switch inputting.displayedText.isEmpty {
case false: stateCallback(inputting)
case true:
stateCallback(IMEState.ofAbortion())
}
return true
}
// MARK: - 90
/// 90
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleClockKey(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("9B6F908D")
errorCallback()
}
stateCallback(state)
return true
}
// MARK: - Home
/// Home
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleHome(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("ABC44080")
errorCallback()
stateCallback(state)
return true
}
if compositor.cursor != 0 {
compositor.cursor = 0
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("66D97F90")
errorCallback()
stateCallback(state)
}
return true
}
// MARK: - End
/// End
/// - Parameters:
/// - state:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleEnd(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("9B69908D")
errorCallback()
stateCallback(state)
return true
}
if compositor.cursor != compositor.length {
compositor.cursor = compositor.length
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("9B69908E")
errorCallback()
stateCallback(state)
}
return true
}
// MARK: - Esc
/// Esc
/// - Parameters:
/// - state:
/// - stateCallback:
/// - Returns: ctlInputMethod IMK
func handleEsc(
state: IMEStateProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if mgrPrefs.escToCleanInputBuffer {
///
/// macOS Windows 使
stateCallback(IMEState.ofAbortion())
} else {
if composer.isEmpty { return true }
///
composer.clear()
switch compositor.isEmpty {
case false: stateCallback(buildInputtingState)
case true:
stateCallback(IMEState.ofAbortion())
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleForward(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("B3BA5257")
errorCallback()
stateCallback(state)
return true
}
if input.isShiftHold {
// Shift + Right
if compositor.cursor < compositor.width {
compositor.marker = compositor.cursor + 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .front, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: compositor.walkedNodes.values,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.data.tooltipBackupForInputting = state.tooltip
stateCallback(marking)
} else {
IME.prtDebugIntel("BB7F6DB9")
errorCallback()
stateCallback(state)
}
} else if input.isOptionHold {
if input.isControlHold {
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
//
if !compositor.jumpCursorBySpan(to: .front) {
IME.prtDebugIntel("33C3B580")
errorCallback()
stateCallback(state)
return true
}
stateCallback(buildInputtingState)
} else {
if compositor.cursor < compositor.length {
compositor.cursor += 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .front)
}
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("A96AAD58")
errorCallback()
stateCallback(state)
}
}
return true
}
// MARK: -
///
/// - Parameters:
/// - state:
/// - input:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleBackward(
state: IMEStateProtocol,
input: InputSignalProtocol,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
guard state.type == .ofInputting else { return false }
if !composer.isEmpty {
IME.prtDebugIntel("6ED95318")
errorCallback()
stateCallback(state)
return true
}
if input.isShiftHold {
// Shift + left
if compositor.cursor > 0 {
compositor.marker = compositor.cursor - 1
if isCursorCuttingChar(isMarker: true) {
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
}
var marking = IMEState.ofMarking(
displayTextSegments: compositor.walkedNodes.values,
markedReadings: Array(compositor.keys[currentMarkedRange()]),
cursor: convertCursorForDisplay(compositor.cursor),
marker: convertCursorForDisplay(compositor.marker)
)
marking.data.tooltipBackupForInputting = state.tooltip
stateCallback(marking)
} else {
IME.prtDebugIntel("D326DEA3")
errorCallback()
stateCallback(state)
}
} else if input.isOptionHold {
if input.isControlHold {
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
}
//
if !compositor.jumpCursorBySpan(to: .rear) {
IME.prtDebugIntel("8D50DD9E")
errorCallback()
stateCallback(state)
return true
}
stateCallback(buildInputtingState)
} else {
if compositor.cursor > 0 {
compositor.cursor -= 1
if isCursorCuttingChar() {
compositor.jumpCursorBySpan(to: .rear)
}
stateCallback(buildInputtingState)
} else {
IME.prtDebugIntel("7045E6F3")
errorCallback()
stateCallback(state)
}
}
return true
}
// MARK: - Tab Shift+Space
///
/// - Parameters:
/// - state:
/// - reverseModifier:
/// - stateCallback:
/// - errorCallback:
/// - Returns: ctlInputMethod IMK
func handleInlineCandidateRotation(
state: IMEStateProtocol,
reverseModifier: Bool,
stateCallback: @escaping (IMEStateProtocol) -> Void,
errorCallback: @escaping () -> Void
) -> Bool {
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
guard state.type == .ofInputting else {
guard state.type == .ofEmpty else {
IME.prtDebugIntel("6044F081")
errorCallback()
return true
}
// 使 Tab
return false
}
guard composer.isEmpty else {
IME.prtDebugIntel("A2DAF7BC")
errorCallback()
return true
}
let candidates = getCandidatesArray(fixOrder: true)
guard !candidates.isEmpty else {
IME.prtDebugIntel("3378A6DF")
errorCallback()
return true
}
var length = 0
var currentNode: Megrez.Compositor.Node?
let cursorIndex = actualCandidateCursor
for node in compositor.walkedNodes {
length += node.spanLength
if length > cursorIndex {
currentNode = node
break
}
}
guard let currentNode = currentNode else {
IME.prtDebugIntel("F58DEA95")
errorCallback()
return true
}
let currentPaired = (currentNode.key, currentNode.value)
var currentIndex = 0
if !currentNode.isOverriden {
/// 使
/// 使
/// 2 使
///
///
/// 使
/// (Shift+)Tab ()
/// Shift(+Command)+Space Alt+/ Alt+/
/// Tab
if candidates[0] == currentPaired {
///
///
currentIndex = reverseModifier ? candidates.count - 1 : 1
}
} else {
for candidate in candidates {
if candidate == currentPaired {
if reverseModifier {
if currentIndex == 0 {
currentIndex = candidates.count - 1
} else {
currentIndex -= 1
}
} else {
currentIndex += 1
}
break
}
currentIndex += 1
}
}
if currentIndex >= candidates.count {
currentIndex = 0
}
fixNode(candidate: candidates[currentIndex], respectCursorPushing: false, preConsolidate: false)
stateCallback(buildInputtingState)
return true
}
}

View File

@ -0,0 +1,352 @@
// (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 Cocoa
// MARK: - NSEvent Extension - Reconstructors
extension NSEvent {
public func reinitiate(
with type: NSEvent.EventType? = nil,
location: NSPoint? = nil,
modifierFlags: NSEvent.ModifierFlags? = nil,
timestamp: TimeInterval? = nil,
windowNumber: Int? = nil,
characters: String? = nil,
charactersIgnoringModifiers: String? = nil,
isARepeat: Bool? = nil,
keyCode: UInt16? = nil
) -> NSEvent? {
let oldChars: String = {
if self.type == .flagsChanged { return "" }
return self.characters ?? ""
}()
return NSEvent.keyEvent(
with: type ?? self.type,
location: location ?? locationInWindow,
modifierFlags: modifierFlags ?? self.modifierFlags,
timestamp: timestamp ?? self.timestamp,
windowNumber: windowNumber ?? self.windowNumber,
context: nil,
characters: characters ?? oldChars,
charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters ?? oldChars,
isARepeat: isARepeat ?? self.isARepeat,
keyCode: keyCode ?? self.keyCode
)
}
/// Emacs NSEvent NSEvent NSEvent
/// - Parameter isVerticalTyping:
/// - Returns:
public func convertFromEmacKeyEvent(isVerticalContext: Bool) -> NSEvent {
guard isEmacsKey else { return self }
let newKeyCode: UInt16 = {
switch isVerticalContext {
case false: return IME.vChewingEmacsKey.charKeyMapHorizontal[charCode] ?? 0
case true: return IME.vChewingEmacsKey.charKeyMapVertical[charCode] ?? 0
}
}()
guard newKeyCode != 0 else { return self }
let newCharScalar: Unicode.Scalar = {
switch charCode {
case 6:
return isVerticalContext
? NSEvent.SpecialKey.downArrow.unicodeScalar : NSEvent.SpecialKey.rightArrow.unicodeScalar
case 2:
return isVerticalContext
? NSEvent.SpecialKey.upArrow.unicodeScalar : NSEvent.SpecialKey.leftArrow.unicodeScalar
case 1: return NSEvent.SpecialKey.home.unicodeScalar
case 5: return NSEvent.SpecialKey.end.unicodeScalar
case 4: return NSEvent.SpecialKey.deleteForward.unicodeScalar // Use "deleteForward" for PC delete.
case 22: return NSEvent.SpecialKey.pageDown.unicodeScalar
default: return .init(0)
}
}()
let newChar = String(newCharScalar)
return reinitiate(modifierFlags: [], characters: newChar, charactersIgnoringModifiers: newChar, keyCode: newKeyCode)
?? self
}
}
// MARK: - NSEvent Extension - InputSignalProtocol
extension NSEvent: InputSignalProtocol {
public var isASCIIModeInput: Bool { ctlInputMethod.isASCIIModeSituation }
public var isTypingVertical: Bool { ctlInputMethod.isVerticalTypingSituation }
public var text: String { AppleKeyboardConverter.cnvStringApple2ABC(characters ?? "") }
public var inputTextIgnoringModifiers: String? {
guard let charIgnoringModifiers = charactersIgnoringModifiers else { return nil }
return AppleKeyboardConverter.cnvStringApple2ABC(charIgnoringModifiers)
}
public var charCode: UInt16 {
guard type != .flagsChanged else { return 0 }
guard characters != nil else { return 0 }
// count > 0!isEmpty滿
guard !text.isEmpty else { return 0 }
let scalars = text.unicodeScalars
let result = scalars[scalars.startIndex].value
return result <= UInt16.max ? UInt16(result) : UInt16.max
}
public var isFlagChanged: Bool { type == .flagsChanged }
public var isEmacsKey: Bool {
// isControlHold
[6, 2, 1, 5, 4, 22].contains(charCode) && modifierFlags == .control
}
// Alt+Shift+ macOS
// KeyCode
//
public var mainAreaNumKeyChar: String? { mapMainAreaNumKey[keyCode] }
// ANSI charCode Swift KeyHandler
public var isInvalid: Bool {
(0x20...0xFF).contains(charCode) ? false : !(isReservedKey && !isKeyCodeBlacklisted)
}
public var isKeyCodeBlacklisted: Bool {
guard let code = KeyCodeBlackListed(rawValue: keyCode) else { return false }
return code.rawValue != KeyCode.kNone.rawValue
}
public var isReservedKey: Bool {
guard let code = KeyCode(rawValue: keyCode) else { return false }
return code.rawValue != KeyCode.kNone.rawValue
}
public var isCandidateKey: Bool {
mgrPrefs.candidateKeys.contains(text)
|| mgrPrefs.candidateKeys.contains(inputTextIgnoringModifiers ?? "114514")
}
/// flags KeyCode
public var isNumericPadKey: Bool { arrNumpadKeyCodes.contains(keyCode) }
public var isMainAreaNumKey: Bool { arrMainAreaNumKey.contains(keyCode) }
public var isShiftHold: Bool { modifierFlags.contains([.shift]) }
public var isCommandHold: Bool { modifierFlags.contains([.command]) }
public var isControlHold: Bool { modifierFlags.contains([.control]) }
public var isControlHotKey: Bool { modifierFlags.contains([.control]) && text.first?.isLetter ?? false }
public var isOptionHold: Bool { modifierFlags.contains([.option]) }
public var isOptionHotKey: Bool { modifierFlags.contains([.option]) && text.first?.isLetter ?? false }
public var isCapsLockOn: Bool { modifierFlags.contains([.capsLock]) }
public var isFunctionKeyHold: Bool { modifierFlags.contains([.function]) }
public var isNonLaptopFunctionKey: Bool { modifierFlags.contains([.numericPad]) && !isNumericPadKey }
public var isEnter: Bool { [KeyCode.kCarriageReturn, KeyCode.kLineFeed].contains(KeyCode(rawValue: keyCode)) }
public var isTab: Bool { KeyCode(rawValue: keyCode) == KeyCode.kTab }
public var isUp: Bool { KeyCode(rawValue: keyCode) == KeyCode.kUpArrow }
public var isDown: Bool { KeyCode(rawValue: keyCode) == KeyCode.kDownArrow }
public var isLeft: Bool { KeyCode(rawValue: keyCode) == KeyCode.kLeftArrow }
public var isRight: Bool { KeyCode(rawValue: keyCode) == KeyCode.kRightArrow }
public var isPageUp: Bool { KeyCode(rawValue: keyCode) == KeyCode.kPageUp }
public var isPageDown: Bool { KeyCode(rawValue: keyCode) == KeyCode.kPageDown }
public var isSpace: Bool { KeyCode(rawValue: keyCode) == KeyCode.kSpace }
public var isBackSpace: Bool { KeyCode(rawValue: keyCode) == KeyCode.kBackSpace }
public var isEsc: Bool { KeyCode(rawValue: keyCode) == KeyCode.kEscape }
public var isHome: Bool { KeyCode(rawValue: keyCode) == KeyCode.kHome }
public var isEnd: Bool { KeyCode(rawValue: keyCode) == KeyCode.kEnd }
public var isDelete: Bool { KeyCode(rawValue: keyCode) == KeyCode.kWindowsDelete }
public var isCursorBackward: Bool {
isTypingVertical
? KeyCode(rawValue: keyCode) == .kUpArrow
: KeyCode(rawValue: keyCode) == .kLeftArrow
}
public var isCursorForward: Bool {
isTypingVertical
? KeyCode(rawValue: keyCode) == .kDownArrow
: KeyCode(rawValue: keyCode) == .kRightArrow
}
public var isCursorClockRight: Bool {
isTypingVertical
? KeyCode(rawValue: keyCode) == .kRightArrow
: KeyCode(rawValue: keyCode) == .kUpArrow
}
public var isCursorClockLeft: Bool {
isTypingVertical
? KeyCode(rawValue: keyCode) == .kLeftArrow
: KeyCode(rawValue: keyCode) == .kDownArrow
}
public var isASCII: Bool { charCode < 0x80 }
// flags == .shift Shift
public var isUpperCaseASCIILetterKey: Bool {
(65...90).contains(charCode) && modifierFlags == .shift
}
// KeyCode macOS Apple
// ![input isShiftHold] 使 Shift
public var isSymbolMenuPhysicalKey: Bool {
[KeyCode.kSymbolMenuPhysicalKeyIntl, KeyCode.kSymbolMenuPhysicalKeyJIS].contains(KeyCode(rawValue: keyCode))
}
}
// MARK: - InputSignalProtocol
public protocol InputSignalProtocol {
var isASCIIModeInput: Bool { get }
var isTypingVertical: Bool { get }
var text: String { get }
var inputTextIgnoringModifiers: String? { get }
var charCode: UInt16 { get }
var keyCode: UInt16 { get }
var isFlagChanged: Bool { get }
var mainAreaNumKeyChar: String? { get }
var isASCII: Bool { get }
var isInvalid: Bool { get }
var isKeyCodeBlacklisted: Bool { get }
var isReservedKey: Bool { get }
var isCandidateKey: Bool { get }
var isNumericPadKey: Bool { get }
var isMainAreaNumKey: Bool { get }
var isShiftHold: Bool { get }
var isCommandHold: Bool { get }
var isControlHold: Bool { get }
var isControlHotKey: Bool { get }
var isOptionHold: Bool { get }
var isOptionHotKey: Bool { get }
var isCapsLockOn: Bool { get }
var isFunctionKeyHold: Bool { get }
var isNonLaptopFunctionKey: Bool { get }
var isEnter: Bool { get }
var isTab: Bool { get }
var isUp: Bool { get }
var isDown: Bool { get }
var isLeft: Bool { get }
var isRight: Bool { get }
var isPageUp: Bool { get }
var isPageDown: Bool { get }
var isSpace: Bool { get }
var isBackSpace: Bool { get }
var isEsc: Bool { get }
var isHome: Bool { get }
var isEnd: Bool { get }
var isDelete: Bool { get }
var isCursorBackward: Bool { get }
var isCursorForward: Bool { get }
var isCursorClockRight: Bool { get }
var isCursorClockLeft: Bool { get }
var isUpperCaseASCIILetterKey: Bool { get }
var isSymbolMenuPhysicalKey: Bool { get }
}
// MARK: - Enums of Constants
// Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts.
// KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes
// Also: HIToolbox.framework/Versions/A/Headers/Events.h
public enum KeyCode: UInt16 {
case kNone = 0
case kCarriageReturn = 36 // Renamed from "kReturn" to avoid nomenclatural confusions.
case kTab = 48
case kSpace = 49
case kSymbolMenuPhysicalKeyIntl = 50 // vChewing Specific (Non-JIS)
case kBackSpace = 51 // Renamed from "kDelete" to avoid nomenclatural confusions.
case kEscape = 53
case kCommand = 55
case kShift = 56
case kCapsLock = 57
case kOption = 58
case kControl = 59
case kRightShift = 60
case kRightOption = 61
case kRightControl = 62
case kFunction = 63
case kF17 = 64
case kVolumeUp = 72
case kVolumeDown = 73
case kMute = 74
case kLineFeed = 76 // Another keyCode to identify the Enter Key, typable by Fn+Enter.
case kF18 = 79
case kF19 = 80
case kF20 = 90
case kSymbolMenuPhysicalKeyJIS = 94 // vChewing Specific (JIS)
case kF5 = 96
case kF6 = 97
case kF7 = 98
case kF3 = 99
case kF8 = 100
case kF9 = 101
case kF11 = 103
case kF13 = 105 // PrtSc
case kF16 = 106
case kF14 = 107
case kF10 = 109
case kF12 = 111
case kF15 = 113
case kHelp = 114 // Insert
case kHome = 115
case kPageUp = 116
case kWindowsDelete = 117 // Renamed from "kForwardDelete" to avoid nomenclatural confusions.
case kF4 = 118
case kEnd = 119
case kF2 = 120
case kPageDown = 121
case kF1 = 122
case kLeftArrow = 123
case kRightArrow = 124
case kDownArrow = 125
case kUpArrow = 126
}
enum KeyCodeBlackListed: UInt16 {
case kF17 = 64
case kVolumeUp = 72
case kVolumeDown = 73
case kMute = 74
case kF18 = 79
case kF19 = 80
case kF20 = 90
case kF5 = 96
case kF6 = 97
case kF7 = 98
case kF3 = 99
case kF8 = 100
case kF9 = 101
case kF11 = 103
case kF13 = 105 // PrtSc
case kF16 = 106
case kF14 = 107
case kF10 = 109
case kF12 = 111
case kF15 = 113
case kHelp = 114 // Insert
case kF4 = 118
case kF2 = 120
case kF1 = 122
}
// Alt+Shift+ macOS
// KeyCode
let mapMainAreaNumKey: [UInt16: String] = [
18: "1", 19: "2", 20: "3", 21: "4", 23: "5", 22: "6", 26: "7", 28: "8", 25: "9", 29: "0",
]
/// KeyCode
///
/// 95 Key Code JIS
let arrNumpadKeyCodes: [UInt16] = [65, 67, 69, 71, 75, 78, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 95]
/// KeyCode
let arrMainAreaNumKey: [UInt16] = [18, 19, 20, 21, 22, 23, 25, 26, 28, 29]
// CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html
enum CharCode: UInt16 {
case yajuusenpaiA = 114
case yajuusenpaiB = 514
case yajuusenpaiC = 1919
case yajuusenpaiD = 810
// CharCode is not reliable at all. KeyCode is the most appropriate choice due to its accuracy.
// KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts ...
// ... but only focuses on which physical key is pressed.
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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
extension ctlInputMethod {
/// handle() IMK
/// handle()
/// - Parameter event: IMK
/// - Returns: `true` IMK`false`
func commonEventHandler(_ event: NSEvent) -> Bool {
//
// KeyHandler
if !event.charCode.isPrintable { return false }
/// 調
/// result bool IMK
/// keyHandler.handleCandidate()
let result = keyHandler.handle(input: event, state: state) { newState in
self.handle(state: newState)
} errorCallback: {
clsSFX.beep()
}
return result
}
/// handle() IMK
/// handle()
/// - Parameter event: IMK
/// - Returns: `true` IMK`false`
func imkCandidatesEventHandler(event eventToDeal: NSEvent) -> Bool? {
// IMK IMK
// interpretKeyEvents()
// - super.interpretKeyEvents()
// - delegate ctlInputMethod KeyHandler
if let imkCandidates = ctlInputMethod.ctlCandidateCurrent as? ctlCandidateIMK, imkCandidates.visible {
let event: NSEvent = ctlCandidateIMK.replaceNumPadKeyCodes(target: eventToDeal) ?? eventToDeal
// Shift+Enter delegate keyHandler
// Shift Flags
if event.isShiftHold, event.isEnter {
guard let newEvent = event.reinitiate(modifierFlags: []) else {
NSSound.beep()
return true
}
imkCandidates.interpretKeyEvents([newEvent])
return true
}
//
if let newChar = ctlCandidateIMK.defaultIMKSelectionKey[event.keyCode],
event.isShiftHold, isAssociatedPhrasesState,
let newEvent = event.reinitiate(modifierFlags: [], characters: newChar)
{
imkCandidates.handleKeyboardEvent(newEvent)
}
imkCandidates.interpretKeyEvents([event])
return true
}
return nil
}
}

View File

@ -0,0 +1,433 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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
///
///
/// IMKInputController
///
/// / IMKInputController
/// 調
/// - Remark: IMKServer
/// IMKInputController
@objc(ctlInputMethod) // ObjC IMK ObjC
class ctlInputMethod: IMKInputController {
///
static var areWeNerfing = false
///
static var ctlCandidateCurrent: ctlCandidateProtocol =
mgrPrefs.useIMKCandidateWindow ? ctlCandidateIMK.init(.horizontal) : ctlCandidateUniversal.init(.horizontal)
///
static var tooltipInstance = ctlTooltip()
///
static var popupCompositionBuffer = ctlPopupCompositionBuffer()
// MARK: -
/// ctlInputMethod
static var isASCIIModeSituation: Bool = false
/// ctlInputMethod
static var isVerticalTypingSituation: Bool = false
/// ctlInputMethod
static var isVerticalCandidateSituation: Bool = false
/// ctlInputMethod
var isASCIIMode: Bool = false { didSet { setKeyLayout() } }
/// 調
var keyHandler: KeyHandler = .init()
///
var state: IMEStateProtocol = IMEState.ofEmpty() {
didSet {
IME.prtDebugIntel("Current State: \(state.type.rawValue)")
}
}
/// ctlInputMethod
func toggleASCIIMode() -> Bool {
resetKeyHandler()
isASCIIMode = !isASCIIMode
return isASCIIMode
}
/// `handle(event:)` Shift
var rencentKeyHandledByKeyHandlerEtc = false
// MARK: -
///
func setKeyLayout() {
guard let client = client() else { return }
if isASCIIMode, AppleKeyboardConverter.isDynamicBasicKeyboardLayoutEnabled {
client.overrideKeyboard(withKeyboardNamed: mgrPrefs.alphanumericalKeyboardLayout)
return
}
client.overrideKeyboard(withKeyboardNamed: mgrPrefs.basicKeyboardLayout)
}
/// 調
func resetKeyHandler() {
//
if state.type == .ofInputting, mgrPrefs.trimUnfinishedReadingsOnCommit {
keyHandler.composer.clear()
handle(state: keyHandler.buildInputtingState)
}
let isSecureMode = mgrPrefs.clientsIMKTextInputIncapable.contains(clientBundleIdentifier)
if state.hasComposition, !isSecureMode {
/// 調
handle(state: IMEState.ofCommitting(textToCommit: state.displayedText))
}
handle(state: isSecureMode ? IMEState.ofAbortion() : IMEState.ofEmpty())
}
// MARK: - IMKInputController
///
///
/// inputClient IMKServer IMKTextInput
/// - Remark: IMKInputController client()
/// - Parameters:
/// - server: IMKServer
/// - delegate:
/// - inputClient:
override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) {
super.init(server: server, delegate: delegate, client: inputClient)
keyHandler.delegate = self
//
resetKeyHandler()
activateServer(inputClient)
}
// MARK: - IMKStateSetting
///
/// - Parameter sender: 使
override func activateServer(_ sender: Any!) {
_ = sender //
UserDefaults.standard.synchronize()
// activateServer nil
//
if keyHandler.delegate == nil { keyHandler.delegate = self }
setValue(IME.currentInputMode.rawValue, forTag: 114_514, client: client())
keyHandler.clear() // handle State.Empty()
keyHandler.ensureParser()
if isASCIIMode, mgrPrefs.disableShiftTogglingAlphanumericalMode { isASCIIMode = false }
///
/// macOS
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
// 使
setKeyLayout()
handle(state: IMEState.ofEmpty())
} //
UpdateSputnik.shared.checkForUpdate()
}
///
/// - Parameter sender: 使
override func deactivateServer(_ sender: Any!) {
_ = sender //
resetKeyHandler() // Empty
handle(state: IMEState.ofDeactivated())
}
///
/// - Parameters:
/// - value: identifier bundle identifier info.plist
/// - tag: 使
/// - sender: 使
override func setValue(_ value: Any!, forTag tag: Int, client sender: Any!) {
_ = tag //
_ = sender //
var newInputMode = InputMode(rawValue: value as? String ?? "") ?? InputMode.imeModeNULL
switch newInputMode {
case InputMode.imeModeCHS:
newInputMode = InputMode.imeModeCHS
case InputMode.imeModeCHT:
newInputMode = InputMode.imeModeCHT
default:
newInputMode = InputMode.imeModeNULL
}
mgrLangModel.loadDataModel(newInputMode)
if keyHandler.inputMode != newInputMode {
UserDefaults.standard.synchronize()
keyHandler.clear() // handle State.Empty()
keyHandler.inputMode = newInputMode
///
/// macOS
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
// 使
setKeyLayout()
handle(state: IMEState.ofEmpty())
} //
}
//
IME.currentInputMode = keyHandler.inputMode
}
// MARK: - IMKServerInput
///
///
///
/// Swift `NSEvent.EventTypeMask = [.keyDown]` ObjC `NSKeyDownMask`
/// IMK
/// 使
/// `commitComposition(_ message)`
/// - Parameter sender: 使
/// - Returns: uint NSEvent NSEvent.h
override func recognizedEvents(_ sender: Any!) -> Int {
_ = sender //
let events: NSEvent.EventTypeMask = [.keyDown, .flagsChanged]
return Int(events.rawValue)
}
/// NSEvent
/// - Parameters:
/// - event: nil
/// - sender: 使
/// - Returns: `true` IMK`false`
@objc(handleEvent:client:) override func handle(_ event: NSEvent!, client sender: Any!) -> Bool {
_ = sender //
// MARK:
//
ctlInputMethod.isASCIIModeSituation = isASCIIMode
ctlInputMethod.isVerticalTypingSituation = isVerticalTyping
// NSEvent nilApple InputMethodKit
// client()
guard let event = event, sender is IMKTextInput else {
resetKeyHandler()
return false
}
// Shift macOS 10.15 macOS
let shouldUseShiftToggleHandle: Bool = {
switch mgrPrefs.shiftKeyAccommodationBehavior {
case 0: return false
case 1: return IME.arrClientShiftHandlingExceptionList.contains(clientBundleIdentifier)
case 2: return true
default: return false
}
}()
/// event event var Shift
if #available(macOS 10.15, *) {
if ShiftKeyUpChecker.check(event), !mgrPrefs.disableShiftTogglingAlphanumericalMode {
if !shouldUseShiftToggleHandle || (!rencentKeyHandledByKeyHandlerEtc && shouldUseShiftToggleHandle) {
NotifierController.notify(
message: toggleASCIIMode()
? NSLocalizedString("Alphanumerical Input Mode", comment: "")
: NSLocalizedString("Chinese Input Mode", comment: "")
)
}
if shouldUseShiftToggleHandle {
rencentKeyHandledByKeyHandlerEtc = false
}
return false
}
}
// MARK:
/// flags使 KeyHandler
/// flags
/// event.type == .flagsChanged return false
/// NSInternalInconsistencyException
if event.type == .flagsChanged { return false }
///
guard client() != nil else { return false }
var eventToDeal = event
// 使 NSEvent Emacs NSEvent NSEvent
if eventToDeal.isEmacsKey {
let verticalProcessing =
(state.isCandidateContainer)
? ctlInputMethod.isVerticalCandidateSituation : ctlInputMethod.isVerticalTypingSituation
eventToDeal = eventToDeal.convertFromEmacKeyEvent(isVerticalContext: verticalProcessing)
}
//
ctlInputMethod.areWeNerfing = eventToDeal.modifierFlags.contains([.shift, .command])
// IMK IMK
if let result = imkCandidatesEventHandler(event: eventToDeal) {
if shouldUseShiftToggleHandle {
rencentKeyHandledByKeyHandlerEtc = result
}
return result
}
/// NSEvent commonEventHandler
/// IMK 便
let result = commonEventHandler(eventToDeal)
if shouldUseShiftToggleHandle {
rencentKeyHandledByKeyHandlerEtc = result
}
return result
}
/// App Ctrl+Enter / Shift+Enter
/// handle(event:) Event
/// commitComposition
/// - Parameter sender: 使
override func commitComposition(_ sender: Any!) {
_ = sender //
resetKeyHandler()
// super.commitComposition(sender) //
}
///
/// - Parameter sender: 使
/// - Returns: nil
override func composedString(_ sender: Any!) -> Any! {
_ = sender //
guard state.hasComposition else { return "" }
return state.displayedText
}
///
/// IMK Bug
override func inputControllerWillClose() {
//
resetKeyHandler()
super.inputControllerWillClose()
}
// MARK: - IMKCandidates
/// IMK
/// - Parameter sender: 使
/// - Returns: IMK
override func candidates(_ sender: Any!) -> [Any]! {
_ = sender //
var arrResult = [String]()
// 便 IMEState
func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") {
for theCandidate in candidates {
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
var result = (theCandidate.1 == theConverted) ? theCandidate.1 : "\(theConverted)\u{1A}(\(theCandidate.1))"
if arrResult.contains(result) {
let reading: String =
mgrPrefs.showHanyuPinyinInCompositionBuffer
? Tekkon.cnvPhonaToHanyuPinyin(target: Tekkon.restoreToneOneInZhuyinKey(target: theCandidate.0))
: theCandidate.0
result = "\(result)\u{17}(\(reading))"
}
arrResult.append(prefix + result)
}
}
if state.type == .ofAssociates {
handleCandidatesPrepared(state.candidates, prefix: "")
} else if state.type == .ofSymbolTable {
// / JIS 使
arrResult = state.candidates.map(\.1)
} else if state.type == .ofCandidates {
guard !state.candidates.isEmpty else { return .init() }
if state.candidates[0].0.contains("_punctuation") {
arrResult = state.candidates.map(\.1) //
} else {
handleCandidatesPrepared(state.candidates)
}
}
return arrResult
}
/// IMK
/// - Parameter _:
override open func candidateSelectionChanged(_: NSAttributedString!) {
//
// IMKServer.commitCompositionWithReply() commitComposition()
// keyHandler
//
//
// ctlCandidateIMK identifier
// NSNotFound NSLog identifier
// console ips
// candidateSelected() identifier NSNotFound
// IMK 西
}
/// IMK
/// - Parameter candidateString:
override open func candidateSelected(_ candidateString: NSAttributedString!) {
let candidateString: NSAttributedString = candidateString ?? .init(string: "")
if state.type == .ofAssociates {
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
handle(state: IMEState.ofAbortion())
return
}
}
var indexDeducted = 0
// 便 IMEState
func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") {
for (i, neta) in candidates.enumerated() {
let theConverted = IME.kanjiConversionIfRequired(neta.1)
let netaShown = (neta.1 == theConverted) ? neta.1 : "\(theConverted)\u{1A}(\(neta.1))"
let reading: String =
mgrPrefs.showHanyuPinyinInCompositionBuffer
? Tekkon.cnvPhonaToHanyuPinyin(target: Tekkon.restoreToneOneInZhuyinKey(target: neta.0)) : neta.0
let netaShownWithPronunciation = "\(netaShown)\u{17}(\(reading))"
if candidateString.string == prefix + netaShownWithPronunciation {
indexDeducted = i
break
}
if candidateString.string == prefix + netaShown {
indexDeducted = i
break
}
}
}
// / JIS 使
func handleSymbolCandidatesSelected(_ candidates: [(String, String)]) {
for (i, neta) in candidates.enumerated() {
if candidateString.string == neta.1 {
indexDeducted = i
break
}
}
}
if state.type == .ofAssociates {
handleCandidatesSelected(state.candidates, prefix: "")
} else if state.type == .ofSymbolTable {
handleSymbolCandidatesSelected(state.candidates)
} else if state.type == .ofCandidates {
guard !state.candidates.isEmpty else { return }
if state.candidates[0].0.contains("_punctuation") {
handleSymbolCandidatesSelected(state.candidates) //
} else {
handleCandidatesSelected(state.candidates)
}
}
keyHandler(
keyHandler,
didSelectCandidateAt: indexDeducted,
ctlCandidate: ctlInputMethod.ctlCandidateCurrent
)
}
}

View File

@ -0,0 +1,170 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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 Cocoa
// MARK: - KeyHandler Delegate
extension ctlInputMethod: KeyHandlerDelegate {
///
public var isVerticalTyping: Bool {
guard let client = client() else { return false }
var textFrame = NSRect.seniorTheBeast
let attributes: [AnyHashable: Any]? = client.attributes(
forCharacterIndex: 0, lineHeightRectangle: &textFrame
)
return (attributes?["IMKTextOrientation"] as? NSNumber)?.intValue == 0 || false
}
var clientBundleIdentifier: String {
guard let client = client() else { return "" }
return client.bundleIdentifier() ?? ""
}
func ctlCandidate() -> ctlCandidateProtocol { ctlInputMethod.ctlCandidateCurrent }
func keyHandler(
_: KeyHandler, didSelectCandidateAt index: Int,
ctlCandidate controller: ctlCandidateProtocol
) {
ctlCandidate(controller, didSelectCandidateAtIndex: index)
}
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
-> Bool
{
guard state.type == .ofMarking else { return false }
let refInputModeReversed: InputMode =
(keyHandler.inputMode == InputMode.imeModeCHT)
? InputMode.imeModeCHS : InputMode.imeModeCHT
if !mgrLangModel.writeUserPhrase(
state.data.userPhrase, inputMode: keyHandler.inputMode,
areWeDuplicating: state.data.chkIfUserPhraseExists,
areWeDeleting: addToFilter
)
|| !mgrLangModel.writeUserPhrase(
state.data.userPhraseConverted, inputMode: refInputModeReversed,
areWeDuplicating: false,
areWeDeleting: addToFilter
)
{
return false
}
return true
}
}
// MARK: - Candidate Controller Delegate
extension ctlInputMethod: ctlCandidateDelegate {
var isAssociatedPhrasesState: Bool { state.type == .ofAssociates }
/// handle() IMK
/// handle()
/// IMK commonEventHandler()
/// - Parameter event: IMK
/// - Returns: `true` IMK`false`
@discardableResult func sharedEventHandler(_ event: NSEvent) -> Bool {
commonEventHandler(event)
}
func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int {
_ = controller //
if state.isCandidateContainer {
return state.candidates.count
}
return 0
}
///
/// - Parameter controller:
/// - Returns:
func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] {
_ = controller //
if state.isCandidateContainer {
return state.candidates
}
return .init()
}
func ctlCandidate(_ controller: ctlCandidateProtocol, candidateAtIndex index: Int)
-> (String, String)
{
_ = controller //
if state.isCandidateContainer {
return state.candidates[index]
}
return ("", "")
}
func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) {
_ = controller //
if state.type == .ofSymbolTable,
let node = state.node.children?[index]
{
if let children = node.children, !children.isEmpty {
handle(state: IMEState.ofEmpty()) //
handle(state: IMEState.ofSymbolTable(node: node))
} else {
handle(state: IMEState.ofCommitting(textToCommit: node.title))
handle(state: IMEState.ofEmpty())
}
return
}
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
let selectedValue = state.candidates[index]
keyHandler.fixNode(
candidate: selectedValue, respectCursorPushing: true,
preConsolidate: mgrPrefs.consolidateContextOnCandidateSelection
)
let inputting = keyHandler.buildInputtingState
if mgrPrefs.useSCPCTypingMode {
handle(state: IMEState.ofCommitting(textToCommit: inputting.displayedText))
// selectedValue.1
if mgrPrefs.associatedPhrasesEnabled {
let associates = keyHandler.buildAssociatePhraseState(
withPair: .init(key: selectedValue.0, value: selectedValue.1)
)
handle(state: associates.candidates.isEmpty ? IMEState.ofEmpty() : associates)
} else {
handle(state: IMEState.ofEmpty())
}
} else {
handle(state: inputting)
}
return
}
if state.type == .ofAssociates {
let selectedValue = state.candidates[index]
handle(state: IMEState.ofCommitting(textToCommit: selectedValue.1))
// selectedValue.1
//
guard let valueKept = selectedValue.1.last else {
handle(state: IMEState.ofEmpty())
return
}
if mgrPrefs.associatedPhrasesEnabled {
let associates = keyHandler.buildAssociatePhraseState(
withPair: .init(key: selectedValue.0, value: String(valueKept))
)
if !associates.candidates.isEmpty {
handle(state: associates)
return
}
}
handle(state: IMEState.ofEmpty())
}
}
}

View File

@ -0,0 +1,207 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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 Cocoa
// MARK: - Tooltip Display and Candidate Display Methods
extension ctlInputMethod {
// App 使
// App App
// 使 20
var attributedStringSecured: (NSAttributedString, NSRange) {
mgrPrefs.clientsIMKTextInputIncapable.contains(clientBundleIdentifier)
? (state.data.attributedStringPlaceholder, NSRange(location: 0, length: 0))
: (state.attributedString, NSRange(state.data.u16MarkedRange))
}
func lineHeightRect(zeroCursor: Bool = false) -> NSRect {
var lineHeightRect = NSRect.seniorTheBeast
guard let client = client() else {
return lineHeightRect
}
var u16Cursor: Int = {
// iMessage cursor == 0
if clientBundleIdentifier == "com.apple.MobileSMS" { return state.data.u16Cursor }
if state.data.marker >= state.data.cursor { return state.data.u16Cursor }
return state.data.u16Marker //
}()
u16Cursor = max(min(state.data.displayedTextConverted.utf16.count, u16Cursor), 0)
if zeroCursor { u16Cursor = 0 }
while lineHeightRect.origin.x == 0, lineHeightRect.origin.y == 0, u16Cursor >= 0 {
client.attributes(
forCharacterIndex: u16Cursor, lineHeightRectangle: &lineHeightRect
)
u16Cursor -= 1
}
return lineHeightRect
}
func show(tooltip: String) {
guard client() != nil else { return }
let lineHeightRect = lineHeightRect()
var finalOrigin: NSPoint = lineHeightRect.origin
let delta: CGFloat = lineHeightRect.size.height + 4.0 // bottomOutOfScreenAdjustmentHeight
if isVerticalTyping {
finalOrigin = NSPoint(
x: lineHeightRect.origin.x + lineHeightRect.size.width + 5, y: lineHeightRect.origin.y
)
}
let tooltipContentDirection: NSAttributedTextView.writingDirection = {
if mgrPrefs.alwaysShowTooltipTextsHorizontally { return .horizontal }
return isVerticalTyping ? .vertical : .horizontal
}()
// NSAttributedTextView
do {
ctlInputMethod.tooltipInstance.hide()
ctlInputMethod.tooltipInstance = .init()
if state.type == .ofMarking {
ctlInputMethod.tooltipInstance.setColor(state: state.data.tooltipColorState)
}
}
//
ctlInputMethod.tooltipInstance.show(
tooltip: tooltip, at: finalOrigin,
bottomOutOfScreenAdjustmentHeight: delta, direction: tooltipContentDirection
)
}
func show(candidateWindowWith state: IMEStateProtocol) {
guard let client = client() else { return }
var isCandidateWindowVertical: Bool {
var candidates: [(String, String)] = .init()
if state.isCandidateContainer {
candidates = state.candidates
}
if isVerticalTyping { return true }
// IMK
guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false }
// 使
//
// 使
// Beer emoji
let maxCandidatesPerPage = mgrPrefs.candidateKeys.count
let firstPageCandidates = candidates[0..<min(maxCandidatesPerPage, candidates.count)].map(\.1)
return firstPageCandidates.joined().count > Int(round(Double(maxCandidatesPerPage) * 1.8))
// true
}
ctlInputMethod.isVerticalCandidateSituation = (isCandidateWindowVertical || !mgrPrefs.useHorizontalCandidateList)
ctlInputMethod.ctlCandidateCurrent.delegate = nil
/// currentLayout
/// ctlCandidate() SymbolTable
///
/// layoutCandidateView
/// macOS 10.x SwiftUI
let candidateLayout: CandidateLayout =
((isCandidateWindowVertical || !mgrPrefs.useHorizontalCandidateList)
? CandidateLayout.vertical
: CandidateLayout.horizontal)
ctlInputMethod.ctlCandidateCurrent =
mgrPrefs.useIMKCandidateWindow
? ctlCandidateIMK.init(candidateLayout) : ctlCandidateUniversal.init(candidateLayout)
// set the attributes for the candidate panel (which uses NSAttributedString)
let textSize = mgrPrefs.candidateListTextSize
let keyLabelSize = max(textSize / 2, mgrPrefs.minKeyLabelSize)
func labelFont(name: String?, size: CGFloat) -> NSFont {
if let name = name {
return NSFont(name: name, size: size) ?? NSFont.systemFont(ofSize: size)
}
return NSFont.systemFont(ofSize: size)
}
ctlInputMethod.ctlCandidateCurrent.keyLabelFont = labelFont(
name: mgrPrefs.candidateKeyLabelFontName, size: keyLabelSize
)
ctlInputMethod.ctlCandidateCurrent.candidateFont = ctlInputMethod.candidateFont(
name: mgrPrefs.candidateTextFontName, size: textSize
)
let candidateKeys = mgrPrefs.candidateKeys
let keyLabels =
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
let keyLabelSuffix = state.type == .ofAssociates ? "^" : ""
ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map {
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
}
ctlInputMethod.ctlCandidateCurrent.delegate = self
ctlInputMethod.ctlCandidateCurrent.reloadData()
// Spotlight IMK
if let ctlCandidateCurrent = ctlInputMethod.ctlCandidateCurrent as? ctlCandidateIMK {
while ctlCandidateCurrent.windowLevel() <= client.windowLevel() {
ctlCandidateCurrent.setWindowLevel(UInt64(max(0, client.windowLevel() + 1000)))
}
}
ctlInputMethod.ctlCandidateCurrent.visible = true
if isVerticalTyping {
ctlInputMethod.ctlCandidateCurrent.set(
windowTopLeftPoint: NSPoint(
x: lineHeightRect().origin.x + lineHeightRect().size.width + 4.0, y: lineHeightRect().origin.y - 4.0
),
bottomOutOfScreenAdjustmentHeight: lineHeightRect().size.height + 4.0
)
} else {
ctlInputMethod.ctlCandidateCurrent.set(
windowTopLeftPoint: NSPoint(x: lineHeightRect().origin.x, y: lineHeightRect().origin.y - 4.0),
bottomOutOfScreenAdjustmentHeight: lineHeightRect().size.height + 4.0
)
}
}
/// FB10978412: Since macOS 11 Big Sur, CTFontCreateUIFontForLanguage cannot
/// distinguish zh-Hans and zh-Hant with correct adoptation of proper PingFang SC/TC variants.
///
/// Instructions for Apple Developer relations to reveal this bug:
///
/// 0) Disable IMK Candidate window in the vChewing preferences (disabled by default).
/// **REASON**: IMKCandidates has bug that it does not respect font attributes attached to the
/// results generated from `candidiates() -> [Any]!` function. IMKCandidates is plagued with
/// bugs which are not dealt in the recent decade, regardless Radar complaints from input method developers.
/// 1) Remove the usage of ".languageIdentifier" from ctlCandidateUniversal.swift (already done).
/// 2) Run "make update" in the project folder to download the latest git-submodule of dictionary file.
/// 3) Compile the target "vChewingInstaller", run it. It will install the input method into
/// "~/Library/Input Methods/" folder. Remember to ENABLE BOTH "vChewing-CHS"
/// and "vChewing-CHT" input sources in System Preferences / Settings.
/// 4) Type Zhuyin "ej3" (ˇ) (or "gu3" in Pinyin if you enabled Pinyin typing in vChewing preferences.)
/// using both "vChewing-CHS" and "vChewing-CHT", and check the candidate window by pressing SPACE key.
/// 5) Do NOT enable either KangXi conversion mode nor JIS conversion mode. They are disabled by default.
/// 6) Expecting the glyph differences of the candidate "" between PingFang SC and PingFang TC when rendering
/// the candidate window in different "vChewing-CHS" and "vChewing-CHT" input modes.
static func candidateFont(name: String? = nil, size: CGFloat) -> NSFont {
let finalReturnFont: NSFont =
{
switch IME.currentInputMode {
case InputMode.imeModeCHS:
return CTFontCreateUIFontForLanguage(.system, size, "zh-Hans" as CFString)
case InputMode.imeModeCHT:
return (mgrPrefs.shiftJISShinjitaiOutputEnabled || mgrPrefs.chineseConversionEnabled)
? CTFontCreateUIFontForLanguage(.system, size, "ja" as CFString)
: CTFontCreateUIFontForLanguage(.system, size, "zh-Hant" as CFString)
default:
return CTFontCreateUIFontForLanguage(.system, size, nil)
}
}()
?? NSFont.systemFont(ofSize: size)
if let name = name, !name.isEmpty {
return NSFont(name: name, size: size) ?? finalReturnFont
}
return finalReturnFont
}
}

View File

@ -0,0 +1,143 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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: - 調 (State Handling)
extension ctlInputMethod {
/// 調
///
///
///
/// - Parameter newState:
func handle(state newState: IMEStateProtocol) {
let previous = state
state = newState
switch state.type {
case .ofDeactivated:
ctlInputMethod.ctlCandidateCurrent.delegate = nil
ctlInputMethod.ctlCandidateCurrent.visible = false
ctlInputMethod.tooltipInstance.hide()
if previous.hasComposition {
commit(text: previous.displayedText)
}
clearInlineDisplay()
//
keyHandler.clear()
case .ofEmpty, .ofAbortion:
var previous = previous
if state.type == .ofAbortion {
state = IMEState.ofEmpty()
previous = state
}
ctlInputMethod.ctlCandidateCurrent.visible = false
ctlInputMethod.tooltipInstance.hide()
// .Abortion
if previous.hasComposition, state.type != .ofAbortion {
commit(text: previous.displayedText)
}
//
ctlInputMethod.ctlCandidateCurrent.visible = false
ctlInputMethod.tooltipInstance.hide()
clearInlineDisplay()
//
keyHandler.clear()
case .ofCommitting:
ctlInputMethod.ctlCandidateCurrent.visible = false
ctlInputMethod.tooltipInstance.hide()
let textToCommit = state.textToCommit
if !textToCommit.isEmpty { commit(text: textToCommit) }
clearInlineDisplay()
//
keyHandler.clear()
case .ofInputting:
ctlInputMethod.ctlCandidateCurrent.visible = false
ctlInputMethod.tooltipInstance.hide()
let textToCommit = state.textToCommit
if !textToCommit.isEmpty { commit(text: textToCommit) }
setInlineDisplayWithCursor()
if !state.tooltip.isEmpty {
show(tooltip: state.tooltip)
}
case .ofMarking:
ctlInputMethod.ctlCandidateCurrent.visible = false
setInlineDisplayWithCursor()
if state.tooltip.isEmpty {
ctlInputMethod.tooltipInstance.hide()
} else {
show(tooltip: state.tooltip)
}
case .ofCandidates, .ofAssociates, .ofSymbolTable:
ctlInputMethod.tooltipInstance.hide()
setInlineDisplayWithCursor()
show(candidateWindowWith: state)
default: break
}
//
if state.hasComposition, mgrPrefs.clientsIMKTextInputIncapable.contains(clientBundleIdentifier) {
ctlInputMethod.popupCompositionBuffer.isTypingDirectionVertical = isVerticalTyping
ctlInputMethod.popupCompositionBuffer.show(
state: state, at: lineHeightRect(zeroCursor: true).origin
)
} else {
ctlInputMethod.popupCompositionBuffer.hide()
}
}
/// .NotEmpty()
func setInlineDisplayWithCursor() {
guard let client = client() else { return }
if state.type == .ofAssociates {
client.setMarkedText(
state.data.attributedStringPlaceholder, selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
)
return
}
if state.hasComposition || state.isCandidateContainer {
/// selectionRange
/// 0 replacementRangeNSNotFound
///
client.setMarkedText(
attributedStringSecured.0, selectionRange: attributedStringSecured.1,
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
)
return
}
//
clearInlineDisplay()
}
/// .NotEmpty()
/// setInlineDisplayWithCursor()
private func clearInlineDisplay() {
guard let theClient = client() else { return }
theClient.setMarkedText(
"", selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
)
}
///
/// IMK commitComposition
private func commit(text: String) {
guard let client = client() else { return }
let buffer = IME.kanjiConversionIfRequired(text)
if buffer.isEmpty {
return
}
client.insertText(
buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
)
}
}

View File

@ -0,0 +1,391 @@
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// 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 Cocoa
extension Bool {
fileprivate var state: NSControl.StateValue {
self ? .on : .off
}
}
// MARK: - IME Menu Manager
//
extension ctlInputMethod {
override func menu() -> NSMenu! {
let optionKeyPressed = NSEvent.modifierFlags.contains(.option)
let menu = NSMenu(title: "Input Method Menu")
let useSCPCTypingModeItem = menu.addItem(
withTitle: NSLocalizedString("Per-Char Select Mode", comment: ""),
action: #selector(toggleSCPCTypingMode(_:)), keyEquivalent: mgrPrefs.usingHotKeySCPC ? "P" : ""
)
useSCPCTypingModeItem.keyEquivalentModifierMask = [.command, .control]
useSCPCTypingModeItem.state = mgrPrefs.useSCPCTypingMode.state
let userAssociatedPhrasesItem = menu.addItem(
withTitle: NSLocalizedString("Per-Char Associated Phrases", comment: ""),
action: #selector(toggleAssociatedPhrasesEnabled(_:)), keyEquivalent: mgrPrefs.usingHotKeyAssociates ? "O" : ""
)
userAssociatedPhrasesItem.keyEquivalentModifierMask = [.command, .control]
userAssociatedPhrasesItem.state = mgrPrefs.associatedPhrasesEnabled.state
let useCNS11643SupportItem = menu.addItem(
withTitle: NSLocalizedString("CNS11643 Mode", comment: ""),
action: #selector(toggleCNS11643Enabled(_:)), keyEquivalent: mgrPrefs.usingHotKeyCNS ? "L" : ""
)
useCNS11643SupportItem.keyEquivalentModifierMask = [.command, .control]
useCNS11643SupportItem.state = mgrPrefs.cns11643Enabled.state
if IME.getInputMode() == InputMode.imeModeCHT {
let chineseConversionItem = menu.addItem(
withTitle: NSLocalizedString("Force KangXi Writing", comment: ""),
action: #selector(toggleChineseConverter(_:)), keyEquivalent: mgrPrefs.usingHotKeyKangXi ? "K" : ""
)
chineseConversionItem.keyEquivalentModifierMask = [.command, .control]
chineseConversionItem.state = mgrPrefs.chineseConversionEnabled.state
let shiftJISConversionItem = menu.addItem(
withTitle: NSLocalizedString("JIS Shinjitai Output", comment: ""),
action: #selector(toggleShiftJISShinjitaiOutput(_:)), keyEquivalent: mgrPrefs.usingHotKeyJIS ? "J" : ""
)
shiftJISConversionItem.keyEquivalentModifierMask = [.command, .control]
shiftJISConversionItem.state = mgrPrefs.shiftJISShinjitaiOutputEnabled.state
}
let currencyNumeralsItem = menu.addItem(
withTitle: NSLocalizedString("Currency Numeral Output", comment: ""),
action: #selector(toggleCurrencyNumerals(_:)), keyEquivalent: mgrPrefs.usingHotKeyCurrencyNumerals ? "M" : ""
)
currencyNumeralsItem.keyEquivalentModifierMask = [.command, .control]
currencyNumeralsItem.state = mgrPrefs.currencyNumeralsEnabled.state
let halfWidthPunctuationItem = menu.addItem(
withTitle: NSLocalizedString("Half-Width Punctuation Mode", comment: ""),
action: #selector(toggleHalfWidthPunctuation(_:)), keyEquivalent: mgrPrefs.usingHotKeyHalfWidthASCII ? "H" : ""
)
halfWidthPunctuationItem.keyEquivalentModifierMask = [.command, .control]
halfWidthPunctuationItem.state = mgrPrefs.halfWidthPunctuationEnabled.state
if optionKeyPressed || mgrPrefs.phraseReplacementEnabled {
let phaseReplacementItem = menu.addItem(
withTitle: NSLocalizedString("Use Phrase Replacement", comment: ""),
action: #selector(togglePhraseReplacement(_:)), keyEquivalent: ""
)
phaseReplacementItem.state = mgrPrefs.phraseReplacementEnabled.state
}
if optionKeyPressed {
let toggleSymbolInputItem = menu.addItem(
withTitle: NSLocalizedString("Symbol & Emoji Input", comment: ""),
action: #selector(toggleSymbolEnabled(_:)), keyEquivalent: ""
)
toggleSymbolInputItem.state = mgrPrefs.symbolInputEnabled.state
}
menu.addItem(NSMenuItem.separator()) // ---------------------
menu.addItem(
withTitle: NSLocalizedString("Open User Dictionary Folder", comment: ""),
action: #selector(openUserDataFolder(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("Edit User Phrases…", comment: ""),
action: #selector(openUserPhrases(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("Edit Excluded Phrases…", comment: ""),
action: #selector(openExcludedPhrases(_:)), keyEquivalent: ""
)
if optionKeyPressed || mgrPrefs.associatedPhrasesEnabled {
menu.addItem(
withTitle: NSLocalizedString("Edit Associated Phrases…", comment: ""),
action: #selector(openAssociatedPhrases(_:)), keyEquivalent: ""
)
}
if optionKeyPressed {
menu.addItem(
withTitle: NSLocalizedString("Edit Phrase Replacement Table…", comment: ""),
action: #selector(openPhraseReplacement(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("Edit User Symbol & Emoji Data…", comment: ""),
action: #selector(openUserSymbols(_:)), keyEquivalent: ""
)
}
if optionKeyPressed || !mgrPrefs.shouldAutoReloadUserDataFiles {
menu.addItem(
withTitle: NSLocalizedString("Reload User Phrases", comment: ""),
action: #selector(reloadUserPhrasesData(_:)), keyEquivalent: ""
)
}
menu.addItem(NSMenuItem.separator()) // ---------------------
menu.addItem(
withTitle: NSLocalizedString("Optimize Memorized Phrases", comment: ""),
action: #selector(removeUnigramsFromUOM(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("Clear Memorized Phrases", comment: ""),
action: #selector(clearUOM(_:)), keyEquivalent: ""
)
menu.addItem(NSMenuItem.separator()) // ---------------------
menu.addItem(
withTitle: NSLocalizedString("vChewing Preferences…", comment: ""),
action: #selector(showPreferences(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("Client Manager", comment: "") + "",
action: #selector(showClientListMgr(_:)), keyEquivalent: ""
)
if !optionKeyPressed {
menu.addItem(
withTitle: NSLocalizedString("Check for Updates…", comment: ""),
action: #selector(checkForUpdate(_:)), keyEquivalent: ""
)
}
menu.addItem(
withTitle: NSLocalizedString("Reboot vChewing…", comment: ""),
action: #selector(selfTerminate(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("About vChewing…", comment: ""),
action: #selector(showAbout(_:)), keyEquivalent: ""
)
menu.addItem(
withTitle: NSLocalizedString("CheatSheet", comment: "") + "",
action: #selector(showCheatSheet(_:)), keyEquivalent: ""
)
if optionKeyPressed {
menu.addItem(
withTitle: NSLocalizedString("Uninstall vChewing…", comment: ""),
action: #selector(selfUninstall(_:)), keyEquivalent: ""
)
}
// NSMenu
setKeyLayout()
return menu
}
}
// MARK: - IME Menu Items
extension ctlInputMethod {
@objc override func showPreferences(_: Any?) {
if #unavailable(macOS 10.15) {
showLegacyPreferences()
} else if NSEvent.modifierFlags.contains(.option) {
showLegacyPreferences()
} else {
NSApp.setActivationPolicy(.accessory)
ctlPrefUI.shared.controller.show(preferencePane: Preferences.PaneIdentifier(rawValue: "General"))
ctlPrefUI.shared.controller.window?.level = .statusBar
}
}
func showLegacyPreferences() {
(NSApp.delegate as? AppDelegate)?.showPreferences()
NSApp.activate(ignoringOtherApps: true)
}
@objc func showCheatSheet(_: Any?) {
guard let url = Bundle.main.url(forResource: "shortcuts", withExtension: "html") else { return }
DispatchQueue.main.async {
NSWorkspace.shared.openFile(url.path, withApplication: "Safari")
}
}
@objc func showClientListMgr(_: Any?) {
(NSApp.delegate as? AppDelegate)?.showClientListMgr()
NSApp.activate(ignoringOtherApps: true)
}
@objc func toggleSCPCTypingMode(_: Any? = nil) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n"
+ (mgrPrefs.toggleSCPCTypingModeEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleChineseConverter(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n"
+ (mgrPrefs.toggleChineseConversionEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleShiftJISShinjitaiOutput(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n"
+ (mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleCurrencyNumerals(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n"
+ (mgrPrefs.toggleCurrencyNumeralsEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleHalfWidthPunctuation(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n"
+ (mgrPrefs.toggleHalfWidthPunctuationEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleCNS11643Enabled(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n"
+ (mgrPrefs.toggleCNS11643Enabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleSymbolEnabled(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n"
+ (mgrPrefs.toggleSymbolInputEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func toggleAssociatedPhrasesEnabled(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n"
+ (mgrPrefs.toggleAssociatedPhrasesEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func togglePhraseReplacement(_: Any?) {
resetKeyHandler()
NotifierController.notify(
message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n"
+ (mgrPrefs.togglePhraseReplacementEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
)
}
@objc func selfUninstall(_: Any?) {
(NSApp.delegate as? AppDelegate)?.selfUninstall()
}
@objc func selfTerminate(_: Any?) {
NSApp.activate(ignoringOtherApps: true)
NSApp.terminate(nil)
}
@objc func checkForUpdate(_: Any?) {
UpdateSputnik.shared.checkForUpdate(forced: true)
}
@objc func openUserDataFolder(_: Any?) {
if !mgrLangModel.userDataFolderExists {
return
}
NSWorkspace.shared.openFile(
mgrLangModel.dataFolderPath(isDefaultFolder: false), withApplication: "Finder"
)
}
@objc func openUserPhrases(_: Any?) {
IME.openPhraseFile(fromURL: mgrLangModel.userPhrasesDataURL(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option) {
IME.openPhraseFile(fromURL: mgrLangModel.userPhrasesDataURL(IME.getInputMode(isReversed: true)))
}
}
@objc func openExcludedPhrases(_: Any?) {
IME.openPhraseFile(fromURL: mgrLangModel.userFilteredDataURL(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option) {
IME.openPhraseFile(fromURL: mgrLangModel.userFilteredDataURL(IME.getInputMode(isReversed: true)))
}
}
@objc func openUserSymbols(_: Any?) {
IME.openPhraseFile(fromURL: mgrLangModel.userSymbolDataURL(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option) {
IME.openPhraseFile(fromURL: mgrLangModel.userSymbolDataURL(IME.getInputMode(isReversed: true)))
}
}
@objc func openPhraseReplacement(_: Any?) {
IME.openPhraseFile(fromURL: mgrLangModel.userReplacementsDataURL(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option) {
IME.openPhraseFile(fromURL: mgrLangModel.userReplacementsDataURL(IME.getInputMode(isReversed: true)))
}
}
@objc func openAssociatedPhrases(_: Any?) {
IME.openPhraseFile(fromURL: mgrLangModel.userAssociatesDataURL(IME.getInputMode()))
if NSEvent.modifierFlags.contains(.option) {
IME.openPhraseFile(
fromURL: mgrLangModel.userAssociatesDataURL(IME.getInputMode(isReversed: true)))
}
}
@objc func reloadUserPhrasesData(_: Any?) {
IME.initLangModels(userOnly: true)
}
@objc func removeUnigramsFromUOM(_: Any?) {
mgrLangModel.removeUnigramsFromUserOverrideModel(IME.getInputMode())
if NSEvent.modifierFlags.contains(.option) {
mgrLangModel.removeUnigramsFromUserOverrideModel(IME.getInputMode(isReversed: true))
}
}
@objc func clearUOM(_: Any?) {
mgrLangModel.clearUserOverrideModelData(IME.getInputMode())
if NSEvent.modifierFlags.contains(.option) {
mgrLangModel.clearUserOverrideModelData(IME.getInputMode(isReversed: true))
}
}
@objc func showAbout(_: Any?) {
(NSApp.delegate as? AppDelegate)?.showAbout()
NSApp.activate(ignoringOtherApps: true)
}
}

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