Starting from vChewing 2.6.2 SP3.
This commit is contained in:
parent
ce6e8453e7
commit
498ddcc153
|
@ -0,0 +1 @@
|
|||
BasedOnStyle: Microsoft
|
|
@ -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
|
||||
}
|
|
@ -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/
|
|
@ -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)
|
||||
- 其他特殊設備:(請填寫是否有外接螢幕、特製鍵盤如非標準美式鍵盤等)
|
||||
- 特殊設定:(是否原本正常,改了某個系統設定後就遇到問題)
|
||||
|
||||
**其他**
|
||||
其他你覺得問題發生的疑點,或其他你想跟威注音開發團隊說的話。
|
|
@ -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)
|
||||
- 其他特殊設備:(請填寫是否有外接螢幕、特製鍵盤如非標準美式鍵盤等)
|
||||
- 特殊設定:(是否原本正常,改了某個系統設定後就遇到問題)
|
||||
|
||||
**其他**
|
||||
其他你覺得問題發生的疑點,或其他你想跟威注音開發團隊說的話。
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
name: 功能建議
|
||||
about: 告訴我們您還有什麼沒被滿足的需求
|
||||
title: "[功能建議]"
|
||||
labels: "Q&A / DISCUSSION"
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**免責聲明**
|
||||
|
||||
威注音輸入法保留拒絕任何種類的功能建議的權利(本來就有)。
|
||||
|
||||
閱讀來自各位使用者的功能建議請求,是一回事;**但**威注音研發方怎樣利用這些建議請求背後的潛在資訊、來制定威注音接下來的研發方向(或可能棄用相關想法),則是**另一回事**。
|
||||
|
||||
**痛點**
|
||||
請告訴我們為什麼目前威注音沒辦法解決您的問題。您可以講一個能讓讀者得以感同身受的小故事,比如:
|
||||
|
||||
- 我從 xx 年前開始,使用了 xx 輸入法,我相當依賴其中一項功能,但是威注音沒有...
|
||||
- 一直以來我在輸入 xx 這類內容的時候,都很花時間,我想要有更有效率的辦法…
|
||||
|
||||
**功能說明**
|
||||
請大致說明您想要的功能,最後會看起來會像是怎樣。當然最簡單的說明可能會像是「跟 xxx 輸入法一樣」,但請您諒解,不是什麼人都用過你之前用過的那套輸入法(更何況像 mac 版漢音輸入法那樣的必須買來不一定能買得來的老機種+灌上不一定能下載到的老系統才能重新體驗,成本很大),其他人想了解「跟 xxx 輸入法一樣」,首先還得找到那套輸入法,而且有些年代久遠的輸入法還找不到,這樣雙方的認知不同,會大幅降低溝通效率。
|
||||
|
||||
比方說,您想要某種符號表設計,或許可以提供我們一個草圖,或是其他你喜歡的輸入法的符號表的截圖。遇到某些更複雜的情境,您也可以考慮錄製螢幕畫面供大家參考。
|
||||
|
||||
**替代方案**
|
||||
請問,您目前是用什麼方法,可以做到就算沒有這項功能,也可以達最後目的。或著,如果這項功能開發與其他功能衝突,您起碼可以接受什麼方案?
|
||||
|
||||
**其他**
|
||||
其他你想跟威注音開發團隊說的話。
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: 原廠辭典問題提報
|
||||
about: 請告訴我們與原廠辭典有關的問題
|
||||
title: "[原廠辭典問題提報]"
|
||||
labels: "BUG / DEBUFF / REGRESSION"
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### **請在開新的工單之前閱讀《[故障提報與用儀器捉蟲](https://vchewing.github.io/BUGREPORT.html)》按照裡面的方法先電郵聯絡、或至少在開工單之後電郵知會之。因各種可能原因,威注音的主程式師可能無法隨時收取 GitHub 與 Gitee 的工單提醒訊息。**
|
||||
|
||||
### 請勿利用工單遞交**詞語新增請求**。所有「詞語新增請求」請一律使用電郵提報。
|
||||
|
||||
**分類**
|
||||
|
||||
- [ ] 錯字或錯誤讀音
|
||||
- [ ] 缺字/詞
|
||||
- [ ] 字詞權重(優先級)問題
|
||||
- [ ] 其他
|
||||
|
||||
**說明**
|
||||
請說明是哪個字寫錯或是標音錯誤,以及正確的寫法與念法。
|
||||
|
||||
**相關資料**
|
||||
像是字典的連結等。
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "Source/Data"]
|
||||
path = Source/Data
|
||||
url = https://gitee.com/vchewing/libvchewing-data
|
|
@ -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.
|
|
@ -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)。")
|
||||
}
|
|
@ -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.
|
||||
|
||||
因內容管理上的便利性需求,請改洽上述網址檢視歷代發行說明。
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
|
@ -0,0 +1 @@
|
|||
Place the notarized archive here for producing the release installer.
|
|
@ -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>
|
|
@ -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";
|
|
@ -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";
|
|
@ -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…";
|
|
@ -0,0 +1,5 @@
|
|||
/* Localized versions of Info.plist keys */
|
||||
|
||||
CFBundleName = "威注音入力 実装用アプリ";
|
||||
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
|
||||
CFEULAContent = "以下に定める条件に従い、本ソフトウェアおよび関連文書のファイル(以下「ソフトウェア」)の複製を取得するすべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、ソフトウェアの複製を使用、複写、変更、結合、掲載、頒布、サブライセンス、および/または販売する権利、およびソフトウェアを提供する相手に同じことを許可する権利も無制限に含まれます。\n\nイ)上記の著作権表示および本許諾表示を、ソフトウェアのすべての複製または重要な部分に記載するものとします。\n\nロ)上記の通知要件を満たすために必要な場合を除き、コントリビューターの商号、商標、サービスマーク、または製品名を使用するための商標ライセンスは付与されていません。\n\nソフトウェアは「現状のまま」で、明示であるか暗黙であるかを問わず、何らの保証もなく提供されます。ここでいう保証とは、商品性、特定の目的への適合性、および権利非侵害についての保証も含みますが、それに限定されるものではありません。\n作者または著作権者は、契約行為、不法行為、またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの使用またはその他の扱いによって生じる一切の請求、損害、その他の義務について何らの責任も負わないものとします。";
|
|
@ -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" = "続行";
|
|
@ -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分かかると恐れ入りますが……";
|
|
@ -0,0 +1,5 @@
|
|||
/* Localized versions of Info.plist keys */
|
||||
|
||||
CFBundleName = "威注音安装程式";
|
||||
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
|
||||
CFEULAContent = "软件之著作权利人依此麻理授权条款,将其对于软件之著作权利授权释出,只须使用者践履以下二项麻理授权条款叙明之义务性规定,其即享有对此软件程式及其相关说明文档自由不受限制地进行利用之权利,范围包括「使用、重制、修改、合并、出版、散布、再授权、及贩售程式重制作品」等诸多方面之应用,而散布程式之人、更可将上述权利传递予其后收受程式之后手,倘若其后收受程式之人亦服膺以下二项麻理授权条款之义务性规定,则其对程式亦享有与前手运用范围相同之同一权利。\n\n甲、散布此一软件程式者,须将本条款其上之「著作权声明」及以下之「免责声明」内嵌于软件程式及其重制作品之实体之中。\n\n乙、敝授权合约不提供对「贡献者」之商品名称、商标、服务标志或产品名称之商标许可,除非用以满足履行上文所述义务之必要。\n\n因麻理软件程式之授权模式乃是无偿提供,是以在现行法律之架构下可以主张合理之免除担保责任。麻理软件之著作权人或任何之后续散布者,对于其所散布之麻理软件程式皆不负任何形式上实质上之担保责任,明示亦或隐喻、商业利用性亦或特定目之使用性,这些均不在保障之列。利用麻理软件程式之所有风险均由使用者自行担负。假如所使用之麻理程式发生缺陷性问题,使用者需自行担负修正、改正及必要之服务支出。麻理软件程式之著作权人不负任何形式上实质上之担保责任,无论任何一般之、特殊之、偶发之、因果关系式之损害,或是麻理软件程式之不适用性,均须由使用者自行负担。\n";
|
|
@ -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" = "继续";
|
|
@ -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" = "等待旧版完全停用,大约需要一分钟…";
|
|
@ -0,0 +1,5 @@
|
|||
/* Localized versions of Info.plist keys */
|
||||
|
||||
CFBundleName = "威注音安裝程式";
|
||||
NSHumanReadableCopyright = "© 2021-2022 vChewing Project.";
|
||||
CFEULAContent = "軟體之著作權利人依此麻理授權條款,將其對於軟體之著作權利授權釋出,只須使用者踐履以下二項麻理授權條款敘明之義務性規定,其即享有對此軟體程式及其相關說明文檔自由不受限制地進行利用之權利,範圍包括「使用、重製、修改、合併、出版、散布、再授權、及販售程式重製作品」等諸多方面之應用,而散布程式之人、更可將上述權利傳遞予其後收受程式之後手,倘若其後收受程式之人亦服膺以下二項麻理授權條款之義務性規定,則其對程式亦享有與前手運用範圍相同之同一權利。\n\n甲、散布此一軟體程式者,須將本條款其上之「著作權聲明」及以下之「免責聲明」內嵌於軟體程式及其重製作品之實體之中。\n\n乙、敝授權合約不提供對「貢獻者」之商品名稱、商標、服務標誌或產品名稱之商標許可,除非用以滿足履行上文所述義務之必要。\n\n因麻理軟體程式之授權模式乃是無償提供,是以在現行法律之架構下可以主張合理之免除擔保責任。麻理軟體之著作權人或任何之後續散布者,對於其所散布之麻理軟體程式皆不負任何形式上實質上之擔保責任,明示亦或隱喻、商業利用性亦或特定目之使用性,這些均不在保障之列。利用麻理軟體程式之所有風險均由使用者自行擔負。假如所使用之麻理程式發生缺陷性問題,使用者需自行擔負修正、改正及必要之服務支出。麻理軟體程式之著作權人不負任何形式上實質上之擔保責任,無論任何一般之、特殊之、偶發之、因果關係式之損害,或是麻理軟體程式之不適用性,均須由使用者自行負擔。\n";
|
|
@ -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" = "繼續";
|
|
@ -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" = "等待舊版完全停用,大約需要一分鐘…";
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
顺颂时祺
|
||||
威注音输入法
|
|
@ -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
|
||||
|
||||
順頌時祺
|
||||
威注音輸入法
|
|
@ -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
|
|
@ -0,0 +1,11 @@
|
|||
ご注意:
|
||||
● macOS 12 Monterey 以前の OS(macOS 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
|
||||
|
||||
よろしくお願いいたします。
|
||||
威注音入力アプリ
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -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>
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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
|
||||
}
|
||||
}
|
19
Source/3rdParty/SindreSorhus/Preferences/PreferencesStyleController.swift
vendored
Executable file
19
Source/3rdParty/SindreSorhus/Preferences/PreferencesStyleController.swift
vendored
Executable 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)
|
||||
}
|
250
Source/3rdParty/SindreSorhus/Preferences/PreferencesTabViewController.swift
vendored
Executable file
250
Source/3rdParty/SindreSorhus/Preferences/PreferencesTabViewController.swift
vendored
Executable 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))
|
||||
}
|
||||
}
|
172
Source/3rdParty/SindreSorhus/Preferences/PreferencesWindowController.swift
vendored
Executable file
172
Source/3rdParty/SindreSorhus/Preferences/PreferencesWindowController.swift
vendored
Executable 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
143
Source/3rdParty/SindreSorhus/Preferences/SegmentedControlStyleViewController.swift
vendored
Executable file
143
Source/3rdParty/SindreSorhus/Preferences/SegmentedControlStyleViewController.swift
vendored
Executable 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)
|
||||
}
|
||||
}
|
61
Source/3rdParty/SindreSorhus/Preferences/ToolbarItemStyleViewController.swift
vendored
Executable file
61
Source/3rdParty/SindreSorhus/Preferences/ToolbarItemStyleViewController.swift
vendored
Executable 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 9642b1adc0404184655e03f3e884334f5d2bef3a
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
cmake-build-debug
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 "a": strProcessed = "A"
|
||||
case "b": strProcessed = "B"
|
||||
case "c": strProcessed = "C"
|
||||
case "d": strProcessed = "D"
|
||||
case "e": strProcessed = "E"
|
||||
case "f": strProcessed = "F"
|
||||
case "g": strProcessed = "G"
|
||||
case "h": strProcessed = "H"
|
||||
case "i": strProcessed = "I"
|
||||
case "j": strProcessed = "J"
|
||||
case "k": strProcessed = "K"
|
||||
case "l": strProcessed = "L"
|
||||
case "m": strProcessed = "M"
|
||||
case "n": strProcessed = "N"
|
||||
case "o": strProcessed = "O"
|
||||
case "p": strProcessed = "P"
|
||||
case "q": strProcessed = "Q"
|
||||
case "r": strProcessed = "R"
|
||||
case "s": strProcessed = "S"
|
||||
case "t": strProcessed = "T"
|
||||
case "u": strProcessed = "U"
|
||||
case "v": strProcessed = "V"
|
||||
case "w": strProcessed = "W"
|
||||
case "x": strProcessed = "X"
|
||||
case "y": strProcessed = "Y"
|
||||
case "z": 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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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,所以可以用「.utf16」取代「NSString.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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
/// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。
|
||||
/// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來
|
||||
/// 累加、以此為依據,來校正「可見游標位置」。
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 都還有可能是 nil,Apple 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
||||
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue