From 71c61001607b8ed4b5a6fe5c4f5b1eac1ed12e96 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Thu, 13 Jun 2024 18:44:27 +0200 Subject: [PATCH] fix bugs --- Assets.xcassets/Contents.json | 3 + .../Symbols/lock.fill.symbolset/lock.fill.svg | 4 +- .../lock.vertical.fill.svg | 4 +- .../rectangle.compress.vertical.svg | 4 +- Assets.xcassets/rime.imageset/Contents.json | 2 +- Assets.xcassets/rime.imageset/rime.pdf | Bin 1831 -> 0 bytes Assets.xcassets/rime.imageset/rime.svg | 9 + Squirrel.xcodeproj/project.pbxproj | 20 +- resources/Info.plist | 6 +- resources/InfoPlist.xcstrings | 11 +- resources/Localizable.xcstrings | 957 ------- resources/MainMenu.xcstrings | 209 ++ resources/Notifications.xcstrings | 267 ++ resources/Tooltips.xcstrings | 296 +++ sources/RimeKeycode.swift | 103 +- sources/SquirrelApplicationDelegate.swift | 235 +- sources/SquirrelConfig.swift | 457 ++-- sources/SquirrelInputController.swift | 408 ++- sources/SquirrelInputSource.swift | 135 +- sources/SquirrelPanel.swift | 2198 ++++++++--------- 20 files changed, 2368 insertions(+), 2960 deletions(-) delete mode 100644 Assets.xcassets/rime.imageset/rime.pdf create mode 100644 Assets.xcassets/rime.imageset/rime.svg delete mode 100644 resources/Localizable.xcstrings create mode 100644 resources/MainMenu.xcstrings create mode 100644 resources/Notifications.xcstrings create mode 100644 resources/Tooltips.xcstrings diff --git a/Assets.xcassets/Contents.json b/Assets.xcassets/Contents.json index 73c00596a..2aa0709eb 100644 --- a/Assets.xcassets/Contents.json +++ b/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "lossless" } } diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg index 56b1c665e..fd807ca09 100644 --- a/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg +++ b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg @@ -71,8 +71,8 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN" - - + + diff --git a/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg index fef0094d6..d25c206dd 100644 --- a/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg +++ b/Assets.xcassets/Symbols/lock.vertical.fill.symbolset/lock.vertical.fill.svg @@ -71,8 +71,8 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN" - - + + diff --git a/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg index ea10765e2..0ffb00f70 100644 --- a/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg +++ b/Assets.xcassets/Symbols/rectangle.compress.vertical.symbolset/rectangle.compress.vertical.svg @@ -71,8 +71,8 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN" - - + + diff --git a/Assets.xcassets/rime.imageset/Contents.json b/Assets.xcassets/rime.imageset/Contents.json index 133c44ea2..0199deea0 100644 --- a/Assets.xcassets/rime.imageset/Contents.json +++ b/Assets.xcassets/rime.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "rime.pdf", + "filename" : "rime.svg", "idiom" : "universal" } ], diff --git a/Assets.xcassets/rime.imageset/rime.pdf b/Assets.xcassets/rime.imageset/rime.pdf deleted file mode 100644 index 04c66569702bac1d57e39c222156ede8bdf979b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1831 zcmY!laB6FSK|k6DqY?faZVuf4t|$R*$S&SdMd7z=8268u|l04r@doa?kJzGToK&!%m0R3akrE~Q7%{K*ECJD^Y4pn6jsN4 za5xvLowxT;2>Z*$Zkb0f8w;J8a875t&0_!T?H%sdCcHRvnAf@RL1WL}jncmZum1~w zYm{S^f3ho$ZG(T3%V#BZ=dF!C_F;R(m8<7{JlaC4Gvn@iW2wW5@4_@*w}ysfIvUd0LW3$cXm`TRM1aO1d_nOhRGSi z@+b1a%Ea2;?fHXg4$fDFZtW zTfBoD7*bh~ssJ+`tTMPXsRS(OmJbYVkQyha{0fC=piu@2Mn(#TW(u)j;h@yw{L-T2 z)M5oour)xZz>|@0pCi{{1p(LZ#a;D{5~fi_%}xuuO-^5M-8doWj8e+P*X3zzG6m;8 zpKl)@p7s1}(!CQ-*WUJgTUg#dcPsaMbKmlaP1A5ZX#1a;eAm2a&66{TE(VAWiiB~6I1${qYFeT%M5P7X1+WP6Ps$1|fy4$hOB))3107_hpMoLS zrEZx;#U%^m7-dX05mi6RE$Z^E@ycCa{Y?^CGqSxV_5FNbe(Bb^K#e)- z&(B)&FLY3v<H>AZBd9GQ^ONadD%V8%uK8@QCm!W zpEkOEuAE(wyKe)tKcDT*sN$R@n+zs7MKhZ$yFBB-izl^j(>yO-y43Y+$kF$t&9ZXZe0y*t>1nO2zZeg0J$NysNi3 zXSqIoW#AcCwTCfZ`f9lMo2Zrfx}h86SURNH<22Hxw5K+g%-DH&!I!&__~O6JzY|nZ zX!i6f@BNQ%x>wFWa-J(#c0KWn`1+8}KUm@t6d};KG&VAXM<#NK0WAlDQq#b31Wjb- zu!Mz~m_P|HII}8M!O#M%BU~ZcP{ByS2wFDe=a(oLBB~wFytI5^9s?%~c8A>qgW z`JIjJjf^e^#ztWV1_nkSm^qKBOpsuoaP$b9$_)J`j(UMaMwJLB2Ny9HY2}UtCPAH^ o94-e=vtl+jhA>0chnPMtE=epZsVD+^#?aK#kV{q7)!&T^08!kF+5i9m diff --git a/Assets.xcassets/rime.imageset/rime.svg b/Assets.xcassets/rime.imageset/rime.svg new file mode 100644 index 000000000..9051289d9 --- /dev/null +++ b/Assets.xcassets/rime.imageset/rime.svg @@ -0,0 +1,9 @@ + + + rime + + + + + + \ No newline at end of file diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 2798739c4..192f4fc4b 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B8E1B20F645B870094E08B /* Carbon.framework */; }; E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; F40501D62C01743A008BD25B /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F40501D32C01743A008BD25B /* InfoPlist.xcstrings */; }; - F40501D82C01743A008BD25B /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F40501D52C01743A008BD25B /* Localizable.xcstrings */; }; + F40501D82C01743A008BD25B /* Notifications.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F40501D52C01743A008BD25B /* Notifications.xcstrings */; }; F40501E02C01745C008BD25B /* SquirrelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40501D92C01745C008BD25B /* SquirrelConfig.swift */; }; F40501E12C01745C008BD25B /* SquirrelPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40501DA2C01745C008BD25B /* SquirrelPanel.swift */; }; F40501E22C01745C008BD25B /* SquirrelInputSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40501DB2C01745C008BD25B /* SquirrelInputSource.swift */; }; @@ -79,6 +79,8 @@ F43A1B4C2C08387200AC8DB4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43A1B4B2C08386D00AC8DB4 /* Foundation.framework */; }; F43A1B502C08388500AC8DB4 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43A1B4F2C08387F00AC8DB4 /* QuartzCore.framework */; }; F43EFA3C2C10239D00A785D5 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43EFA3B2C10239600A785D5 /* Cocoa.framework */; }; + F45E9F0A2C1B335200B0A052 /* MainMenu.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F45E9F082C1B335200B0A052 /* MainMenu.xcstrings */; }; + F45E9F0B2C1B335200B0A052 /* Tooltips.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F45E9F092C1B335200B0A052 /* Tooltips.xcstrings */; }; F48CFB6B2B327A2E00DB9CF9 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; }; F492C3D42BDE2D2A0031987C /* librime-lua.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F492C3CF2BDE2D040031987C /* librime-lua.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F492C3D52BDE2D2A0031987C /* librime-octagram.dylib in Copy Rime plugins */ = {isa = PBXBuildFile; fileRef = F492C3CE2BDE2D040031987C /* librime-octagram.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -250,8 +252,8 @@ 447765C725C30E6B002415AF /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = Frameworks/Sparkle.framework; sourceTree = ""; }; 448363D925BDBBBF0022C7BA /* pinyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = pinyin.yaml; path = data/plum/pinyin.yaml; sourceTree = ""; }; 448363DA25BDBBBF0022C7BA /* zhuyin.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; name = zhuyin.yaml; path = data/plum/zhuyin.yaml; sourceTree = ""; }; - 44986A93184B421700B3278D /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; - 44986A94184B421700B3278D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.md; sourceTree = ""; }; + 44986A93184B421700B3278D /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = LICENSE.txt; sourceTree = ""; }; + 44986A94184B421700B3278D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = README.md; sourceTree = ""; }; 44AEBC7121F569CF00344375 /* punctuation.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = punctuation.yaml; path = data/plum/punctuation.yaml; sourceTree = ""; }; 44AEBC7221F569CF00344375 /* key_bindings.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = key_bindings.yaml; path = data/plum/key_bindings.yaml; sourceTree = ""; }; 44CD640915E2633D0021234E /* librime.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = librime.1.dylib; path = lib/librime.1.dylib; sourceTree = ""; }; @@ -297,7 +299,7 @@ E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; F40501D32C01743A008BD25B /* InfoPlist.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; lineEnding = 0; name = InfoPlist.xcstrings; path = resources/InfoPlist.xcstrings; sourceTree = ""; }; F40501D42C01743A008BD25B /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = resources/Info.plist; sourceTree = ""; }; - F40501D52C01743A008BD25B /* Localizable.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; lineEnding = 0; name = Localizable.xcstrings; path = resources/Localizable.xcstrings; sourceTree = ""; }; + F40501D52C01743A008BD25B /* Notifications.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; lineEnding = 0; name = Notifications.xcstrings; path = resources/Notifications.xcstrings; sourceTree = ""; }; F40501D92C01745C008BD25B /* SquirrelConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = SquirrelConfig.swift; path = sources/SquirrelConfig.swift; sourceTree = ""; }; F40501DA2C01745C008BD25B /* SquirrelPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = SquirrelPanel.swift; path = sources/SquirrelPanel.swift; sourceTree = ""; }; F40501DB2C01745C008BD25B /* SquirrelInputSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = SquirrelInputSource.swift; path = sources/SquirrelInputSource.swift; sourceTree = ""; }; @@ -308,6 +310,8 @@ F43A1B4B2C08386D00AC8DB4 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; F43A1B4F2C08387F00AC8DB4 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; F43EFA3B2C10239600A785D5 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + F45E9F082C1B335200B0A052 /* MainMenu.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; name = MainMenu.xcstrings; path = resources/MainMenu.xcstrings; sourceTree = ""; }; + F45E9F092C1B335200B0A052 /* Tooltips.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; name = Tooltips.xcstrings; path = resources/Tooltips.xcstrings; sourceTree = ""; }; F492C3CD2BDE2D040031987C /* librime-predict.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-predict.dylib"; path = "lib/rime-plugins/librime-predict.dylib"; sourceTree = ""; }; F492C3CE2BDE2D040031987C /* librime-octagram.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-octagram.dylib"; path = "lib/rime-plugins/librime-octagram.dylib"; sourceTree = ""; }; F492C3CF2BDE2D040031987C /* librime-lua.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "librime-lua.dylib"; path = "lib/rime-plugins/librime-lua.dylib"; sourceTree = ""; }; @@ -421,7 +425,9 @@ 44986A94184B421700B3278D /* README.md */, F40501D42C01743A008BD25B /* Info.plist */, F40501D32C01743A008BD25B /* InfoPlist.xcstrings */, - F40501D52C01743A008BD25B /* Localizable.xcstrings */, + F45E9F082C1B335200B0A052 /* MainMenu.xcstrings */, + F45E9F092C1B335200B0A052 /* Tooltips.xcstrings */, + F40501D52C01743A008BD25B /* Notifications.xcstrings */, ); name = Resources; sourceTree = ""; @@ -616,10 +622,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F45E9F0A2C1B335200B0A052 /* MainMenu.xcstrings in Resources */, F40501D62C01743A008BD25B /* InfoPlist.xcstrings in Resources */, 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */, - F40501D82C01743A008BD25B /* Localizable.xcstrings in Resources */, + F40501D82C01743A008BD25B /* Notifications.xcstrings in Resources */, 44986A95184B421700B3278D /* LICENSE.txt in Resources */, + F45E9F0B2C1B335200B0A052 /* Tooltips.xcstrings in Resources */, 44986A96184B421700B3278D /* README.md in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/resources/Info.plist b/resources/Info.plist index a86717720..0780037d1 100644 --- a/resources/Info.plist +++ b/resources/Info.plist @@ -7,13 +7,15 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFile - RimeIcon.icns + $(ASSETCATALOG_COMPILER_APPICON_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} + CFBundleDisplayName + ${INFOPLIST_KEY_CFBundleDisplayName} CFBundlePackageType APPL CFBundleSignature @@ -138,6 +140,8 @@ ukvWq2dKOWn3B9AsdsQIwOptiDdDKdUjAVNgFxSvB2o= TICapsLockLanguageSwitchCapable + NSSupportsSuddenTermination + SUEnableInstallerLauncherService TISIconIsTemplate diff --git a/resources/InfoPlist.xcstrings b/resources/InfoPlist.xcstrings index e2269b5fb..1a9702ec3 100644 --- a/resources/InfoPlist.xcstrings +++ b/resources/InfoPlist.xcstrings @@ -7,32 +7,31 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Squirrel" + "value" : "Squirrel Input Method" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "鼠须管" + "value" : "鼠须管输入法" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "鼠鬚管" + "value" : "鼠鬚管輸入法" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "鼠鬚筆" + "value" : "鼠鬚筆輸入法" } } } }, "CFBundleName" : { - "comment" : "Bundle name", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { diff --git a/resources/Localizable.xcstrings b/resources/Localizable.xcstrings deleted file mode 100644 index 39c48c1cb..000000000 --- a/resources/Localizable.xcstrings +++ /dev/null @@ -1,957 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "candidate" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold ⌃control to temporarily disable mouse interactions.\nPress and hold ⌥option to display tooltips." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。\n按住⌃control键以暂时停用鼠标与“鼠须管”互动。\n按住⌥Option键以显示工具提示" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。\n按住⌃control鍵來暫時停用滑鼠與「鼠鬚管」互動。\n按住⌥Option鍵來顯示工具提示。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以⎆選取候選字。\n點按輔助按鈕以⎌清除所選的記憶字詞。\n按住⌃control鍵以暫時停用滑鼠與「鼠鬚筆」互動。\n按住⌥Option鍵以顯示工具提示" - } - } - } - }, - "checkForUpdates" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Check for updates…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "检查更新…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢查更新項目⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢查更新項目⋯" - } - } - } - }, - "compress" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to compress candidate window.\nSecondary click to lock this multiple-row view" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以收合候選字視窗。點按輔助按鈕以鎖定當前的多橫列顯示方式。" - } - } - } - }, - "configure" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Settings…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "用户设置…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用者設定⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "用户設定⋯" - } - } - } - }, - "delete" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以逐字⌫刪除輸入。\n點按輔助按鈕以⎋取消輸入。" - } - } - } - }, - "deploy" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deploy" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新部署" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新部署" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新部署" - } - } - } - }, - "deploy_failure" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error occurred. See log file $TMPDIR/rime.squirrel.INFO." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO" - } - } - } - }, - "deploy_start" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deploying Rime input method engine…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "部署输入法引擎…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "部署輸入法引擎⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "部署輸入法引擎⋯" - } - } - } - }, - "deploy_success" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Squirrel is ready." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "部署完成。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "部署完成。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "部署完成。" - } - } - } - }, - "deploy_update" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deploying Rime for updates…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新输入法引擎…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新輸入法引擎⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新輸入法引擎⋯" - } - } - } - }, - "end" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cannot page down any further.\nSecondary click to jump to ↘End." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "不能再向下翻页。\n辅助点按以跳到↘结尾。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法再向下翻頁。\n點按輔助按鈕以跳至↘結尾。" - } - } - } - }, - "escape" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cannot delete any further.\nSecondary click to ⎋Escape the composing." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "不能再删除。\n辅助点按以⎋取消输入。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法再刪除。\n點按輔助按鈕來⎋取消輸入。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法再刪除。\n點按輔助按鈕以⎋取消輸入。" - } - } - } - }, - "expand" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to expand candidate window.\nSecondary click to lock this single-row view." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以展開候選字視窗。點按輔助按鈕以鎖定當前的單橫列顯示方式。" - } - } - } - }, - "home" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cannot page up any further.\nSecondary click to jump to ↖Home." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "不能再向上翻页。\n辅助点按以跳到↖开头。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法再向上翻頁。\n點按輔助按鈕以跳至↖起點。" - } - } - } - }, - "new_update" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A new update is available." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "有新的更新。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "有新的更新項目。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "有新的更新項目可用。" - } - } - } - }, - "openLogFolder" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error and warning logs" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "错误和警告日志" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "錯誤和警告記錄" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "錯誤與警告記錄" - } - } - } - }, - "openWiki" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rime Wiki…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "在线帮助…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "線上輔助說明⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "線上輔助説明⋯" - } - } - } - }, - "page_down" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to ⇞Page Down.\nSecondary click to jump to ↘End." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以⇟向下翻頁。\n點按輔助按鈕以跳至↘結尾。" - } - } - } - }, - "page_up" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to ⇞Page Up.\nSecondary click to jump to ↖Home." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以⇞向上翻页。\n辅助点按以跳到↖开头。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以⇞向上翻頁。\n點按輔助按鈕以跳至↖起點。" - } - } - } - }, - "problematic_launch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Problematic launch detected!\nSquirrel may be suffering a crash due to improper configurations.\nRevert previous modifications to see if the problem recurs." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "检测到启动有问题!\n“鼠须管”可能因错误设置而崩溃。\n请尝试撤销之前的修改,然后查看问题是否仍旧存在。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "啟動時偵測到問題!\n「鼠鬚管」可能因設定不當而崩潰。\n請嘗試回退先前的修改,然後查看問題是否依然存在。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "啟動時偵測到錯誤!\n「鼠鬚筆」可能由於設定不當而崩潰。\n請嘗試回退先前的改動,然後查看問題是否仍然存在。" - } - } - } - }, - "say_voice" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alex" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "TingTing" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "MeiJia" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sinji" - } - } - } - }, - "showSwitcher" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣Squirrel Switcher" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣鼠须管〔方案菜单〕" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣鼠鬚管〔方案選單〕" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣鼠鬚筆〔方案選單〕" - } - } - } - }, - "Squirrel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Squirrel" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "鼠须管" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "鼠鬚管" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "鼠鬚筆" - } - } - } - }, - "syncUserData" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sync user data" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "同步用户数据" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "同步使用者資料" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "同步用户資料" - } - } - } - }, - "unlock" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Click to unlock the view and allow it to be expanded or collapsed." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "点按以解锁视图,允许展开或折叠候选字窗口。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按來解鎖顯示方式,允許展開或收合候選字視窗。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "點按以解鎖顯示方式,允許展開或收合候選字視窗。" - } - } - } - }, - "update_version" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Version %@ is now available." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "版本%@现已可用。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "版本%@已經可供使用。" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "版本%@已經可供使用。" - } - } - } - }, - "deploy" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Deploy" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新部署" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新部署" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "重新部署" - } - } - } - }, - "checkForUpdates" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Check for updates…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "检查更新…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢查更新項目⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "檢查更新項目⋯" - } - } - } - }, - "showSwitcher" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "ㄓ⃣Squirrel Switcher" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣鼠须管〔方案菜单〕" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣鼠鬚管〔方案選單〕" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "ㄓ⃣鼠鬚筆〔方案選單〕" - } - } - } - }, - "openWiki" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Rime Wiki…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "在线帮助…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "線上輔助說明⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "線上輔助説明⋯" - } - } - } - }, - "configure" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Settings…" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "用户设置…" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "使用者設定⋯" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "用户設定⋯" - } - } - } - }, - "syncUserData" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Sync user data" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "同步用户数据" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "同步使用者資料" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "同步用户資料" - } - } - } - }, - "openLogFolder" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Error and warning logs" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "错误和警告日志" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "錯誤和警告記錄" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "錯誤與警告記錄" - } - } - } - } - }, - "version" : "1.0" -} diff --git a/resources/MainMenu.xcstrings b/resources/MainMenu.xcstrings new file mode 100644 index 000000000..0a91a71bb --- /dev/null +++ b/resources/MainMenu.xcstrings @@ -0,0 +1,209 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "checkForUpdates" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check for updates…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查更新…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查更新項目⋯" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "檢查更新項目⋯" + } + } + } + }, + "configure" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户设置…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用者設定⋯" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户設定⋯" + } + } + } + }, + "deploy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新部署" + } + } + } + }, + "openLogFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error and warning logs" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "错误和警告日志" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "錯誤和警告記錄" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "錯誤與警告記錄" + } + } + } + }, + "openWiki" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rime Wiki…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在线帮助…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "線上輔助說明⋯" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "線上輔助説明⋯" + } + } + } + }, + "showSwitcher" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣Squirrel Switcher" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠须管〔方案菜单〕" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠鬚管〔方案選單〕" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "ㄓ⃣鼠鬚筆〔方案選單〕" + } + } + } + }, + "syncUserData" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sync user data" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步用户数据" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步使用者資料" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "同步用户資料" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/resources/Notifications.xcstrings b/resources/Notifications.xcstrings new file mode 100644 index 000000000..8ecf5e82d --- /dev/null +++ b/resources/Notifications.xcstrings @@ -0,0 +1,267 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "deploy_failure" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error occurred. See log file $TMPDIR/rime.squirrel.INFO." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO" + } + } + } + }, + "deploy_start" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploying Rime input method engine…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署输入法引擎…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署輸入法引擎⋯" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署輸入法引擎⋯" + } + } + } + }, + "deploy_success" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel is ready." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "部署完成。" + } + } + } + }, + "deploy_update" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deploying Rime for updates…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新输入法引擎…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新輸入法引擎⋯" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新輸入法引擎⋯" + } + } + } + }, + "new_update" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "A new update is available." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有新的更新。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "有新的更新項目。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "有新的更新項目可用。" + } + } + } + }, + "problematic_launch" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Problematic launch detected!\nSquirrel may be suffering a crash due to improper configurations.\nRevert previous modifications to see if the problem recurs." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检测到启动有问题!\n“鼠须管”可能因错误设置而崩溃。\n请尝试撤销之前的修改,然后查看问题是否仍旧存在。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟動時偵測到問題!\n「鼠鬚管」可能因設定不當而崩潰。\n請嘗試回退先前的修改,然後查看問題是否依然存在。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟動時偵測到錯誤!\n「鼠鬚筆」可能由於設定不當而崩潰。\n請嘗試回退先前的改動,然後查看問題是否仍然存在。" + } + } + } + }, + "say_voice" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alex" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TingTing" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "MeiJia" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sinji" + } + } + } + }, + "Squirrel" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Squirrel" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠须管" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚管" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠鬚筆" + } + } + } + }, + "update_version" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Version %@ is now available." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本%@现已可用。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本%@已經可供使用。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本%@已經可供使用。" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/resources/Tooltips.xcstrings b/resources/Tooltips.xcstrings new file mode 100644 index 000000000..c1b8e1010 --- /dev/null +++ b/resources/Tooltips.xcstrings @@ -0,0 +1,296 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "candidate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold ⌃control to temporarily disable mouse interactions.\nPress and hold ⌥option to display tooltips." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。\n按住⌃control键以暂时停用鼠标与“鼠须管”互动。\n按住⌥Option键以显示工具提示" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。\n按住⌃control鍵來暫時停用滑鼠與「鼠鬚管」互動。\n按住⌥Option鍵來顯示工具提示。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以⎆選取候選字。\n點按輔助按鈕以⎌清除所選的記憶字詞。\n按住⌃control鍵以暫時停用滑鼠與「鼠鬚筆」互動。\n按住⌥Option鍵以顯示工具提示" + } + } + } + }, + "compress" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to compress candidate window.\nSecondary click to lock this multiple-row view" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以收合候選字視窗。點按輔助按鈕以鎖定當前的多橫列顯示方式。" + } + } + } + }, + "delete" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以逐字⌫刪除輸入。\n點按輔助按鈕以⎋取消輸入。" + } + } + } + }, + "end" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot page down any further.\nSecondary click to jump to ↘End." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不能再向下翻页。\n辅助点按以跳到↘结尾。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再向下翻頁。\n點按輔助按鈕以跳至↘結尾。" + } + } + } + }, + "escape" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot delete any further.\nSecondary click to ⎋Escape the composing." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不能再删除。\n辅助点按以⎋取消输入。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再刪除。\n點按輔助按鈕來⎋取消輸入。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再刪除。\n點按輔助按鈕以⎋取消輸入。" + } + } + } + }, + "expand" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to expand candidate window.\nSecondary click to lock this single-row view." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以展開候選字視窗。點按輔助按鈕以鎖定當前的單橫列顯示方式。" + } + } + } + }, + "home" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot page up any further.\nSecondary click to jump to ↖Home." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不能再向上翻页。\n辅助点按以跳到↖开头。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法再向上翻頁。\n點按輔助按鈕以跳至↖起點。" + } + } + } + }, + "page_down" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to ⇞Page Down.\nSecondary click to jump to ↘End." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以⇟向下翻頁。\n點按輔助按鈕以跳至↘結尾。" + } + } + } + }, + "page_up" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to ⇞Page Up.\nSecondary click to jump to ↖Home." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以⇞向上翻页。\n辅助点按以跳到↖开头。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以⇞向上翻頁。\n點按輔助按鈕以跳至↖起點。" + } + } + } + }, + "unlock" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Click to unlock the view and allow it to be expanded or collapsed." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点按以解锁视图,允许展开或折叠候选字窗口。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按來解鎖顯示方式,允許展開或收合候選字視窗。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點按以解鎖顯示方式,允許展開或收合候選字視窗。" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/sources/RimeKeycode.swift b/sources/RimeKeycode.swift index 4c4155b32..e3af97351 100644 --- a/sources/RimeKeycode.swift +++ b/sources/RimeKeycode.swift @@ -16,54 +16,32 @@ struct RimeModifiers: OptionSet, Sendable { static let Release = RimeModifiers(rawValue: 1 << 30) static let ModifierMask = RimeModifiers(rawValue: 0x5F001FFF) - init(rawValue: CInt) { - self.rawValue = rawValue - } + init(rawValue: CInt) { self.rawValue = rawValue } init(macModifiers: NSEvent.ModifierFlags) { var modifiers: RimeModifiers = [] - if macModifiers.contains(.shift) { - modifiers.insert(.Shift) - } - if macModifiers.contains(.capsLock) { - modifiers.insert(.Lock) - } - if macModifiers.contains(.control) { - modifiers.insert(.Control) - } - if macModifiers.contains(.option) { - modifiers.insert(.Alt) - } - if macModifiers.contains(.command) { - modifiers.insert(.Super) - } - if macModifiers.contains(.function) { - modifiers.insert(.Hyper) - } + if macModifiers.contains(.shift) { modifiers.insert(.Shift) } + if macModifiers.contains(.capsLock) { modifiers.insert(.Lock) } + if macModifiers.contains(.control) { modifiers.insert(.Control) } + if macModifiers.contains(.option) { modifiers.insert(.Alt) } + if macModifiers.contains(.command) { modifiers.insert(.Super) } + if macModifiers.contains(.function) { modifiers.insert(.Hyper) } self = modifiers } init?(name: String) { switch name { - case "Shift": - self = .Shift - case "Lock": - self = .Lock - case "Control": - self = .Control - case "Alt": - self = .Alt - case "Super": - self = .Super - case "Hyper": - self = .Hyper - case "Meta": - self = .Meta - default: - return nil + case "Shift": self = .Shift + case "Lock": self = .Lock + case "Control": self = .Control + case "Alt": self = .Alt + case "Super": self = .Super + case "Hyper": self = .Hyper + case "Meta": self = .Meta + default: return nil } } -} +} // RimeModifiers // powerbook let kVK_Enter_Powerbook: Int = 0x34 @@ -71,7 +49,7 @@ let kVK_Enter_Powerbook: Int = 0x34 let kVK_PC_Application: Int = 0x6E let kVK_PC_Power: Int = 0x7F -enum RimeKeycode: CInt, Sendable { +enum RimeKeycode: CInt, Sendable, Comparable { case XK_VoidSymbol = 0xFFFFFF case XK_BackSpace = 0xFF08 @@ -536,12 +514,12 @@ enum RimeKeycode: CInt, Sendable { init(keychar: unichar, shift: Bool, caps: Bool) { // NOTE: IBus/Rime use different keycodes for uppercase/lowercase letters. - if keychar >= 0x61 && keychar <= 0x7A && (shift != caps) { + if 0x61...0x7A ~= keychar && shift != caps { // lowercase -> Uppercase self.init(rawValue: CInt(keychar) - 0x20)!; return } - if keychar >= 0x20 && keychar <= 0x7E { + if 0x20...0x7E ~= keychar { self.init(rawValue: CInt(keychar))!; return } @@ -588,43 +566,14 @@ enum RimeKeycode: CInt, Sendable { } } - init(name: String) { - self.init(rawValue: Self.nameToRawValue(name))! - } - - static func < (left: Self, right: Self) -> Bool { - return left.rawValue < right.rawValue - } - - static func > (left: Self, right: Self) -> Bool { - return left.rawValue > right.rawValue - } + init(name: String) { self.init(rawValue: Self.nameToRawValue(name))! } - static func == (left: Self, right: Self) -> Bool { - return left.rawValue == right.rawValue - } - - static func <= (left: Self, right: Self) -> Bool { - return left.rawValue <= right.rawValue - } - - static func >= (left: Self, right: Self) -> Bool { - return left.rawValue >= right.rawValue - } - - static func != (left: Self, right: Self) -> Bool { - return left.rawValue != right.rawValue - } - - static func + (left: Self, right: Self) -> Self { - return Self(rawValue: left.rawValue + right.rawValue) ?? XK_VoidSymbol - } - - static func - (left: Self, right: Self) -> Self { - return Self(rawValue: left.rawValue - right.rawValue) ?? XK_VoidSymbol - } + static func < (lhs: Self, rhs: Self) -> Bool { lhs.rawValue < rhs.rawValue } + static func == (lhs: Self, rhs: Self) -> Bool { lhs.rawValue == rhs.rawValue } + static func + (lhs: Self, rhs: Self) -> Self { .init(rawValue: lhs.rawValue + rhs.rawValue) ?? .XK_VoidSymbol } + static func - (lhs: Self, rhs: Self) -> Self { .init(rawValue: lhs.rawValue - rhs.rawValue) ?? .XK_VoidSymbol } - private static func nameToRawValue(_ name: String) -> CInt { + static private func nameToRawValue(_ name: String) -> CInt { switch name { // ascii case "space": return 0x000020 @@ -1938,4 +1887,4 @@ enum RimeKeycode: CInt, Sendable { default: return 0xFFFFFF } } -} +} // RimeKeycode diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index 6b0f521c2..b3ac3213a 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -12,38 +12,36 @@ import UserNotifications if args.count > 1 { switch args[1] { case "--quit": - let runningSquirrels: [NSRunningApplication] = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId) - runningSquirrels.forEach { $0.terminate() } + NSRunningApplication.runningApplications(withBundleIdentifier: bundleId).forEach { $0.terminate() } return case "--reload": DistributedNotificationCenter.default().postNotificationName(.init("SquirrelReloadNotification"), object: nil) return case "--register-input-source", "--install": - SquirrelInputSource.RegisterInputSource() + RegisterInputSource() return case "--enable-input-source": var inputModes: RimeInputModes = [] if args.count > 2 { args[2...].forEach { if let mode = RimeInputModes(code: $0) { inputModes.insert(mode) } } } - SquirrelInputSource.EnableInputSource(inputModes) + EnableInputSource(inputModes) return case "--disable-input-source": - SquirrelInputSource.DisableInputSource() + DisableInputSource() return case "--select-input-source": var inputModes: RimeInputModes = [] if args.count > 2 { args[2...].forEach { if let mode = RimeInputModes(code: $0) { inputModes.insert(mode) } } } - SquirrelInputSource.SelectInputSource(inputModes) + SelectInputSource(inputModes) return case "--build": - // notification showNotification(message: "deploy_update") // build all schemas in current directory var builderTraits: RimeTraits = RimeStructInit() - builderTraits.app_name = ("rime.squirrel-builder" as NSString).utf8String + builderTraits.app_name = "rime.squirrel-builder".utf8CString.withUnsafeBufferPointer(\.baseAddress) RimeApi.setup(&builderTraits) RimeApi.deployer_initialize(nil) _ = RimeApi.deploy() @@ -57,25 +55,22 @@ import UserNotifications } autoreleasepool { // find the bundle identifier and then initialize the input method server - let connectionName = Bundle.main.object(forInfoDictionaryKey: "InputMethodConnectionName") - _ = IMKServer(name: connectionName as? String, bundleIdentifier: bundleId) - + _ = IMKServer(name: Bundle.main.object(forInfoDictionaryKey: "InputMethodConnectionName") as? String, bundleIdentifier: bundleId) // load the bundle explicitly because in this case the input method is a background only application let delegate = SquirrelApplicationDelegate() NSApplication.shared.delegate = delegate NSApplication.shared.setActivationPolicy(.accessory) - // opencc will be configured with relative dictionary paths FileManager.default.changeCurrentDirectoryPath(Bundle.main.sharedSupportPath!) if delegate.problematicLaunchDetected() { print("Problematic launch detected!") - let args: [String] = ["-v", NSLocalizedString("say_voice", comment: ""), NSLocalizedString("problematic_launch", comment: "")] + let args: [String] = ["-v", NSLocalizedString("say_voice", tableName: "Notifications", comment: ""), NSLocalizedString("problematic_launch", tableName: "Notifications", comment: "")] if #available(macOS 10.13, *) { do { try Process.run(URL(fileURLWithPath: "/usr/bin/say", isDirectory: false), arguments: args, terminationHandler: nil) } catch { - print("Error message cannot be communicated through audio:\n", NSLocalizedString("problematic_launch", comment: "")) + print(args[2]) } } else { Process.launchedProcess(launchPath: "/usr/bin/say", arguments: args) @@ -94,7 +89,7 @@ import UserNotifications RimeApi.finalize() } } -} +} // SquirrelApp final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate { @frozen enum SquirrelNotificationPolicy { @@ -110,7 +105,8 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta var supportsGentleScheduledUpdateReminders: Bool { true } static let userDataDir = URL(fileURLWithPath: "Library/Rime/", isDirectory: true, relativeTo: FileManager.default.homeDirectoryForCurrentUser).standardizedFileURL - static let RimeWiki = URL(string: "https://github.com/rime/home/wiki")! + static private let RimeWiki = URL(string: "https://github.com/rime/home/wiki")! + static private let updaterIdentifier = "SquirrelUpdateNotification" /*** updater ***/ func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) { @@ -118,16 +114,16 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta if !state.userInitiated { NSApp.dockTile.badgeLabel = "1" let content = UNMutableNotificationContent() - content.title = NSLocalizedString("new_update", comment: "") - content.body = String(format: NSLocalizedString("update_version", comment: ""), update.displayVersionString) - let request = UNNotificationRequest(identifier: "SquirrelUpdateNotification", content: content, trigger: nil) + content.title = NSLocalizedString("new_update", tableName: "Notifications", comment: "") + content.body = String(format: NSLocalizedString("update_version", tableName: "Notifications", comment: ""), update.displayVersionString) + let request = UNNotificationRequest(identifier: Self.updaterIdentifier, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } } func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) { NSApp.dockTile.badgeLabel = "" - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["SquirrelUpdateNotification"]) + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [Self.updaterIdentifier]) } func standardUserDriverWillFinishUpdateSession() { @@ -135,7 +131,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if response.notification.request.identifier == "SquirrelUpdateNotification" && response.actionIdentifier == UNNotificationDefaultActionIdentifier { + if response.notification.request.identifier == Self.updaterIdentifier && response.actionIdentifier == UNNotificationDefaultActionIdentifier { updateController.updater.checkForUpdates() } completionHandler() @@ -160,26 +156,26 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } private func setupMenu() { - let showSwitcher = NSMenuItem(title: NSLocalizedString("showSwitcher", comment: ""), action: #selector(showSwitcher(_:)), keyEquivalent: "") + let showSwitcher = NSMenuItem(title: NSLocalizedString("showSwitcher", tableName: "MainMenu", comment: ""), action: #selector(showSwitcher(_:)), keyEquivalent: "") showSwitcher.target = self menu.addItem(showSwitcher) - let deploy = NSMenuItem(title: NSLocalizedString("deploy", comment: ""), action: #selector(deploy(_:)), keyEquivalent: "`") + let deploy = NSMenuItem(title: NSLocalizedString("deploy", tableName: "MainMenu", comment: ""), action: #selector(deploy(_:)), keyEquivalent: "`") deploy.target = self deploy.keyEquivalentModifierMask = [.control, .option] menu.addItem(deploy) - let syncUserData = NSMenuItem(title: NSLocalizedString("syncUserData", comment: ""), action: #selector(syncUserData(_:)), keyEquivalent: "") + let syncUserData = NSMenuItem(title: NSLocalizedString("syncUserData", tableName: "MainMenu", comment: ""), action: #selector(syncUserData(_:)), keyEquivalent: "") syncUserData.target = self menu.addItem(syncUserData) - let configure = NSMenuItem(title: NSLocalizedString("configure", comment: ""), action: #selector(configure(_:)), keyEquivalent: "") + let configure = NSMenuItem(title: NSLocalizedString("configure", tableName: "MainMenu", comment: ""), action: #selector(configure(_:)), keyEquivalent: "") configure.target = self menu.addItem(configure) - let openWiki = NSMenuItem(title: NSLocalizedString("openWiki", comment: ""), action: #selector(openWiki(_:)), keyEquivalent: "") + let openWiki = NSMenuItem(title: NSLocalizedString("openWiki", tableName: "MainMenu", comment: ""), action: #selector(openWiki(_:)), keyEquivalent: "") openWiki.target = self menu.addItem(openWiki) - let checkForUpdates = NSMenuItem(title: NSLocalizedString("checkForUpdates", comment: ""), action: #selector(checkForUpdates(_:)), keyEquivalent: "") + let checkForUpdates = NSMenuItem(title: NSLocalizedString("checkForUpdates", tableName: "MainMenu", comment: ""), action: #selector(checkForUpdates(_:)), keyEquivalent: "") checkForUpdates.target = self menu.addItem(checkForUpdates) - let openLogFolder = NSMenuItem(title: NSLocalizedString("openLogFolder", comment: ""), action: #selector(openLogFolder(_:)), keyEquivalent: "") + let openLogFolder = NSMenuItem(title: NSLocalizedString("openLogFolder", tableName: "MainMenu", comment: ""), action: #selector(openLogFolder(_:)), keyEquivalent: "") openLogFolder.target = self menu.addItem(openLogFolder) } @@ -187,8 +183,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta /*** menu selectors ***/ @objc func showSwitcher(_ sender: Any?) { print("Show Switcher") - if switcherKeyEquivalent != .XK_VoidSymbol { - let session = RimeSessionId((sender as! NSNumber).uint64Value) + if switcherKeyEquivalent != .XK_VoidSymbol, let session = sender as? RimeSessionId { _ = RimeApi.process_key(session, switcherKeyEquivalent.rawValue, switcherKeyModifierMask.rawValue) } } @@ -232,17 +227,17 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta do { try FileManager.default.createDirectory(at: Self.userDataDir, withIntermediateDirectories: true) } catch { - print("Error creating user data directory: \(Self.userDataDir.absoluteString)") + print("Error creating user data directory: \(Self.userDataDir.path)") } } RimeApi.set_notification_handler(notificationHandler, bridge(obj: self)) var squirrelTraits: RimeTraits = RimeStructInit() - squirrelTraits.shared_data_dir = (Bundle.main.sharedSupportURL as? NSURL)?.fileSystemRepresentation - squirrelTraits.user_data_dir = (Self.userDataDir as NSURL).fileSystemRepresentation - squirrelTraits.distribution_code_name = ("Squirrel" as NSString).utf8String - squirrelTraits.distribution_name = ("鼠鬚管" as NSString).utf8String - squirrelTraits.distribution_version = (Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? NSString)?.utf8String - squirrelTraits.app_name = ("rime.squirrel" as NSString).utf8String + squirrelTraits.shared_data_dir = Bundle.main.sharedSupportURL!.withUnsafeFileSystemRepresentation(\.unsafelyUnwrapped) + squirrelTraits.user_data_dir = Self.userDataDir.withUnsafeFileSystemRepresentation(\.unsafelyUnwrapped) + squirrelTraits.distribution_code_name = "Squirrel".utf8CString.withUnsafeBufferPointer(\.baseAddress) + squirrelTraits.distribution_name = "鼠鬚管".utf8CString.withUnsafeBufferPointer(\.baseAddress) + squirrelTraits.distribution_version = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? UnsafePointer + squirrelTraits.app_name = "rime.squirrel".utf8CString.withUnsafeBufferPointer(\.baseAddress) RimeApi.setup(&squirrelTraits) } @@ -266,23 +261,16 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta func loadSettings() { switcherKeyModifierMask = [] switcherKeyEquivalent = .XK_VoidSymbol - let defaulConfig = SquirrelConfig("default") + let defaulConfig = SquirrelConfig(.default) if let hotkey = defaulConfig.string(forOption: "switcher/hotkeys/@0") { let keys: [String] = hotkey.components(separatedBy: "+") - for i in 0 ..< (keys.count - 1) { - if let modifier = RimeModifiers(name: keys[i]) { - switcherKeyModifierMask.insert(modifier) - } - } + keys.dropLast().forEach { if let modifier = RimeModifiers(name: $0) { switcherKeyModifierMask.insert(modifier) } } switcherKeyEquivalent = RimeKeycode(name: keys.last!) } defaulConfig.close() let config = SquirrelConfig() - if !config.openBaseConfig() { - return - } - + guard config.openBaseConfig() else { return } let showNotificationsWhen = config.string(forOption: "show_notifications_when") if showNotificationsWhen?.caseInsensitiveCompare("never") == .orderedSame { showNotifications = .never @@ -291,16 +279,16 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } else { showNotifications = .whenAppropriate } + SquirrelInputController.chordDuration = if let duration = config.nullableDouble(forOption: "chord_duration"), duration.isNormal { duration } else { 0.1 } + panel.optionSwitcher = SquirrelOptionSwitcher() panel.loadConfig(config) config.close() } func loadSchemaSpecificSettings(schemaId: String, withRimeSession sessionId: RimeSessionId) { - if schemaId.isEmpty || schemaId.hasPrefix(".") { - return - } + guard !schemaId.isEmpty && !schemaId.hasPrefix(".") else { return } // update the list of switchers that change styles and color-themes - let baseConfig = SquirrelConfig("squirrel") + let baseConfig = SquirrelConfig(.base) let schema = SquirrelConfig() if schema.open(withSchemaId: schemaId, baseConfig: baseConfig) && schema.hasSection("style") { panel.optionSwitcher = schema.optionSwitcherForSchema() @@ -315,19 +303,18 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } func loadSchemaSpecificLabels(schemaId: String) { - let defaultConfig = SquirrelConfig("default") + let defaultConfig = SquirrelConfig(.default) if schemaId.isEmpty || schemaId.hasPrefix(".") { panel.loadLabelConfig(defaultConfig, directUpdate: true) - defaultConfig.close() - return - } - let schema = SquirrelConfig() - if schema.open(withSchemaId: schemaId, baseConfig: defaultConfig) && schema.hasSection("menu") { - panel.loadLabelConfig(schema, directUpdate: false) } else { - panel.loadLabelConfig(defaultConfig, directUpdate: false) + let schema = SquirrelConfig() + if schema.open(withSchemaId: schemaId, baseConfig: defaultConfig) && schema.hasSection("menu") { + panel.loadLabelConfig(schema, directUpdate: false) + } else { + panel.loadLabelConfig(defaultConfig, directUpdate: false) + } + schema.close() } - schema.close() defaultConfig.close() } @@ -370,11 +357,11 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta func inputSourceChanged(_ notification: Notification) { let inputSource: TISInputSource = TISCopyCurrentKeyboardInputSource().takeUnretainedValue() - if let inputSourceID: CFString = bridge(ptr: TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)), !(inputSourceID as String).hasPrefix(SquirrelApp.bundleId) { + if let inputSourceID: CFString = bridge(ptr: TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)), !CFStringHasPrefix(inputSourceID, SquirrelApp.bundleId as CFString) { isCurrentInputMethod = false } } -} +} // SquirrelApplicationDelegate private func showNotification(message: String) { if #available(macOS 10.14, *) { @@ -385,26 +372,22 @@ private func showNotification(message: String) { } } center.getNotificationSettings { settings in - if (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional) && (settings.alertSetting == .enabled) { - let content = UNMutableNotificationContent() - content.title = NSLocalizedString("Squirrel", comment: "") - content.subtitle = NSLocalizedString(message, comment: "") - if #available(macOS 12.0, *) { - content.interruptionLevel = .active - } - let request = UNNotificationRequest(identifier: "SquirrelNotification", content: content, trigger: nil) - center.add(request) { error in - if error != nil { - print("User notification request error: \(error.debugDescription)") - } + guard (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional) && settings.alertSetting == .enabled else { return } + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("Squirrel", tableName: "Notifications", comment: "") + content.subtitle = NSLocalizedString(message, tableName: "Notifications", comment: "") + if #available(macOS 12.0, *) { content.interruptionLevel = .active } + let request = UNNotificationRequest(identifier: "SquirrelNotification", content: content, trigger: nil) + center.add(request) { error in + if error != nil { + print("User notification request error: \(error.debugDescription)") } } } } else { let notification = NSUserNotification() - notification.title = NSLocalizedString("Squirrel", comment: "") - notification.subtitle = NSLocalizedString(message, comment: "") - + notification.title = NSLocalizedString("Squirrel", tableName: "Notifications", comment: "") + notification.subtitle = NSLocalizedString(message, tableName: "Notifications", comment: "") let notificationCenter = NSUserNotificationCenter.default notificationCenter.removeAllDeliveredNotifications() notificationCenter.deliver(notification) @@ -412,57 +395,47 @@ private func showNotification(message: String) { } private func notificationHandler(context_object: UnsafeMutableRawPointer?, session_id: RimeSessionId, message_type: UnsafePointer?, message_value: UnsafePointer?) { - if let type = message_type { - switch String(cString: type) { - case "deploy": - if let message = message_value { - switch String(cString: message) { - case "start": - showNotification(message: "deploy_start") - case "success": - showNotification(message: "deploy_success") - case "failure": - showNotification(message: "deploy_failure") - default: - break - } - } - case "schema": - if let appDelegate: SquirrelApplicationDelegate = bridge(ptr: context_object), appDelegate.showNotifications != .never, let message = message_value { - let schemaName = String(cString: message).components(separatedBy: "/") - if schemaName.count == 2 { - appDelegate.panel.updateStatus(long: schemaName[1], short: schemaName[1]) - } - } - case "option": - if let appDelegate: SquirrelApplicationDelegate = bridge(ptr: context_object), let message = message_value { - let optionState = String(cString: message) - let state: Bool = !optionState.hasPrefix("!") - let optionName = state ? optionState : String(optionState.suffix(optionState.count - 1)) - let updateScriptVariant: Bool = appDelegate.panel.optionSwitcher.updateCurrentScriptVariant(optionState) - var updateStyleOptions: Bool = false - if appDelegate.panel.optionSwitcher.updateGroupState(optionState, ofOption: optionName) { - updateStyleOptions = true - let schemaId: String = appDelegate.panel.optionSwitcher.schemaId - appDelegate.loadSchemaSpecificLabels(schemaId: schemaId) - appDelegate.loadSchemaSpecificSettings(schemaId: schemaId, withRimeSession: session_id) - } - if updateScriptVariant && !updateStyleOptions { - appDelegate.panel.updateScriptVariant() - } - if appDelegate.showNotifications != .never { - let longLabel = RimeApi.get_state_label_abbreviated(session_id, optionName, state, false) - let shortLabel = RimeApi.get_state_label_abbreviated(session_id, optionName, state, true) - if longLabel.str != nil || shortLabel.str != nil { - let long = longLabel.str == nil ? nil : String(cString: longLabel.str!) - let short = shortLabel.str == nil || shortLabel.length < strlen(shortLabel.str) ? nil : String(cString: shortLabel.str!) - appDelegate.panel.updateStatus(long: long, short: short) - } - } - } - default: - break + guard let message_type = message_type else { return } + switch String(cString: message_type) { + case "deploy": + guard let message_value = message_value else { break } + switch String(cString: message_value) { + case "start": showNotification(message: "deploy_start") + case "success": showNotification(message: "deploy_success") + case "failure": showNotification(message: "deploy_failure") + default: break + } + case "schema": + guard let appDelegate: SquirrelApplicationDelegate = bridge(ptr: context_object), appDelegate.showNotifications != .never, let message_value = message_value else { break } + let schemaName = String(cString: message_value).components(separatedBy: "/") + if schemaName.count == 2 { + appDelegate.panel.updateStatus(long: schemaName[1], short: schemaName[1]) + } + case "option": + guard let appDelegate: SquirrelApplicationDelegate = bridge(ptr: context_object), let message_value = message_value else { break } + let optionState = String(cString: message_value) + let state: Bool = !optionState.hasPrefix("!") + let optionName = state ? optionState : String(optionState.suffix(optionState.count - 1)) + let updateScriptVariant: Bool = appDelegate.panel.optionSwitcher.updateCurrentScriptVariant(optionState) + var updateStyleOptions: Bool = false + if appDelegate.panel.optionSwitcher.updateGroupState(optionState, ofOption: optionName) { + updateStyleOptions = true + let schemaId: String = appDelegate.panel.optionSwitcher.schemaId + appDelegate.loadSchemaSpecificLabels(schemaId: schemaId) + appDelegate.loadSchemaSpecificSettings(schemaId: schemaId, withRimeSession: session_id) } + if updateScriptVariant && !updateStyleOptions { + appDelegate.panel.updateScriptVariant() + } + guard appDelegate.showNotifications != .never else { break } + let longLabel = RimeApi.get_state_label_abbreviated(session_id, optionName, state, false) + let shortLabel = RimeApi.get_state_label_abbreviated(session_id, optionName, state, true) + if longLabel.str != nil || shortLabel.str != nil { + let long = longLabel.str == nil ? nil : String(cString: longLabel.str!) + let short = shortLabel.str == nil || shortLabel.length < strlen(shortLabel.str) ? nil : String(cString: shortLabel.str!) + appDelegate.panel.updateStatus(long: long, short: short) + } + default: break } } @@ -474,14 +447,18 @@ extension NSApplication { // MARK: Bridging -func bridge(obj: T?) -> UnsafeMutableRawPointer? { +func bridge(obj: T!) -> UnsafeMutableRawPointer! { return obj != nil ? Unmanaged.passUnretained(obj!).toOpaque() : nil } -func bridge(ptr: UnsafeMutableRawPointer?) -> T? { +func bridge(ptr: UnsafeMutableRawPointer!) -> T! { return ptr != nil ? Unmanaged.fromOpaque(ptr!).takeUnretainedValue() : nil } +func bridge(ptr: UnsafeRawPointer!) -> T! { + return ptr != nil ? Unmanaged.fromOpaque(ptr).takeUnretainedValue() : nil +} + let RimeApi = rime_get_api_stdbool().pointee typealias RimeSessionId = UInt diff --git a/sources/SquirrelConfig.swift b/sources/SquirrelConfig.swift index 630f75a7a..6102c98a2 100644 --- a/sources/SquirrelConfig.swift +++ b/sources/SquirrelConfig.swift @@ -2,17 +2,17 @@ import AppKit import Cocoa final class SquirrelOptionSwitcher: NSObject { - static let Scripts: [String] = ["zh-Hans", "zh-Hant", "zh-TW", "zh-HK", "zh-MO", "zh-SG", "zh-CN", "zh"] + static private let Scripts: [String] = ["zh-Hans", "zh-Hant", "zh-TW", "zh-HK", "zh-MO", "zh-SG", "zh-CN", "zh"] private(set) var schemaId: String private(set) var currentScriptVariant: String private var optionNames: Set private(set) var optionStates: Set - private var scriptVariantOptions: [String: String] - private var switcher: [String: String] - private var optionGroups: [String: Set] + private var scriptVariantOptions: [String : String] + private var switcher: [String : String] + private var optionGroups: [String : Set] - init(schemaId: String?, switcher: [String: String]?, optionGroups: [String: Set]?, defaultScriptVariant: String?, scriptVariantOptions: [String: String]?) { + init(schemaId: String?, switcher: [String : String]?, optionGroups: [String : Set]?, defaultScriptVariant: String?, scriptVariantOptions: [String : String]?) { self.schemaId = schemaId ?? "" self.switcher = switcher ?? [:] self.optionGroups = optionGroups ?? [:] @@ -32,10 +32,8 @@ final class SquirrelOptionSwitcher: NSObject { } // return whether switcher options has been successfully updated - func updateSwitcher(_ switcher: [String: String]!) -> Bool { - if self.switcher.isEmpty || switcher?.count != self.switcher.count { - return false - } + func updateSwitcher(_ switcher: [String : String]!) -> Bool { + guard !self.switcher.isEmpty && switcher?.count == self.switcher.count else { return false } let optionNames: Set = Set(switcher.keys) if optionNames == self.optionNames { self.switcher = switcher @@ -46,128 +44,65 @@ final class SquirrelOptionSwitcher: NSObject { } func updateGroupState(_ optionState: String, ofOption optionName: String) -> Bool { - if let optionGroup = optionGroups[optionName] { - if optionGroup.count == 1 { - if optionName != (optionState.hasPrefix("!") ? String(optionState.dropFirst()) : optionState) { - return false - } - switcher[optionName] = optionState - } else if optionGroup.contains(optionState) { - for option in optionGroup { - switcher[option] = optionState - } + guard let optionGroup = optionGroups[optionName] else { return false } + if optionGroup.count == 1 { + if optionName != (optionState.hasPrefix("!") ? String(optionState.dropFirst()) : optionState) { + return false } - optionStates = Set(switcher.values) - return true - } else { - return false + switcher[optionName] = optionState + } else if optionGroup.contains(optionState) { + optionGroup.forEach { switcher[$0] = optionState } } + optionStates = Set(switcher.values) + return true } func updateCurrentScriptVariant(_ scriptVariant: String) -> Bool { - if scriptVariantOptions.isEmpty { - return false - } - if let scriptVariantCode = scriptVariantOptions[scriptVariant] { - currentScriptVariant = scriptVariantCode - return true - } else { - return false - } + guard !scriptVariantOptions.isEmpty else { return false } + guard let scriptVariantCode = scriptVariantOptions[scriptVariant] else { return false } + currentScriptVariant = scriptVariantCode + return true } func update(withRimeSession session: RimeSessionId) { - if switcher.isEmpty || session == 0 { return } + guard !switcher.isEmpty && session != 0 else { return } for state in optionStates { var updatedState: String? - let optionGroup: [String] = Array(switcher.filter { (key, value) in value == state }.keys) - for option in optionGroup { - if RimeApi.get_option(session, option) { - updatedState = option; break - } - } + let optionGroup: [String] = Array(switcher.filter({ $0.value == state }).keys) + _ = optionGroup.first(where: { if RimeApi.get_option(session, $0) { updatedState = $0; return true } else { return false } }) updatedState ?= "!" + optionGroup[0] if updatedState != state { _ = updateGroupState(updatedState!, ofOption: state) } } // update script variant - for (option, _) in scriptVariantOptions { - if option.hasPrefix("!") ? !RimeApi.get_option(session, option.suffix(option.count - 1).withCString { $0 }) : RimeApi.get_option(session, option.withCString { $0 }) { - _ = updateCurrentScriptVariant(option); break - } - } - } -} // SquirrelOptionSwitcher - -struct SquirrelAppOptions { - private var appOptions: [String: Any] - - init() { appOptions = [:] } - - subscript(key: String) -> Any? { - get { - if let value = appOptions[key] { - if value is Bool.Type { - return value as! Bool - } else if value is Int.Type { - return value as! Int - } else if value is Double.Type { - return value as! Double - } - } - return nil - } - set (newValue) { - if newValue is Bool.Type || newValue is Int.Type || newValue is Double.Type { - appOptions[key] = newValue - } - } + _ = scriptVariantOptions.first(where: { if $0.key.hasPrefix("!") ? !RimeApi.get_option(session, String($0.key.dropFirst())) : RimeApi.get_option(session, $0.key) { _ = updateCurrentScriptVariant($0.key); return true } else { return false } }) } +} // SquirrelOptionSwitcher - mutating func setValue(_ value: Bool, forKey key: String) { - appOptions[key] = value - } - - mutating func setValue(_ value: Int, forKey key: String) { - appOptions[key] = value - } +final class SquirrelAppOptions: NSObject { + private var appOptions: [String : Any] = [:] - mutating func setValue(_ value: Double, forKey key: String) { - appOptions[key] = value + subscript(option: String) -> T? { + get { appOptions[option] as? T } + set { appOptions[option] = newValue } } - func boolValue(forKey key: String) -> Bool { - if let value = appOptions[key], value is Bool.Type { - return value as! Bool - } - return false + func boolValue(forOption option: String) -> Bool { + if let value = appOptions[option] as? Bool { return value } else { return false } } - - func intValue(forKey key: String) -> Int { - if let value = appOptions[key], value is Int.Type { - return value as! Int - } - return 0 + func intValue(forOption option: String) -> Int { + if let value = appOptions[option] as? Int { return value } else { return .zero } } - - func doubleValue(forKey key: String) -> Double { - if let value = appOptions[key], value is Double.Type { - return value as! Double - } - return 0.0 + func doubleValue(forOption option: String) -> Double { + if let value = appOptions[option] as? Double { return value } else { return .zero } } -} +} // SquirrelAppOptions final class SquirrelConfig: NSObject { - static let colorSpaceMap: [String: NSColorSpace] = ["deviceRGB": .deviceRGB, - "genericRGB": .genericRGB, - "sRGB": .sRGB, - "displayP3": .displayP3, - "adobeRGB": .adobeRGB1998, - "extendedSRGB": .extendedSRGB] - - private var cache: [String: Any] + static private let colorSpaceMap: [String : NSColorSpace] = ["deviceRGB" : .deviceRGB, "genericRGB" : .genericRGB, "sRGB" : .sRGB, "displayP3" : .displayP3, "adobeRGB" : .adobeRGB1998, "extendedSRGB" : .extendedSRGB] + + private var cache: [String : Any] private var config: RimeConfig = RimeConfig() private var baseConfig: SquirrelConfig? private var isOpen: Bool @@ -176,15 +111,10 @@ final class SquirrelConfig: NSObject { private var colorSpaceName: String var colorSpace: String { get { return colorSpaceName } - set (newValue) { + set { let name: String = newValue.replacingOccurrences(of: "_", with: "") - if name == colorSpaceName { return } - for (csName, csObject) in Self.colorSpaceMap { - if csName.caseInsensitiveCompare(name) == .orderedSame { - colorSpaceName = csName - colorSpaceObject = csObject - return - } + if name != colorSpaceName, let (key, value) = Self.colorSpaceMap.first(where:{ $0.key.caseInsensitiveCompare(name) == .orderedSame }) { + colorSpaceName = key; colorSpaceObject = value } } } @@ -197,17 +127,41 @@ final class SquirrelConfig: NSObject { super.init() } - convenience init(_ arg: String) { + enum RimeConfigType: RawRepresentable, Sendable { + case base, `default`, user, installation, schema(String) + + init?(rawValue: String) { + switch rawValue { + case "squirrel": self = .base + case "default": self = .default + case "user": self = .user + case "installation": self = .installation + default: self = .schema(rawValue) + } + } + + var rawValue: String { + switch self { + case .base: return "squirrel" + case .default: return "default" + case .user: return "user" + case .installation: return "installation" + case .schema(let id): return id + } + } + } + + convenience init(_ type: RimeConfigType) { self.init() - switch arg { - case "squirrel": + switch type { + case .base: _ = openBaseConfig() - case "default": - _ = open(withConfigId: arg) - case "user", "installation": - _ = open(userConfig: arg) - default: - _ = open(withSchemaId: arg, baseConfig: nil) + case .default: + _ = open(withConfigId: "default") + case .user, .installation: + _ = open(userConfig: type.rawValue) + case .schema(let id): + _ = open(withSchemaId: id, baseConfig: SquirrelConfig(.base)) } } @@ -222,11 +176,7 @@ final class SquirrelConfig: NSObject { isOpen = RimeApi.schema_open(schemaId, &config) if isOpen { self.schemaId = schemaId - if baseConfig == nil { - self.baseConfig = SquirrelConfig("squirrel") - } else { - self.baseConfig = baseConfig - } + self.baseConfig = baseConfig } return isOpen } @@ -245,10 +195,10 @@ final class SquirrelConfig: NSObject { func close() { if isOpen && RimeApi.config_close(&config) { - baseConfig = nil - schemaId = nil isOpen = false } + baseConfig = nil + schemaId = nil } deinit { @@ -257,14 +207,11 @@ final class SquirrelConfig: NSObject { } func hasSection(_ section: String) -> Bool { - if isOpen { - var iterator = RimeConfigIterator() - if RimeApi.config_begin_map(&iterator, &config, section) { - RimeApi.config_end(&iterator) - return true - } - } - return false + guard isOpen else { return false } + var iterator = RimeConfigIterator() + guard RimeApi.config_begin_map(&iterator, &config, section) else { return false } + RimeApi.config_end(&iterator) + return true } func setOption(_ option: String, withBool value: Bool) -> Bool { @@ -276,7 +223,7 @@ final class SquirrelConfig: NSObject { } func setOption(_ option: String, withDouble value: Double) -> Bool { - return RimeApi.config_set_double(&config, option, CDouble(value)) + return RimeApi.config_set_double(&config, option, value) } func setOption(_ option: String, withString value: String) -> Bool { @@ -288,30 +235,29 @@ final class SquirrelConfig: NSObject { } func intValue(forOption option: String) -> Int { - return nullableInt(forOption: option, alias: nil) ?? 0 + return nullableInt(forOption: option, alias: nil) ?? .zero } func doubleValue(forOption option: String) -> Double { - return nullableDouble(forOption: option, alias: nil) ?? 0.0 + return nullableDouble(forOption: option, alias: nil) ?? .zero } func doubleValue(forOption option: String, constraint function: (Double) -> Double) -> Double { - return function(nullableDouble(forOption: option, alias: nil) ?? 0.0) + return function(nullableDouble(forOption: option, alias: nil) ?? .zero) } func nullableBool(forOption option: String, alias: String?) -> Bool? { if let cachedValue = cachedValue(ofType: Bool.self, forKey: option) { return cachedValue } - var value: CBool = false + var value: Bool = false if isOpen && RimeApi.config_get_bool(&config, option, &value) { - cache[option] = Bool(value) - return Bool(value) + cache[option] = value + return value } - if let aliasOption = option.replaceLastPathComponent(with: alias), - isOpen && RimeApi.config_get_bool(&config, aliasOption, &value) { - cache[option] = Bool(value) - return Bool(value) + if let aliasOption = option.replaceLastPathComponent(with: alias), isOpen && RimeApi.config_get_bool(&config, aliasOption, &value) { + cache[option] = value + return value } return baseConfig?.nullableBool(forOption: option, alias: alias) } @@ -325,8 +271,7 @@ final class SquirrelConfig: NSObject { cache[option] = Int(value) return Int(value) } - if let aliasOption = option.replaceLastPathComponent(with: alias), - isOpen && RimeApi.config_get_int(&config, aliasOption, &value) { + if let aliasOption = option.replaceLastPathComponent(with: alias), isOpen && RimeApi.config_get_int(&config, aliasOption, &value) { cache[option] = Int(value) return Int(value) } @@ -337,20 +282,19 @@ final class SquirrelConfig: NSObject { if let cachedValue = cachedValue(ofType: Double.self, forKey: option) { return cachedValue } - var value: CDouble = 0 + var value: Double = 0 if isOpen && RimeApi.config_get_double(&config, option, &value) { - cache[option] = Double(value) - return Double(value) + cache[option] = value + return value } - if let aliasOption = option.replaceLastPathComponent(with: alias), - isOpen && RimeApi.config_get_double(&config, aliasOption, &value) { - cache[option] = Double(value) - return Double(value) + if let aliasOption = option.replaceLastPathComponent(with: alias), isOpen && RimeApi.config_get_double(&config, aliasOption, &value) { + cache[option] = value + return value } return baseConfig?.nullableDouble(forOption: option, alias: alias) } - func nullableDouble(forOption option: String, alias: String?, constraint function: (CDouble) -> CDouble) -> Double? { + func nullableDouble(forOption option: String, alias: String?, constraint function: (Double) -> Double) -> Double? { if let value = nullableDouble(forOption: option, alias: alias) { return function(value) } @@ -369,8 +313,7 @@ final class SquirrelConfig: NSObject { return nullableDouble(forOption: option, alias: nil) } - func nullableDouble(forOption option: String, - constraint function: (CDouble) -> CDouble) -> Double? { + func nullableDouble(forOption option: String, constraint function: (Double) -> Double) -> Double? { if let value = nullableDouble(forOption: option, alias: nil) { return function(value) } @@ -382,15 +325,14 @@ final class SquirrelConfig: NSObject { return cachedValue } if isOpen, let value = RimeApi.config_get_cstring(&config, option) { - let str = String(cString: value).trimmingCharacters(in: .whitespaces) - cache[option] = str - return str + let string = String(cString: value).trimmingCharacters(in: .whitespaces) + cache[option] = string + return string } - if let aliasOption: String = option.replaceLastPathComponent(with: alias), isOpen, - let value = RimeApi.config_get_cstring(&config, aliasOption) { - let str = String(cString: value).trimmingCharacters(in: .whitespaces) - cache[option] = str - return str + if let aliasOption: String = option.replaceLastPathComponent(with: alias), isOpen, let value = RimeApi.config_get_cstring(&config, aliasOption) { + let string = String(cString: value).trimmingCharacters(in: .whitespaces) + cache[option] = string + return string } return baseConfig?.string(forOption: option, alias: alias) } @@ -435,9 +377,7 @@ final class SquirrelConfig: NSObject { func list(forOption option: String) -> [String]? { var iterator = RimeConfigIterator() - if !RimeApi.config_begin_list(&iterator, &config, option) { - return nil - } + guard RimeApi.config_begin_list(&iterator, &config, option) else { return nil } var strList: [String] = [] while RimeApi.config_next(&iterator) { strList.append(string(forOption: String(cString: iterator.path!))!) @@ -446,45 +386,25 @@ final class SquirrelConfig: NSObject { return strList.count == 0 ? nil : strList } - static let localeScript: [String: String] = ["simplification": "zh-Hans", - "simplified": "zh-Hans", - "!traditional": "zh-Hans", - "traditional": "zh-Hant", - "!simplification": "zh-Hant", - "!simplified": "zh-Hant"] - static let localeRegion: [String: String] = ["tw": "zh-TW", "taiwan": "zh-TW", - "hk": "zh-HK", "hongkong": "zh-HK", - "hong_kong": "zh-HK", "mo": "zh-MO", - "macau": "zh-MO", "macao": "zh-MO", - "sg": "zh-SG", "singapore": "zh-SG", - "cn": "zh-CN", "china": "zh-CN"] + static private let localeScript: [String : String] = ["simplification" : "zh-Hans", "simplified" : "zh-Hans", "!traditional" : "zh-Hans", "traditional" : "zh-Hant", "!simplification" : "zh-Hant", "!simplified" : "zh-Hant"] + static private let localeRegion: [String : String] = ["tw" : "zh-TW", "taiwan" : "zh-TW", "hk" : "zh-HK", "hongkong" : "zh-HK", "hong_kong" : "zh-HK", "mo" : "zh-MO", "macau" : "zh-MO", "macao" : "zh-MO", "sg" : "zh-SG", "singapore" : "zh-SG", "cn" : "zh-CN", "china" : "zh-CN"] static func code(scriptVariant: String) -> String { - for (script, locale) in localeScript { - if script.caseInsensitiveCompare(scriptVariant) == .orderedSame { - return locale - } - } - for (region, locale) in localeRegion { - if scriptVariant.range(of: region, options: [.caseInsensitive]) != nil { - return locale - } - } - return "zh" + return localeScript.first(where: { $0.key.caseInsensitiveCompare(scriptVariant) == .orderedSame })?.value ?? localeRegion.first(where: { scriptVariant.range(of: $0.key, options: [.caseInsensitive]) != nil })?.value ?? "zh" } func optionSwitcherForSchema() -> SquirrelOptionSwitcher { - if schemaId == nil || schemaId!.isEmpty || schemaId == "." { + guard let schemaId = schemaId, !schemaId.isEmpty && schemaId != "." else { return SquirrelOptionSwitcher() } var switchIter = RimeConfigIterator() - if !RimeApi.config_begin_list(&switchIter, &config, "switches") { + guard RimeApi.config_begin_list(&switchIter, &config, "switches") else { return SquirrelOptionSwitcher(schemaId: schemaId) } - var switcher: [String: String] = [:] - var optionGroups: [String: Set] = [:] + var switcher: [String : String] = [:] + var optionGroups: [String : Set] = [:] var defaultScriptVariant: String? - var scriptVariantOptions: [String: String] = [:] + var scriptVariantOptions: [String : String] = [:] while RimeApi.config_next(&switchIter) { let reset = intValue(forOption: String(cString: switchIter.path!) + "/reset") if let name = string(forOption: String(cString: switchIter.path!) + "/name") { @@ -492,40 +412,29 @@ final class SquirrelConfig: NSObject { switcher[name] = reset != 0 ? name : "!" + name optionGroups[name] = [name] } - if defaultScriptVariant == nil && (name.caseInsensitiveCompare("simplification") == .orderedSame || - name.caseInsensitiveCompare("simplified") == .orderedSame || - name.caseInsensitiveCompare("traditional") == .orderedSame) { + if defaultScriptVariant == nil && (name.caseInsensitiveCompare("simplification") == .orderedSame || name.caseInsensitiveCompare("simplified") == .orderedSame || name.caseInsensitiveCompare("traditional") == .orderedSame) { defaultScriptVariant = reset != 0 ? name : "!" + name scriptVariantOptions[name] = Self.code(scriptVariant: name) scriptVariantOptions["!" + name] = Self.code(scriptVariant: "!" + name) } } else { var optionIter = RimeConfigIterator() - if !RimeApi.config_begin_list(&optionIter, &config, String(cString: switchIter.path!) + "/options") { - continue - } + guard RimeApi.config_begin_list(&optionIter, &config, String(cString: switchIter.path!) + "/options") else { continue } var optGroup: [String] = [] var hasStyleSection: Bool = false var hasScriptVariant = defaultScriptVariant != nil while RimeApi.config_next(&optionIter) { let option: String = string(forOption: String(cString: optionIter.path!))! optGroup.append(option) - hasStyleSection = hasStyleSection || hasSection("style/" + option) - hasScriptVariant = hasScriptVariant || option.caseInsensitiveCompare("simplification") == .orderedSame || - option.caseInsensitiveCompare("simplified") == .orderedSame || - option.caseInsensitiveCompare("traditional") == .orderedSame + hasStyleSection |= hasSection("style/" + option) + hasScriptVariant |= option.caseInsensitiveCompare("simplification") == .orderedSame || option.caseInsensitiveCompare("simplified") == .orderedSame || option.caseInsensitiveCompare("traditional") == .orderedSame } RimeApi.config_end(&optionIter) if hasStyleSection { - for i in 0 ..< optGroup.count { - switcher[optGroup[i]] = optGroup[reset] - optionGroups[optGroup[i]] = Set(optGroup) - } + optGroup.forEach { switcher[$0] = optGroup[reset]; optionGroups[$0] = Set(optGroup) } } if defaultScriptVariant == nil && hasScriptVariant { - for opt in optGroup { - scriptVariantOptions[opt] = Self.code(scriptVariant: opt) - } + optGroup.forEach { scriptVariantOptions[$0] = Self.code(scriptVariant: $0) } defaultScriptVariant = scriptVariantOptions[optGroup[reset]] } } @@ -535,77 +444,107 @@ final class SquirrelConfig: NSObject { } func appOptions(forApp bundleId: String) -> SquirrelAppOptions { - if let cachedValue = cachedValue(ofType: SquirrelAppOptions.self, forKey: bundleId) { + let rootKey = "app_options/" + bundleId + if let cachedValue = cachedValue(ofType: SquirrelAppOptions.self, forKey: rootKey) { return cachedValue } - let rootKey = "app_options/" + bundleId - var appOptions = SquirrelAppOptions() + let appOptions = SquirrelAppOptions() var iterator = RimeConfigIterator() if !RimeApi.config_begin_map(&iterator, &config, rootKey) { - cache[bundleId] = appOptions + cache[rootKey] = appOptions return appOptions } while RimeApi.config_next(&iterator) { // print("DEBUG option[\(iterator.index)]: \(iterator.key) (\(iterator.path))") - if let value: Any = nullableBool(forOption: String(cString: iterator.path!)) ?? - nullableInt(forOption: String(cString: iterator.path!)) ?? - nullableDouble(forOption: String(cString: iterator.path!)) { - appOptions[String(cString: iterator.key!)] = value + let path = String(cString: iterator.path!), key = String(cString: iterator.key!) + if let boolValue = nullableBool(forOption: path) { + appOptions[key] = boolValue + } else if let intValue = nullableInt(forOption: path) { + appOptions[key] = intValue + } else if let doubleValue = nullableDouble(forOption: path) { + appOptions[key] = doubleValue } } RimeApi.config_end(&iterator) - cache[bundleId] = appOptions + cache[rootKey] = appOptions return appOptions } // MARK: Private functions private func cachedValue(ofType: T.Type, forKey key: String) -> T? { - if let value = cache[key], value is T.Type { - return value as? T - } - return nil + if let value = cache[key] as? T { return value } else { return nil } } private func color(hexCode: String?) -> NSColor? { - if hexCode == nil || (hexCode!.count != 8 && hexCode!.count != 10) || (!hexCode!.hasPrefix("0x") && !hexCode!.hasPrefix("0X")) { - return nil - } - let hexScanner = Scanner(string: hexCode!) - var hex: UInt32 = 0x0 - if hexScanner.scanHexInt32(&hex) && hexScanner.isAtEnd { - let r = hex % 0x100 - let g = hex / 0x100 % 0x100 - let b = hex / 0x10000 % 0x100 - // 0xaaBBGGRR or 0xBBGGRR - let a = hexCode!.count == 10 ? hex / 0x1000000 : 0xFF - let components: [CGFloat] = [CGFloat(r) / 255.0, CGFloat(g) / 255.0, CGFloat(b) / 255.0, CGFloat(a) / 255.0] - return NSColor(colorSpace: colorSpaceObject, components: components, count: 4) - } - return nil + guard let hexCode = hexCode, hexCode.count == 8 || hexCode.count == 10, hexCode.hasPrefix("0x") || hexCode.hasPrefix("0X") else { return nil } + let hexScanner = Scanner(string: hexCode) + var hex: UInt64 = 0x0 + guard hexScanner.scanHexInt64(&hex) && hexScanner.isAtEnd else { return nil } + let r = CGFloat(hex % 0x100) + let g = CGFloat(hex / 0x100 % 0x100) + let b = CGFloat(hex / 0x10000 % 0x100) + // 0xaaBBGGRR or 0xBBGGRR + let a = hexCode.count == 10 ? CGFloat(hex / 0x1000000) : 255.0 + let components: [CGFloat] = [r / 255.0, g / 255.0, b / 255.0, a / 255.0] + return NSColor(colorSpace: colorSpaceObject, components: components, count: 4) } private func image(filePath: String?) -> NSImage? { - if filePath == nil { - return nil - } - let imageFile = URL(fileURLWithPath: filePath!, isDirectory: false, relativeTo: SquirrelApplicationDelegate.userDataDir).standardizedFileURL - if FileManager.default.fileExists(atPath: imageFile.path) { - return NSImage(byReferencing: imageFile) - } - return nil + guard let filePath = filePath else { return nil } + let imageFile = URL(fileURLWithPath: filePath, isDirectory: false, relativeTo: SquirrelApplicationDelegate.userDataDir).standardizedFileURL + guard FileManager.default.fileExists(atPath: imageFile.path) else { return nil } + return NSImage(byReferencing: imageFile) } -} // SquirrelConfig +} // SquirrelConfig extension String { - func unicharIndex(charIndex offset: CInt) -> Int { - return utf8.index(utf8.startIndex, offsetBy: Int(offset)).utf16Offset(in: self) + var length: Int { utf16.count } + + subscript(index: Int) -> unichar { + utf16[utf16.index(utf8.startIndex, offsetBy: index.clamp(min: 0, max: utf16.count))] + } + subscript(range: Range) -> String { + String(self[String.Index(utf16Offset: range.lowerBound.clamp(min: 0, max: utf16.count), in: self) ..< String.Index(utf16Offset: range.upperBound.clamp(min: 0, max: utf16.count), in: self)]) + } + subscript(range: ClosedRange) -> String { + String(self[String.Index(utf16Offset: range.lowerBound.clamp(min: 0, max: utf16.count), in: self) ... String.Index(utf16Offset: range.upperBound.clamp(min: 0, max: utf16.count - 1), in: self)]) + } + subscript(range: PartialRangeFrom) -> String { + String(self[String.Index(utf16Offset: range.lowerBound.clamp(min: 0, max: utf16.count), in: self)...]) + } + subscript(range: PartialRangeUpTo) -> String { + String(self[..) -> String { + String(self[...String.Index(utf16Offset: range.upperBound.clamp(min: 0, max: utf16.count - 1), in: self)]) + } + + subscript(index: CInt) -> CChar { + utf8CString[Int(index).clamp(min: 0, max: utf8.count)] + } + subscript(range: Range) -> String { + String(utf8[utf8.index(utf8.startIndex, offsetBy: Int(range.lowerBound).clamp(min: 0, max: utf8.count)) ..< utf8.index(utf8.startIndex, offsetBy: Int(range.upperBound).clamp(min: 0, max: utf8.count))])! + } + subscript(range: ClosedRange) -> String { + String(utf8[utf8.index(utf8.startIndex, offsetBy: Int(range.lowerBound).clamp(min: 0, max: utf8.count)) ... utf8.index(utf8.startIndex, offsetBy: Int(range.upperBound).clamp(min: 0, max: utf8.count - 1))])! + } + subscript(range: PartialRangeFrom) -> String { + String(utf8[utf8.index(utf8.startIndex, offsetBy: Int(range.lowerBound).clamp(min: 0, max: utf8.count))...])! + } + subscript(range: PartialRangeUpTo) -> String { + String(utf8[..) -> String { + String(utf8[...utf8.index(utf8.startIndex, offsetBy: Int(range.upperBound).clamp(min: 0, max: utf8.count - 1))])! + } + + func UniCharIndex(CCharIndex: CInt) -> Int { + utf8.index(utf8.startIndex, offsetBy: Int(CCharIndex).clamp(min: 0, max: utf8.count)).utf16Offset(in: self) } func replaceLastPathComponent(with replacement: String?) -> String? { - if let replacement = replacement, let sep = range(of: "/", options: .backwards) { - return String(self[.. 0 { - if rime_keycode == .XK_Delete || (rime_keycode >= .XK_Home && rime_keycode <= .XK_KP_Delete) || - (rime_keycode >= .XK_BackSpace && rime_keycode <= .XK_Escape) { + if rime_keycode == .XK_Delete || .XK_Home ... .XK_KP_Delete ~= rime_keycode || .XK_BackSpace ... .XK_Escape ~= rime_keycode { showPlaceholder("") } else if modifiers.isDisjoint(with: [.control, .command]) && !event.characters!.isEmpty { showPlaceholder(nil) @@ -207,8 +198,7 @@ final class SquirrelInputController: IMKInputController { } } } - default: - break + default: break } return handled } @@ -216,19 +206,15 @@ final class SquirrelInputController: IMKInputController { override func mouseDown(onCharacterIndex index: Int, coordinate point: NSPoint, withModifier flags: Int, continueTracking keepTracking: UnsafeMutablePointer!, client sender: Any!) -> Bool { keepTracking.pointee = false - if (!inlinePreedit && !inlineCandidate) || composedString?.isEmpty ?? true || inlineCaretPos == index || !NSEvent.ModifierFlags(rawValue: UInt(flags)).intersection(.deviceIndependentFlagsMask).isEmpty { - return false - } + guard inlinePreedit || inlineCandidate, let composed = composedString, !composed.isEmpty, inlineCaretPos != index, NSEvent.ModifierFlags(rawValue: UInt(flags)).intersection(.deviceIndependentFlagsMask).isEmpty else { return false } let markedRange: NSRange = client().markedRange() - let head: NSPoint = (client().attributes(forCharacterIndex: 0, lineHeightRectangle: nil)["IMKBaseline"] as! NSValue).pointValue - let tail: NSPoint = (client().attributes(forCharacterIndex: markedRange.length - 1, lineHeightRectangle: nil)["IMKBaseline"] as! NSValue).pointValue - if point.x > tail.x || index >= markedRange.length { - if inlineCandidate && !inlinePreedit { - return false - } - perform(action: .PROCESS, onIndex: .EndKey) - } else if point.x < head.x || index <= 0 { - perform(action: .PROCESS, onIndex: .HomeKey) + let head = client().attributes(forCharacterIndex: 0, lineHeightRectangle: nil)["IMKBaseline"] as! NSPoint + let tail = client().attributes(forCharacterIndex: markedRange.length - 1, lineHeightRectangle: nil)["IMKBaseline"] as! NSPoint + if point.x > tail.x.nextUp || index >= markedRange.length { + if inlineCandidate && !inlinePreedit { return false } + perform(action: .Process, onIndex: .EndKey) + } else if point.x < head.x.nextDown || index <= 0 { + perform(action: .Process, onIndex: .HomeKey) } else { moveCursor(inlineCaretPos, to: index, inlinePreedit: inlinePreedit, inlineCandidate: inlineCandidate) } @@ -251,7 +237,7 @@ final class SquirrelInputController: IMKInputController { let isNavigatorInTabular = panel.isTabular && modifiers.isEmpty && panel.isVisible && (isVertical ? keycode == .XK_Left || keycode == .XK_KP_Left || keycode == .XK_Right || keycode == .XK_KP_Right : keycode == .XK_Up || keycode == .XK_KP_Up || keycode == .XK_Down || keycode == .XK_KP_Down) if isNavigatorInTabular { var keycode: RimeKeycode = keycode - if keycode >= .XK_KP_Left && keycode <= .XK_KP_Down { + if .XK_KP_Left ... .XK_KP_Down ~= keycode { keycode = keycode - .XK_KP_Left + .XK_Left } if let newIndex = panel.candidateIndex(onDirection: SquirrelIndex(rawValue: Int(keycode.rawValue))!) { @@ -281,7 +267,7 @@ final class SquirrelInputController: IMKInputController { // Simulate key-ups for every interesting key-down for chord-typing. if handled { - let isChordingKey = (keycode >= .XK_space && keycode <= .XK_asciitilde) || keycode == .XK_Control_L || keycode == .XK_Control_R || keycode == .XK_Alt_L || keycode == .XK_Alt_R || keycode == .XK_Shift_L || keycode == .XK_Shift_R + let isChordingKey = .XK_space ... .XK_asciitilde ~= keycode || keycode == .XK_Control_L || keycode == .XK_Control_R || keycode == .XK_Alt_L || keycode == .XK_Alt_R || keycode == .XK_Shift_L || keycode == .XK_Shift_R if isChordingKey && RimeApi.get_option(session, "_chord_typing") { updateChord(keycode, modifiers: modifiers) } else if modifiers.isDisjoint(with: .Release) { @@ -299,32 +285,26 @@ final class SquirrelInputController: IMKInputController { let composition: String = !inlinePreedit && !inlineCandidate ? composedString! : inlineString!.string var ctx: RimeContext_stdbool = RimeStructInit() if cursorPosition > targetPosition { - let targetRange = ..= 0xFF08 && index.rawValue <= 0xFFFF { + case .Process: + if 0xFF08 ... 0xFFFF ~= index.rawValue { handled = RimeApi.process_key(session, CInt(index.rawValue), 0) - } else if index >= .ExpandButton && index <= .LockButton { + } else if .ExpandButton ... .LockButton ~= index { handled = true currentIndex = nil } - case .SELECT: + case .Select: handled = RimeApi.select_candidate(session, index.rawValue) - case .HIGHLIGHT: + case .Highlight: handled = RimeApi.highlight_candidate(session, index.rawValue) currentIndex = nil - case .DELETE: + case .Delete: handled = RimeApi.delete_candidate(session, index.rawValue) } if handled { @@ -361,13 +341,9 @@ final class SquirrelInputController: IMKInputController { private func onChordTimer() { // chord release triggered by timer - var processed_keys: CInt = 0 - if chordKeyCount != 0 && session != 0 { - for i in 0 ..< chordKeyCount { // simulate key-ups - if RimeApi.process_key(session, chordKeyCodes[i].rawValue, chordModifiers[i].union(.Release).rawValue) { - processed_keys += 1 - } - } + var processed_keys: Int = 0 + if !chordKeyCombos.isEmpty && session != 0 { + chordKeyCombos.forEach { if RimeApi.process_key(session, $0.keycode.rawValue, $0.modifiers.union(.Release).rawValue) { processed_keys += 1 } } } clearChord() if processed_keys > 0 { @@ -377,63 +353,54 @@ final class SquirrelInputController: IMKInputController { private func updateChord(_ keycode: RimeKeycode, modifiers: RimeModifiers) { // print("update chord: {\(_chord)} << \(keycode)") - for i in 0 ..< chordKeyCount { - if chordKeyCodes[i] == keycode { return } + for (k, _) in chordKeyCombos { + if k == keycode { return } } - if chordKeyCount >= kNumKeyRollOver { - // you are cheating. only one human typist (fingers <= 10) is supported. - return - } - chordKeyCodes.append(keycode) - chordModifiers.append(modifiers) - chordKeyCount += 1 + // you are cheating. only one human typist (fingers <= 10) is supported. + if chordKeyCombos.count >= Self.kNumKeyRollOver { return } + chordKeyCombos.append((keycode: keycode, modifiers: modifiers)) // reset timer chordTimer?.invalidate() - chordTimer = Timer.scheduledTimer(withTimeInterval: chordDuration, repeats: false) { _ in self.onChordTimer() } + chordTimer = Timer.scheduledTimer(withTimeInterval: Self.chordDuration, repeats: false) { _ in self.onChordTimer() } } private func clearChord() { - chordKeyCount = 0 + chordKeyCombos = [] chordTimer?.invalidate() } override func recognizedEvents(_ sender: Any!) -> Int { - // print("recognizedEvents:") return Int(NSEvent.EventTypeMask([.keyDown, .flagsChanged, .leftMouseDown]).rawValue) } private func showInitialStatus() { var status: RimeStatus_stdbool = RimeStructInit() - if session != 0 && RimeApi.get_status(session, &status) { - schemaId = String(cString: status.schema_id) - let schemaName = status.schema_name == nil ? schemaId : String(cString: status.schema_name!) - var options: [String] = [] - if let asciiMode = getOptionLabel(session: session, option: "ascii_mode", state: status.is_ascii_mode) { - options.append(asciiMode) - } - if let fullShape = getOptionLabel(session: session, option: "full_shape", state: status.is_full_shape) { - options.append(fullShape) - } - if let asciiPunct = getOptionLabel(session: session, option: "ascii_punct", state: status.is_ascii_punct) { - options.append(asciiPunct) - } - _ = RimeApi.free_status(&status) - let foldedOptions = options.isEmpty ? schemaName : schemaName + "|" + options.joined(separator: " ") + guard session != 0 && RimeApi.get_status(session, &status) else { return } + let schemaName = String(cString: status.schema_name ?? status.schema_id) + var options: [String] = [] + if let asciiMode = getOptionLabel(session: session, option: "ascii_mode", state: status.is_ascii_mode) { + options.append(asciiMode) + } + if let fullShape = getOptionLabel(session: session, option: "full_shape", state: status.is_full_shape) { + options.append(fullShape) + } + if let asciiPunct = getOptionLabel(session: session, option: "ascii_punct", state: status.is_ascii_punct) { + options.append(asciiPunct) + } + _ = RimeApi.free_status(&status) + let foldedOptions = options.isEmpty ? schemaName : schemaName + " │ " + options.joined(separator: " ") - NSApp.SquirrelAppDelegate.panel.updateStatus(long: foldedOptions, short: schemaName) - if #available(macOS 14.0, *) { - lastModifiers.insert(.help) - } - rimeUpdate() + NSApp.SquirrelAppDelegate.panel.updateStatus(long: foldedOptions, short: schemaName) + if #available(macOS 14.0, *) { + lastModifiers.insert(.help) // as manual indicator of initial status } + rimeUpdate() } override func commitComposition(_ sender: Any!) { // print("commitComposition:") commitString(composedString(sender)) - if session != 0 { - RimeApi.clear_composition(session) - } + if session != 0 { RimeApi.clear_composition(session) } hidePalettes() } @@ -519,9 +486,7 @@ final class SquirrelInputController: IMKInputController { override func cancelComposition() { commitString(originalString(client)) hidePalettes() - if session != 0 { - RimeApi.clear_composition(session) - } + if session != 0 { RimeApi.clear_composition(session) } } override func updateComposition() { @@ -529,7 +494,7 @@ final class SquirrelInputController: IMKInputController { } private func showPlaceholder(_ placeholder: String?) { - let attrs = mark(forStyle: kTSMHiliteSelectedRawText, at: NSRange(location: 0, length: placeholder?.utf16.count ?? 1)) as! [NSAttributedString.Key: Any] + let attrs = mark(forStyle: kTSMHiliteSelectedRawText, at: NSRange(location: 0, length: placeholder?.length ?? 1)) as! [NSAttributedString.Key: Any] inlineString = NSMutableAttributedString(string: placeholder ?? "█", attributes: attrs) inlineCaretPos = 0 updateComposition() @@ -543,13 +508,13 @@ final class SquirrelInputController: IMKInputController { inlineSelRange = selRange inlineCaretPos = caretPos // print("selRange = \(selRange), caretPos = \(caretPos)") - let attrs = mark(forStyle: kTSMHiliteRawText, at: NSRange(location: 0, length: string.utf16.count)) as! [NSAttributedString.Key: Any] + let attrs = mark(forStyle: kTSMHiliteRawText, at: NSRange(location: 0, length: string.length)) as! [NSAttributedString.Key : Any] inlineString = NSMutableAttributedString(string: string, attributes: attrs) if selRange.lowerBound > 0 { - inlineString?.addAttributes(mark(forStyle: kTSMHiliteConvertedText, at: NSRange(location: 0, length: selRange.lowerBound)) as! [NSAttributedString.Key: Any], range: NSRange(location: 0, length: selRange.lowerBound)) + inlineString?.addAttributes(mark(forStyle: kTSMHiliteConvertedText, at: NSRange(location: 0, length: selRange.lowerBound)) as! [NSAttributedString.Key : Any], range: NSRange(location: 0, length: selRange.lowerBound)) } if selRange.lowerBound < caretPos { - inlineString?.addAttributes(mark(forStyle: kTSMHiliteSelectedRawText, at: NSRange(selRange)) as! [NSAttributedString.Key: Any], range: NSRange(selRange)) + inlineString?.addAttributes(mark(forStyle: kTSMHiliteSelectedRawText, at: NSRange(selRange)) as! [NSAttributedString.Key : Any], range: NSRange(selRange)) } updateComposition() } @@ -568,40 +533,37 @@ final class SquirrelInputController: IMKInputController { } } if IbeamRect.isEmpty { - return .init(origin: NSEvent.mouseLocation, size: .zero) + return NSRect(origin: NSEvent.mouseLocation, size: .zero) } if IbeamRect.width > IbeamRect.height { IbeamRect.origin.x += CGFloat(inlineOffset) } else { IbeamRect.origin.y += CGFloat(inlineOffset) } - if #available(macOS 14.0, *) { // avoid overlapping with cursor effects view - if (goodOldCapsLock && lastModifiers.contains(.capsLock)) || lastModifiers.contains(.help) { - lastModifiers.subtract(.help) - var screenRect: NSRect = NSScreen.main?.frame ?? .zero - if IbeamRect.intersects(screenRect) { - screenRect = NSScreen.main?.visibleFrame ?? .zero - if IbeamRect.width > IbeamRect.height { - var capslockAccessory = NSRect(x: IbeamRect.minX - 30, y: IbeamRect.minY, width: 27, height: IbeamRect.height) - if capslockAccessory.minX < screenRect.minX { - capslockAccessory.origin.x = screenRect.minX - } - if capslockAccessory.maxX > screenRect.maxX { - capslockAccessory.origin.x = screenRect.maxX - capslockAccessory.width - } - IbeamRect = IbeamRect.union(capslockAccessory) - } else { - var capslockAccessory = NSRect(x: IbeamRect.minX, y: IbeamRect.minY - 26, width: IbeamRect.width, height: 23) - if capslockAccessory.minY < screenRect.minY { - capslockAccessory.origin.y = screenRect.maxY + 3 - } - if capslockAccessory.maxY > screenRect.maxY { - capslockAccessory.origin.y = screenRect.maxY - capslockAccessory.height - } - IbeamRect = IbeamRect.union(capslockAccessory) - } - } + guard #available(macOS 14.0, *), (goodOldCapsLock && lastModifiers.contains(.capsLock)) || lastModifiers.contains(.help) else { return IbeamRect } + // avoid overlapping with cursor effects view + lastModifiers.subtract(.help) // manual indicator of initial status + var screenRect: NSRect = NSScreen.main?.frame ?? .zero + guard !IbeamRect.intersects(screenRect) else { return IbeamRect } + screenRect = NSScreen.main?.visibleFrame ?? .zero + if IbeamRect.width > IbeamRect.height { + var capslockAccessory = NSRect(x: IbeamRect.minX - 30, y: IbeamRect.minY, width: 27, height: IbeamRect.height) + if capslockAccessory.minX < screenRect.minX.nextUp { + capslockAccessory.origin.x = screenRect.minX + } + if capslockAccessory.maxX > screenRect.maxX.nextDown { + capslockAccessory.origin.x = screenRect.maxX - capslockAccessory.width + } + IbeamRect = IbeamRect.union(capslockAccessory) + } else { + var capslockAccessory = NSRect(x: IbeamRect.minX, y: IbeamRect.minY - 26, width: IbeamRect.width, height: 23) + if capslockAccessory.minY < screenRect.minY.nextUp { + capslockAccessory.origin.y = screenRect.maxY + 3 + } + if capslockAccessory.maxY > screenRect.maxY.nextDown { + capslockAccessory.origin.y = screenRect.maxY - capslockAccessory.height } + IbeamRect = IbeamRect.union(capslockAccessory) } return IbeamRect } @@ -623,22 +585,24 @@ final class SquirrelInputController: IMKInputController { let app: String = client().bundleIdentifier() // print("createSession: \(app)") session = RimeApi.create_session() - schemaId = "" - if session != 0 { - let config = SquirrelConfig("squirrel") - appOptions = config.appOptions(forApp: app) - chordDuration = if let duration = config.nullableDouble(forOption: "chord_duration"), duration > 0 { duration } else { 0.1 } - config.close() - panellessCommitFix = appOptions.boolValue(forKey: "panelless_commit_fix") - inlinePlaceholder = appOptions.boolValue(forKey: "inline_placeholder") - inlineOffset = appOptions.intValue(forKey: "inline_offset") - if let asciiMode = Self.asciiMode, app == Self.currentApp { - RimeApi.set_option(session, "ascii_mode", asciiMode) - } - Self.currentApp = app - Self.asciiMode = nil - rimeUpdate() - } + let panel = NSApp.SquirrelAppDelegate.panel + schemaId = panel.optionSwitcher.schemaId + guard session != 0 else { return } + let config = SquirrelConfig(.base) + appOptions = config.appOptions(forApp: app) + config.close() + inlinePreedit = (panel.inlinePreedit && !appOptions.boolValue(forOption: "no_inline")) || appOptions.boolValue(forOption: "inline") + inlineCandidate = panel.inlineCandidate && !appOptions.boolValue(forOption: "no_inline") + RimeApi.set_option(session, "soft_cursor", !inlinePreedit) + panellessCommitFix = appOptions.boolValue(forOption: "panelless_commit_fix") + inlinePlaceholder = appOptions.boolValue(forOption: "inline_placeholder") + inlineOffset = appOptions.intValue(forOption: "inline_offset") + if let asciiMode = Self.asciiMode, app == Self.currentApp { + RimeApi.set_option(session, "ascii_mode", asciiMode) + } + Self.currentApp = app + Self.asciiMode = nil + rimeUpdate() } private func destroySession() { @@ -662,7 +626,7 @@ final class SquirrelInputController: IMKInputController { commitString(commitText) showPlaceholder("") } - var _ = RimeApi.free_commit(&commit) + _ = RimeApi.free_commit(&commit) return true } return false @@ -677,15 +641,15 @@ final class SquirrelInputController: IMKInputController { var status: RimeStatus_stdbool = RimeStructInit() if RimeApi.get_status(session, &status) { // enable schema specific ui style - if schemaId.isEmpty || strcmp(schemaId, status.schema_id) != 0 { + if strcmp(schemaId, status.schema_id) != 0 { schemaId = String(cString: status.schema_id) showingSwitcherMenu = RimeApi.get_option(session, "dumb") if !showingSwitcherMenu { NSApp.SquirrelAppDelegate.loadSchemaSpecificLabels(schemaId: schemaId) NSApp.SquirrelAppDelegate.loadSchemaSpecificSettings(schemaId: schemaId, withRimeSession: session) // inline preedit - inlinePreedit = (panel.inlinePreedit && !appOptions.boolValue(forKey: "no_inline")) || appOptions.boolValue(forKey: "inline") - inlineCandidate = panel.inlineCandidate && !appOptions.boolValue(forKey: "no_inline") + inlinePreedit = (panel.inlinePreedit && !appOptions.boolValue(forOption: "no_inline")) || appOptions.boolValue(forOption: "inline") + inlineCandidate = panel.inlineCandidate && !appOptions.boolValue(forOption: "no_inline") // if not inline, embed soft cursor in preedit string RimeApi.set_option(session, "soft_cursor", !inlinePreedit) } else { @@ -706,24 +670,22 @@ final class SquirrelInputController: IMKInputController { // update raw input let raw_input: UnsafePointer? = RimeApi.get_input(session) let originalString = raw_input == nil ? "" : String(cString: raw_input!) - didCompose = didCommit || originalString != self.originalString + didCompose |= originalString != self.originalString self.originalString = originalString // update composed string if preedit == nil || showingSwitcherMenu { composedString = "" } else if !inlinePreedit { // remove soft cursor - let prefixRange = ..= end { // subtract length of soft cursor + var suffixLength = preeditText[start...].replacingOccurrences(of: " ", with: "").length + let selLength = preeditText[start ..< end].replacingOccurrences(of: " ", with: "").length + if !inlinePreedit && end ..< length ~= caretPos { // subtract length of soft cursor suffixLength -= 1 } - let selSegment: Range = self.originalString == nil ? 0 ..< 0 : self.originalString!.utf16.count - suffixLength - selLength ..< self.originalString!.utf16.count - suffixLength - didCompose = didCompose || selSegment.lowerBound != self.selSegment.lowerBound || (selSegment.count != self.selSegment.count && hilitedCandidate == 0 && pageNum == 0) + let selSegment: Range = self.originalString == nil ? 0 ..< 0 : (self.originalString!.length - suffixLength - selLength) ..< (self.originalString!.length - suffixLength) + didCompose |= selSegment.lowerBound != self.selSegment.lowerBound || (selSegment.count != self.selSegment.count && hilitedCandidate == 0 && pageNum == 0) self.selSegment = selSegment // update `expanded` and `sectionNum` variables in tabular layout - // already processed the action if _currentIndex == nil + // already processed the action if `currentIndex` == nil if panel.isTabular && !showingStatus { if numCandidates == 0 || didCompose { panel.sectionNum = 0 @@ -766,42 +726,38 @@ final class SquirrelInputController: IMKInputController { candidateIndices = indexStart ..< indexStart + numCandidates + extraCandidates currentIndex = hilitedCandidate == nil ? nil : hilitedCandidate! + indexStart - if showingStatus { - clearBuffer() - } else if showingSwitcherMenu { + if showingSwitcherMenu { if inlinePlaceholder { updateComposition() } } else if inlineCandidate { let candidatePreview: UnsafeMutablePointer? = ctx.commit_text_preview var candidatePreviewText = candidatePreview == nil ? "" : String(cString: candidatePreview!) if inlinePreedit { if end <= caretPos && caretPos < length { - candidatePreviewText += String(preeditText[String.Index(utf16Offset: caretPos, in: preeditText)...]) + candidatePreviewText += preeditText[caretPos...] } if !didCommit || !candidatePreviewText.isEmpty { - showInlineString(candidatePreviewText, withSelRange: start ..< candidatePreviewText.utf16.count - (length - end), caretPos: caretPos < end ? caretPos : candidatePreviewText.utf16.count - (length - caretPos)) + showInlineString(candidatePreviewText, withSelRange: start ..< candidatePreviewText.length - (length - end), caretPos: caretPos < end ? caretPos : candidatePreviewText.length - (length - caretPos)) } } else { // preedit includes the soft cursor if end < caretPos && caretPos <= length { - let endIndex = String.Index(utf16Offset: candidatePreviewText.utf16.count - (caretPos - end), in: candidatePreviewText) - candidatePreviewText = String(candidatePreviewText.utf16[.. 0 { - showPlaceholder(kFullWidthSpace) + showPlaceholder(Self.kFullWidthSpace) } else if !didCommit || !preeditText.isEmpty { showInlineString(preeditText, withSelRange: start ..< end, caretPos: caretPos) } } else { if inlinePlaceholder && preedit != nil { - showPlaceholder(kFullWidthSpace) + showPlaceholder(Self.kFullWidthSpace) } else if !didCommit || preedit != nil { showInlineString("", withSelRange: 0 ..< 0, caretPos: 0) } @@ -874,36 +830,36 @@ final class SquirrelInputController: IMKInputController { } } } -} +} // SquirrelInputController -private func set_CapsLock_LED_state(target_state: CBool) { +private func updateCapsLockLEDState(targetState: Bool) { let ioService: io_service_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(kIOHIDSystemClass)) var ioConnect: io_connect_t = 0 IOServiceOpen(ioService, mach_task_self_, UInt32(kIOHIDParamConnectType), &ioConnect) - var current_state: CBool = false - IOHIDGetModifierLockState(ioConnect, CInt(kIOHIDCapsLockState), ¤t_state) - if current_state != target_state { - IOHIDSetModifierLockState(ioConnect, CInt(kIOHIDCapsLockState), target_state) + var currentState: Bool = false + IOHIDGetModifierLockState(ioConnect, CInt(kIOHIDCapsLockState), ¤tState) + if currentState != targetState { + IOHIDSetModifierLockState(ioConnect, CInt(kIOHIDCapsLockState), targetState) } IOServiceClose(ioConnect) } private func getOptionLabel(session: RimeSessionId, option: UnsafePointer, state: Bool) -> String? { let labelShort: RimeStringSlice = RimeApi.get_state_label_abbreviated(session, option, state, true) - if (labelShort.str != nil) && labelShort.length >= strlen(labelShort.str) { + if labelShort.str != nil && labelShort.length >= strlen(labelShort.str) { return String(cString: labelShort.str!) } else { let labelLong: RimeStringSlice = RimeApi.get_state_label_abbreviated(session, option, state, false) let label: String? = labelLong.str == nil ? nil : String(cString: labelLong.str!) - return label == nil ? nil : String(label![label!.rangeOfComposedCharacterSequence(at: label!.startIndex)]) + return label == nil ? nil : String(label!.first!) } } @frozen enum SquirrelAction: Sendable { - case PROCESS, SELECT, HIGHLIGHT, DELETE + case Process, Select, Highlight, Delete } -enum SquirrelIndex: RawRepresentable, Sendable { +enum SquirrelIndex: RawRepresentable, Comparable, Sendable { // 0, 1, 2 ... are ordinal digits, used as (int) indices case Ordinal(Int) // 0xFFXX are rime keycodes (as function keys), for paging etc. @@ -925,6 +881,7 @@ enum SquirrelIndex: RawRepresentable, Sendable { init?(rawValue: Int) { switch rawValue { + case 0x0 ... 0xFFF: self = .Ordinal(rawValue) case 0xFF08: self = .BackSpaceKey case 0xFF1B: self = .EscapeKey case 0xFF37: self = .CodeInputArea @@ -939,14 +896,14 @@ enum SquirrelIndex: RawRepresentable, Sendable { case 0xFF04: self = .ExpandButton case 0xFF05: self = .CompressButton case 0xFF06: self = .LockButton - case 0x0 ... 0xFFF: self = .Ordinal(rawValue) - default: self = .VoidSymbol + case 0xFFFFFF: self = .VoidSymbol + default: return nil } } var rawValue: Int { switch self { - case let .Ordinal(num): return num + case .Ordinal(let num): return num case .BackSpaceKey: return 0xFF08 case .EscapeKey: return 0xFF1B case .CodeInputArea: return 0xFF37 @@ -964,28 +921,29 @@ enum SquirrelIndex: RawRepresentable, Sendable { case .VoidSymbol: return 0xFFFFFF } } - - static func < (left: Self, right: Self) -> Bool { return left.rawValue < right.rawValue } - static func > (left: Self, right: Self) -> Bool { return left.rawValue > right.rawValue } - static func <= (left: Self, right: Self) -> Bool { return left.rawValue <= right.rawValue } - static func >= (left: Self, right: Self) -> Bool { return left.rawValue >= right.rawValue } - static func == (left: Self, right: Self) -> Bool { return left.rawValue == right.rawValue } - static func != (left: Self, right: Self) -> Bool { return left.rawValue != right.rawValue } - static func < (left: Self, right: Int) -> Bool { return left.rawValue < right } - static func > (left: Self, right: Int) -> Bool { return left.rawValue > right } - static func <= (left: Self, right: Int) -> Bool { return left.rawValue <= right } - static func >= (left: Self, right: Int) -> Bool { return left.rawValue >= right } - static func == (left: Self, right: Int) -> Bool { return left.rawValue == right } - static func != (left: Self, right: Int) -> Bool { return left.rawValue != right } - static func == (left: Self, right: Int?) -> Bool { return left.rawValue == (right ?? 0xFFFFFF) } - static func != (left: Self, right: Int?) -> Bool { return left.rawValue != (right ?? 0xFFFFFF) } - static func + (left: Self, right: Int) -> Self { - if left.rawValue >= 0x0 && left.rawValue <= 0xFFF { - let result = left.rawValue + right - if result >= 0x0 && result <= 0xFFF { - return Self(rawValue: left.rawValue + right)! - } + var isOrdinal: Bool { 0x0 ... 0xFFF ~= rawValue } + + static func < (lhs: Self, rhs: Self) -> Bool { return lhs.isOrdinal && rhs.isOrdinal && lhs.rawValue < rhs.rawValue } + static func > (lhs: Self, rhs: Self) -> Bool { return lhs.isOrdinal && rhs.isOrdinal && lhs.rawValue > rhs.rawValue } + static func <= (lhs: Self, rhs: Self) -> Bool { return lhs.isOrdinal && rhs.isOrdinal && lhs.rawValue <= rhs.rawValue } + static func >= (lhs: Self, rhs: Self) -> Bool { return lhs.isOrdinal && rhs.isOrdinal && lhs.rawValue >= rhs.rawValue } + static func == (lhs: Self, rhs: Self) -> Bool { return lhs.rawValue == rhs.rawValue } + static func != (lhs: Self, rhs: Self) -> Bool { return lhs.rawValue != rhs.rawValue } + static func < (lhs: Self, rhs: Int) -> Bool { return lhs.isOrdinal && 0x0 ... 0xFFF ~= rhs && lhs.rawValue < rhs } + static func > (lhs: Self, rhs: Int) -> Bool { return lhs.isOrdinal && 0x0 ... 0xFFF ~= rhs && lhs.rawValue > rhs } + static func <= (lhs: Self, rhs: Int) -> Bool { return lhs.isOrdinal && 0x0 ... 0xFFF ~= rhs && lhs.rawValue <= rhs } + static func >= (lhs: Self, rhs: Int) -> Bool { return lhs.isOrdinal && 0x0 ... 0xFFF ~= rhs && lhs.rawValue >= rhs } + static func == (lhs: Self, rhs: Int!) -> Bool { return lhs.rawValue == (rhs ?? 0xFFFFFF) } + static func != (lhs: Self, rhs: Int!) -> Bool { return lhs.rawValue != (rhs ?? 0xFFFFFF) } + static func + (lhs: Self, rhs: Int) -> Self { + if lhs.isOrdinal, let result = Self(rawValue: lhs.rawValue + rhs), result.isOrdinal { + return result } return .VoidSymbol } +} // SquirrelIndex + +extension Bool { + static func |= (lhs: inout Bool, rhs: Bool) { if !lhs && rhs { lhs = true } } + static func &= (lhs: inout Bool, rhs: Bool) { if lhs && !rhs { lhs = false } } } diff --git a/sources/SquirrelInputSource.swift b/sources/SquirrelInputSource.swift index e9297cbc0..c1ee625f5 100644 --- a/sources/SquirrelInputSource.swift +++ b/sources/SquirrelInputSource.swift @@ -14,41 +14,38 @@ struct RimeInputModes: OptionSet, Sendable { init?(code: String) { switch code { - case "HANS", "Hans", "hans": - self = .HANS - case "HANT", "Hant", "hant": - self = .HANT - case "CANT", "Cant", "cant": - self = .CANT - default: - return nil + case "HANS", "Hans", "hans": self = .HANS + case "HANT", "Hant", "hant": self = .HANT + case "CANT", "Cant", "cant": self = .CANT + default: return nil } } -} +} // RimeInputModes -final class SquirrelInputSource { - static let property: NSDictionary = [kTISPropertyBundleID!: Bundle.main.bundleIdentifier! as NSString] - static let InputModeIDHans = "im.rime.inputmethod.Squirrel.Hans" - static let InputModeIDHant = "im.rime.inputmethod.Squirrel.Hant" - static let InputModeIDCant = "im.rime.inputmethod.Squirrel.Cant" - static let preferences = Bundle.preferredLocalizations(from: ["zh-Hans", "zh-Hant", "zh-HK"], forPreferences: nil) +extension SquirrelApp { + static private let property = [kTISPropertyBundleID : Bundle.main.bundleIdentifier!] as CFDictionary + static private let InputModeIDHans = "im.rime.inputmethod.Squirrel.Hans" as CFString + static private let InputModeIDHant = "im.rime.inputmethod.Squirrel.Hant" as CFString + static private let InputModeIDCant = "im.rime.inputmethod.Squirrel.Cant" as CFString + static private let preferences = Bundle.preferredLocalizations(from: ["zh-Hans", "zh-Hant", "zh-HK"], forPreferences: nil) static func RegisterInputSource() { - if !GetEnabledInputModes().isEmpty { // Already registered + guard !GetEnabledInputModes(includeAllInstalled: true).isEmpty else { + // Already registered print("Squirrel is already registered."); return } - let bundlePath = NSURL(fileURLWithPath: "/Library/Input Methods/Squirrel.App", isDirectory: false) + let bundlePath: NSURL = .init(fileURLWithPath: "/Library/Input Methods/Squirrel.App", isDirectory: false) let registerError = TISRegisterInputSource(bundlePath) if registerError == noErr { - print("Squirrel has been successfully registered at \(bundlePath.absoluteString!) .") + print("Squirrel has been successfully registered at \(bundlePath.path!)") } else { - let error = NSError(domain: NSOSStatusErrorDomain, code: Int(registerError), userInfo: nil) - print("Squirrel failed to register at \(bundlePath.absoluteString!) (\(error.debugDescription)") + print("Squirrel failed to register at \(bundlePath.path!) (error code: \(registerError))") } } static func EnableInputSource(_ modes: RimeInputModes) { - if !GetEnabledInputModes().isEmpty { // keep user's manually enabled input modes + guard !GetEnabledInputModes(includeAllInstalled: false).isEmpty else { + // keep user's manually enabled input modes print("Squirrel input method(s) is already enabled."); return } var inputModesToEnable: RimeInputModes = modes @@ -67,25 +64,22 @@ final class SquirrelInputSource { } let sourceList = TISCreateInputSourceList(property, true).takeUnretainedValue() as! [TISInputSource] for source in sourceList { - if let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)), (sourceID as String == InputModeIDHans && inputModesToEnable.contains(.HANS)) || (sourceID as String == InputModeIDHant && inputModesToEnable.contains(.HANT)) || (sourceID as String == InputModeIDCant && inputModesToEnable.contains(.CANT)) { - // print("Examining input source: \(sourceID)") - if let isEnabled: CFBoolean = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled)), !CFBooleanGetValue(isEnabled) { - let enableError: OSStatus = TISEnableInputSource(source) - if enableError != noErr { - let error = NSError(domain: NSOSStatusErrorDomain, code: Int(enableError), userInfo: nil) - print("Failed to enable input source: \(sourceID) (\(error.debugDescription))") - } else { - print("Enabled input source: \(sourceID)") - } - } + guard let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)), (sourceID == InputModeIDHans && inputModesToEnable.contains(.HANS)) || (sourceID == InputModeIDHant && inputModesToEnable.contains(.HANT)) || (sourceID == InputModeIDCant && inputModesToEnable.contains(.CANT)) else { continue } + // print("Examining input source: \(sourceID)") + guard let isEnabled: CFBoolean = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceIsEnabled)), !CFBooleanGetValue(isEnabled) else { continue } + let enableError: OSStatus = TISEnableInputSource(source) + if enableError == noErr { + print("Enabled input source: \(sourceID)") + } else { + print("Failed to enable input source: \(sourceID) (error code: \(enableError))") } } } - static func SelectInputSource(_ mode: RimeInputModes?) { - let enabledInputModes: RimeInputModes = GetEnabledInputModes() - var inputModeToSelect: RimeInputModes? = mode - if inputModeToSelect == nil || !enabledInputModes.contains(inputModeToSelect!) { + static func SelectInputSource(_ mode: RimeInputModes) { + let enabledInputModes: RimeInputModes = GetEnabledInputModes(includeAllInstalled: false) + var inputModeToSelect: RimeInputModes = mode + if inputModeToSelect.isEmpty || !enabledInputModes.contains(inputModeToSelect) { for language in preferences { if language.caseInsensitiveCompare("zh-Hans") == .orderedSame && enabledInputModes.contains(.HANS) { inputModeToSelect = .HANS; break @@ -98,23 +92,20 @@ final class SquirrelInputSource { } } } - if inputModeToSelect == nil { + if inputModeToSelect.isEmpty { print("No enabled input sources."); return } let sourceList = TISCreateInputSourceList(property, false).takeUnretainedValue() as! [TISInputSource] for source in sourceList { - if let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)), (sourceID as String == InputModeIDHans && inputModeToSelect == .HANS) || (sourceID as String == InputModeIDHant && inputModeToSelect == .HANT) || (sourceID as String == InputModeIDCant && inputModeToSelect == .CANT) { - // print("Examining input source: \(sourceID)") - // select the first enabled input mode in Squirrel - if let isSelectable: CFBoolean = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable)), let isSelected: CFBoolean = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelected)), !CFBooleanGetValue(isSelected) && CFBooleanGetValue(isSelectable) { - let selectError: OSStatus = TISSelectInputSource(source) - if selectError != noErr { - let error = NSError(domain: NSOSStatusErrorDomain, code: Int(selectError)) - print("Failed to select input source: \(sourceID) (\(error.debugDescription))") - } else { - print("Selected input source: \(sourceID)"); break - } - } + guard let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)), (sourceID == InputModeIDHans && inputModeToSelect == .HANS) || (sourceID == InputModeIDHant && inputModeToSelect == .HANT) || (sourceID == InputModeIDCant && inputModeToSelect == .CANT) else { continue } + // print("Examining input source: \(sourceID)") + // select the first enabled input mode in Squirrel + guard let isSelectable: CFBoolean = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable)), let isSelected: CFBoolean = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelected)), !CFBooleanGetValue(isSelected) && CFBooleanGetValue(isSelectable) else { continue } + let selectError: OSStatus = TISSelectInputSource(source) + if selectError == noErr { + print("Selected input source: \(sourceID)"); break + } else { + print("Failed to select input source: \(sourceID) (error code: \(selectError))") } } } @@ -122,37 +113,37 @@ final class SquirrelInputSource { static func DisableInputSource() { let sourceList = TISCreateInputSourceList(property, false).takeUnretainedValue() as! [TISInputSource] for source in sourceList { - if let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)), sourceID as String == InputModeIDHans || sourceID as String == InputModeIDHant || sourceID as String == InputModeIDCant { - // print("Examining input source: \(sourceID)") - let disableError: OSStatus = TISDisableInputSource(source) - if disableError != noErr { - let error = NSError(domain: NSOSStatusErrorDomain, code: Int(disableError)) - print("Failed to disable input source: \(sourceID) (\(error.debugDescription))") - } else { - print("Disabled input source: \(sourceID)") - } + guard let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)), sourceID == InputModeIDHans || sourceID == InputModeIDHant || sourceID == InputModeIDCant else { continue } + // print("Examining input source: \(sourceID)") + let disableError: OSStatus = TISDisableInputSource(source) + if disableError == noErr { + print("Disabled input source: \(sourceID)") + } else { + print("Failed to disable input source: \(sourceID) (error code: \(disableError))") } } } - private static func GetEnabledInputModes() -> RimeInputModes { + static private func GetEnabledInputModes(includeAllInstalled: Bool) -> RimeInputModes { var inputModes: RimeInputModes = [] - let sourceList = TISCreateInputSourceList(property, false).takeUnretainedValue() as! [TISInputSource] + let sourceList = TISCreateInputSourceList(property, includeAllInstalled).takeUnretainedValue() as! [TISInputSource] for source in sourceList { - if let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)) { - // print("Examining input source: \(sourceID)") - switch sourceID as String { - case InputModeIDHans: - inputModes.insert(.HANS) - case InputModeIDHant: - inputModes.insert(.HANT) - case InputModeIDCant: - inputModes.insert(.CANT) - default: - break - } + guard let sourceID: CFString = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID)) else { continue } + // print("Examining input source: \(sourceID)") + switch sourceID { + case InputModeIDHans: inputModes.insert(.HANS) + case InputModeIDHant: inputModes.insert(.HANT) + case InputModeIDCant: inputModes.insert(.CANT) + default: continue } } return inputModes } +} // SquirrelApp + +extension CFString: @retroactive Comparable { + static public func < (lhs: CFString, rhs: CFString) -> Bool { CFStringCompare(lhs, rhs, []) == .compareLessThan } + static public func > (lhs: CFString, rhs: CFString) -> Bool { CFStringCompare(lhs, rhs, []) == .compareGreaterThan } + static public func == (lhs: CFString, rhs: CFString) -> Bool { CFStringCompare(lhs, rhs, []) == .compareEqualTo } + var length: Int { CFStringGetLength(self) } } diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift index 21d199696..58ce5817a 100644 --- a/sources/SquirrelPanel.swift +++ b/sources/SquirrelPanel.swift @@ -1,109 +1,89 @@ import AppKit import QuartzCore -private let kDefaultCandidateFormat: String = "%c. %@" -private let kTipSpecifier: String = "%s" -private let kFullWidthSpace: String = " " -private let kShowStatusDuration: TimeInterval = 2.0 -private let kBlendedBackgroundColorFraction: Double = 0.2 -private let kDefaultFontSize: Double = 24 -private let kOffsetGap: Double = 5 - // MARK: Auxiliaries -func clamp(_ x: T, _ min: T, _ max: T) -> T { - let y = x < min ? min : x - return y > max ? max : y +extension Comparable { + func clamp(min: Self, max: Self) -> Self { + self < min ? min : self > max ? max : self + } } // coalesce: assign new value if current value is null infix operator ?= : AssignmentPrecedence -func ?= (left: inout T?, right: T?) { - if left == nil && right != nil { - left = right - } -} +func ?= (lhs: inout T?, rhs: T?) { if lhs == nil && rhs != nil { lhs = rhs } } // overwrite current value with new value (provided not null) infix operator =? : AssignmentPrecedence -func =? (left: inout T?, right: T?) { - if right != nil { - left = right - } -} - -func =? (left: inout T, right: T?) { - if right != nil { - left = right! - } -} - -extension CFString { - static func == (left: CFString, right: CFString) -> Bool { - return CFStringCompare(left, right, []) == .compareEqualTo - } +func =? (lhs: inout T?, rhs: T?) { if rhs != nil { lhs = rhs } } +func =? (lhs: inout T, rhs: T?) { if rhs != nil { lhs = rhs! } } - static func != (left: CFString, right: CFString) -> Bool { - return CFStringCompare(left, right, []) != .compareEqualTo - } +extension CharacterSet { + static let fullWidthDigits = CharacterSet(charactersIn: UnicodeScalar(0xFF10)! ... UnicodeScalar(0xFF19)!) + static let fullWidthLatinCapitals = CharacterSet(charactersIn: UnicodeScalar(0xFF21)! ... UnicodeScalar(0xFF3A)!) } -extension CharacterSet { - static let fullWidthDigits = CharacterSet(charactersIn: Unicode.Scalar(0xFF10)! ... Unicode.Scalar(0xFF19)!) - static let fullWidthLatinCapitals = CharacterSet(charactersIn: Unicode.Scalar(0xFF21)! ... Unicode.Scalar(0xFF3A)!) +extension NSPoint: @retroactive AdditiveArithmetic { + static public func + (lhs: Self, rhs: Self) -> Self { return .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } + static public func - (lhs: Self, rhs: Self) -> Self { return .init(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } + static public func += (lhs: inout Self, rhs: Self) { lhs.x += rhs.x; lhs.y += rhs.y } + static public func -= (lhs: inout Self, rhs: Self) { lhs.x -= rhs.x; lhs.y -= rhs.y } } extension NSRect { // top-left -> bottom-left -> bottom-right -> top-right - var vertices: [NSPoint] { isEmpty ? [] : [origin, .init(x: minX, y: maxY), .init(x: maxX, y: maxY), .init(x: maxX, y: minY)] } + var vertices: [NSPoint] { isEmpty ? [] : [origin, NSPoint(x: minX, y: maxY), NSPoint(x: maxX, y: maxY), NSPoint(x: maxX, y: minY)] } func integral(options: AlignmentOptions) -> NSRect { return NSIntegralRectWithOptions(self, options) } + + func squirclePath(cornerRadius: Double) -> CGPath? { + return CGMutablePath.squirclePath(vertices: vertices, cornerRadius: cornerRadius)?.copy() + } } struct SquirrelTextPolygon: Sendable { var head: NSRect = .zero; var body: NSRect = .zero; var tail: NSRect = .zero - init(head: NSRect, body: NSRect, tail: NSRect) { + init(head: NSRect = .zero, body: NSRect = .zero, tail: NSRect = .zero) { self.head = head; self.body = body; self.tail = tail } -} -extension SquirrelTextPolygon { var origin: NSPoint { head.isEmpty ? body.origin : head.origin } var minY: CGFloat { head.isEmpty ? body.minY : head.minY } var maxY: CGFloat { head.isEmpty ? body.maxY : head.maxY } - var isSeparated: Bool { !head.isEmpty && body.isEmpty && !tail.isEmpty && tail.maxX < head.minX - 0.1 } + var isSeparated: Bool { !head.isEmpty && body.isEmpty && !tail.isEmpty && tail.maxX < head.minX.nextDown } var vertices: [NSPoint] { if isSeparated { return [] } switch (head.vertices, body.vertices, tail.vertices) { - case let (headVertices, [], []): - return headVertices - case let ([], [], tailVertices): - return tailVertices - case let ([], bodyVertices, []): - return bodyVertices - case let (headVertices, bodyVertices, []): - return [headVertices[0], headVertices[1], bodyVertices[0], bodyVertices[1], bodyVertices[2], headVertices[3]] - case let ([], bodyVertices, tailVertices): - return [bodyVertices[0], tailVertices[1], tailVertices[2], tailVertices[3], bodyVertices[2], bodyVertices[3]] - case let (headVertices, [], tailVertices): - return [headVertices[0], headVertices[1], tailVertices[0], tailVertices[1], tailVertices[2], tailVertices[3], headVertices[2], headVertices[3]] - case let (headVertices, bodyVertices, tailVertices): - return [headVertices[0], headVertices[1], bodyVertices[0], tailVertices[1], tailVertices[2], tailVertices[3], bodyVertices[2], headVertices[3]] + case let (h, [], []): return h + case let ([], [], t): return t + case let ([], b, []): return b + case let (h, b, []): return [h[0], h[1], b[0], b[1], b[2], h[3]] + case let ([], b, t): return [b[0], t[1], t[2], t[3], b[2], b[3]] + case let (h, [], t): return [h[0], h[1], t[0], t[1], t[2], t[3], h[2], h[3]] + case let (h, b, t): return [h[0], h[1], b[0], t[1], t[2], t[3], b[2], h[3]] } } func mouseInPolygon(point: NSPoint, flipped: Bool) -> Bool { return (!body.isEmpty && NSMouseInRect(point, body, flipped)) || (!head.isEmpty && NSMouseInRect(point, head, flipped)) || (!tail.isEmpty && NSMouseInRect(point, tail, flipped)) } + + func squirclePath(cornerRadius: Double) -> CGPath? { + if isSeparated { + guard let headPath = CGMutablePath.squirclePath(vertices: head.vertices, cornerRadius: cornerRadius), let tailPath = CGMutablePath.squirclePath(vertices: tail.vertices, cornerRadius: cornerRadius) else { return nil } + headPath.addPath(tailPath) + return headPath.copy() + } else { + return CGMutablePath.squirclePath(vertices: vertices, cornerRadius: cornerRadius)?.copy() + } + } } struct SquirrelTabularIndex: Sendable { var index: Int; var lineNum: Int; var tabNum: Int - init(index: Int, lineNum: Int, tabNum: Int) { - self.index = index; self.lineNum = lineNum; self.tabNum = tabNum - } + init(index: Int, lineNum: Int, tabNum: Int) { self.index = index; self.lineNum = lineNum; self.tabNum = tabNum } } struct SquirrelCandidateInfo: Sendable { @@ -114,9 +94,7 @@ struct SquirrelCandidateInfo: Sendable { self.location = location; self.length = length; self.text = text; self.comment = comment self.idx = idx; self.col = col; self.isTruncated = isTruncated } -} -extension SquirrelCandidateInfo { var candidateRange: NSRange { NSRange(location: location, length: length) } var upperBound: Int { location + length } var labelRange: NSRange { NSRange(location: location, length: text) } @@ -132,216 +110,169 @@ extension CGPath { path?.addPath(y!) return path?.copy() } +} - static func squirclePath(rect: NSRect, cornerRadius: Double) -> CGPath? { - return squircleMutablePath(vertices: rect.vertices, cornerRadius: cornerRadius)?.copy() - } - - static func squirclePath(polygon: SquirrelTextPolygon, cornerRadius: Double) -> CGPath? { - if polygon.isSeparated { - if let headPath = squircleMutablePath(vertices: polygon.head.vertices, cornerRadius: cornerRadius), let tailPath = squircleMutablePath(vertices: polygon.tail.vertices, cornerRadius: cornerRadius) { - headPath.addPath(tailPath) - return headPath.copy() - } else { return nil } - } else { - return squircleMutablePath(vertices: polygon.vertices, cornerRadius: cornerRadius)?.copy() - } - } - +extension CGMutablePath { // Bezier squircle curves, whose rounded corners are smooth (continously differentiable) - static func squircleMutablePath(vertices: [CGPoint], cornerRadius: Double) -> CGMutablePath? { - if vertices.count < 4 { return nil } + static func squirclePath(vertices: [CGPoint], cornerRadius: Double) -> CGMutablePath? { + guard vertices.count >= 4 else { return nil } let path = CGMutablePath() var vertex: CGPoint = vertices.last! var nextVertex: CGPoint = vertices.first! var nextDiff = CGVector(dx: nextVertex.x - vertex.x, dy: nextVertex.y - vertex.y) var lastDiff: CGVector - var arcRadius: CGFloat, arcRadiusDx: CGFloat, arcRadiusDy: CGFloat + var arcRadius: Double var startPoint: CGPoint - var relayA: CGPoint, controlA1: CGPoint, controlA2: CGPoint - var relayB: CGPoint, controlB1: CGPoint, controlB2: CGPoint var endPoint = CGPoint(x: vertex.x + nextDiff.dx * 0.5, y: nextVertex.y) - var control1: CGPoint, control2: CGPoint path.move(to: endPoint) for i in 0 ..< vertices.count { lastDiff = nextDiff vertex = nextVertex nextVertex = vertices[(i + 1) % vertices.count] - nextDiff = .init(dx: nextVertex.x - vertex.x, dy: nextVertex.y - vertex.y) - if abs(nextDiff.dx) >= abs(nextDiff.dy) { - arcRadius = min(cornerRadius, abs(nextDiff.dx) * 0.3, abs(lastDiff.dy) * 0.3) - arcRadiusDy = copysign(arcRadius, lastDiff.dy) - arcRadiusDx = copysign(arcRadius, nextDiff.dx) - startPoint = .init(x: vertex.x, y: fma(arcRadiusDy, -1.528664, nextVertex.y)) - relayA = .init(x: fma(arcRadiusDx, 0.074911, vertex.x), y: fma(arcRadiusDy, -0.631494, nextVertex.y)) - controlA1 = .init(x: vertex.x, y: fma(arcRadiusDy, -1.088493, nextVertex.y)) - controlA2 = .init(x: vertex.x, y: fma(arcRadiusDy, -0.868407, nextVertex.y)) - relayB = .init(x: fma(arcRadiusDx, 0.631494, vertex.x), y: fma(arcRadiusDy, -0.074911, nextVertex.y)) - controlB1 = .init(x: fma(arcRadiusDx, 0.372824, vertex.x), y: fma(arcRadiusDy, -0.169060, nextVertex.y)) - controlB2 = .init(x: fma(arcRadiusDx, 0.169060, vertex.x), y: fma(arcRadiusDy, -0.372824, nextVertex.y)) - endPoint = .init(x: fma(arcRadiusDx, 1.528664, vertex.x), y: nextVertex.y) - control1 = .init(x: fma(arcRadiusDx, 0.868407, vertex.x), y: nextVertex.y) - control2 = .init(x: fma(arcRadiusDx, 1.088493, vertex.x), y: nextVertex.y) + nextDiff = CGVector(dx: nextVertex.x - vertex.x, dy: nextVertex.y - vertex.y) + if nextDiff.dx.magnitude >= nextDiff.dy.magnitude { + arcRadius = min(cornerRadius.magnitude, nextDiff.dx.magnitude * 0.5, lastDiff.dy.magnitude * 0.5).rounded(.down) + startPoint = CGPoint(x: vertex.x, y: vertex.y - Double(signOf: lastDiff.dy, magnitudeOf: arcRadius)) + endPoint = CGPoint(x: vertex.x + Double(signOf: nextDiff.dx, magnitudeOf: arcRadius), y: vertex.y) } else { - arcRadius = min(cornerRadius, abs(nextDiff.dy) * 0.3, abs(lastDiff.dx) * 0.3) - arcRadiusDx = copysign(arcRadius, lastDiff.dx) - arcRadiusDy = copysign(arcRadius, nextDiff.dy) - startPoint = .init(x: fma(arcRadiusDx, -1.528664, nextVertex.x), y: vertex.y) - relayA = .init(x: fma(arcRadiusDx, -0.631494, nextVertex.x), y: fma(arcRadiusDy, 0.074911, vertex.y)) - controlA1 = .init(x: fma(arcRadiusDx, -1.088493, nextVertex.x), y: vertex.y) - controlA2 = .init(x: fma(arcRadiusDx, -0.868407, nextVertex.x), y: vertex.y) - relayB = .init(x: fma(arcRadiusDx, -0.074911, nextVertex.x), y: fma(arcRadiusDy, 0.631494, vertex.y)) - controlB1 = .init(x: fma(arcRadiusDx, -0.169060, nextVertex.x), y: fma(arcRadiusDy, 0.372824, vertex.y)) - controlB2 = .init(x: fma(arcRadiusDx, -0.372824, nextVertex.x), y: fma(arcRadiusDy, 0.169060, vertex.y)) - endPoint = .init(x: nextVertex.x, y: fma(arcRadiusDy, 1.528664, vertex.y)) - control1 = .init(x: nextVertex.x, y: fma(arcRadiusDy, 0.868407, vertex.y)) - control2 = .init(x: nextVertex.x, y: fma(arcRadiusDy, 1.088493, vertex.y)) + arcRadius = min(cornerRadius.magnitude, nextDiff.dy.magnitude * 0.5, lastDiff.dx.magnitude * 0.5).rounded(.down) + startPoint = CGPoint(x: vertex.x - Double(signOf: lastDiff.dx, magnitudeOf: arcRadius), y: vertex.y) + endPoint = CGPoint(x: vertex.x, y: vertex.y + Double(signOf: nextDiff.dy, magnitudeOf: arcRadius)) } path.addLine(to: startPoint) - path.addCurve(to: relayA, control1: controlA1, control2: controlA2) - path.addCurve(to: relayB, control1: controlB1, control2: controlB2) - path.addCurve(to: endPoint, control1: control1, control2: control2) + path.addCurve(to: endPoint, control1: vertex, control2: vertex) } path.closeSubpath() return path } -} // NSBezierPath (NSBezierSquirclePath) +} extension NSAttributedString.Key { static let baselineClass: NSAttributedString.Key = .init(kCTBaselineClassAttributeName as String) static let baselineReferenceInfo: NSAttributedString.Key = .init(kCTBaselineReferenceInfoAttributeName as String) static let rubyAnnotation: NSAttributedString.Key = .init(kCTRubyAnnotationAttributeName as String) static let language: NSAttributedString.Key = .init(kCTLanguageAttributeName as String) + static let controlCharacterSize: NSAttributedString.Key = .init("ControlCharacterSize") } extension NSMutableAttributedString { private func superscriptionRange(_ range: NSRange) { enumerateAttribute(.font, in: range, options: [.longestEffectiveRangeNotRequired]) { value, subRange, stop in - if let oldFont = value as? NSFont { - let newFont = NSFont(descriptor: oldFont.fontDescriptor, size: floor(oldFont.pointSize * 0.55)) - let attrs: [NSAttributedString.Key: Any] = [.font: newFont!, - .baselineClass: kCTBaselineClassIdeographicCentered, - .superscript: NSNumber(value: 1)] - addAttributes(attrs, range: subRange) - } + guard let oldFont = value as? NSFont else { return } + let newFont = NSFont(descriptor: oldFont.fontDescriptor, size: (oldFont.pointSize * 0.55).rounded(.down)) + let attrs: [NSAttributedString.Key : Any] = [.font : newFont!, .superscript : 1] + addAttributes(attrs, range: subRange) } } private func subscriptionRange(_ range: NSRange) { enumerateAttribute(.font, in: range, options: [.longestEffectiveRangeNotRequired]) { value, subRange, stop in - if let oldFont = value as? NSFont { - let newFont = NSFont(descriptor: oldFont.fontDescriptor, size: floor(oldFont.pointSize * 0.55)) - let attrs: [NSAttributedString.Key: Any] = [.font: newFont!, - .baselineClass: kCTBaselineClassIdeographicCentered, - .superscript: NSNumber(value: -1)] - addAttributes(attrs, range: subRange) - } + guard let oldFont = value as? NSFont else { return } + let newFont = NSFont(descriptor: oldFont.fontDescriptor, size: (oldFont.pointSize * 0.55).rounded(.down)) + let attrs: [NSAttributedString.Key : Any] = [.font : newFont!, .superscript : -1] + addAttributes(attrs, range: subRange) } } - static let markDownPattern: String = - "((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|<(b|strong|i|em|u|sup|sub|s)>)(.+?)(\\2|\\3(?=\\b)|<\\/\\4>)" + static private let markDownPattern = "((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|<(b|strong|i|em|u|sup|sub|s)>)(.+?)(\\2|\\3(?=\\b)|<\\/\\4>)" func formatMarkDown() { - if let regex = try? NSRegularExpression(pattern: Self.markDownPattern, options: [.useUnicodeWordBoundaries]) { - var offset: Int = 0 - regex.enumerateMatches(in: string, options: [], range: NSRange(location: 0, length: length)) { match, flags, stop in - guard let match = match else { return } - let adjusted = match.adjustingRanges(offset: offset) - let tag: String! = mutableString.substring(with: adjusted.range(at: 1)) - switch tag { - case "**", "__", "", "": - applyFontTraits(.boldFontMask, range: adjusted.range(at: 5)) - case "*", "_", "", "": - applyFontTraits(.italicFontMask, range: adjusted.range(at: 5)) - case "": - addAttribute(.underlineStyle, value: NSUnderlineStyle.single, range: adjusted.range(at: 5)) - case "~~", "": - addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single, range: adjusted.range(at: 5)) - case "^", "": - superscriptionRange(adjusted.range(at: 5)) - case "~", "": - subscriptionRange(adjusted.range(at: 5)) - default: - break - } - deleteCharacters(in: adjusted.range(at: 6)) - deleteCharacters(in: adjusted.range(at: 1)) - offset -= adjusted.range(at: 6).length + adjusted.range(at: 1).length - } - if offset != 0 { // repeat until no more nested markdown - formatMarkDown() - } - } - } - - static let rubyPattern: String = "(\u{FFF9}\\s*)(\\S+?)(\\s*\u{FFFA}(.+?)\u{FFFB})" + guard let regex = try? NSRegularExpression(pattern: Self.markDownPattern, options: [.useUnicodeWordBoundaries]) else { return } + var offset: Int = 0 + regex.enumerateMatches(in: string, options: [], range: NSRange(location: 0, length: length)) { match, flags, stop in + guard let match = match else { return } + let adjusted = match.adjustingRanges(offset: offset) + switch mutableString.substring(with: adjusted.range(at: 1)) { + case "**", "__", "", "": + applyFontTraits(.boldFontMask, range: adjusted.range(at: 5)) + case "*", "_", "", "": + applyFontTraits(.italicFontMask, range: adjusted.range(at: 5)) + case "": + addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: adjusted.range(at: 5)) + case "~~", "": + addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: adjusted.range(at: 5)) + case "^", "": + superscriptionRange(adjusted.range(at: 5)) + case "~", "": + subscriptionRange(adjusted.range(at: 5)) + default: break + } + deleteCharacters(in: adjusted.range(at: 6)) + deleteCharacters(in: adjusted.range(at: 1)) + offset -= adjusted.range(at: 6).length + adjusted.range(at: 1).length + } + if offset != 0 { // repeat until no more nested markdown + formatMarkDown() + } + } + + static private let rubyPattern = "(\\x{FFF9}\\s*)(\\S+?)(\\s*\\x{FFFA}(.+?)\\x{FFFB})" func annotateRuby(inRange range: NSRange, verticalOrientation isVertical: Bool, maximumLength maxLength: Double, scriptVariant: String) -> Double { - var rubyLineHeight: Double = 0.0 - if let regex = try? NSRegularExpression(pattern: Self.rubyPattern, options: []) { - regex.enumerateMatches(in: string, options: [], range: range) { match, flags, stop in - guard let match = match else { return } - let baseRange: NSRange = match.range(at: 2) - // no ruby annotation if the base string includes line breaks - if attributedSubstring(from: NSRange(location: 0, length: baseRange.upperBound)).size().width > maxLength - 0.1 { - deleteCharacters(in: NSRange(location: match.range.upperBound - 1, length: 1)) - deleteCharacters(in: NSRange(location: match.range(at: 3).location, length: 1)) - deleteCharacters(in: NSRange(location: match.range(at: 1).location, length: 1)) - } else { - /* base string must use only one font so that all fall within one glyph run and + guard let regex = try? NSRegularExpression(pattern: Self.rubyPattern, options: []) else { return .zero } + var rubyLineHeight: Double = .zero + regex.enumerateMatches(in: string, options: [], range: range) { match, flags, stop in + guard let match = match else { return } + let baseRange: NSRange = match.range(at: 2) + // no ruby annotation if the base string includes line breaks + if attributedSubstring(from: NSRange(location: 0, length: baseRange.upperBound)).size().width > maxLength.nextDown { + deleteCharacters(in: NSRange(location: match.range.upperBound - 1, length: 1)) + deleteCharacters(in: NSRange(location: match.range(at: 3).location, length: 1)) + deleteCharacters(in: NSRange(location: match.range(at: 1).location, length: 1)) + } else { + /* base string must use only one font so that all fall within one glyph run and the ruby annotation is aligned with no duplicates */ - var baseFont: NSFont = attribute(.font, at: baseRange.location, effectiveRange: nil) as! NSFont - baseFont = CTFontCreateForStringWithLanguage(baseFont, mutableString, CFRange(location: baseRange.location, length: baseRange.length), scriptVariant as CFString) - let rubyString = mutableString.substring(with: match.range(at: 4)) as CFString - var rubyFont: NSFont = attribute(.font, at: match.range(at: 4).location, effectiveRange: nil) as! NSFont - rubyFont = NSFont(descriptor: rubyFont.fontDescriptor, size: ceil(rubyFont.pointSize * 0.5))! - rubyLineHeight = isVertical ? rubyFont.vertical.ascender - rubyFont.vertical.descender + 1.0 : rubyFont.ascender - rubyFont.descender + 1.0 - let rubyAttrs: [CFString: AnyObject] = [kCTFontAttributeName: rubyFont] - let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(.distributeSpace, .none, .before, rubyString, rubyAttrs as CFDictionary) - + var baseFont = attribute(.font, at: baseRange.location, effectiveRange: nil) as! NSFont + let baseString = mutableString.substring(with: baseRange) as NSString + baseFont = CTFont(font: baseFont, string: baseString, range: CFRange(location: 0, length: baseString.length), language: scriptVariant as CFString) + let rubyString = mutableString.substring(with: match.range(at: 4)) as NSString + rubyLineHeight = baseFont.lineHeight(asVertical: isVertical) * 0.5 + var rubyTexts: [Unmanaged?] = [.passUnretained(rubyString), nil, nil, nil] + let rubyAnnotation = CTRubyAnnotationCreate(.distributeSpace, .none, 0.5, &rubyTexts) + addAttributes([.font : baseFont, .verticalGlyphForm : isVertical ? 1 : 0], range: match.range) + + if #available(macOS 12.0, *) { deleteCharacters(in: match.range(at: 3)) - if #available(macOS 12.0, *) { - } else { // use U+008B as placeholder for line-forward spaces in case ruby is wider than base - replaceCharacters(in: NSRange(location: baseRange.upperBound, length: 0), with: "\u{008B}") - } - let attrs: [NSAttributedString.Key: Any] = [.font: baseFont, - .verticalGlyphForm: NSNumber(value: isVertical), - .rubyAnnotation: rubyAnnotation] - addAttributes(attrs, range: baseRange) - deleteCharacters(in: match.range(at: 1)) + } else { // use U+008B as placeholder for line-forward spaces in case ruby is wider than base + let baseSize = attributedSubstring(from: baseRange).size() + let rubyWidth = attributedSubstring(from: match.range(at: 4)).size().width * 0.5 + deleteCharacters(in: match.range(at: 3)) + replaceCharacters(in: NSRange(location: baseRange.upperBound, length: 0), with: "\u{008B}") + addAttribute(.controlCharacterSize, value: NSSize(width: fdim(rubyWidth.rounded(.up), baseSize.width.rounded(.down)), height: baseSize.height), range: NSRange(location: baseRange.upperBound, length: 1)) } + addAttribute(.rubyAnnotation, value: rubyAnnotation, range: baseRange) + deleteCharacters(in: match.range(at: 1)) } - mutableString.replaceOccurrences(of: "[\u{FFF9}-\u{FFFB}]", with: "", options: [.regularExpression], range: NSRange(location: 0, length: length)) } - return ceil(rubyLineHeight) + mutableString.replaceOccurrences(of: "(.)?[\\x{FFF9}-\\x{FFFB}]", with: "$1", options: [.regularExpression], range: NSRange(location: 0, length: length)) + return rubyLineHeight.rounded(.up) } -} // NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) +} extension NSAttributedString { func horizontalInVerticalForms() -> NSAttributedString { var attrs = attributes(at: 0, effectiveRange: nil) let font = attrs[.font] as! NSFont - let stringWidth = floor(size().width) - let height: Double = floor(font.ascender - font.descender) + let attrString = NSAttributedString(string: string, attributes: fontAttributes(in: NSRange(location: 0, length: length))) + let stringWidth = attrString.size().width.rounded(.up) + let height: Double = attrString.size().height.rounded(.up) let width: Double = max(height, stringWidth) - let image = NSImage(size: .init(width: height, height: width), flipped: true, drawingHandler: { dstRect in + let image = NSImage(size: NSSize(width: height, height: height), flipped: true) { dstRect in NSGraphicsContext.saveGraphicsState() let transform = NSAffineTransform() + transform.scaleX(by: 1.0, yBy: height / width) + transform.translateX(by: (height * 0.5).rounded(.up), yBy: (width * 0.5).rounded(.up)) transform.rotate(byDegrees: -90) transform.concat() - let origin = NSPoint(x: floor((width - stringWidth) * 0.5 - dstRect.height), y: 0) - self.draw(at: origin) + attrString.draw(with: NSRect(x: -(stringWidth * 0.5).rounded(.up), y: -(height * 0.5).rounded(.up), width: stringWidth, height: height), options: .usesLineFragmentOrigin) NSGraphicsContext.restoreGraphicsState() return true - }) - image.resizingMode = .stretch - image.size = .init(width: height, height: height) + } let attm = NSTextAttachment() attm.image = image - attm.bounds = .init(x: 0, y: floor(font.descender), width: height, height: height) + attm.bounds = NSRect(x: 0, y: font.descender.rounded(.up), width: height, height: height) attrs[.attachment] = attm - return .init(string: String(Unicode.Scalar(NSTextAttachment.character)!), attributes: attrs) + return NSAttributedString(string: String(UnicodeScalar(NSTextAttachment.character)!), attributes: attrs) } -} // NSAttributedString (NSAttributedStringHorizontalInVerticalForms) +} extension NSColorSpace { static let labColorSpace: NSColorSpace = { @@ -351,28 +282,26 @@ extension NSColorSpace { let colorSpaceLab = CGColorSpace(labWhitePoint: whitePoint, blackPoint: blackPoint, range: range) return NSColorSpace(cgColorSpace: colorSpaceLab!)! }() -} // NSColorSpace +} extension NSColor { convenience init(lStar: CGFloat, aStar: CGFloat, bStar: CGFloat, alpha: CGFloat) { - let lum: CGFloat = clamp(lStar, 0.0, 100.0) - let green_red: CGFloat = clamp(aStar, -127.0, 127.0) - let blue_yellow: CGFloat = clamp(bStar, -127.0, 127.0) - let opaque: CGFloat = clamp(alpha, 0.0, 1.0) + let lum: CGFloat = lStar.clamp(min: 0.0, max: 100.0) + let green_red: CGFloat = aStar.clamp(min: -127.0, max: 127.0) + let blue_yellow: CGFloat = bStar.clamp(min: -127.0, max: 127.0) + let opaque: CGFloat = alpha.clamp(min: 0.0, max: 1.0) let components: [CGFloat] = [lum, green_red, blue_yellow, opaque] self.init(colorSpace: .labColorSpace, components: components, count: 4) } private var LABComponents: [CGFloat?] { - if let componentBased = usingType(.componentBased)?.usingColorSpace(.labColorSpace) { - var components: [CGFloat] = [0.0, 0.0, 0.0, 1.0] - componentBased.getComponents(&components) - components[0] /= 100.0 // Luminance - components[1] /= 127.0 // Green-Red - components[2] /= 127.0 // Blue-Yellow - return components - } - return [nil, nil, nil, nil] + guard let componentBased = usingType(.componentBased)?.usingColorSpace(.labColorSpace) else { return [nil, nil, nil, nil] } + var components: [CGFloat] = [0.0, 0.0, 0.0, 1.0] + componentBased.getComponents(&components) + components[0] /= 100.0 // Luminance + components[1] /= 127.0 // Green-Red + components[2] /= 127.0 // Blue-Yellow + return components } var lStarComponent: CGFloat? { LABComponents[0] } @@ -391,23 +320,16 @@ extension NSColor { } func invertLuminance(toExtent extent: ColorInversionExtent) -> NSColor { - if let componentBased = usingType(.componentBased), - let labColor = componentBased.usingColorSpace(.labColorSpace) { - var components: [CGFloat] = [0.0, 0.0, 0.0, 1.0] - labColor.getComponents(&components) - switch extent { - case .augmented: - components[0] = 100.0 - components[0] - case .moderate: - components[0] = 80.0 - components[0] * 0.6 - case .standard: - components[0] = 90.0 - components[0] * 0.8 - } - let invertedColor = NSColor(colorSpace: .labColorSpace, components: components, count: 4) - return invertedColor.usingColorSpace(componentBased.colorSpace)! - } else { - return self + guard let componentBased = usingType(.componentBased), let labColor = componentBased.usingColorSpace(.labColorSpace) else { return self } + var components: [CGFloat] = [0.0, 0.0, 0.0, 1.0] + labColor.getComponents(&components) + switch extent { + case .augmented: components[0] = 100.0 - components[0] + case .moderate: components[0] = 80.0 - components[0] * 0.6 + case .standard: components[0] = 90.0 - components[0] * 0.8 } + let invertedColor = NSColor(colorSpace: .labColorSpace, components: components, count: 4) + return invertedColor.usingColorSpace(componentBased.colorSpace)! } var hooverColor: NSColor { @@ -426,8 +348,9 @@ extension NSColor { } } + static private let kBlendedBackgroundColorFraction: Double = 0.2 func blend(background: NSColor?) -> NSColor { - return blended(withFraction: kBlendedBackgroundColorFraction, of: background ?? .lightGray)?.withAlphaComponent(alphaComponent) ?? self + return blended(withFraction: Self.kBlendedBackgroundColorFraction, of: background ?? .lightGray)?.withAlphaComponent(alphaComponent) ?? self } func blendWithColor(_ color: NSColor, ofFraction fraction: CGFloat) -> NSColor? { @@ -435,7 +358,7 @@ extension NSColor { let opaqueColor: NSColor = withAlphaComponent(1.0).blended(withFraction: fraction, of: color.withAlphaComponent(1.0))! return opaqueColor.withAlphaComponent(alpha) } -} // NSColor +} // MARK: Theme - color scheme and other user configurations @@ -448,41 +371,30 @@ extension NSColor { } extension NSFontDescriptor { + static private let features: [[NSFontDescriptor.FeatureKey : Int]] = [[.typeIdentifier : kVerticalSubstitutionType, .selectorIdentifier : kSubstituteVerticalFormsOnSelector], [.typeIdentifier : kCJKVerticalRomanPlacementType, .selectorIdentifier : kCJKVerticalRomanCenteredSelector], [.typeIdentifier : kRubyKanaType, .selectorIdentifier : kRubyKanaOffSelector]] static func create(fullname: String?) -> NSFontDescriptor? { - if fullname?.isEmpty ?? true { - return nil - } - let fontNames: [String] = fullname!.components(separatedBy: ",") - var validFontDescriptors: [NSFontDescriptor] = [] - for name in fontNames { - if let font = NSFont(name: name.trimmingCharacters(in: .whitespaces), size: 0.0) { - /* If the font name is not valid, NSFontDescriptor will still create something for us. - However, when we draw the actual text, Squirrel will crash if there is any font descriptor - with invalid font name. */ - let fontDescriptor = font.fontDescriptor - let UIFontDescriptor = fontDescriptor.withSymbolicTraits(.UIOptimized) - validFontDescriptors.append(NSFont(descriptor: UIFontDescriptor, size: 0.0) != nil ? UIFontDescriptor : fontDescriptor) - } - } - if let fontDescriptor = validFontDescriptors.first { - var fallbackDescriptors: [NSFontDescriptor] = Array(validFontDescriptors.dropFirst()) - fallbackDescriptors.append(NSFontDescriptor(name: "AppleColorEmoji", size: 0.0)) - return fontDescriptor.addingAttributes([.cascadeList: fallbackDescriptors as NSArray]) - } else { - return nil - } + guard let fullname = fullname, !fullname.isEmpty else { return nil } + let fontNames: [String] = fullname.components(separatedBy: ",") + let validFontDescriptors: [NSFontDescriptor] = fontNames.compactMap { name in + guard let font = NSFont(name: name.trimmingCharacters(in: .whitespaces), size: 0.0) else { return nil } + let fontDescriptor = font.fontDescriptor.addingAttributes([.featureSettings : features]) + let UIFontDescriptor = fontDescriptor.withSymbolicTraits([.UIOptimized]) + return NSFont(descriptor: UIFontDescriptor, size: 0.0) == nil ? fontDescriptor : UIFontDescriptor + } + guard let fontDescriptor = validFontDescriptors.first else { return nil } + let fallbackDescriptors: [NSFontDescriptor] = validFontDescriptors.dropFirst() + [NSFontDescriptor(name: "AppleColorEmoji", size: 0.0).addingAttributes([.featureSettings : features])] + return fontDescriptor.addingAttributes([.cascadeList : fallbackDescriptors]) } } extension NSFont { func lineHeight(asVertical: Bool) -> Double { - var lineHeight: Double = ceil(asVertical ? vertical.ascender - vertical.descender : ascender - descender) - let fallbackList = fontDescriptor.fontAttributes[.cascadeList] as! [NSFontDescriptor] + var lineHeight: Double = (asVertical ? vertical.ascender - vertical.descender : ascender - descender).rounded(.up) + guard let fallbackList = fontDescriptor.fontAttributes[.cascadeList] as? [NSFontDescriptor] else { return lineHeight } for fallback in fallbackList { - if let fallbackFont = NSFont(descriptor: fallback, size: pointSize) { - let fallbackHeight = asVertical ? fallbackFont.vertical.ascender - fallbackFont.vertical.descender : fallbackFont.ascender - fallbackFont.descender - lineHeight = max(lineHeight, ceil(fallbackHeight)) - } + guard let fallbackFont = NSFont(descriptor: fallback, size: pointSize) else { continue } + let fallbackHeight = asVertical ? fallbackFont.vertical.ascender - fallbackFont.vertical.descender : fallbackFont.ascender - fallbackFont.descender + lineHeight = max(lineHeight, fallbackHeight.rounded(.up)) } return lineHeight } @@ -521,12 +433,15 @@ private func updateTextOrientation(isVertical: inout Bool, config: SquirrelConfi } // functions for post-retrieve processing -func positive(param: Double) -> Double { return param < 0.0 ? 0.0 : param } -func pos_round(param: Double) -> Double { return param < 0.0 ? 0.0 : round(param) } -func pos_ceil(param: Double) -> Double { return param < 0.0 ? 0.0 : ceil(param) } -func clamp_uni(param: Double) -> Double { return param < 0.0 ? 0.0 : param > 1.0 ? 1.0 : param } +@inlinable func positive(param: Double) -> Double { return param < .zero ? .zero : param } +@inlinable func pos_round(param: Double) -> Double { return param < .zero ? .zero : param.rounded() } +@inlinable func pos_ceil(param: Double) -> Double { return param < .zero ? .zero : param.rounded(.up) } +@inlinable func clamp_uni(param: Double) -> Double { return param < .zero ? .zero : param > 1.0 ? 1.0 : param } final class SquirrelTheme: NSObject { + static private let kDefaultFontSize: Double = 24 + static private let kDefaultCandidateFormat: String = "%c. %@ %s" + private(set) var backColor: NSColor = .controlBackgroundColor private(set) var preeditForeColor: NSColor = .textColor private(set) var textForeColor: NSColor = .controlTextColor @@ -545,15 +460,15 @@ final class SquirrelTheme: NSObject { private(set) var backImage: NSImage? private(set) var borderInsets: NSSize = .zero - private(set) var cornerRadius: Double = 0 - private(set) var hilitedCornerRadius: Double = 0 + private(set) var cornerRadius: Double = .zero + private(set) var hilitedCornerRadius: Double = .zero private(set) var fullWidth: Double - private(set) var lineSpacing: Double = 0 - private(set) var preeditSpacing: Double = 0 + private(set) var lineSpacing: Double = .zero + private(set) var preeditSpacing: Double = .zero private(set) var opacity: Double = 1 - private(set) var lineLength: Double = 0 - private(set) var shadowSize: Double = 0 - private(set) var translucency: Float = 0 + private(set) var lineLength: Double = .zero + private(set) var shadowSize: Double = .zero + private(set) var translucency: Float = .zero private(set) var stackColors: Bool = false private(set) var showPaging: Bool = false @@ -564,12 +479,12 @@ final class SquirrelTheme: NSObject { private(set) var inlinePreedit: Bool = false private(set) var inlineCandidate: Bool = true - private(set) var textAttrs: [NSAttributedString.Key: Any] = [:] - private(set) var labelAttrs: [NSAttributedString.Key: Any] = [:] - private(set) var commentAttrs: [NSAttributedString.Key: Any] = [:] - private(set) var preeditAttrs: [NSAttributedString.Key: Any] = [:] - private(set) var pagingAttrs: [NSAttributedString.Key: Any] = [:] - private(set) var statusAttrs: [NSAttributedString.Key: Any] = [:] + private(set) var textAttrs: [NSAttributedString.Key : Any] = [:] + private(set) var labelAttrs: [NSAttributedString.Key : Any] = [:] + private(set) var commentAttrs: [NSAttributedString.Key : Any] = [:] + private(set) var preeditAttrs: [NSAttributedString.Key : Any] = [:] + private(set) var pagingAttrs: [NSAttributedString.Key : Any] = [:] + private(set) var statusAttrs: [NSAttributedString.Key : Any] = [:] private(set) var candidateParagraphStyle: NSParagraphStyle private(set) var preeditParagraphStyle: NSParagraphStyle @@ -588,12 +503,14 @@ final class SquirrelTheme: NSObject { private(set) var symbolExpand: NSAttributedString? private(set) var symbolLock: NSAttributedString? - private(set) var labels: [String] = ["1", "2", "3", "4", "5"] - private(set) var candidateTemplate: NSAttributedString - private(set) var candidateHilitedTemplate: NSAttributedString + private(set) var rawLabels: [String] = ["1", "2", "3", "4", "5"] + private(set) var labels: [String] = [] + private(set) var candidateTemplate: NSAttributedString = NSAttributedString(string: kDefaultCandidateFormat) + private(set) var candidateHilitedTemplate: NSAttributedString = NSAttributedString(string: kDefaultCandidateFormat) private(set) var candidateDimmedTemplate: NSAttributedString? private(set) var selectKeys: String = "12345" - private(set) var candidateFormat: String = kDefaultCandidateFormat + private(set) var rawCandidateFormat: String = kDefaultCandidateFormat + private(set) var candidateFormat: String = "" private(set) var scriptVariant: String = "zh" private(set) var statusMessageType: SquirrelStatusMessageType = .mixed private(set) var pageSize: Int = 5 @@ -605,7 +522,7 @@ final class SquirrelTheme: NSObject { let candidateParagraphStyle = NSMutableParagraphStyle() candidateParagraphStyle.alignment = .left /* Use left-to-right marks to declare the default writing direction and prevent strong right-to-left - characters from setting the writing direction in case the label are direction-less symbols */ + characters from setting the writing direction in case the label are direction-less symbols */ candidateParagraphStyle.baseWritingDirection = .leftToRight let preeditParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle let pagingParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle @@ -617,22 +534,25 @@ final class SquirrelTheme: NSObject { self.pagingParagraphStyle = pagingParagraphStyle.copy() as! NSParagraphStyle self.statusParagraphStyle = statusParagraphStyle.copy() as! NSParagraphStyle - let userFont: NSFont! = NSFont(descriptor: .create(fullname: NSFont.userFont(ofSize: kDefaultFontSize)!.fontName)!, size: kDefaultFontSize) - let userMonoFont: NSFont! = NSFont(descriptor: .create(fullname: NSFont.userFixedPitchFont(ofSize: kDefaultFontSize)!.fontName)!, size: kDefaultFontSize) - let monoDigitFont: NSFont! = .monospacedDigitSystemFont(ofSize: kDefaultFontSize, weight: .regular) + let userFont: NSFont! = NSFont(descriptor: .create(fullname: NSFont.userFont(ofSize: Self.kDefaultFontSize)!.fontName)!, size: Self.kDefaultFontSize) + let userMonoFont: NSFont! = NSFont(descriptor: .create(fullname: NSFont.userFixedPitchFont(ofSize: Self.kDefaultFontSize)!.fontName)!, size: Self.kDefaultFontSize) + let monoDigitFont: NSFont! = .monospacedDigitSystemFont(ofSize: Self.kDefaultFontSize, weight: .regular) textAttrs[.foregroundColor] = NSColor.controlTextColor textAttrs[.font] = userFont + textAttrs[.kern] = 0 // Use left-to-right embedding to prevent right-to-left text from changing the layout of the candidate. - textAttrs[.writingDirection] = NSArray(object: NSNumber(value: 0)) + textAttrs[.writingDirection] = [0] labelAttrs[.foregroundColor] = NSColor.labelColor labelAttrs[.font] = userMonoFont - labelAttrs[.strokeWidth] = NSNumber(value: -2.0 / kDefaultFontSize) + labelAttrs[.kern] = 0 + labelAttrs[.strokeWidth] = -2.0 / Self.kDefaultFontSize commentAttrs[.foregroundColor] = NSColor.secondaryLabelColor commentAttrs[.font] = userFont + commentAttrs[.kern] = 0 preeditAttrs[.foregroundColor] = NSColor.textColor preeditAttrs[.font] = userFont - preeditAttrs[.ligature] = NSNumber(value: 0) + preeditAttrs[.ligature] = 0 preeditAttrs[.paragraphStyle] = preeditParagraphStyle pagingAttrs[.font] = monoDigitFont pagingAttrs[.foregroundColor] = NSColor.controlTextColor @@ -641,14 +561,12 @@ final class SquirrelTheme: NSObject { statusAttrs[.paragraphStyle] = statusParagraphStyle separator = NSAttributedString(string: "\n", attributes: commentAttrs) - fullWidth = ceil(NSAttributedString(string: kFullWidthSpace, attributes: commentAttrs).size().width) - let template = NSMutableAttributedString(string: "%c. ", attributes: labelAttrs) - template.append(.init(string: "%@", attributes: textAttrs)) - candidateTemplate = template.copy() as! NSAttributedString - candidateHilitedTemplate = template.copy() as! NSAttributedString + let glyphs = UnsafeMutablePointer.allocate(capacity: 1) + CTFontGetGlyphsForCharacters(userFont, [UniChar(3000)], glyphs, 1) + fullWidth = userFont.advancement(forCGGlyph: glyphs[0]).width.rounded(.up) super.init() - updateCandidateTemplates(forAttributesOnly: false) + updateCandidateTemplates() updateSeperatorAndSymbolAttrs() } @@ -657,40 +575,40 @@ final class SquirrelTheme: NSObject { } private func updateSeperatorAndSymbolAttrs() { - var sepAttrs: [NSAttributedString.Key: Any] = commentAttrs - sepAttrs[.verticalGlyphForm] = NSNumber(value: false) - sepAttrs[.kern] = NSNumber(value: 0.0) + var sepAttrs: [NSAttributedString.Key : Any] = commentAttrs + sepAttrs[.verticalGlyphForm] = 0 + sepAttrs[.kern] = 0 separator = NSAttributedString(string: isLinear ? (isTabular ? "\u{3000}\t\u{001D}" : "\u{3000}\u{001D}") : "\n", attributes: sepAttrs) // Symbols for function buttons - let attmCharacter = String(Unicode.Scalar(NSTextAttachment.character)!) + let attmCharacter = String(UnicodeScalar(NSTextAttachment.character)!) let attmDeleteFill = NSTextAttachment() attmDeleteFill.image = NSImage(named: "Symbols/delete.backward.fill") - var attrsDeleteFill: [NSAttributedString.Key: Any] = preeditAttrs + var attrsDeleteFill: [NSAttributedString.Key : Any] = preeditAttrs attrsDeleteFill[.attachment] = attmDeleteFill - attrsDeleteFill[.verticalGlyphForm] = NSNumber(value: false) + attrsDeleteFill[.verticalGlyphForm] = 0 symbolDeleteFill = NSAttributedString(string: attmCharacter, attributes: attrsDeleteFill) let attmDeleteStroke = NSTextAttachment() attmDeleteStroke.image = NSImage(named: "Symbols/delete.backward") - var attrsDeleteStroke: [NSAttributedString.Key: Any] = preeditAttrs + var attrsDeleteStroke: [NSAttributedString.Key : Any] = preeditAttrs attrsDeleteStroke[.attachment] = attmDeleteStroke - attrsDeleteStroke[.verticalGlyphForm] = NSNumber(value: false) + attrsDeleteStroke[.verticalGlyphForm] = 0 symbolDeleteStroke = NSAttributedString(string: attmCharacter, attributes: attrsDeleteStroke) if isTabular { let attmCompress = NSTextAttachment() attmCompress.image = NSImage(named: "Symbols/rectangle.compress.vertical") - var attrsCompress: [NSAttributedString.Key: Any] = pagingAttrs + var attrsCompress: [NSAttributedString.Key : Any] = pagingAttrs attrsCompress[.attachment] = attmCompress symbolCompress = NSAttributedString(string: attmCharacter, attributes: attrsCompress) let attmExpand = NSTextAttachment() attmExpand.image = NSImage(named: "Symbols/rectangle.expand.vertical") - var attrsExpand: [NSAttributedString.Key: Any] = pagingAttrs + var attrsExpand: [NSAttributedString.Key : Any] = pagingAttrs attrsExpand[.attachment] = attmExpand symbolExpand = NSAttributedString(string: attmCharacter, attributes: attrsExpand) let attmLock = NSTextAttachment() attmLock.image = NSImage(named: "Symbols/lock\(isVertical ? ".vertical" : "").fill") - var attrsLock: [NSAttributedString.Key: Any] = pagingAttrs + var attrsLock: [NSAttributedString.Key : Any] = pagingAttrs attrsLock[.attachment] = attmLock symbolLock = NSAttributedString(string: attmCharacter, attributes: attrsLock) } else { @@ -702,22 +620,22 @@ final class SquirrelTheme: NSObject { if showPaging { let attmBackFill = NSTextAttachment() attmBackFill.image = NSImage(named: "Symbols/chevron.\(isLinear ? "up" : "left").circle.fill") - var attrsBackFill: [NSAttributedString.Key: Any] = pagingAttrs + var attrsBackFill: [NSAttributedString.Key : Any] = pagingAttrs attrsBackFill[.attachment] = attmBackFill symbolBackFill = NSAttributedString(string: attmCharacter, attributes: attrsBackFill) let attmBackStroke = NSTextAttachment() attmBackStroke.image = NSImage(named: "Symbols/chevron.\(isLinear ? "up" : "left").circle") - var attrsBackStroke: [NSAttributedString.Key: Any] = pagingAttrs + var attrsBackStroke: [NSAttributedString.Key : Any] = pagingAttrs attrsBackStroke[.attachment] = attmBackStroke symbolBackStroke = NSAttributedString(string: attmCharacter, attributes: attrsBackStroke) let attmForwardFill = NSTextAttachment() attmForwardFill.image = NSImage(named: "Symbols/chevron.\(isLinear ? "down" : "right").circle.fill") - var attrsForwardFill: [NSAttributedString.Key: Any] = pagingAttrs + var attrsForwardFill: [NSAttributedString.Key : Any] = pagingAttrs attrsForwardFill[.attachment] = attmForwardFill symbolForwardFill = NSAttributedString(string: attmCharacter, attributes: attrsForwardFill) let attmForwardStroke = NSTextAttachment() attmForwardStroke.image = NSImage(named: "Symbols/chevron.\(isLinear ? "down" : "right").circle") - var attrsForwardStroke: [NSAttributedString.Key: Any] = pagingAttrs + var attrsForwardStroke: [NSAttributedString.Key : Any] = pagingAttrs attrsForwardStroke[.attachment] = attmForwardStroke symbolForwardStroke = NSAttributedString(string: attmCharacter, attributes: attrsForwardStroke) } else { @@ -728,205 +646,136 @@ final class SquirrelTheme: NSObject { } } - func updateLabelsWithConfig(_ config: SquirrelConfig, directUpdate update: Bool) { + func updateLabels(withConfig config: SquirrelConfig, directUpdate: Bool) { let menuSize: Int = config.nullableInt(forOption: "menu/page_size") ?? 5 - var labels: [String] = [] - var selectKeys: String? = config.string(forOption: "menu/alternative_select_keys") - let selectLabels: [String] = config.list(forOption: "menu/alternative_select_labels") ?? [] - if !selectLabels.isEmpty { - for i in 0 ..< menuSize { - labels.append(selectLabels[i]) - } - } - if selectKeys != nil { - if selectLabels.isEmpty { - for i in 0 ..< menuSize { - let keyCap = String(selectKeys![selectKeys!.index(selectKeys!.startIndex, offsetBy: i)]) - labels.append(keyCap.uppercased().applyingTransform(.fullwidthToHalfwidth, reverse: true)!) - } - } - } else { - selectKeys = String("1234567890".prefix(menuSize)) - if selectLabels.isEmpty { - for i in 0 ..< menuSize { - let numeral = String(selectKeys![selectKeys!.index(selectKeys!.startIndex, offsetBy: i)]) - labels.append(numeral.applyingTransform(.fullwidthToHalfwidth, reverse: true)!) - } - } - } - updateSelectKeys(selectKeys!, labels: labels, directUpdate: update) + let selectKeys: String = String((config.string(forOption: "menu/alternative_select_keys") ?? "1234567890").prefix(menuSize)) + let selectLabels: [String]? = config.list(forOption: "menu/alternative_select_labels") + let labels: [String] = selectLabels == nil ? selectKeys.map { $0.uppercased().applyingTransform(.fullwidthToHalfwidth, reverse: true)! } : Array(selectLabels!.prefix(menuSize)) + updateSelectKeys(selectKeys, labels: labels, directUpdate: directUpdate) } - func updateSelectKeys(_ selectKeys: String, labels: [String], directUpdate update: Bool) { + private func updateSelectKeys(_ selectKeys: String, labels rawLabels: [String], directUpdate: Bool) { + if self.selectKeys == selectKeys && self.rawLabels == rawLabels { return } self.selectKeys = selectKeys - self.labels = labels - pageSize = labels.count - if update { - updateCandidateTemplates(forAttributesOnly: true) - } + self.rawLabels = rawLabels + pageSize = rawLabels.count + labels = [] + if directUpdate { updateCandidateTemplates() } } - func updateCandidateFormat(_ candidateFormat: String) { - let attrsOnly: Bool = candidateFormat == self.candidateFormat - if !attrsOnly { - self.candidateFormat = candidateFormat + private func updateCandidateFormat(_ rawCandidateFormat: String) { + if self.rawCandidateFormat != rawCandidateFormat { + self.rawCandidateFormat = rawCandidateFormat + candidateFormat = "" } - updateCandidateTemplates(forAttributesOnly: attrsOnly) - updateSeperatorAndSymbolAttrs() + updateCandidateTemplates() } - private func updateCandidateTemplates(forAttributesOnly attrsOnly: Bool) { - var candidateTemplate: NSMutableAttributedString - if !attrsOnly { + private func updateCandidateTemplates() { + if candidateFormat.isEmpty || labels.isEmpty { + candidateFormat = rawCandidateFormat // validate candidate format: must have enumerator '%c' before candidate '%@' - var candidateFormat: String = self.candidateFormat var textRange: Range? = candidateFormat.range(of: "%@", options: [.literal]) if textRange == nil { - candidateFormat += "%@" + candidateFormat.append("%@") } var labelRange: Range? = candidateFormat.range(of: "%c", options: [.literal]) if labelRange == nil { - candidateFormat = "%c" + candidateFormat + candidateFormat.insert(contentsOf: "%c", at: candidateFormat.startIndex) labelRange = candidateFormat.range(of: "%c", options: [.literal]) } textRange = candidateFormat.range(of: "%@", options: [.literal]) if labelRange!.lowerBound > textRange!.lowerBound { - candidateFormat = kDefaultCandidateFormat + candidateFormat = Self.kDefaultCandidateFormat + } + textRange = candidateFormat.range(of: "(\\x{FFF9})?%@", options: [.regularExpression]) + let commentRange: Range = textRange!.upperBound ..< candidateFormat.endIndex + if commentRange.isEmpty || !candidateFormat[commentRange].contains("%s") { + candidateFormat.insert(contentsOf: "%s", at: textRange!.upperBound) + } + if !isLinear { + candidateFormat.insert("\t", at: textRange!.lowerBound) } - var labels: [String] = self.labels - var enumRange: Range? - let labelCharacters: CharacterSet = CharacterSet(charactersIn: labels.joined()) + + labels = rawLabels + let labelCharacters: CharacterSet = CharacterSet(charactersIn: rawLabels.joined()) if CharacterSet.fullWidthDigits.isSuperset(of: labelCharacters) { // 01...9 if let range = candidateFormat.range(of: "%c\u{20E3}", options: [.literal]) { // 1︎⃣...9︎⃣0︎⃣ - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value - 0xFF10 + 0x0030 - labels[i] = String(Character(UnicodeScalar(wchar)!)) + "\u{FE0E}\u{20E3}" - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF10 + 0x0030)!) + "\u{FE0E}\u{20E3}" } } else if let range = candidateFormat.range(of: "%c\u{20DD}", options: [.literal]) { // ①...⑨⓪ - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value == 0xFF10 ? 0x24EA : labels[i].unicodeScalars.first!.value - 0xFF11 + 0x2460 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value == 0xFF10 ? 0x24EA : $0.unicodeScalars.first!.value - 0xFF11 + 0x2460)!) } } else if let range = candidateFormat.range(of: "(%c)", options: [.literal]) { // ⑴...⑼⑽ - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value == 0xFF10 ? 0x247D : labels[i].unicodeScalars.first!.value - 0xFF11 + 0x2474 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value == 0xFF10 ? 0x247D : $0.unicodeScalars.first!.value - 0xFF11 + 0x2474)!) } } else if let range = candidateFormat.range(of: "%c.", options: [.literal]) { // ⒈...⒐🄀 - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value == 0xFF10 ? 0x1F100 : labels[i].unicodeScalars.first!.value - 0xFF11 + 0x2488 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value == 0xFF10 ? 0x1F100 : $0.unicodeScalars.first!.value - 0xFF11 + 0x2488)!) } } else if let range = candidateFormat.range(of: "%c,", options: [.literal]) { // 🄂...🄊🄁 - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value - 0xFF10 + 0x1F101 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF10 + 0x1F101)!) } } } else if CharacterSet.fullWidthLatinCapitals.isSuperset(of: labelCharacters) { if let range = candidateFormat.range(of: "%c\u{20DD}", options: [.literal]) { // Ⓐ...Ⓩ - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value - 0xFF21 + 0x24B6 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF21 + 0x24B6)!) } } else if let range = candidateFormat.range(of: "(%c)", options: [.literal]) { // 🄐...🄩 - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value - 0xFF21 + 0x1F110 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF21 + 0x1F110)!) } } else if let range = candidateFormat.range(of: "%c\u{20DE}", options: [.literal]) { // 🄰...🅉 - enumRange = range - for i in 0 ..< pageSize { - let wchar: UInt32 = labels[i].unicodeScalars.first!.value - 0xFF21 + 0x1F130 - labels[i] = String(Character(UnicodeScalar(wchar)!)) - } + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF21 + 0x1F130)!) } } } - if enumRange != nil { - candidateFormat = candidateFormat.replacingCharacters(in: enumRange!, with: "%c") - self.labels = labels - } - candidateTemplate = NSMutableAttributedString(string: candidateFormat) - } else { - candidateTemplate = self.candidateTemplate.mutableCopy() as! NSMutableAttributedString } + // make sure label font can render all possible enumerators let labelFont = labelAttrs[.font] as! NSFont - let labelString = labels.joined() as NSString - let substituteFont: NSFont = CTFontCreateForStringWithLanguage(labelFont, labelString, CFRange(location: 0, length: labelString.length), scriptVariant as CFString) - if substituteFont != labelFont { - let monoDigitAttrs: [NSFontDescriptor.AttributeName: [[NSFontDescriptor.FeatureKey: NSNumber]]] = - [.featureSettings: [[.typeIdentifier: NSNumber(value: kNumberSpacingType), - .selectorIdentifier: NSNumber(value: kMonospacedNumbersSelector)], - [.typeIdentifier: NSNumber(value: kTextSpacingType), - .selectorIdentifier: NSNumber(value: kHalfWidthTextSelector)]]] - let subFontDescriptor = substituteFont.fontDescriptor.addingAttributes(monoDigitAttrs) - labelAttrs[.font] = NSFont(descriptor: subFontDescriptor, size: labelFont.pointSize) - } - - var textRange: NSRange = candidateTemplate.mutableString.range(of: "%@", options: [.literal]) + let labelString = labels.joined() as CFString + let substituteFont: NSFont = CTFont(font: labelFont, string: labelString, range: CFRange(location: 0, length: labelString.length)) + if substituteFont.isNotEqual(to: labelFont) { + labelAttrs[.font] = CTFont(font: substituteFont, string: labelString, range: CFRange(location: 0, length: labelString.length)) + } + + // parse markdown formats + let candidateTemplate = NSMutableAttributedString(string: candidateFormat) + var textRange: NSRange = candidateTemplate.mutableString.range(of: "(\\x{FFF9})?%@", options: [.regularExpression]) var labelRange = NSRange(location: 0, length: textRange.location) var commentRange = NSRange(location: textRange.upperBound, length: candidateTemplate.length - textRange.upperBound) - // parse markdown formats candidateTemplate.setAttributes(labelAttrs, range: labelRange) candidateTemplate.setAttributes(textAttrs, range: textRange) - if commentRange.length > 0 { - candidateTemplate.setAttributes(commentAttrs, range: commentRange) - } - - // parse markdown formats - if !attrsOnly { - candidateTemplate.formatMarkDown() - // add placeholder for comment `%s` - textRange = candidateTemplate.mutableString.range(of: "%@", options: [.literal]) - labelRange = NSRange(location: 0, length: textRange.location) - commentRange = NSRange(location: textRange.upperBound, length: candidateTemplate.length - textRange.upperBound) - if commentRange.length > 0 { - candidateTemplate.replaceCharacters(in: commentRange, with: kTipSpecifier + candidateTemplate.mutableString.substring(with: commentRange)) - } else { - candidateTemplate.append(NSAttributedString(string: kTipSpecifier, attributes: commentAttrs)) - } - commentRange.length += kTipSpecifier.utf16.count - - if !isLinear { - candidateTemplate.replaceCharacters(in: NSRange(location: textRange.location, length: 0), with: "\t") - labelRange.length += 1 - textRange.location += 1 - commentRange.location += 1 - } - } + candidateTemplate.setAttributes(commentAttrs, range: commentRange) + candidateTemplate.formatMarkDown() + textRange = candidateTemplate.mutableString.range(of: "(\\x{FFF9})?%@", options: [.regularExpression]) + labelRange = NSRange(location: 0, length: textRange.location) + commentRange = NSRange(location: textRange.upperBound, length: candidateTemplate.length - textRange.upperBound) // for stacked layout, calculate head indent let candidateParagraphStyle = self.candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle if !isLinear { - var indent: Double = 0.0 - let labelFormat = candidateTemplate.attributedSubstring(from: NSRange(location: 0, length: labelRange.length - 1)) + let enumRange: NSRange = candidateTemplate.mutableString.range(of: "%c", options: [.literal]) + let textStorage = NSTextStorage() + let textView = SquirrelTextView(contentBlock: .stackedCandidates, textStorage: textStorage) + textView.setLayoutOrientation(isVertical ? .vertical : .horizontal) for label in labels { - let enumString = labelFormat.mutableCopy() as! NSMutableAttributedString - let enumRange = enumString.mutableString.range(of: "%c", options: [.literal]) - enumString.mutableString.replaceCharacters(in: enumRange, with: label) - enumString.addAttribute(.verticalGlyphForm, value: NSNumber(value: isVertical), range: NSRange(location: enumRange.location, length: label.utf16.count)) - indent = max(indent, enumString.size().width) + let labelString = candidateTemplate.attributedSubstring(from: NSRange(location: 0, length: labelRange.length - 1)).mutableCopy() as! NSMutableAttributedString + labelString.replaceCharacters(in: enumRange, with: label) + textStorage.append(labelString) + textStorage.append(NSAttributedString(string: "\n")) } - indent = floor(indent) + 1.0 + let indent: Double = textView.layoutText().maxX.rounded(.down) + 1.0 candidateParagraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: indent)] candidateParagraphStyle.headIndent = indent self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle truncatedParagraphStyle = nil } else { candidateParagraphStyle.tabStops = [] - candidateParagraphStyle.headIndent = 0.0 + candidateParagraphStyle.headIndent = .zero self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle let truncatedParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle truncatedParagraphStyle.lineBreakMode = .byTruncatingMiddle - truncatedParagraphStyle.tighteningFactorForTruncation = 0.0 + truncatedParagraphStyle.tighteningFactorForTruncation = .zero self.truncatedParagraphStyle = truncatedParagraphStyle.copy() as? NSParagraphStyle } @@ -961,7 +810,9 @@ final class SquirrelTheme: NSObject { } } - func updateThemeWithConfig(_ config: SquirrelConfig, styleOptions: Set, scriptVariant: String) { + static private let monoDigitFeatures: [[NSFontDescriptor.FeatureKey : Int]] = [[.typeIdentifier : kNumberSpacingType, .selectorIdentifier : kMonospacedNumbersSelector], [.typeIdentifier : kTextSpacingType, .selectorIdentifier : kHalfWidthTextSelector], [.typeIdentifier : kCJKRomanSpacingType, .selectorIdentifier : kHalfWidthCJKRomanSelector]] + + func updateTheme(withConfig config: SquirrelConfig, styleOptions: Set, scriptVariant: String) { /*** INTERFACE ***/ var isLinear: Bool = false var isTabular: Bool = false @@ -1012,19 +863,11 @@ final class SquirrelTheme: NSObject { var colorScheme: String? if style == .dark { - for option in styleOptions { - if let value = config.string(forOption: "style/\(option)/color_scheme_dark") { - colorScheme = value; break - } - } + _ = styleOptions.first(where: { if let value = config.string(forOption: "style/\($0)/color_scheme_dark") { colorScheme = value; return true } else { return false } }) colorScheme ?= config.string(forOption: "style/color_scheme_dark") } if colorScheme == nil { - for option in styleOptions { - if let value = config.string(forOption: "style/\(option)/color_scheme") { - colorScheme = value; break - } - } + _ = styleOptions.first(where: { if let value = config.string(forOption: "style/\($0)/color_scheme") { colorScheme = value; return true } else { return false } }) colorScheme ?= config.string(forOption: "style/color_scheme") } let isNative: Bool = (colorScheme == nil) || (colorScheme! == "native") @@ -1087,37 +930,33 @@ final class SquirrelTheme: NSObject { } /*** TYPOGRAPHY refinement ***/ - fontSize ?= kDefaultFontSize + fontSize ?= Self.kDefaultFontSize labelFontSize ?= fontSize commentFontSize ?= fontSize - let monoDigitAttrs: [NSFontDescriptor.AttributeName: [[NSFontDescriptor.FeatureKey: NSNumber]]] = - [.featureSettings: [[.typeIdentifier: NSNumber(value: kNumberSpacingType), - .selectorIdentifier: NSNumber(value: kMonospacedNumbersSelector)], - [.typeIdentifier: NSNumber(value: kTextSpacingType), - .selectorIdentifier: NSNumber(value: kHalfWidthTextSelector)]]] - - let fontDescriptor: NSFontDescriptor = .create(fullname: fontName) ?? .create(fullname: NSFont.userFont(ofSize: 0)?.fontName)! + let fontDescriptor: NSFontDescriptor = .create(fullname: fontName) ?? .create(fullname: NSFont.userFont(ofSize: .zero)?.fontName)! let font = NSFont(descriptor: fontDescriptor, size: fontSize!)! - let labelFontDescriptor: NSFontDescriptor? = (.create(fullname: labelFontName) ?? fontDescriptor)!.addingAttributes(monoDigitAttrs) - let labelFont: NSFont = labelFontDescriptor != nil ? NSFont(descriptor: labelFontDescriptor!, size: labelFontSize!)! : .monospacedDigitSystemFont(ofSize: labelFontSize!, weight: .regular) - let commentFontDescriptor: NSFontDescriptor? = .create(fullname: commentFontName) - let commentFont = NSFont(descriptor: commentFontDescriptor ?? fontDescriptor, size: commentFontSize!)! - let pagingFont: NSFont = .monospacedDigitSystemFont(ofSize: labelFontSize!, weight: .regular) + let labelFont = NSFont(descriptor: (.create(fullname: labelFontName) ?? fontDescriptor).addingAttributes([.featureSettings : Self.monoDigitFeatures]), size: labelFontSize!)! + let commentFont = NSFont(descriptor: .create(fullname: commentFontName) ?? fontDescriptor, size: commentFontSize!)! + let systemFont: NSFont = .systemFont(ofSize: labelFontSize!) + let pagingFont = NSFont(descriptor: labelFont.fontDescriptor.addingAttributes([.cascadeList : [systemFont.fontDescriptor]]), size: labelFontSize!)! let fontHeight: Double = font.lineHeight(asVertical: isVertical) let labelFontHeight: Double = labelFont.lineHeight(asVertical: isVertical) let commentFontHeight: Double = commentFont.lineHeight(asVertical: isVertical) + let pagingFontHeight: Double = pagingFont.lineHeight(asVertical: false) let lineHeight: Double = max(fontHeight, labelFontHeight, commentFontHeight) - let fullWidth: Double = ceil(NSAttributedString(string: kFullWidthSpace, attributes: [.font: commentFont]).size().width) - preeditSpacing ?= 0 - lineSpacing ?= 0 + let glyphs: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) + CTFontGetGlyphsForCharacters(commentFont, [UniChar(3000)], glyphs, 1) + let fullWidth: Double = commentFont.advancement(forCGGlyph: glyphs[0]).width.rounded(.up) + preeditSpacing ?= .zero + lineSpacing ?= .zero let candidateParagraphStyle = self.candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle candidateParagraphStyle.minimumLineHeight = lineHeight candidateParagraphStyle.maximumLineHeight = lineHeight - candidateParagraphStyle.paragraphSpacingBefore = isLinear ? 0.0 : ceil(lineSpacing! * 0.5) - candidateParagraphStyle.paragraphSpacing = isLinear ? 0.0 : floor(lineSpacing! * 0.5) - candidateParagraphStyle.lineSpacing = isLinear ? lineSpacing! : 0.0 + candidateParagraphStyle.paragraphSpacingBefore = isLinear ? .zero : (lineSpacing! * 0.5).rounded(.down) + candidateParagraphStyle.paragraphSpacing = isLinear ? .zero : (lineSpacing! * 0.5).rounded(.up) + candidateParagraphStyle.lineSpacing = isLinear ? lineSpacing! : .zero candidateParagraphStyle.tabStops = [] candidateParagraphStyle.defaultTabInterval = fullWidth * 2 self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle @@ -1130,8 +969,8 @@ final class SquirrelTheme: NSObject { self.preeditParagraphStyle = preeditParagraphStyle.copy() as! NSParagraphStyle let pagingParagraphStyle = self.pagingParagraphStyle.mutableCopy() as! NSMutableParagraphStyle - pagingParagraphStyle.minimumLineHeight = ceil(pagingFont.ascender - pagingFont.descender) - pagingParagraphStyle.maximumLineHeight = ceil(pagingFont.ascender - pagingFont.descender) + pagingParagraphStyle.minimumLineHeight = pagingFontHeight + pagingParagraphStyle.maximumLineHeight = pagingFontHeight pagingParagraphStyle.tabStops = [] self.pagingParagraphStyle = pagingParagraphStyle.copy() as! NSParagraphStyle @@ -1146,9 +985,12 @@ final class SquirrelTheme: NSObject { preeditAttrs[.font] = font pagingAttrs[.font] = pagingFont statusAttrs[.font] = commentFont - labelAttrs[.strokeWidth] = NSNumber(value: -2.0 / labelFontSize!) + labelAttrs[.strokeWidth] = -2.0 / labelFontSize! + textAttrs[.kern] = isVertical ? 0.1 * fontSize! : .zero + labelAttrs[.kern] = isVertical ? 0.1 * labelFontSize! : .zero + commentAttrs[.kern] = isVertical ? 0.1 * commentFontSize! : .zero - var zhFont: NSFont = CTFontCreateUIFontForLanguage(.system, fontSize!, scriptVariant as CFString)! + var zhFont: NSFont = CTFont(.system, size: fontSize!, language: scriptVariant as CFString) var zhCommentFont = NSFont(descriptor: zhFont.fontDescriptor, size: commentFontSize!)! let maxFontSize: Double = max(fontSize!, commentFontSize!, labelFontSize!) var refFont = NSFont(descriptor: zhFont.fontDescriptor, size: maxFontSize)! @@ -1157,66 +999,58 @@ final class SquirrelTheme: NSObject { zhCommentFont = zhCommentFont.vertical refFont = refFont.vertical } - let baselineRefInfo: NSDictionary = - [kCTBaselineReferenceFont: refFont, - kCTBaselineClassIdeographicCentered: NSNumber(value: isVertical ? 0.0 : (refFont.ascender + refFont.descender) * 0.5), - kCTBaselineClassRoman: NSNumber(value: isVertical ? -(refFont.ascender + refFont.descender) * 0.5 : 0.0), - kCTBaselineClassIdeographicLow: NSNumber(value: isVertical ? (refFont.descender - refFont.ascender) * 0.5 : refFont.descender)] + let baselineRefInfo: NSDictionary = [kCTBaselineReferenceFont : refFont, kCTBaselineClassIdeographicCentered : isVertical ? .zero : (refFont.ascender + refFont.descender) * 0.5, kCTBaselineClassRoman : isVertical ? -(refFont.ascender + refFont.descender) * 0.5 : .zero, kCTBaselineClassIdeographicLow : isVertical ? (refFont.descender - refFont.ascender) * 0.5 : refFont.descender] textAttrs[.baselineReferenceInfo] = baselineRefInfo labelAttrs[.baselineReferenceInfo] = baselineRefInfo commentAttrs[.baselineReferenceInfo] = baselineRefInfo - preeditAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont: zhFont] as NSDictionary - pagingAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont: pagingFont] as NSDictionary - statusAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont: zhCommentFont] as NSDictionary + preeditAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : zhFont] + pagingAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : systemFont] + statusAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : zhCommentFont] textAttrs[.baselineClass] = isVertical ? kCTBaselineClassIdeographicCentered : kCTBaselineClassRoman labelAttrs[.baselineClass] = kCTBaselineClassIdeographicCentered commentAttrs[.baselineClass] = isVertical ? kCTBaselineClassIdeographicCentered : kCTBaselineClassRoman preeditAttrs[.baselineClass] = isVertical ? kCTBaselineClassIdeographicCentered : kCTBaselineClassRoman statusAttrs[.baselineClass] = isVertical ? kCTBaselineClassIdeographicCentered : kCTBaselineClassRoman - pagingAttrs[.baselineClass] = kCTBaselineClassIdeographicCentered - - textAttrs[.language] = scriptVariant as NSString - labelAttrs[.language] = scriptVariant as NSString - commentAttrs[.language] = scriptVariant as NSString - preeditAttrs[.language] = scriptVariant as NSString - statusAttrs[.language] = scriptVariant as NSString - - baseOffset ?= 0 - textAttrs[.baselineOffset] = NSNumber(value: baseOffset!) - labelAttrs[.baselineOffset] = NSNumber(value: baseOffset!) - commentAttrs[.baselineOffset] = NSNumber(value: baseOffset!) - preeditAttrs[.baselineOffset] = NSNumber(value: baseOffset!) - pagingAttrs[.baselineOffset] = NSNumber(value: baseOffset!) - statusAttrs[.baselineOffset] = NSNumber(value: baseOffset!) + pagingAttrs[.baselineClass] = kCTBaselineClassRoman + + textAttrs[.language] = scriptVariant + labelAttrs[.language] = scriptVariant + commentAttrs[.language] = scriptVariant + preeditAttrs[.language] = scriptVariant + statusAttrs[.language] = scriptVariant + + baseOffset ?= .zero + textAttrs[.baselineOffset] = baseOffset + labelAttrs[.baselineOffset] = baseOffset + commentAttrs[.baselineOffset] = baseOffset + preeditAttrs[.baselineOffset] = baseOffset + pagingAttrs[.baselineOffset] = baseOffset + statusAttrs[.baselineOffset] = baseOffset preeditAttrs[.paragraphStyle] = preeditParagraphStyle pagingAttrs[.paragraphStyle] = pagingParagraphStyle statusAttrs[.paragraphStyle] = statusParagraphStyle - - labelAttrs[.verticalGlyphForm] = NSNumber(value: isVertical) - pagingAttrs[.verticalGlyphForm] = NSNumber(value: false) + pagingAttrs[.verticalGlyphForm] = 0 // CHROMATICS refinement - translucency ?= 0.0 - if #available(macOS 10.14, *) { - if translucency! > 0.001 && !isNative && backColor != nil && (style == .dark ? backColor!.lStarComponent! > 0.6 : backColor!.lStarComponent! < 0.4) { - backColor = backColor?.invertLuminance(toExtent: .standard) - borderColor = borderColor?.invertLuminance(toExtent: .standard) - preeditBackColor = preeditBackColor?.invertLuminance(toExtent: .standard) - preeditForeColor = preeditForeColor?.invertLuminance(toExtent: .standard) - candidateBackColor = candidateBackColor?.invertLuminance(toExtent: .standard) - textForeColor = textForeColor?.invertLuminance(toExtent: .standard) - commentForeColor = commentForeColor?.invertLuminance(toExtent: .standard) - labelForeColor = labelForeColor?.invertLuminance(toExtent: .standard) - hilitedPreeditBackColor = hilitedPreeditBackColor?.invertLuminance(toExtent: .moderate) - hilitedPreeditForeColor = hilitedPreeditForeColor?.invertLuminance(toExtent: .augmented) - hilitedCandidateBackColor = hilitedCandidateBackColor?.invertLuminance(toExtent: .moderate) - hilitedTextForeColor = hilitedTextForeColor?.invertLuminance(toExtent: .augmented) - hilitedCommentForeColor = hilitedCommentForeColor?.invertLuminance(toExtent: .augmented) - hilitedLabelForeColor = hilitedLabelForeColor?.invertLuminance(toExtent: .augmented) - } + translucency ?= .zero + if #available(macOS 10.14, *), translucency!.isNormal && !isNative && backColor != nil && (style == .dark ? backColor!.lStarComponent! > 0.6 : backColor!.lStarComponent! < 0.4) { + backColor = backColor?.invertLuminance(toExtent: .standard) + borderColor = borderColor?.invertLuminance(toExtent: .standard) + preeditBackColor = preeditBackColor?.invertLuminance(toExtent: .standard) + preeditForeColor = preeditForeColor?.invertLuminance(toExtent: .standard) + candidateBackColor = candidateBackColor?.invertLuminance(toExtent: .standard) + textForeColor = textForeColor?.invertLuminance(toExtent: .standard) + commentForeColor = commentForeColor?.invertLuminance(toExtent: .standard) + labelForeColor = labelForeColor?.invertLuminance(toExtent: .standard) + hilitedPreeditBackColor = hilitedPreeditBackColor?.invertLuminance(toExtent: .moderate) + hilitedPreeditForeColor = hilitedPreeditForeColor?.invertLuminance(toExtent: .augmented) + hilitedCandidateBackColor = hilitedCandidateBackColor?.invertLuminance(toExtent: .moderate) + hilitedTextForeColor = hilitedTextForeColor?.invertLuminance(toExtent: .augmented) + hilitedCommentForeColor = hilitedCommentForeColor?.invertLuminance(toExtent: .augmented) + hilitedLabelForeColor = hilitedLabelForeColor?.invertLuminance(toExtent: .augmented) } self.backImage = backImage @@ -1243,16 +1077,16 @@ final class SquirrelTheme: NSObject { pagingAttrs[.foregroundColor] = self.preeditForeColor statusAttrs[.foregroundColor] = self.commentForeColor - borderInsets = isVertical ? .init(width: borderHeight ?? 0, height: borderWidth ?? 0) : .init(width: borderWidth ?? 0, height: borderHeight ?? 0) - self.cornerRadius = min(cornerRadius ?? 0, lineHeight * 0.5) - self.hilitedCornerRadius = min(hilitedCornerRadius ?? 0, lineHeight * 0.5) + borderInsets = isVertical ? NSSize(width: borderHeight ?? .zero, height: borderWidth ?? .zero) : NSSize(width: borderWidth ?? .zero, height: borderHeight ?? .zero) + self.cornerRadius = min(cornerRadius ?? .zero, lineHeight * 0.5) + self.hilitedCornerRadius = min(hilitedCornerRadius ?? .zero, lineHeight * 0.5) self.fullWidth = fullWidth self.lineSpacing = lineSpacing! self.preeditSpacing = preeditSpacing! self.opacity = opacity ?? 1.0 - self.lineLength = lineLength != nil && lineLength! > 0.1 ? max(ceil(lineLength!), fullWidth * 5) : 0 - self.shadowSize = shadowSize ?? 0.0 - self.translucency = Float(translucency ?? 0.0) + self.lineLength = lineLength != nil && lineLength!.isNormal ? max(lineLength!.rounded(.up), fullWidth * 5) : .zero + self.shadowSize = shadowSize ?? .zero + self.translucency = Float(translucency ?? .zero) self.stackColors = stackColors ?? false self.showPaging = showPaging ?? false self.rememberSize = rememberSize ?? false @@ -1261,43 +1095,43 @@ final class SquirrelTheme: NSObject { self.isVertical = isVertical self.inlinePreedit = inlinePreedit ?? false self.inlineCandidate = inlineCandidate ?? false - self.scriptVariant = scriptVariant - updateCandidateFormat(candidateFormat ?? kDefaultCandidateFormat) + updateStatusMessageType(statusMessageType) + updateCandidateFormat(candidateFormat ?? Self.kDefaultCandidateFormat) + updateSeperatorAndSymbolAttrs() } func updateAnnotationHeight(_ height: Double) { - if height > 0.1 && lineSpacing < height * 2 { - lineSpacing = height * 2 - let candidateParagraphStyle = self.candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle - if isLinear { - candidateParagraphStyle.lineSpacing = height * 2 - let truncatedParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle - truncatedParagraphStyle.lineBreakMode = .byTruncatingMiddle - truncatedParagraphStyle.tighteningFactorForTruncation = 0.0 - self.truncatedParagraphStyle = truncatedParagraphStyle.copy() as? NSParagraphStyle - } else { - candidateParagraphStyle.paragraphSpacingBefore = height - candidateParagraphStyle.paragraphSpacing = height - } - self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle + guard height.isNormal && lineSpacing < height * 2 else { return } + lineSpacing = height * 2 + let candidateParagraphStyle = self.candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + if isLinear { + candidateParagraphStyle.lineSpacing = height * 2 + let truncatedParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + truncatedParagraphStyle.lineBreakMode = .byTruncatingMiddle + truncatedParagraphStyle.tighteningFactorForTruncation = .zero + self.truncatedParagraphStyle = truncatedParagraphStyle.copy() as? NSParagraphStyle + } else { + candidateParagraphStyle.paragraphSpacingBefore = height + candidateParagraphStyle.paragraphSpacing = height + } + self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle - textAttrs[.paragraphStyle] = candidateParagraphStyle - commentAttrs[.paragraphStyle] = candidateParagraphStyle - labelAttrs[.paragraphStyle] = candidateParagraphStyle + textAttrs[.paragraphStyle] = candidateParagraphStyle + commentAttrs[.paragraphStyle] = candidateParagraphStyle + labelAttrs[.paragraphStyle] = candidateParagraphStyle - let candidateTemplate = self.candidateTemplate.mutableCopy() as! NSMutableAttributedString - candidateTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateTemplate.length)) - self.candidateTemplate = candidateTemplate.copy() as! NSAttributedString - let candidateHilitedTemplate = self.candidateHilitedTemplate.mutableCopy() as! NSMutableAttributedString - candidateHilitedTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateHilitedTemplate.length)) - self.candidateHilitedTemplate = candidateHilitedTemplate.copy() as! NSAttributedString - if isTabular { - let candidateDimmedTemplate = self.candidateDimmedTemplate!.mutableCopy() as! NSMutableAttributedString - candidateDimmedTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateDimmedTemplate.length)) - self.candidateDimmedTemplate = candidateDimmedTemplate.copy() as? NSAttributedString - } + let candidateTemplate = self.candidateTemplate.mutableCopy() as! NSMutableAttributedString + candidateTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateTemplate.length)) + self.candidateTemplate = candidateTemplate.copy() as! NSAttributedString + let candidateHilitedTemplate = self.candidateHilitedTemplate.mutableCopy() as! NSMutableAttributedString + candidateHilitedTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateHilitedTemplate.length)) + self.candidateHilitedTemplate = candidateHilitedTemplate.copy() as! NSAttributedString + if isTabular { + let candidateDimmedTemplate = self.candidateDimmedTemplate!.mutableCopy() as! NSMutableAttributedString + candidateDimmedTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateDimmedTemplate.length)) + self.candidateDimmedTemplate = candidateDimmedTemplate.copy() as? NSAttributedString } } @@ -1308,7 +1142,7 @@ final class SquirrelTheme: NSObject { let textFontSize: Double = (textAttrs[.font] as! NSFont).pointSize let commentFontSize: Double = (commentAttrs[.font] as! NSFont).pointSize let labelFontSize: Double = (labelAttrs[.font] as! NSFont).pointSize - var zhFont: NSFont = CTFontCreateUIFontForLanguage(.system, textFontSize, scriptVariant as CFString)! + var zhFont: NSFont = CTFont(.system, size: textFontSize, language: scriptVariant as CFString) var zhCommentFont = NSFont(descriptor: zhFont.fontDescriptor, size: commentFontSize)! let maxFontSize: Double = max(textFontSize, commentFontSize, labelFontSize) var refFont = NSFont(descriptor: zhFont.fontDescriptor, size: maxFontSize)! @@ -1317,50 +1151,43 @@ final class SquirrelTheme: NSObject { zhCommentFont = zhCommentFont.vertical refFont = refFont.vertical } - let baselineRefInfo: NSDictionary = - [kCTBaselineReferenceFont: refFont, - kCTBaselineClassIdeographicCentered: NSNumber(value: isVertical ? 0.0 : (refFont.ascender + refFont.descender) * 0.5), - kCTBaselineClassRoman: NSNumber(value: isVertical ? -(refFont.ascender + refFont.descender) * 0.5 : 0.0), - kCTBaselineClassIdeographicLow: NSNumber(value: isVertical ? (refFont.descender - refFont.ascender) * 0.5 : refFont.descender)] + let baselineRefInfo: NSDictionary = [kCTBaselineReferenceFont : refFont, kCTBaselineClassIdeographicCentered : isVertical ? .zero : (refFont.ascender + refFont.descender) * 0.5, kCTBaselineClassRoman : isVertical ? -(refFont.ascender + refFont.descender) * 0.5 : .zero, kCTBaselineClassIdeographicLow : isVertical ? (refFont.descender - refFont.ascender) * 0.5 : refFont.descender] textAttrs[.baselineReferenceInfo] = baselineRefInfo labelAttrs[.baselineReferenceInfo] = baselineRefInfo commentAttrs[.baselineReferenceInfo] = baselineRefInfo - preeditAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont: zhFont] as NSDictionary - statusAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont: zhCommentFont] as NSDictionary + preeditAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : zhFont] + statusAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : zhCommentFont] - textAttrs[.language] = scriptVariant as NSString - labelAttrs[.language] = scriptVariant as NSString - commentAttrs[.language] = scriptVariant as NSString - preeditAttrs[.language] = scriptVariant as NSString - statusAttrs[.language] = scriptVariant as NSString + textAttrs[.language] = scriptVariant + labelAttrs[.language] = scriptVariant + commentAttrs[.language] = scriptVariant + preeditAttrs[.language] = scriptVariant + statusAttrs[.language] = scriptVariant let candidateTemplate = self.candidateTemplate.mutableCopy() as! NSMutableAttributedString - let textRange: NSRange = candidateTemplate.mutableString.range(of: "%@", options: [.literal]) - let labelRange = NSRange(location: 0, length: textRange.location) - let commentRange = NSRange(location: textRange.upperBound, length: candidateTemplate.length - textRange.upperBound) - candidateTemplate.addAttributes(labelAttrs, range: labelRange) - candidateTemplate.addAttributes(textAttrs, range: textRange) - candidateTemplate.addAttributes(commentAttrs, range: commentRange) + let templateRange = NSRange(location: 0, length: candidateTemplate.length) + candidateTemplate.addAttribute(.baselineReferenceInfo, value: baselineRefInfo, range: templateRange) + candidateTemplate.addAttribute(.language, value: scriptVariant, range: templateRange) self.candidateTemplate = candidateTemplate.copy() as! NSAttributedString - let candidateHilitedTemplate = candidateTemplate.mutableCopy() as! NSMutableAttributedString - candidateHilitedTemplate.addAttribute(.foregroundColor, value: hilitedLabelForeColor, range: labelRange) - candidateHilitedTemplate.addAttribute(.foregroundColor, value: hilitedTextForeColor, range: textRange) - candidateHilitedTemplate.addAttribute(.foregroundColor, value: hilitedCommentForeColor, range: commentRange) + let candidateHilitedTemplate = self.candidateHilitedTemplate.mutableCopy() as! NSMutableAttributedString + candidateHilitedTemplate.addAttribute(.baselineReferenceInfo, value: baselineRefInfo, range: templateRange) + candidateHilitedTemplate.addAttribute(.language, value: scriptVariant, range: templateRange) self.candidateHilitedTemplate = candidateHilitedTemplate.copy() as! NSAttributedString if isTabular { - let candidateDimmedTemplate = candidateTemplate.mutableCopy() as! NSMutableAttributedString - candidateDimmedTemplate.addAttribute(.foregroundColor, value: dimmedLabelForeColor!, range: labelRange) - self.candidateDimmedTemplate = candidateDimmedTemplate.copy() as? NSAttributedString + let candidateDimmedTemplate = self.candidateDimmedTemplate!.mutableCopy() as? NSMutableAttributedString + candidateDimmedTemplate!.addAttribute(.baselineReferenceInfo, value: baselineRefInfo, range: templateRange) + candidateDimmedTemplate!.addAttribute(.language, value: scriptVariant, range: templateRange) + self.candidateDimmedTemplate = candidateDimmedTemplate!.copy() as? NSAttributedString } } -} // SquirrelTheme +} // SquirrelTheme // MARK: Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) -@frozen enum SquirrelContentBlock: Int, Sendable { +@frozen enum SquirrelContentBlock: Sendable { case preedit, linearCandidates, stackedCandidates, paging, status } @@ -1369,6 +1196,7 @@ final class SquirrelLayoutManager: NSLayoutManager, NSLayoutManagerDelegate { override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: NSPoint) { let textContainer = textContainer(forGlyphAt: glyphsToShow.location, effectiveRange: nil, withoutAdditionalLayout: true)! + let view = textContainer.textView! let verticalOrientation: Bool = textContainer.layoutOrientation == .vertical let context = NSGraphicsContext.current!.cgContext context.resetClip() @@ -1376,75 +1204,66 @@ final class SquirrelLayoutManager: NSLayoutManager, NSLayoutManagerDelegate { let charRange: NSRange = self.characterRange(forGlyphRange: lineRange, actualGlyphRange: nil) self.textStorage!.enumerateAttributes(in: charRange, options: [.longestEffectiveRangeNotRequired]) { attrs, runRange, stop in let runGlyphRange = self.glyphRange(forCharacterRange: runRange, actualCharacterRange: nil) - if let _ = attrs[.rubyAnnotation] { + let runFont = attrs[.font] as! NSFont + if attrs[.rubyAnnotation] != nil || (verticalOrientation && runFont.fontName == "AppleColorEmoji" && runFont.pointSize < 24) { context.saveGState() context.scaleBy(x: 1.0, y: -1.0) - var glyphIndex: Int = runGlyphRange.location - let line: CTLine = CTLineCreateWithAttributedString(self.textStorage!.attributedSubstring(from: runRange)) + var position: NSPoint = self.location(forGlyphAt: runGlyphRange.location) + lineRect.origin + origin + var line: CTLine + if attrs[.rubyAnnotation] == nil { + let subString = self.textStorage!.attributedSubstring(from: runRange).mutableCopy() as! NSMutableAttributedString + subString.addAttribute(.verticalGlyphForm, value: 1, range: NSRange(location: 0, length: runRange.length)) + line = CTLineCreateWithAttributedString(subString) + if let superscript = attrs[.superscript] as? Int { + position.y -= runFont.descender * Double(superscript) * 0.5 + } + } else { + line = CTLineCreateWithAttributedString(self.textStorage!.attributedSubstring(from: runRange)) + } let runs: CFArray = CTLineGetGlyphRuns(line) for i in 0 ..< CFArrayGetCount(runs) { - let position: NSPoint = self.location(forGlyphAt: glyphIndex) - let run: CTRun = Unmanaged.fromOpaque(CFArrayGetValueAtIndex(runs, i)).takeUnretainedValue() - let glyphCount: Int = CTRunGetGlyphCount(run) + let run: CTRun = bridge(ptr: CFArrayGetValueAtIndex(runs, i)) var matrix: CGAffineTransform = CTRunGetTextMatrix(run) - var glyphOrigin = NSPoint(x: origin.x + lineRect.origin.x + position.x, y: -origin.y - lineRect.origin.y - position.y) - glyphOrigin = textContainer.textView!.convertToBacking(glyphOrigin) - glyphOrigin.x = round(glyphOrigin.x) - glyphOrigin.y = round(glyphOrigin.y) - glyphOrigin = textContainer.textView!.convertFromBacking(glyphOrigin) + var glyphOrigin: NSPoint = view.convertToBacking(position) + glyphOrigin = view.convertFromBacking(NSPoint(x: glyphOrigin.x.rounded(.up), y: glyphOrigin.y.rounded(.up))) matrix.tx = glyphOrigin.x - matrix.ty = glyphOrigin.y + matrix.ty = -glyphOrigin.y context.textMatrix = matrix - CTRunDraw(run, context, CFRange(location: 0, length: glyphCount)) - glyphIndex += glyphCount + CTRunDraw(run, context, CFRange(location: 0, length: 0)) + if i < CFArrayGetCount(runs) - 1 { + position.x += CTRunGetTypographicBounds(run, CFRange(location: 0, length: 0), nil, nil, nil) + } } context.restoreGState() } else { - var position: NSPoint = self.location(forGlyphAt: runGlyphRange.location) - position.x += origin.x - position.y += origin.y - let runFont = attrs[.font] as! NSFont - let baselineClass = attrs[.baselineClass] as! CFString? - var offset: NSPoint = .zero - if !verticalOrientation && (baselineClass == kCTBaselineClassIdeographicCentered || baselineClass == kCTBaselineClassMath) { + var glyphOrigin: NSPoint = origin + if !verticalOrientation { let refFont = (attrs[.baselineReferenceInfo] as! NSDictionary)[kCTBaselineReferenceFont] as! NSFont - offset.y += (runFont.ascender + runFont.descender - refFont.ascender - refFont.descender) * 0.5 - } else if verticalOrientation && runFont.pointSize < 24 && (runFont.fontName == "AppleColorEmoji") { - let superscript = (attrs[.superscript, default: NSNumber(value: 0)] as! NSNumber).intValue - offset.x += runFont.capHeight - runFont.pointSize - offset.y += (runFont.capHeight - runFont.pointSize) * (superscript == 0 ? 0.25 : (superscript == 1 ? 0.5 / 0.55 : 0.0)) + glyphOrigin.y = (runFont.ascender + runFont.descender - refFont.ascender - refFont.descender) * 0.5 } - var glyphOrigin: NSPoint = textContainer.textView!.convertToBacking(NSPoint(x: position.x + offset.x, y: position.y + offset.y)) - glyphOrigin = textContainer.textView!.convertFromBacking(NSPoint(x: round(glyphOrigin.x), y: round(glyphOrigin.y))) - super.drawGlyphs(forGlyphRange: runGlyphRange, at: NSPoint(x: glyphOrigin.x - position.x, y: glyphOrigin.y - position.y)) + glyphOrigin = view.convertToBacking(glyphOrigin) + glyphOrigin = view.convertFromBacking(NSPoint(x: glyphOrigin.x.rounded(.up), y: glyphOrigin.y.rounded(.up))) + super.drawGlyphs(forGlyphRange: runGlyphRange, at: glyphOrigin) } } } - context.clip(to: textContainer.textView!.superview!.bounds) } func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer, lineFragmentUsedRect: UnsafeMutablePointer, baselineOffset: UnsafeMutablePointer, in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) -> Bool { + guard let rulerAttrs = textContainer.textView!.defaultParagraphStyle else { return false } var didModify: Bool = false let verticalOrientation: Bool = textContainer.layoutOrientation == .vertical let charRange: NSRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) - let rulerAttrs = textContainer.textView!.defaultParagraphStyle! - let lineSpacing: Double = rulerAttrs.lineSpacing let lineHeight: Double = rulerAttrs.minimumLineHeight var baseline: Double = lineHeight * 0.5 if !verticalOrientation { let refFont = (layoutManager.textStorage!.attribute(.baselineReferenceInfo, at: charRange.location, effectiveRange: nil) as! NSDictionary)[kCTBaselineReferenceFont] as! NSFont baseline += (refFont.ascender + refFont.descender) * 0.5 } - let lineHeightDelta: Double = lineFragmentUsedRect.pointee.size.height - lineHeight - lineSpacing - if abs(lineHeightDelta) > 0.1 { - lineFragmentUsedRect.pointee.size.height = round(lineFragmentUsedRect.pointee.size.height - lineHeightDelta) - lineFragmentRect.pointee.size.height = round(lineFragmentRect.pointee.size.height - lineHeightDelta) - didModify = true - } - let newBaselineOffset: Double = floor(lineFragmentUsedRect.pointee.origin.y - lineFragmentRect.pointee.origin.y + baseline) - if abs(baselineOffset.pointee - newBaselineOffset) > 0.1 { + let newBaselineOffset: Double = (lineFragmentUsedRect.pointee.minY - lineFragmentRect.pointee.minY + baseline).rounded() + if (baselineOffset.pointee - newBaselineOffset).isNormal { baselineOffset.pointee = newBaselineOffset - didModify = true + didModify |= true } return didModify } @@ -1459,8 +1278,7 @@ final class SquirrelLayoutManager: NSLayoutManager, NSLayoutManagerDelegate { } func layoutManager(_ layoutManager: NSLayoutManager, shouldUse action: NSLayoutManager.ControlCharacterAction, forControlCharacterAt charIndex: Int) -> NSLayoutManager.ControlCharacterAction { - if charIndex > 0 && layoutManager.textStorage!.mutableString.character(at: charIndex) == 0x8B && - layoutManager.textStorage!.attribute(.rubyAnnotation, at: charIndex - 1, effectiveRange: nil) != nil { + if charIndex > 0 && layoutManager.textStorage!.mutableString.character(at: charIndex) == 0x8B && layoutManager.textStorage!.attribute(.rubyAnnotation, at: charIndex - 1, effectiveRange: nil) != nil { return .whitespace } else { return action @@ -1468,24 +1286,33 @@ final class SquirrelLayoutManager: NSLayoutManager, NSLayoutManagerDelegate { } func layoutManager(_ layoutManager: NSLayoutManager, boundingBoxForControlGlyphAt glyphIndex: Int, for textContainer: NSTextContainer, proposedLineFragment proposedRect: NSRect, glyphPosition: NSPoint, characterIndex charIndex: Int) -> NSRect { - var width: Double = 0.0 - if charIndex > 0 && layoutManager.textStorage!.mutableString.character(at: charIndex) == 0x8B { - var rubyRange = NSRange(location: NSNotFound, length: 0) - if layoutManager.textStorage!.attribute(.rubyAnnotation, at: charIndex - 1, effectiveRange: &rubyRange) != nil { - let rubyString = layoutManager.textStorage!.attributedSubstring(from: rubyRange) - let line: CTLine = CTLineCreateWithAttributedString(rubyString) - let rubyRect: NSRect = CTLineGetBoundsWithOptions(line, []) - width = fdim(rubyRect.size.width, rubyString.size().width) - } + var rect = NSRect(origin: glyphPosition, size: .zero) + if layoutManager.textStorage!.mutableString.character(at: charIndex) == 0x8B, let controlCharacterSize = layoutManager.textStorage!.attribute(.controlCharacterSize, at: charIndex, effectiveRange: nil) as? NSSize { + rect.size = controlCharacterSize } - return .init(x: glyphPosition.x, y: glyphPosition.y, width: width, height: proposedRect.maxY - glyphPosition.y) + return rect } -} // SquirrelLayoutManager +} // SquirrelLayoutManager // MARK: Typesetting extensions for TextKit 2 (MacOS 12 or higher) -@available(macOS 12.0, *) -final class SquirrelTextLayoutFragment: NSTextLayoutFragment { +@available(macOS 12.0, *) final class SquirrelTextLayoutFragment: NSTextLayoutFragment, NSTextLayoutOrientationProvider { + var layoutOrientation: NSLayoutManager.TextLayoutOrientation { textLayoutManager?.textContainer?.layoutOrientation ?? .horizontal } + + override var renderingSurfaceBounds: CGRect { + var bounds = super.renderingSurfaceBounds + guard state == .layoutAvailable, let contentBlock = (textLayoutManager?.textContainer?.textView as? SquirrelTextView)?.contentBlock, contentBlock == .linearCandidates || contentBlock == .stackedCandidates, let documentRange = textLayoutManager?.documentRange, let rulerStyle = textLayoutManager?.textContainer?.textView?.defaultParagraphStyle else { return bounds } + if rangeInElement.location.isEqual(documentRange.location) { + let spacing = contentBlock == .stackedCandidates ? rulerStyle.paragraphSpacingBefore : (rulerStyle.lineSpacing * 0.5).rounded(.down) + bounds.origin.y -= spacing + bounds.size.height += spacing + } + if rangeInElement.endLocation.isEqual(documentRange.endLocation) { + bounds.size.height += contentBlock == .stackedCandidates ? rulerStyle.paragraphSpacing : (rulerStyle.lineSpacing * 0.5).rounded(.up) + } + return bounds + } + override func draw(at point: NSPoint, in context: CGContext) { var origin: NSPoint = point if #available(macOS 14.0, *) { @@ -1493,24 +1320,22 @@ final class SquirrelTextLayoutFragment: NSTextLayoutFragment { origin.x -= layoutFragmentFrame.minX origin.y -= layoutFragmentFrame.minY } - let verticalOrientation: Bool = textLayoutManager!.textContainer!.layoutOrientation == .vertical for lineFrag in textLineFragments { let lineRect: NSRect = lineFrag.typographicBounds.offsetBy(dx: origin.x, dy: origin.y) var baseline: Double = lineRect.midY - if !verticalOrientation { + if layoutOrientation == .horizontal { let refFont = (lineFrag.attributedString.attribute(.baselineReferenceInfo, at: lineFrag.characterRange.location, effectiveRange: nil) as! NSDictionary)[kCTBaselineReferenceFont] as! NSFont baseline += (refFont.ascender + refFont.descender) * 0.5 } - var renderOrigin = NSPoint(x: lineRect.minX + lineFrag.glyphOrigin.x, y: floor(baseline) - lineFrag.glyphOrigin.y) - let deviceOrigin: NSPoint = context.convertToDeviceSpace(renderOrigin) - renderOrigin = context.convertToUserSpace(NSPoint(x: round(deviceOrigin.x), y: round(deviceOrigin.y))) + var renderOrigin = NSPoint(x: lineRect.minX + lineFrag.glyphOrigin.x, y: baseline.rounded() - lineFrag.glyphOrigin.y) + renderOrigin = context.convertToDeviceSpace(renderOrigin) + renderOrigin = context.convertToUserSpace(NSPoint(x: renderOrigin.x.rounded(.up), y: renderOrigin.y.rounded(.up))) lineFrag.draw(at: renderOrigin, in: context) } } -} // SquirrelTextLayoutFragment +} // SquirrelTextLayoutFragment -@available(macOS 12.0, *) -final class SquirrelTextLayoutManager: NSTextLayoutManager, NSTextLayoutManagerDelegate { +@available(macOS 12.0, *) final class SquirrelTextLayoutManager: NSTextLayoutManager, NSTextLayoutManagerDelegate { var contentBlock: SquirrelContentBlock? { (textContainer?.textView as? SquirrelTextView)?.contentBlock } func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, shouldBreakLineBefore location: any NSTextLocation, hyphenating: Bool) -> Bool { @@ -1528,7 +1353,7 @@ final class SquirrelTextLayoutManager: NSTextLayoutManager, NSTextLayoutManagerD let textRange = NSTextRange(location: location, end: textElement.elementRange!.endLocation) return SquirrelTextLayoutFragment(textElement: textElement, range: textRange) } -} // SquirrelTextLayoutManager +} // SquirrelTextLayoutManager final class NSFlippedView: NSView { override var isFlipped: Bool { true } @@ -1566,31 +1391,22 @@ final class SquirrelTextView: NSTextView { clipsToBounds = false } - @available(*, unavailable) - required init?(coder _: NSCoder) { + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - @available(macOS 12.0, *) - private func textRange(fromCharRange charRange: NSRange) -> NSTextRange? { - if charRange.location == NSNotFound { - return nil - } else { - let start = textContentStorage!.location(textContentStorage!.documentRange.location, offsetBy: charRange.location)! - let end = textContentStorage!.location(start, offsetBy: charRange.length)! - return NSTextRange(location: start, end: end) - } + @available(macOS 12.0, *) private func textRange(fromCharRange charRange: NSRange) -> NSTextRange? { + if charRange.location == NSNotFound { return nil } + let start = textContentStorage!.location(textContentStorage!.documentRange.location, offsetBy: charRange.location)! + let end = textContentStorage!.location(start, offsetBy: charRange.length)! + return NSTextRange(location: start, end: end) } - @available(macOS 12.0, *) - private func charRange(fromTextRange textRange: NSTextRange?) -> NSRange { - if textRange == nil { - return NSRange(location: NSNotFound, length: 0) - } else { - let location = textContentStorage!.offset(from: textContentStorage!.documentRange.location, to: textRange!.location) - let length = textContentStorage!.offset(from: textRange!.location, to: textRange!.endLocation) - return NSRange(location: location, length: length) - } + @available(macOS 12.0, *) private func charRange(fromTextRange textRange: NSTextRange?) -> NSRange { + guard let textRange = textRange else { return NSRange(location: NSNotFound, length: 0) } + let location = textContentStorage!.offset(from: textContentStorage!.documentRange.location, to: textRange.location) + let length = textContentStorage!.offset(from: textRange.location, to: textRange.endLocation) + return NSRange(location: location, length: length) } func layoutText() -> NSRect { @@ -1607,24 +1423,21 @@ final class SquirrelTextView: NSTextView { // Get the rectangle containing the range of text func blockRect(for charRange: NSRange) -> NSRect { - if charRange.location == NSNotFound { - return .zero - } + if charRange.location == NSNotFound { return .zero } if #available(macOS 12.0, *) { - let textRange: NSTextRange! = textRange(fromCharRange: charRange) + let textRange: NSTextRange = textRange(fromCharRange: charRange)! var firstLineRect: NSRect = .null var finalLineRect: NSRect = .null textLayoutManager?.enumerateTextSegments(in: textRange, type: .standard, options: [.rangeNotRequired]) { segRange, segFrame, baseline, textContainer in - if !segFrame.isEmpty { - if firstLineRect.isEmpty || segFrame.minY < firstLineRect.maxY - 0.1 { - firstLineRect = segFrame.union(firstLineRect) - } else { - finalLineRect = segFrame.union(finalLineRect) - } + guard !segFrame.isEmpty else { return true } + if firstLineRect.isEmpty || segFrame.minY < firstLineRect.maxY.nextDown { + firstLineRect = segFrame.union(firstLineRect) + } else { + finalLineRect = segFrame.union(finalLineRect) } return true } - if contentBlock == .linearCandidates, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing > 0.1 { + if contentBlock == .linearCandidates, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { firstLineRect.size.height += lineSpacing if !finalLineRect.isEmpty { finalLineRect.size.height += lineSpacing @@ -1635,50 +1448,56 @@ final class SquirrelTextView: NSTextView { return firstLineRect } else { let containerWidth: CGFloat = textLayoutManager?.usageBoundsForTextContainer.width ?? 0 - return .init(x: 0.0, y: firstLineRect.minY, width: containerWidth, height: finalLineRect.maxY - firstLineRect.minY) + return NSRect(x: .zero, y: firstLineRect.minY, width: containerWidth, height: finalLineRect.maxY - firstLineRect.minY) } } else { let glyphRange: NSRange = layoutManager!.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil) var firstLineRange = NSRange(location: NSNotFound, length: 0) - let firstLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: &firstLineRange) + let firstLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: &firstLineRange, withoutAdditionalLayout: true) if glyphRange.upperBound <= firstLineRange.upperBound { let leading: Double = layoutManager!.location(forGlyphAt: glyphRange.location).x let trailing: Double = glyphRange.upperBound < firstLineRange.upperBound ? layoutManager!.location(forGlyphAt: glyphRange.upperBound).x : firstLineRect.width - return .init(x: firstLineRect.minX + leading, y: firstLineRect.minY, width: trailing - leading, height: firstLineRect.height) + var height: Double = firstLineRect.height + if contentBlock == .linearCandidates, firstLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { + height += lineSpacing + } + return NSRect(x: firstLineRect.minX + leading, y: firstLineRect.minY, width: trailing - leading, height: height) } else { - let finalLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: nil) + var finalLineRange = NSRange(location: NSNotFound, length: 0) + let finalLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: &finalLineRange, withoutAdditionalLayout: true) let containerWidth: Double = layoutManager!.usedRect(for: textContainer!).width - return .init(x: 0.0, y: firstLineRect.minY, width: containerWidth, height: finalLineRect.maxY - firstLineRect.minY) + var height: Double = finalLineRect.maxY - firstLineRect.minY + if contentBlock == .linearCandidates, finalLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { + height += lineSpacing + } + return NSRect(x: .zero, y: firstLineRect.minY, width: containerWidth, height: height) } } } /* Calculate 3 rectangles encloding the text in range. TextPolygon.head & .tail are incomplete line fragments - TextPolygon.body is the complete line fragment in the middle if the range spans no less than one full line */ + TextPolygon.body is the complete line fragment in the middle if the range spans no less than one full line */ func textPolygon(forRange charRange: NSRange) -> SquirrelTextPolygon { - var textPolygon: SquirrelTextPolygon = .init(head: .zero, body: .zero, tail: .zero) - if charRange.location == NSNotFound { - return textPolygon - } + var textPolygon = SquirrelTextPolygon(head: .zero, body: .zero, tail: .zero) + if charRange.location == NSNotFound { return textPolygon } if #available(macOS 12.0, *) { - let textRange: NSTextRange! = textRange(fromCharRange: charRange) + let textRange: NSTextRange = textRange(fromCharRange: charRange)! var headLineRect: NSRect = .null var tailLineRect: NSRect = .null var headLineRange: NSTextRange? var tailLineRange: NSTextRange? textLayoutManager?.enumerateTextSegments(in: textRange, type: .standard, options: [.middleFragmentsExcluded]) { segRange, segFrame, baseline, textContainer in - if !segFrame.isEmpty { - if headLineRect.isEmpty || segFrame.minY < headLineRect.maxY - 0.1 { - headLineRect = segFrame.union(headLineRect) - headLineRange = headLineRange == nil ? segRange! : segRange!.union(headLineRange!) - } else { - tailLineRect = segFrame.union(tailLineRect) - tailLineRange = tailLineRange == nil ? segRange! : segRange!.union(tailLineRange!) - } + guard !segFrame.isEmpty else { return true } + if headLineRect.isEmpty || segFrame.minY < headLineRect.maxY.nextDown { + headLineRect = segFrame.union(headLineRect) + headLineRange = headLineRange == nil ? segRange! : segRange!.union(headLineRange!) + } else { + tailLineRect = segFrame.union(tailLineRect) + tailLineRange = tailLineRange == nil ? segRange! : segRange!.union(tailLineRange!) } return true } - if contentBlock == .linearCandidates, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing > 0.1 { + if contentBlock == .linearCandidates, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { headLineRect.size.height += lineSpacing if !tailLineRect.isEmpty { tailLineRect.size.height += lineSpacing @@ -1690,21 +1509,21 @@ final class SquirrelTextView: NSTextView { } else { let containerWidth: CGFloat = textLayoutManager?.usageBoundsForTextContainer.width ?? 0 headLineRect.size.width = containerWidth - headLineRect.minX - if abs(tailLineRect.maxX - headLineRect.maxX) < 1 { - if abs(headLineRect.minX - tailLineRect.minX) < 1 { + if (tailLineRect.maxX - headLineRect.maxX).magnitude < 1 { + if (headLineRect.minX - tailLineRect.minX).magnitude < 1 { textPolygon.body = headLineRect.union(tailLineRect) } else { textPolygon.head = headLineRect - textPolygon.body = NSRect(x: 0.0, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.maxY - headLineRect.maxY) + textPolygon.body = NSRect(x: .zero, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.maxY - headLineRect.maxY) } } else { textPolygon.tail = tailLineRect - if abs(headLineRect.minX - tailLineRect.minX) < 1 { - textPolygon.body = NSRect(x: 0.0, y: headLineRect.minY, width: containerWidth, height: tailLineRect.minY - headLineRect.minY) + if (headLineRect.minX - tailLineRect.minX).magnitude < 1 { + textPolygon.body = NSRect(x: .zero, y: headLineRect.minY, width: containerWidth, height: tailLineRect.minY - headLineRect.minY) } else { textPolygon.head = headLineRect if !tailLineRange!.contains(headLineRange!.endLocation) { - textPolygon.body = NSRect(x: 0.0, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.minY - headLineRect.maxY) + textPolygon.body = NSRect(x: .zero, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.minY - headLineRect.maxY) } } } @@ -1712,31 +1531,39 @@ final class SquirrelTextView: NSTextView { } else { let glyphRange: NSRange = layoutManager!.glyphRange(forCharacterRange: charRange, actualCharacterRange: nil) var headLineRange = NSRange(location: NSNotFound, length: 0) - let headLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: &headLineRange) + var headLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: &headLineRange, withoutAdditionalLayout: true) let leading: Double = layoutManager!.location(forGlyphAt: glyphRange.location).x if headLineRange.upperBound >= glyphRange.upperBound { let trailing: Double = glyphRange.upperBound < headLineRange.upperBound ? layoutManager!.location(forGlyphAt: glyphRange.upperBound).x : headLineRect.width - textPolygon.body = NSRect(x: leading, y: headLineRect.minY, width: trailing - leading, height: headLineRect.height) + var height: Double = headLineRect.height + if contentBlock == .linearCandidates, headLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { + height += lineSpacing + } + textPolygon.body = NSRect(x: leading, y: headLineRect.minY, width: trailing - leading, height: height) } else { let containerWidth: Double = layoutManager!.usedRect(for: textContainer!).width + headLineRect.size.width = containerWidth - headLineRect.minX var tailLineRange = NSRange(location: NSNotFound, length: 0) - let tailLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: &tailLineRange) + var tailLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: &tailLineRange, withoutAdditionalLayout: true) + if contentBlock == .linearCandidates, tailLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { + tailLineRect.size.height += lineSpacing + } let trailing: Double = glyphRange.upperBound < tailLineRange.upperBound ? layoutManager!.location(forGlyphAt: glyphRange.upperBound).x : tailLineRect.width if tailLineRange.upperBound == glyphRange.upperBound { if glyphRange.location == headLineRange.location { - textPolygon.body = NSRect(x: 0.0, y: headLineRect.minY, width: containerWidth, height: tailLineRect.maxY - headLineRect.minY) + textPolygon.body = NSRect(x: .zero, y: headLineRect.minY, width: containerWidth, height: tailLineRect.maxY - headLineRect.minY) } else { textPolygon.head = NSRect(x: leading, y: headLineRect.minY, width: containerWidth - leading, height: headLineRect.height) - textPolygon.body = NSRect(x: 0.0, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.maxY - headLineRect.maxY) + textPolygon.body = NSRect(x: .zero, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.maxY - headLineRect.maxY) } } else { - textPolygon.tail = NSRect(x: 0.0, y: tailLineRect.minY, width: trailing, height: tailLineRect.height) + textPolygon.tail = NSRect(x: .zero, y: tailLineRect.minY, width: trailing, height: tailLineRect.height) if glyphRange.location == headLineRange.location { - textPolygon.body = NSRect(x: 0.0, y: headLineRect.minY, width: containerWidth, height: tailLineRect.minY - headLineRect.minY) + textPolygon.body = NSRect(x: .zero, y: headLineRect.minY, width: containerWidth, height: tailLineRect.minY - headLineRect.minY) } else { textPolygon.head = NSRect(x: leading, y: headLineRect.minY, width: containerWidth - leading, height: headLineRect.height) if tailLineRange.location > headLineRange.upperBound { - textPolygon.body = NSRect(x: 0.0, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.minY - headLineRect.maxY) + textPolygon.body = NSRect(x: .zero, y: headLineRect.maxY, width: containerWidth, height: tailLineRect.minY - headLineRect.maxY) } } } @@ -1744,7 +1571,7 @@ final class SquirrelTextView: NSTextView { } return textPolygon } -} // SquirrelTextView +} // SquirrelTextView // MARK: View behind text, containing drawings of backgrounds and highlights @@ -1788,30 +1615,21 @@ final class SquirrelView: NSView { private(set) var expanderRect: NSRect = .zero private(set) var pageUpRect: NSRect = .zero private(set) var pageDownRect: NSRect = .zero - private(set) var clippedHeight: Double = 0.0 + private(set) var clippedHeight: Double = .zero private(set) var functionButton: SquirrelIndex = .VoidSymbol private(set) var hilitedCandidate: Int? private(set) var hilitedPreeditRange = NSRange(location: NSNotFound, length: 0) var sectionNum: Int = 0 var isExpanded: Bool = false var isLocked: Bool = false - // Need flipped coordinate system, as required by textStorage override var isFlipped: Bool { true } override var wantsUpdateLayer: Bool { true } var style: SquirrelStyle { didSet { - if #available(macOS 10.14, *) { - if oldValue != style { - if style == .dark { - theme = Self.darkTheme - scrollView.scrollerKnobStyle = .light - } else { - theme = Self.lightTheme - scrollView.scrollerKnobStyle = .dark - } - updateColors() - } - } + guard #available(macOS 10.14, *), oldValue != style else { return } + theme = style == .dark ? Self.darkTheme : Self.lightTheme + scrollView.scrollerKnobStyle = style == .dark ? .light : .dark + updateColors() } } @@ -1834,20 +1652,20 @@ final class SquirrelView: NSView { scrollView.hasVerticalScroller = true scrollView.scrollerStyle = .overlay scrollView.scrollerKnobStyle = .dark - scrollView.contentView.wantsLayer = true - scrollView.contentView.layer!.isGeometryFlipped = true + scrollView.wantsLayer = true + scrollView.layer!.isGeometryFlipped = true style = .light theme = Self.lightTheme if #available(macOS 10.14, *) { - shape.fillColor = CGColor.white + shape.fillColor = .white } super.init(frame: frameRect) wantsLayer = true layer!.isGeometryFlipped = true layerContentsRedrawPolicy = .onSetNeedsDisplay - backImageLayer.actions = ["transform": NSNull()] + backImageLayer.actions = ["transform" : NSNull()] backColorLayer.fillRule = .evenOdd borderLayer.fillRule = .evenOdd layer!.addSublayer(backImageLayer) @@ -1860,18 +1678,18 @@ final class SquirrelView: NSView { documentLayer.fillRule = .evenOdd documentLayer.allowsGroupOpacity = true activePageLayer.fillRule = .evenOdd + gridLayer.lineCap = .round gridLayer.lineWidth = 1.0 - clipLayer.fillColor = CGColor.white + clipLayer.fillColor = .white documentView.layer!.addSublayer(documentLayer) documentLayer.addSublayer(activePageLayer) documentView.layer!.addSublayer(gridLayer) documentView.layer!.addSublayer(nonHilitedCandidateLayer) documentView.layer!.addSublayer(hilitedCandidateLayer) - scrollView.contentView.layer!.mask = clipLayer + scrollView.layer!.mask = clipLayer } - @available(*, unavailable) - required init?(coder _: NSCoder) { + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -1901,11 +1719,15 @@ final class SquirrelView: NSView { } if let hilitedCandidateBackColor = theme.hilitedCandidateBackColor { hilitedCandidateLayer.fillColor = hilitedCandidateBackColor.cgColor - if theme.shadowSize > 0.1 { - hilitedCandidateLayer.shadowOffset = .init(width: theme.shadowSize, height: theme.shadowSize) + if theme.shadowSize.isNormal { + hilitedCandidateLayer.shadowOffset = NSSize(width: theme.shadowSize, height: theme.shadowSize) hilitedCandidateLayer.shadowOpacity = 1.0 + hilitedCandidateLayer.shadowColor = hilitedCandidateBackColor.shadow(withLevel: 0.7)?.cgColor + functionButtonLayer.shadowOffset = NSSize(width: theme.shadowSize, height: theme.shadowSize) + functionButtonLayer.shadowOpacity = 1.0 } else { - hilitedCandidateLayer.shadowOpacity = 0.0 + hilitedCandidateLayer.shadowOpacity = .zero + functionButtonLayer.shadowOpacity = .zero } } else { hilitedCandidateLayer.isHidden = true @@ -1928,7 +1750,7 @@ final class SquirrelView: NSView { func estimateBounds(onScreen screen: NSRect, withPreedit hasPreedit: Bool, candidates candidateInfos: [SquirrelCandidateInfo], paging hasPaging: Bool) { self.candidateInfos = candidateInfos preeditView.isHidden = !hasPreedit - candidateView.isHidden = candidateInfos.isEmpty + scrollView.isHidden = candidateInfos.isEmpty pagingView.isHidden = !hasPaging statusView.isHidden = hasPreedit || !candidateInfos.isEmpty // layout textviews and get their sizes @@ -1936,7 +1758,7 @@ final class SquirrelView: NSView { documentRect = .zero // in textView's own coordinates clipRect = .zero pagingRect = .zero - clippedHeight = 0.0 + clippedHeight = .zero if !hasPreedit && candidateInfos.isEmpty { // status contentRect = statusView.layoutText(); return } @@ -1944,33 +1766,28 @@ final class SquirrelView: NSView { preeditRect = preeditView.layoutText() contentRect = preeditRect } - if !candidateInfos.isEmpty { - documentRect = candidateView.layoutText() - if #available(macOS 12.0, *) { - documentRect.size.height += theme.lineSpacing - } else { - documentRect.size.height += theme.isLinear ? 0.0 : theme.lineSpacing - } - if theme.isLinear && candidateInfos.reduce(true, { $0 && !$1.isTruncated }) { - documentRect.size.width -= theme.fullWidth - } - clipRect = documentRect - if hasPreedit { - clipRect.origin.y = preeditRect.maxY + theme.preeditSpacing - contentRect = preeditRect.union(clipRect) - } else { - contentRect = clipRect - } - clipRect.size.width += theme.fullWidth - if hasPaging { - pagingRect = pagingView.layoutText() - pagingRect.origin.y = clipRect.maxY - contentRect = contentRect.union(pagingRect) - } - } else { return } + if candidateInfos.isEmpty { return } + documentRect = candidateView.layoutText() + documentRect.size.height += theme.lineSpacing + if theme.isLinear && candidateInfos.reduce(true, { $0 && !$1.isTruncated }) { + documentRect.size.width -= theme.fullWidth + } + clipRect = documentRect + if hasPreedit { + clipRect.origin.y = preeditRect.maxY + theme.preeditSpacing + contentRect = preeditRect.union(clipRect) + } else { + contentRect = clipRect + } + clipRect.size.width += theme.fullWidth + if hasPaging { + pagingRect = pagingView.layoutText() + pagingRect.origin.y = clipRect.maxY + contentRect = contentRect.union(pagingRect) + } // clip candidate block if it has too many lines let maxHeight: Double = (theme.isVertical ? screen.width : screen.height) * 0.5 - theme.borderInsets.height * 2 - clippedHeight = fdim(ceil(contentRect.height), ceil(maxHeight)) + clippedHeight = fdim(contentRect.height.rounded(.up), maxHeight.rounded(.up)) contentRect.size.height -= clippedHeight clipRect.size.height -= clippedHeight scrollView.verticalScroller?.knobProportion = clipRect.height / documentRect.height @@ -1980,7 +1797,7 @@ final class SquirrelView: NSView { func layoutContents() { let origin = NSPoint(x: theme.borderInsets.width, y: theme.borderInsets.height) if !statusView.isHidden { // status - contentRect.origin = NSPoint(x: origin.x + ceil(theme.fullWidth * 0.5), y: origin.y) + contentRect.origin = NSPoint(x: origin.x + (theme.fullWidth * 0.5).rounded(.up), y: origin.y) return } if !preeditView.isHidden { @@ -1990,8 +1807,6 @@ final class SquirrelView: NSView { contentRect = preeditRect } if !scrollView.isHidden { - clipRect.size.width = documentRect.width - clipRect.size.height = documentRect.height - clippedHeight if !preeditView.isHidden { clipRect.origin.x = origin.x clipRect.origin.y = preeditRect.maxY + theme.preeditSpacing @@ -2007,9 +1822,9 @@ final class SquirrelView: NSView { pagingRect.origin.y = clipRect.maxY contentRect = contentRect.union(pagingRect) } - contentRect.size.width -= theme.fullWidth - contentRect.origin.x += ceil(theme.fullWidth * 0.5) } + contentRect.size.width -= theme.fullWidth + contentRect.origin.x += (theme.fullWidth * 0.5).rounded(.up) } // Will triger `updateLayer()` @@ -2044,10 +1859,10 @@ final class SquirrelView: NSView { } func highlightCandidate(_ hilitedCandidate: Int?) { - if hilitedCandidate == nil || self.hilitedCandidate == nil { return } + guard let hilitedCandidate = hilitedCandidate, let priorHilitedCandidate = self.hilitedCandidate else { return } if isExpanded { - let priorActivePage: Int = self.hilitedCandidate! / theme.pageSize - let newActivePage: Int = hilitedCandidate! / theme.pageSize + let priorActivePage: Int = priorHilitedCandidate / theme.pageSize + let newActivePage: Int = hilitedCandidate / theme.pageSize if newActivePage != priorActivePage { setNeedsDisplay(convert(sectionRects[priorActivePage], from: documentView)) candidateView.setNeedsDisplay(documentView.convert(sectionRects[priorActivePage], to: candidateView)) @@ -2066,29 +1881,29 @@ final class SquirrelView: NSView { } func unclipHighlightedCandidate() { - if hilitedCandidate == nil || clippedHeight < 0.1 { return } + guard let hilitedCandidate = hilitedCandidate, clippedHeight.isNormal else { return } if isExpanded { - let activePage: Int = hilitedCandidate! / theme.pageSize - if sectionRects[activePage].minY < scrollView.documentVisibleRect.minY - 0.1 { + let activePage: Int = hilitedCandidate / theme.pageSize + if sectionRects[activePage].minY < scrollView.documentVisibleRect.minY.nextDown { var origin = scrollView.contentView.bounds.origin origin.y -= scrollView.documentVisibleRect.minY - sectionRects[activePage].minY scrollView.contentView.scroll(to: origin) scrollView.verticalScroller?.doubleValue = scrollView.documentVisibleRect.minY / clippedHeight - } else if sectionRects[activePage].maxY > scrollView.documentVisibleRect.maxY + 0.1 { + } else if sectionRects[activePage].maxY > scrollView.documentVisibleRect.maxY.nextUp { var origin = scrollView.contentView.bounds.origin origin.y += sectionRects[activePage].maxY - scrollView.documentVisibleRect.maxY scrollView.contentView.scroll(to: origin) scrollView.verticalScroller?.doubleValue = scrollView.documentVisibleRect.minY / clippedHeight } } else { - if scrollView.documentVisibleRect.minY > candidatePolygons[hilitedCandidate!].minY + 0.1 { + if scrollView.documentVisibleRect.minY > candidatePolygons[hilitedCandidate].minY.nextUp { var origin = scrollView.contentView.bounds.origin - origin.y -= scrollView.documentVisibleRect.minY - candidatePolygons[hilitedCandidate!].minY + origin.y -= scrollView.documentVisibleRect.minY - candidatePolygons[hilitedCandidate].minY scrollView.contentView.scroll(to: origin) scrollView.verticalScroller?.doubleValue = scrollView.documentVisibleRect.minY / clippedHeight - } else if scrollView.documentVisibleRect.maxY < candidatePolygons[hilitedCandidate!].maxY - 0.1 { + } else if scrollView.documentVisibleRect.maxY < candidatePolygons[hilitedCandidate].maxY.nextDown { var origin = scrollView.contentView.bounds.origin - origin.y += candidatePolygons[hilitedCandidate!].maxY - scrollView.documentVisibleRect.maxY + origin.y += candidatePolygons[hilitedCandidate].maxY - scrollView.documentVisibleRect.maxY scrollView.contentView.scroll(to: origin) scrollView.verticalScroller?.doubleValue = scrollView.documentVisibleRect.minY / clippedHeight } @@ -2110,18 +1925,14 @@ final class SquirrelView: NSView { case .ExpandButton, .CompressButton, .LockButton: setNeedsDisplay(expanderRect) pagingView.setNeedsDisplay(convert(expanderRect, to: pagingView), avoidAdditionalLayout: true) - default: - break + default: break } } self.functionButton = functionButton } private func updateFunctionButtonLayer() -> CGPath? { - if functionButton == .VoidSymbol { - functionButtonLayer.isHidden = true - return nil - } + guard functionButton != .VoidSymbol else { return nil } var buttonColor: NSColor? var buttonRect: NSRect = .zero switch functionButton { @@ -2146,20 +1957,17 @@ final class SquirrelView: NSView { case .EscapeKey: buttonColor = theme.hilitedPreeditBackColor?.disabledColor buttonRect = deleteBackRect - default: - break - } - if !buttonRect.isEmpty && buttonColor != nil { - let cornerRadius: Double = min(theme.hilitedCornerRadius, buttonRect.height * 0.5) - let buttonPath: CGPath? = .squirclePath(rect: buttonRect, cornerRadius: cornerRadius) - functionButtonLayer.path = buttonPath - functionButtonLayer.fillColor = buttonColor!.cgColor - functionButtonLayer.isHidden = false - return buttonPath - } else { - functionButtonLayer.isHidden = true - return nil + default: break } + guard !buttonRect.isEmpty, let buttonColor = buttonColor else { return nil } + let cornerRadius: Double = min(theme.hilitedCornerRadius, buttonRect.height * 0.5) + let buttonPath: CGPath? = buttonRect.squirclePath(cornerRadius: cornerRadius) + functionButtonLayer.path = buttonPath + functionButtonLayer.fillColor = buttonColor.cgColor + functionButtonLayer.isHidden = false + functionButtonLayer.actions = ["fillColor" : NSNull()] + functionButtonLayer.shadowColor = buttonColor.shadow(withLevel: 0.7)?.cgColor + return buttonPath } // All draws happen here @@ -2176,29 +1984,32 @@ final class SquirrelView: NSView { preeditRect = backingAlignedRect(preeditRect, options: [.alignAllEdgesNearest]) // Draw the highlighted part of preedit text if hilitedPreeditRange.length > 0 && (theme.hilitedPreeditBackColor != nil) { - let padding: Double = ceil(theme.preeditParagraphStyle.minimumLineHeight * 0.05) + let padding: Double = (theme.preeditParagraphStyle.minimumLineHeight * 0.05).rounded(.up) var innerBox: NSRect = preeditRect - innerBox.origin.x += ceil(theme.fullWidth * 0.5) - padding + innerBox.origin.x += (theme.fullWidth * 0.5).rounded(.up) - padding innerBox.size.width = backgroundRect.width - theme.fullWidth + padding * 2 innerBox = backingAlignedRect(innerBox, options: [.alignAllEdgesNearest]) var textPolygon = preeditView.textPolygon(forRange: hilitedPreeditRange) if !textPolygon.head.isEmpty { - textPolygon.head.origin.x += theme.borderInsets.width + ceil(theme.fullWidth * 0.5) - padding + textPolygon.head.origin.x += theme.borderInsets.width + (theme.fullWidth * 0.5).rounded(.up) - padding textPolygon.head.origin.y += theme.borderInsets.height textPolygon.head.size.width += padding * 2 textPolygon.head = backingAlignedRect(textPolygon.head.intersection(innerBox), options: [.alignAllEdgesNearest]) } if !textPolygon.body.isEmpty { - textPolygon.body.origin.x += theme.borderInsets.width + ceil(theme.fullWidth * 0.5) - padding + textPolygon.body.origin.x += theme.borderInsets.width + (theme.fullWidth * 0.5).rounded(.up) - padding textPolygon.body.origin.y += theme.borderInsets.height textPolygon.body.size.width += padding if !textPolygon.tail.isEmpty || hilitedPreeditRange.upperBound + 2 == preeditContents.length { textPolygon.body.size.width += padding } + if textPolygon.body.maxX > innerBox.maxX - 2 { + textPolygon.body.size.width = innerBox.maxX - textPolygon.body.minX + } textPolygon.body = backingAlignedRect(textPolygon.body.intersection(innerBox), options: [.alignAllEdgesNearest]) } if !textPolygon.tail.isEmpty { - textPolygon.tail.origin.x += theme.borderInsets.width + ceil(theme.fullWidth * 0.5) - padding + textPolygon.tail.origin.x += theme.borderInsets.width + (theme.fullWidth * 0.5).rounded(.up) - padding textPolygon.tail.origin.y += theme.borderInsets.height textPolygon.tail.size.width += padding if hilitedPreeditRange.upperBound + 2 == preeditContents.length { @@ -2206,7 +2017,7 @@ final class SquirrelView: NSView { } textPolygon.tail = backingAlignedRect(textPolygon.tail.intersection(innerBox), options: [.alignAllEdgesNearest]) } - hilitedPreeditPath = .squirclePath(polygon: textPolygon, cornerRadius: hilitedCornerRadius) + hilitedPreeditPath = textPolygon.squirclePath(cornerRadius: hilitedCornerRadius) } deleteBackRect = preeditView.blockRect(for: NSRange(location: preeditContents.length - 1, length: 1)) deleteBackRect.size.width += theme.fullWidth @@ -2220,13 +2031,13 @@ final class SquirrelView: NSView { sectionRects = [] tabularIndices = [] var clipPath: CGPath?, documentPath: CGMutablePath?, gridPath: CGMutablePath? - if !candidateView.isHidden { + if !scrollView.isHidden { clipRect.size.width = backgroundRect.width clipRect = backingAlignedRect(clipRect.intersection(backgroundRect), options: [.alignAllEdgesNearest]) documentRect.size.width = backgroundRect.width documentRect = documentView.backingAlignedRect(documentRect, options: [.alignAllEdgesNearest]) - clipPath = .squirclePath(rect: clipRect, cornerRadius: hilitedCornerRadius) - documentPath = .squircleMutablePath(vertices: documentRect.vertices, cornerRadius: hilitedCornerRadius) + clipPath = clipRect.squirclePath(cornerRadius: hilitedCornerRadius) + documentPath = .squirclePath(vertices: documentRect.vertices, cornerRadius: hilitedCornerRadius) // Draw candidate highlight rect candidatePolygons.reserveCapacity(candidateInfos.count) @@ -2257,37 +2068,39 @@ final class SquirrelView: NSView { candidatePolygon.body.size.width = documentRect.width } else if !candidatePolygon.tail.isEmpty { candidatePolygon.body.size.width += theme.fullWidth + } else if candidatePolygon.body.maxX > documentRect.maxX - 2 { + candidatePolygon.body.size.width = documentRect.maxX - candidatePolygon.body.minX } candidatePolygon.body = documentView.backingAlignedRect(candidatePolygon.body.intersection(documentRect), options: [.alignAllEdgesNearest]) } if theme.isTabular { if isExpanded { if candInfo.col == 0 { - sectionRect.origin.y = ceil(sectionRect.maxY) + sectionRect.origin.y = sectionRect.maxY.rounded(.up) } if candInfo.col == theme.pageSize - 1 || candInfo.idx == candidateInfos.count - 1 { - sectionRect.size.height = ceil(candidatePolygon.maxY) - sectionRect.minY + sectionRect.size.height = candidatePolygon.maxY.rounded(.up) - sectionRect.minY sectionRects.append(sectionRect) } } let bottomEdge: Double = candidatePolygon.maxY - if abs(bottomEdge - gridOriginY) > 2 { + if (bottomEdge - gridOriginY).magnitude > 2 { lineNum += candInfo.idx > 0 ? 1 : 0 // horizontal border except for the last line if bottomEdge < documentRect.maxY - 2 { - gridPath!.move(to: .init(x: ceil(theme.fullWidth * 0.5), y: bottomEdge)) - gridPath!.addLine(to: .init(x: documentRect.maxX - floor(theme.fullWidth * 0.5), y: bottomEdge)) + gridPath!.move(to: NSPoint(x: (theme.fullWidth * 0.5).rounded(.up), y: bottomEdge)) + gridPath!.addLine(to: NSPoint(x: documentRect.maxX - (theme.fullWidth * 0.5).rounded(.down), y: bottomEdge)) } gridOriginY = bottomEdge } let leadOrigin: NSPoint = candidatePolygon.origin - let leadTabColumn = Int(round((leadOrigin.x - documentRect.minX) / tabInterval)) + let leadTabColumn = Int(((leadOrigin.x - documentRect.minX) / tabInterval).rounded()) // vertical bar if leadOrigin.x > documentRect.minX + theme.fullWidth { - gridPath!.move(to: .init(x: leadOrigin.x, y: leadOrigin.y + ceil(theme.lineSpacing * 0.5) + theme.candidateParagraphStyle.minimumLineHeight * 0.2)) - gridPath!.addLine(to: .init(x: leadOrigin.x, y: candidatePolygon.maxY - floor(theme.lineSpacing * 0.5) - theme.candidateParagraphStyle.minimumLineHeight * 0.2)) + gridPath!.move(to: NSPoint(x: leadOrigin.x, y: leadOrigin.y + (theme.lineSpacing * 0.5).rounded(.down) + theme.candidateParagraphStyle.minimumLineHeight * 0.2)) + gridPath!.addLine(to: NSPoint(x: leadOrigin.x, y: candidatePolygon.maxY - (theme.lineSpacing * 0.5).rounded(.up) - theme.candidateParagraphStyle.minimumLineHeight * 0.2)) } - tabularIndices.append(.init(index: candInfo.idx, lineNum: lineNum, tabNum: leadTabColumn)) + tabularIndices.append(SquirrelTabularIndex(index: candInfo.idx, lineNum: lineNum, tabNum: leadTabColumn)) } candidatePolygons.append(candidatePolygon) } @@ -2297,7 +2110,7 @@ final class SquirrelView: NSView { candidateRect.size.width = documentRect.width candidateRect.size.height += theme.lineSpacing candidateRect = documentView.backingAlignedRect(candidateRect.intersection(documentRect), options: [.alignAllEdgesNearest]) - candidatePolygons.append(.init(head: .zero, body: candidateRect, tail: .zero)) + candidatePolygons.append(SquirrelTextPolygon(head: .zero, body: candidateRect, tail: .zero)) } } } @@ -2337,19 +2150,19 @@ final class SquirrelView: NSView { /*** Border Rects ***/ let outerCornerRadius: Double = min(theme.cornerRadius, panelRect.height * 0.5) - let innerCornerRadius: Double = clamp(theme.hilitedCornerRadius, outerCornerRadius - min(theme.borderInsets.width, theme.borderInsets.height), backgroundRect.height * 0.5) + let innerCornerRadius: Double = theme.hilitedCornerRadius.clamp(min: outerCornerRadius - min(theme.borderInsets.width, theme.borderInsets.height), max: backgroundRect.height * 0.5) let panelPath: CGPath?, backgroundPath: CGPath? if !theme.isLinear || pagingView.isHidden { - panelPath = .squirclePath(rect: panelRect, cornerRadius: outerCornerRadius) - backgroundPath = .squirclePath(rect: backgroundRect, cornerRadius: innerCornerRadius) + panelPath = panelRect.squirclePath(cornerRadius: outerCornerRadius) + backgroundPath = backgroundRect.squirclePath(cornerRadius: innerCornerRadius) } else { var mainPanelRect: NSRect = panelRect mainPanelRect.size.height -= pagingRect.height let tailPanelRect = pagingRect.offsetBy(dx: 0, dy: theme.borderInsets.height).insetBy(dx: -theme.borderInsets.width, dy: 0) - panelPath = .squirclePath(polygon: .init(head: mainPanelRect, body: tailPanelRect, tail: .zero), cornerRadius: outerCornerRadius) + panelPath = SquirrelTextPolygon(head: mainPanelRect, body: tailPanelRect, tail: .zero).squirclePath(cornerRadius: outerCornerRadius) var mainBackgroundRect: NSRect = backgroundRect mainBackgroundRect.size.height -= pagingRect.height - backgroundPath = .squirclePath(polygon: .init(head: mainBackgroundRect, body: pagingRect, tail: .zero), cornerRadius: innerCornerRadius) + backgroundPath = SquirrelTextPolygon(head: mainBackgroundRect, body: pagingRect, tail: .zero).squirclePath(cornerRadius: innerCornerRadius) } let borderPath: CGPath? = .combinePaths(panelPath, backgroundPath) var flip = CGAffineTransform(translationX: 0, y: panelRect.height) @@ -2369,22 +2182,19 @@ final class SquirrelView: NSView { } // highlighted candidate layer if !scrollView.isHidden { - var translate = CGAffineTransform(translationX: -clipRect.minX, y: -clipRect.minY) - clipLayer.path = clipPath?.copy(using: &translate) + clipLayer.path = scrollView.bounds.squirclePath(cornerRadius: hilitedCornerRadius) var activePagePath: CGMutablePath? - let expanded: Bool = candidateInfos.count > theme.pageSize + let expanded: Bool = theme.isTabular && candidateInfos.count > theme.pageSize if expanded { let activePageRect: NSRect = sectionRects[sectionNum] - activePagePath = .squircleMutablePath(vertices: activePageRect.vertices, cornerRadius: hilitedCornerRadius) + activePagePath = .squirclePath(vertices: activePageRect.vertices, cornerRadius: hilitedCornerRadius) documentPath?.addPath(activePagePath!.copy()!) } if theme.candidateBackColor != nil { let nonHilitedCandidatePath = CGMutablePath() - let stackColors: Bool = theme.stackColors && theme.candidateBackColor!.alphaComponent < 0.999 + let stackColors: Bool = theme.stackColors && theme.candidateBackColor!.alphaComponent < 1.0.nextDown for i in 0 ..< candidateInfos.count { - if i != hilitedCandidate, let candidatePath: CGPath = theme.isLinear - ? .squirclePath(polygon: candidatePolygons[i], cornerRadius: hilitedCornerRadius) - : .squirclePath(rect: candidatePolygons[i].body, cornerRadius: hilitedCornerRadius) { + if i != hilitedCandidate, let candidatePath = theme.isLinear ? candidatePolygons[i].squirclePath(cornerRadius: hilitedCornerRadius) : candidatePolygons[i].body.squirclePath(cornerRadius: hilitedCornerRadius) { nonHilitedCandidatePath.addPath(candidatePath) if stackColors { (expanded && i / theme.pageSize == hilitedCandidate! / theme.pageSize ? activePagePath : documentPath)?.addPath(candidatePath) @@ -2396,10 +2206,8 @@ final class SquirrelView: NSView { } else { nonHilitedCandidateLayer.isHidden = true } - if hilitedCandidate != nil && theme.hilitedCandidateBackColor != nil, let hilitedCandidatePath: CGPath = theme.isLinear - ? .squirclePath(polygon: candidatePolygons[hilitedCandidate!], cornerRadius: hilitedCornerRadius) - : .squirclePath(rect: candidatePolygons[hilitedCandidate!].body, cornerRadius: hilitedCornerRadius) { - if theme.stackColors && theme.hilitedCandidateBackColor!.alphaComponent < 0.999 { + if hilitedCandidate != nil && theme.hilitedCandidateBackColor != nil, let hilitedCandidatePath = theme.isLinear ? candidatePolygons[hilitedCandidate!].squirclePath(cornerRadius: hilitedCornerRadius) : candidatePolygons[hilitedCandidate!].body.squirclePath(cornerRadius: hilitedCornerRadius) { + if theme.stackColors && theme.hilitedCandidateBackColor!.alphaComponent < 1.0.nextDown { (expanded ? activePagePath : documentPath)?.addPath(hilitedCandidatePath.copy()!) } hilitedCandidateLayer.path = hilitedCandidatePath @@ -2422,10 +2230,8 @@ final class SquirrelView: NSView { } } // function buttons (page up, page down, backspace) layer - var functionButtonPath: CGPath? - if functionButton != .VoidSymbol { - functionButtonPath = updateFunctionButtonLayer() - } else { + let functionButtonPath: CGPath? = updateFunctionButtonLayer() + if functionButtonPath == nil { functionButtonLayer.isHidden = true } // logo at the beginning for status message @@ -2438,7 +2244,7 @@ final class SquirrelView: NSView { // background image (pattern style) layer if theme.backImage != nil { var transform: CGAffineTransform = theme.isVertical ? CGAffineTransform(rotationAngle: .pi / 2) : CGAffineTransformIdentity - transform = transform.translatedBy(x: -backgroundRect.origin.x, y: -backgroundRect.origin.y) + transform = transform.translatedBy(x: -backgroundRect.minX, y: -backgroundRect.minY) backImageLayer.path = backgroundPath?.copy(using: &transform) backImageLayer.setAffineTransform(transform.inverted()) } @@ -2447,7 +2253,7 @@ final class SquirrelView: NSView { if clipPath != nil { let nonCandidatePath = backgroundPath?.mutableCopy() nonCandidatePath?.addPath(clipPath!) - if theme.stackColors && theme.hilitedPreeditBackColor != nil && theme.hilitedPreeditBackColor!.alphaComponent < 0.999 { + if theme.stackColors && theme.hilitedPreeditBackColor != nil && theme.hilitedPreeditBackColor!.alphaComponent < 1.0.nextDown { if hilitedPreeditPath != nil { nonCandidatePath?.addPath(hilitedPreeditPath!) } @@ -2469,42 +2275,38 @@ final class SquirrelView: NSView { unclipHighlightedCandidate() } - func index(mouseSpot spot: NSPoint) -> SquirrelIndex { + func index(mouseSpot spot: NSPoint) -> SquirrelIndex? { var point = convert(spot, from: nil) - if NSMouseInRect(point, bounds, true) { - if NSMouseInRect(point, preeditRect, true) { - return NSMouseInRect(point, deleteBackRect, true) ? .BackSpaceKey : .CodeInputArea - } - if NSMouseInRect(point, expanderRect, true) { - return .ExpandButton - } - if NSMouseInRect(point, pageUpRect, true) { - return .PageUpKey - } - if NSMouseInRect(point, pageDownRect, true) { - return .PageDownKey - } - if NSMouseInRect(point, clipRect, true) { - point = convert(point, to: documentView) - for i in 0 ..< candidateInfos.count { - if candidatePolygons[i].mouseInPolygon(point: point, flipped: true) { - return SquirrelIndex(rawValue: i)! - } - } - } + guard NSMouseInRect(point, bounds, true) else { return nil } + if NSMouseInRect(point, preeditRect, true) { + return NSMouseInRect(point, deleteBackRect, true) ? .BackSpaceKey : .CodeInputArea + } + if NSMouseInRect(point, expanderRect, true) { + return .ExpandButton + } + if NSMouseInRect(point, pageUpRect, true) { + return .PageUpKey + } + if NSMouseInRect(point, pageDownRect, true) { + return .PageDownKey + } + guard NSMouseInRect(point, clipRect, true) else { return nil } + point = convert(point, to: documentView) + if let idx = candidatePolygons.firstIndex(where: { $0.mouseInPolygon(point: point, flipped: true) }) { + return .Ordinal(idx) } - return .VoidSymbol + return nil } -} // SquirrelView +} // SquirrelView @frozen enum SquirrelTooltipDisplay: Sendable { case now, delayed, onRequest, none } /* In order to put SquirrelPanel above client app windows, - SquirrelPanel needs to be assigned a window level higher - than kCGHelpWindowLevelKey that the system tooltips use. - This class makes system-alike tooltips above SquirrelPanel */ + SquirrelPanel needs to be assigned a window level higher + than kCGHelpWindowLevelKey that the system tooltips use. + This class makes system-alike tooltips above SquirrelPanel */ final class SquirrelToolTip: NSPanel { private let backView = NSVisualEffectView() private let textView = NSTextField() @@ -2524,6 +2326,8 @@ final class SquirrelToolTip: NSPanel { textView.bezelStyle = .squareBezel textView.isBordered = true textView.isSelectable = false + textView.usesSingleLineMode = false + textView.lineBreakMode = .byWordWrapping contentView.addSubview(textView) self.contentView = contentView } @@ -2537,10 +2341,13 @@ final class SquirrelToolTip: NSPanel { isEmpty = false textView.stringValue = toolTip + textView.preferredMaxLayoutWidth = panel.screen!.visibleFrame.width * 0.25 textView.font = .toolTipsFont(ofSize: 0) textView.textColor = .windowFrameTextColor textView.sizeToFit() - let contentSize: NSSize = textView.fittingSize + var contentSize: NSSize = textView.fittingSize + contentSize.width += 3 + contentSize.height += 3 var spot: NSPoint = NSEvent.mouseLocation let cursor: NSCursor! = .currentSystem @@ -2549,10 +2356,10 @@ final class SquirrelToolTip: NSPanel { var windowRect = NSRect(x: spot.x, y: spot.y - contentSize.height, width: contentSize.width, height: contentSize.height) let screenRect: NSRect = panel.screen!.visibleFrame - if windowRect.maxX > screenRect.maxX - 0.1 { + if windowRect.maxX > screenRect.maxX.nextDown { windowRect.origin.x = screenRect.maxX - windowRect.width } - if windowRect.minY < screenRect.minY + 0.1 { + if windowRect.minY < screenRect.minY.nextUp { windowRect.origin.y = screenRect.minY } windowRect = panel.screen!.backingAlignedRect(windowRect, options: [.alignAllEdgesNearest]) @@ -2567,8 +2374,7 @@ final class SquirrelToolTip: NSPanel { show() case .delayed: showTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in self.show() } - default: - break + default: break } } @@ -2587,9 +2393,7 @@ final class SquirrelToolTip: NSPanel { showTimer = nil hideTimer?.invalidate() hideTimer = nil - if isVisible { - orderOut(nil) - } + if isVisible { orderOut(nil) } } func clear() { @@ -2597,11 +2401,13 @@ final class SquirrelToolTip: NSPanel { textView.stringValue = "" hide() } -} // SquirrelToolTipView +} // SquirrelToolTipView // MARK: Panel window, dealing with text content and mouse interactions final class SquirrelPanel: NSPanel, NSWindowDelegate { + static private let kShowStatusDuration: TimeInterval = 2.0 + static private let kOffsetGap: Double = 5 // Squirrel panel layouts @available(macOS 10.14, *) private let back = NSVisualEffectView() private let toolTip = SquirrelToolTip() @@ -2609,7 +2415,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { private var statusTimer: Timer? private var maxSizeAttained: NSSize = .zero private var scrollLocus: NSPoint = .zero - private var cursorIndex: SquirrelIndex = .VoidSymbol + private var cursorIndex: SquirrelIndex? private var textWidthLimit: Double = CGFLOAT_MAX private var anchorOffset: Double = 0 private var scrollByLine: Bool = false @@ -2631,65 +2437,61 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { // Linear candidate list layout, as opposed to stacked candidate list layout. var isLinear: Bool { view.theme.isLinear } /* Tabular candidate list layout, initializes as tab-aligned linear layout, - expandable to stack 5 (3 for vertical) pages/sections of candidates */ + expandable to stack 5 (3 for vertical) pages/sections of candidates */ var isTabular: Bool { view.theme.isTabular } var isLocked: Bool { get { return view.isLocked } - set (newValue) { - if view.theme.isTabular && view.isLocked != newValue { - view.isLocked = isLocked - let userConfig = SquirrelConfig("user") - _ = userConfig.setOption("var/option/_isLockedTabular", withBool: newValue) - if newValue { - _ = userConfig.setOption("var/option/_isExpandedTabular", withBool: view.isExpanded) - } - userConfig.close() + set { + guard view.theme.isTabular && view.isLocked != newValue else { return } + view.isLocked = newValue + let userConfig = SquirrelConfig(.user) + _ = userConfig.setOption("var/option/_isLockedTabular", withBool: newValue) + if newValue { + _ = userConfig.setOption("var/option/_isExpandedTabular", withBool: view.isExpanded) } + userConfig.close() } } private func getLocked() { - if view.theme.isTabular { - let userConfig = SquirrelConfig("user") - view.isLocked = userConfig.boolValue(forOption: "var/option/_isLockedTabular") - if view.isLocked { - view.isExpanded = userConfig.boolValue(forOption: "var/option/_isExpandedTabular") - } - userConfig.close() - view.sectionNum = 0 + guard view.theme.isTabular else { return } + let userConfig = SquirrelConfig(.user) + view.isLocked = userConfig.boolValue(forOption: "var/option/_isLockedTabular") + if view.isLocked { + view.isExpanded = userConfig.boolValue(forOption: "var/option/_isExpandedTabular") } + userConfig.close() + view.sectionNum = 0 } var isFirstLine: Bool { view.tabularIndices.isEmpty ? true : view.tabularIndices[highlightedCandidate!].lineNum == 0 } var isExpanded: Bool { get { return view.isExpanded } - set (newValue) { - if view.theme.isTabular && !view.isLocked && !(isLastPage && pageNum == 0) && view.isExpanded != newValue { - view.isExpanded = newValue - view.sectionNum = 0 - needsRedraw = true - } + set { + guard view.theme.isTabular && !view.isLocked && !(isLastPage && pageNum == 0) && view.isExpanded != newValue else { return } + view.isExpanded = newValue + view.sectionNum = 0 + needsRedraw |= true } } var sectionNum: Int { get { return view.sectionNum } - set (newValue) { - if view.theme.isTabular && view.isExpanded && view.sectionNum != newValue { - view.sectionNum = clamp(newValue, 0, view.theme.isVertical ? 2 : 4) - } + set { + guard view.theme.isTabular && view.isExpanded && view.sectionNum != newValue else { return } + view.sectionNum = newValue.clamp(min: 0, max: view.theme.isVertical ? 2 : 4) } } // position of the text input I-beam cursor on screen. var IbeamRect: NSRect = .zero { didSet { - if oldValue != IbeamRect { - needsRedraw = true - if !IbeamRect.intersects(_screen.frame) { - updateScreen() - updateDisplayParameters() - } + guard oldValue != IbeamRect else { return } + needsRedraw |= true + if IbeamRect.isEmpty { initPosition |= true } + if !IbeamRect.intersects(_screen.frame) { + updateScreen() + updateDisplayParameters() } } } @@ -2699,10 +2501,9 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { weak var inputController: SquirrelInputController? var style: SquirrelStyle { didSet { - if oldValue != style { - view.style = style - appearance = NSAppearance(named: style == .dark ? .darkAqua : .aqua) - } + guard oldValue != style else { return } + view.style = style + appearance = NSAppearance(named: style == .dark ? .darkAqua : .aqua) } } @@ -2715,12 +2516,13 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { init() { style = .light super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: true) - level = .init(Int(CGWindowLevelForKey(.cursorWindow) - 100)) + level = NSWindow.Level(Int(CGWindowLevelForKey(.cursorWindow) - 100)) hasShadow = false isOpaque = false backgroundColor = .clear delegate = self acceptsMouseMovedEvents = true + worksWhenModal = true let contentView = NSFlippedView() contentView.autoresizesSubviews = false @@ -2757,75 +2559,73 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { initPosition = true maxSizeAttained = .zero - view.candidateView.setLayoutOrientation(view.theme.isVertical ? .vertical : .horizontal) - view.preeditView.setLayoutOrientation(view.theme.isVertical ? .vertical : .horizontal) - view.pagingView.setLayoutOrientation(view.theme.isVertical ? .vertical : .horizontal) - view.statusView.setLayoutOrientation(view.theme.isVertical ? .vertical : .horizontal) + view.candidateView.setLayoutOrientation(theme.isVertical ? .vertical : .horizontal) + view.preeditView.setLayoutOrientation(theme.isVertical ? .vertical : .horizontal) + view.pagingView.setLayoutOrientation(theme.isVertical ? .vertical : .horizontal) + view.statusView.setLayoutOrientation(theme.isVertical ? .vertical : .horizontal) // rotate the view, the core in vertical mode! - contentView!.boundsRotation = view.theme.isVertical ? 90.0 : 0.0 - view.candidateView.boundsRotation = 0.0 - view.preeditView.boundsRotation = 0.0 - view.pagingView.boundsRotation = 0.0 - view.statusView.boundsRotation = 0.0 + contentView!.boundsRotation = theme.isVertical ? 90 : .zero + view.candidateView.boundsRotation = .zero + view.preeditView.boundsRotation = .zero + view.pagingView.boundsRotation = .zero + view.statusView.boundsRotation = .zero view.candidateView.setBoundsOrigin(.zero) view.preeditView.setBoundsOrigin(.zero) view.pagingView.setBoundsOrigin(.zero) view.statusView.setBoundsOrigin(.zero) - view.scrollView.lineScroll = view.theme.candidateParagraphStyle.minimumLineHeight - view.candidateView.contentBlock = view.theme.isLinear ? .linearCandidates : .stackedCandidates - view.candidateView.defaultParagraphStyle = view.theme.candidateParagraphStyle - view.preeditView.defaultParagraphStyle = view.theme.preeditParagraphStyle - view.pagingView.defaultParagraphStyle = view.theme.pagingParagraphStyle - view.statusView.defaultParagraphStyle = view.theme.statusParagraphStyle + view.scrollView.lineScroll = theme.candidateParagraphStyle.minimumLineHeight + view.candidateView.contentBlock = theme.isLinear ? .linearCandidates : .stackedCandidates + view.candidateView.defaultParagraphStyle = theme.candidateParagraphStyle + view.preeditView.defaultParagraphStyle = theme.preeditParagraphStyle + view.pagingView.defaultParagraphStyle = theme.pagingParagraphStyle + view.statusView.defaultParagraphStyle = theme.statusParagraphStyle // size limits on textContainer let screenRect: NSRect = _screen.visibleFrame let textWidthRatio: Double = min(0.8, 1.0 / (theme.isVertical ? 4 : 3) + (theme.textAttrs[.font] as! NSFont).pointSize / 144.0) - textWidthLimit = ceil((theme.isVertical ? screenRect.height : screenRect.width) * textWidthRatio - theme.borderInsets.width * 2 - theme.fullWidth) - if view.theme.lineLength > 0.1 { - textWidthLimit = min(theme.lineLength, textWidthLimit) + textWidthLimit = ((theme.isVertical ? screenRect.height : screenRect.width) * textWidthRatio - theme.borderInsets.width * 2 - theme.fullWidth).rounded(.up) + if theme.lineLength.isNormal && theme.lineLength < textWidthLimit { + textWidthLimit = theme.lineLength } if view.theme.isTabular { - textWidthLimit = floor((textWidthLimit + theme.fullWidth) / (theme.fullWidth * 2)) * (theme.fullWidth * 2) - theme.fullWidth + textWidthLimit = (textWidthLimit / (theme.fullWidth * 2)).rounded(.down) * (theme.fullWidth * 2) } - view.candidateView.textContainer!.size = .init(width: textWidthLimit, height: CGFLOAT_MAX) - view.preeditView.textContainer!.size = .init(width: textWidthLimit, height: CGFLOAT_MAX) - view.pagingView.textContainer!.size = .init(width: textWidthLimit, height: CGFLOAT_MAX) - view.statusView.textContainer!.size = .init(width: textWidthLimit, height: CGFLOAT_MAX) + view.candidateView.textContainer!.size = NSSize(width: textWidthLimit, height: .zero) + view.preeditView.textContainer!.size = NSSize(width: textWidthLimit, height: .zero) + view.pagingView.textContainer!.size = NSSize(width: textWidthLimit, height: .zero) + view.statusView.textContainer!.size = NSSize(width: textWidthLimit, height: .zero) // color, opacity and transluecency - alphaValue = view.theme.opacity + alphaValue = theme.opacity // resize logo and background image, if any - let statusHeight: Double = view.theme.statusParagraphStyle.minimumLineHeight - let logoRect = NSRect(x: view.theme.borderInsets.width - 0.1 * statusHeight, y: view.theme.borderInsets.height - 0.1 * statusHeight, width: statusHeight * 1.2, height: statusHeight * 1.2) + let statusHeight: Double = theme.statusParagraphStyle.minimumLineHeight + let logoRect = NSRect(x: theme.borderInsets.width - 0.1 * statusHeight, y: theme.borderInsets.height - 0.1 * statusHeight, width: statusHeight * 1.2, height: statusHeight * 1.2) view.logoLayer.frame = logoRect let logoImage = NSImage(named: NSImage.applicationIconName)! logoImage.size = logoRect.size view.logoLayer.contents = logoImage - view.logoLayer.setAffineTransform(view.theme.isVertical ? CGAffineTransform(rotationAngle: -.pi / 2) : CGAffineTransformIdentity) + view.logoLayer.setAffineTransform(theme.isVertical ? .init(rotationAngle: -.pi / 2) : .identity) if let lightBackImage = SquirrelView.lightTheme.backImage, lightBackImage.isValid { let widthLimit: Double = textWidthLimit + SquirrelView.lightTheme.fullWidth lightBackImage.resizingMode = .stretch - lightBackImage.size = SquirrelView.lightTheme.isVertical ? .init(width: lightBackImage.size.width / lightBackImage.size.height * widthLimit, height: widthLimit) : .init(width: widthLimit, height: lightBackImage.size.height / lightBackImage.size.width * widthLimit) + lightBackImage.size = SquirrelView.lightTheme.isVertical ? NSSize(width: lightBackImage.size.width / lightBackImage.size.height * widthLimit, height: widthLimit) : NSSize(width: widthLimit, height: lightBackImage.size.height / lightBackImage.size.width * widthLimit) } if #available(macOS 10.14, *) { - back.isHidden = view.theme.translucency < 0.001 + back.isHidden = view.theme.translucency.isFinite && !view.theme.translucency.isNormal if let darkBackImage = SquirrelView.darkTheme.backImage, darkBackImage.isValid { let widthLimit: Double = textWidthLimit + SquirrelView.darkTheme.fullWidth darkBackImage.resizingMode = .stretch - darkBackImage.size = SquirrelView.darkTheme.isVertical ? .init(width: darkBackImage.size.width / darkBackImage.size.height * widthLimit, height: widthLimit) : .init(width: widthLimit, height: darkBackImage.size.height / darkBackImage.size.width * widthLimit) + darkBackImage.size = SquirrelView.darkTheme.isVertical ? NSSize(width: darkBackImage.size.width / darkBackImage.size.height * widthLimit, height: widthLimit) : NSSize(width: widthLimit, height: darkBackImage.size.height / darkBackImage.size.width * widthLimit) } } view.updateColors() } func candidateIndex(onDirection arrowKey: SquirrelIndex) -> Int? { - if highlightedCandidate == nil || !isTabular || indexRange.count == 0 { - return nil - } - let currentTab: Int = view.tabularIndices[highlightedCandidate!].tabNum - let currentLine: Int = view.tabularIndices[highlightedCandidate!].lineNum + guard let highlightedCandidate = highlightedCandidate, isTabular && !indexRange.isEmpty else { return nil } + let currentTab: Int = view.tabularIndices[highlightedCandidate].tabNum + let currentLine: Int = view.tabularIndices[highlightedCandidate].lineNum let finalLine: Int = view.tabularIndices[indexRange.count - 1].lineNum if arrowKey == (view.theme.isVertical ? .LeftKey : .DownKey) { if highlightedCandidate == indexRange.count - 1 && isLastPage { @@ -2834,10 +2634,8 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { if currentLine == finalLine && !isLastPage { return indexRange.upperBound } - var newIndex: Int = highlightedCandidate! + 1 - while newIndex < indexRange.count && (view.tabularIndices[newIndex].lineNum == currentLine || - (view.tabularIndices[newIndex].lineNum == currentLine + 1 && - view.tabularIndices[newIndex].tabNum <= currentTab)) { + var newIndex: Int = highlightedCandidate + 1 + while newIndex < indexRange.count && (view.tabularIndices[newIndex].lineNum == currentLine || (view.tabularIndices[newIndex].lineNum == currentLine + 1 && view.tabularIndices[newIndex].tabNum <= currentTab)) { newIndex += 1 } if newIndex != indexRange.count || isLastPage { @@ -2848,10 +2646,8 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { if currentLine == 0 { return pageNum == 0 ? nil : indexRange.lowerBound - 1 } - var newIndex: Int = highlightedCandidate! - 1 - while newIndex > 0 && (view.tabularIndices[newIndex].lineNum == currentLine || - (view.tabularIndices[newIndex].lineNum == currentLine - 1 && - view.tabularIndices[newIndex].tabNum > currentTab)) { + var newIndex: Int = highlightedCandidate - 1 + while newIndex > 0 && (view.tabularIndices[newIndex].lineNum == currentLine || (view.tabularIndices[newIndex].lineNum == currentLine - 1 && view.tabularIndices[newIndex].tabNum > currentTab)) { newIndex -= 1 } return newIndex + indexRange.lowerBound @@ -2864,86 +2660,85 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let theme: SquirrelTheme = view.theme switch event.type { case .leftMouseDown: - if event.clickCount == 1 && cursorIndex == .CodeInputArea && caretPos != nil { - let spot: NSPoint = view.preeditView.convert(mouseLocationOutsideOfEventStream, from: nil) - let inputIndex: Int = view.preeditView.characterIndexForInsertion(at: spot) - if inputIndex == 0 { - inputController?.perform(action: .PROCESS, onIndex: .HomeKey) - } else if inputIndex < caretPos! { - inputController?.moveCursor(caretPos!, to: inputIndex, inlinePreedit: false, inlineCandidate: false) - } else if inputIndex >= view.preeditContents.length - 2 { - inputController?.perform(action: .PROCESS, onIndex: .EndKey) - } else if inputIndex > caretPos! + 1 { - inputController?.moveCursor(caretPos!, to: inputIndex - 1, inlinePreedit: false, inlineCandidate: false) - } + guard event.clickCount == 1, let cursorIndex = cursorIndex, cursorIndex == .CodeInputArea, caretPos != nil else { return } + let spot: NSPoint = view.preeditView.convert(mouseLocationOutsideOfEventStream, from: nil) + let inputIndex: Int = view.preeditView.characterIndexForInsertion(at: spot) + if inputIndex == 0 { + inputController?.perform(action: .Process, onIndex: .HomeKey) + } else if inputIndex < caretPos! { + inputController?.moveCursor(caretPos!, to: inputIndex, inlinePreedit: false, inlineCandidate: false) + } else if inputIndex >= view.preeditContents.length - 2 { + inputController?.perform(action: .Process, onIndex: .EndKey) + } else if inputIndex > caretPos! + 1 { + inputController?.moveCursor(caretPos!, to: inputIndex - 1, inlinePreedit: false, inlineCandidate: false) } case .leftMouseUp: - if event.clickCount == 1 && cursorIndex != nil { - if cursorIndex == highlightedCandidate { - inputController?.perform(action: .SELECT, onIndex: cursorIndex + indexRange.lowerBound) - } else if cursorIndex == functionButton { - if cursorIndex == .ExpandButton { - if view.isLocked { - isLocked = false - view.pagingContents.replaceCharacters(in: NSRange(location: view.pagingContents.length / 2, length: 1), with: (view.isExpanded ? theme.symbolCompress : theme.symbolExpand)!) - view.pagingView.setNeedsDisplay(view.convert(view.expanderRect, to: view.pagingView)) - } else { - isExpanded = !view.isExpanded - sectionNum = 0 - } + guard event.clickCount == 1, let cursorIndex = cursorIndex else { return } + if cursorIndex == highlightedCandidate { + inputController?.perform(action: .Select, onIndex: cursorIndex + indexRange.lowerBound) + } else if cursorIndex == functionButton { + if cursorIndex == .ExpandButton { + if view.isLocked { + isLocked = false + view.pagingContents.replaceCharacters(in: NSRange(location: view.pagingContents.length / 2, length: 1), with: (view.isExpanded ? theme.symbolCompress : theme.symbolExpand)!) + view.pagingView.setNeedsDisplay(view.convert(view.expanderRect, to: view.pagingView)) + } else { + isExpanded = !view.isExpanded + sectionNum = 0 } - inputController?.perform(action: .PROCESS, onIndex: cursorIndex) } + inputController?.perform(action: .Process, onIndex: cursorIndex) } case .rightMouseUp: - if event.clickCount == 1 && cursorIndex != nil { - if cursorIndex == highlightedCandidate { - inputController?.perform(action: .DELETE, onIndex: cursorIndex + indexRange.lowerBound) - } else if cursorIndex == functionButton { - switch functionButton { - case .PageUpKey: - inputController?.perform(action: .PROCESS, onIndex: .HomeKey) - case .PageDownKey: - inputController?.perform(action: .PROCESS, onIndex: .EndKey) - case .ExpandButton: - isLocked = !view.isLocked - view.pagingContents.replaceCharacters(in: NSRange(location: view.pagingContents.length / 2, length: 1), with: (view.isLocked ? theme.symbolLock : view.isExpanded ? theme.symbolCompress : theme.symbolExpand)!) - view.pagingContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: NSRange(location: view.pagingContents.length / 2, length: 1)) - view.pagingView.setNeedsDisplay(view.convert(view.expanderRect, to: view.pagingView), avoidAdditionalLayout: true) - inputController?.perform(action: .PROCESS, onIndex: .LockButton) - case .BackSpaceKey: - inputController?.perform(action: .PROCESS, onIndex: .EscapeKey) - default: - break - } + guard event.clickCount == 1, let cursorIndex = cursorIndex else { return } + if cursorIndex == highlightedCandidate { + inputController?.perform(action: .Delete, onIndex: cursorIndex + indexRange.lowerBound) + } else if cursorIndex == functionButton { + switch functionButton { + case .PageUpKey: + inputController?.perform(action: .Process, onIndex: .HomeKey) + case .PageDownKey: + inputController?.perform(action: .Process, onIndex: .EndKey) + case .ExpandButton: + isLocked = !view.isLocked + view.pagingContents.replaceCharacters(in: NSRange(location: view.pagingContents.length / 2, length: 1), with: view.isLocked ? theme.symbolLock! : view.isExpanded ? theme.symbolCompress! : theme.symbolExpand!) + view.pagingContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: NSRange(location: view.pagingContents.length / 2, length: 1)) + view.pagingView.setNeedsDisplay(view.convert(view.expanderRect, to: view.pagingView), avoidAdditionalLayout: true) + inputController?.perform(action: .Process, onIndex: .LockButton) + case .BackSpaceKey: + inputController?.perform(action: .Process, onIndex: .EscapeKey) + default: break } } case .mouseMoved: if event.modifierFlags.intersection(.deviceIndependentFlagsMask) == [.control] { return } let noDelay: Bool = event.modifierFlags.intersection(.deviceIndependentFlagsMask) == [.option] cursorIndex = view.index(mouseSpot: mouseLocationOutsideOfEventStream) - if cursorIndex == .VoidSymbol { - toolTip.clear() - highlightFunctionButton(.VoidSymbol, displayToolTip: .none) - } else { + if let cursorIndex = cursorIndex { if cursorIndex != highlightedCandidate && cursorIndex != functionButton { toolTip.clear() } else if noDelay { toolTip.show() } - if cursorIndex >= 0 && cursorIndex < indexRange.count && cursorIndex != highlightedCandidate { - highlightFunctionButton(.VoidSymbol, displayToolTip: .none) + if 0 ..< indexRange.count ~= cursorIndex.rawValue && cursorIndex != highlightedCandidate { + if functionButton != .VoidSymbol { + highlightFunctionButton(.VoidSymbol, displayToolTip: .none) + } if theme.isLinear && view.candidateInfos[cursorIndex.rawValue].isTruncated { toolTip.show(withToolTip: view.candidateContents.mutableString.substring(with: view.candidateInfos[cursorIndex.rawValue].candidateRange), display: .now) } else { - toolTip.show(withToolTip: NSLocalizedString("candidate", comment: ""), display: .onRequest ) + toolTip.show(withToolTip: NSLocalizedString("candidate", tableName: "Tooltips", comment: ""), display: .onRequest ) } sectionNum = cursorIndex.rawValue / theme.pageSize - inputController?.perform(action: .HIGHLIGHT, onIndex: cursorIndex + indexRange.lowerBound) - } else if (cursorIndex == .PageUpKey || cursorIndex == .PageDownKey || - cursorIndex == .ExpandButton || cursorIndex == .BackSpaceKey) && functionButton != cursorIndex { + inputController?.perform(action: .Highlight, onIndex: cursorIndex + indexRange.lowerBound) + } else if (cursorIndex == .PageUpKey || cursorIndex == .PageDownKey || cursorIndex == .ExpandButton || cursorIndex == .BackSpaceKey) && functionButton != cursorIndex { highlightFunctionButton(cursorIndex, displayToolTip: noDelay ? .now : .delayed) } + } else { + toolTip.clear() + if functionButton != .VoidSymbol { + highlightFunctionButton(.VoidSymbol, displayToolTip: .none) + } } case .mouseExited: cursorIndex = .VoidSymbol @@ -2958,88 +2753,80 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { scrollLocus = .zero scrollByLine = false } else if event.phase == .changed && scrollLocus.x.isFinite && scrollLocus.y.isFinite { - var scrollDistance: Double = 0.0 + var scrollDistance: Double = .zero // determine scrolling direction by confining to sectors within ±30º of any axis - if abs(event.scrollingDeltaX) > abs(event.scrollingDeltaY) * sqrt(3.0) { + if event.scrollingDeltaX.magnitude > event.scrollingDeltaY.magnitude * 3.squareRoot() { scrollDistance = event.scrollingDeltaX * (event.hasPreciseScrollingDeltas ? 1 : scrollThreshold) scrollLocus.x += scrollDistance - } else if abs(event.scrollingDeltaY) > abs(event.scrollingDeltaX) * sqrt(3.0) { + } else if event.scrollingDeltaY.magnitude > event.scrollingDeltaX.magnitude * 3.squareRoot() { scrollDistance = event.scrollingDeltaY * (event.hasPreciseScrollingDeltas ? 1 : scrollThreshold) scrollLocus.y += scrollDistance } // compare accumulated locus length against threshold and limit paging to max once - if scrollLocus.x > scrollThreshold { - if theme.isVertical && view.scrollView.documentVisibleRect.maxY < view.documentRect.maxY - 0.1 { + switch scrollLocus { + case let p where p.x > scrollThreshold: + if theme.isVertical && view.scrollView.documentVisibleRect.maxY < view.documentRect.maxY.nextDown { scrollByLine = true var origin: NSPoint = view.scrollView.contentView.bounds.origin origin.y += min(scrollDistance, view.documentRect.maxY - view.scrollView.documentVisibleRect.maxY) view.scrollView.contentView.scroll(to: origin) - view.scrollView.verticalScroller?.doubleValue = - view.scrollView.documentVisibleRect.minY / view.clippedHeight + view.scrollView.verticalScroller?.doubleValue = view.scrollView.documentVisibleRect.minY / view.clippedHeight } else if !scrollByLine { - inputController?.perform(action: .PROCESS, onIndex: theme.isVertical ? .PageDownKey : .PageUpKey) - scrollLocus = .init(x: Double.infinity, y: Double.infinity) + inputController?.perform(action: .Process, onIndex: theme.isVertical ? .PageDownKey : .PageUpKey) + scrollLocus = NSPoint(x: Double.infinity, y: Double.infinity) } - } else if scrollLocus.y > scrollThreshold { - if view.scrollView.documentVisibleRect.minY > view.documentRect.minY + 0.1 { + case let p where p.y > scrollThreshold: + if view.scrollView.documentVisibleRect.minY > view.documentRect.minY.nextUp { scrollByLine = true var origin: NSPoint = view.scrollView.contentView.bounds.origin origin.y -= min(scrollDistance, view.scrollView.documentVisibleRect.minY - view.documentRect.minY) view.scrollView.contentView.scroll(to: origin) - view.scrollView.verticalScroller?.doubleValue = - view.scrollView.documentVisibleRect.minY / view.clippedHeight + view.scrollView.verticalScroller?.doubleValue = view.scrollView.documentVisibleRect.minY / view.clippedHeight } else if !scrollByLine { - inputController?.perform(action: .PROCESS, onIndex: .PageUpKey) - scrollLocus = .init(x: Double.infinity, y: Double.infinity) + inputController?.perform(action: .Process, onIndex: .PageUpKey) + scrollLocus = NSPoint(x: Double.infinity, y: Double.infinity) } - } else if scrollLocus.x < -scrollThreshold { - if theme.isVertical && view.scrollView.documentVisibleRect.minY > view.documentRect.minY + 0.1 { + case let p where p.x < -scrollThreshold: + if theme.isVertical && view.scrollView.documentVisibleRect.minY > view.documentRect.minY.nextUp { scrollByLine = true var origin: NSPoint = view.scrollView.contentView.bounds.origin origin.y += max(scrollDistance, view.documentRect.minY - view.scrollView.documentVisibleRect.minY) view.scrollView.contentView.scroll(to: origin) - view.scrollView.verticalScroller?.doubleValue = - view.scrollView.documentVisibleRect.minY / view.clippedHeight + view.scrollView.verticalScroller?.doubleValue = view.scrollView.documentVisibleRect.minY / view.clippedHeight } else if !scrollByLine { - inputController?.perform(action: .PROCESS, onIndex: theme.isVertical ? .PageUpKey : .PageDownKey) - scrollLocus = .init(x: Double.infinity, y: Double.infinity) + inputController?.perform(action: .Process, onIndex: theme.isVertical ? .PageUpKey : .PageDownKey) + scrollLocus = NSPoint(x: Double.infinity, y: Double.infinity) } - } else if scrollLocus.y < -scrollThreshold { - if view.scrollView.documentVisibleRect.maxY < view.documentRect.maxY - 0.1 { + case let p where p.y < -scrollThreshold: + if view.scrollView.documentVisibleRect.maxY < view.documentRect.maxY.nextDown { scrollByLine = true var origin: NSPoint = view.scrollView.contentView.bounds.origin origin.y -= max(scrollDistance, view.scrollView.documentVisibleRect.maxY - view.documentRect.maxY) view.scrollView.contentView.scroll(to: origin) - view.scrollView.verticalScroller?.doubleValue = - view.scrollView.documentVisibleRect.minY / view.clippedHeight + view.scrollView.verticalScroller?.doubleValue = view.scrollView.documentVisibleRect.minY / view.clippedHeight } else if !scrollByLine { - inputController?.perform(action: .PROCESS, onIndex: .PageDownKey) - scrollLocus = .init(x: Double.infinity, y: Double.infinity) + inputController?.perform(action: .Process, onIndex: .PageDownKey) + scrollLocus = NSPoint(x: Double.infinity, y: Double.infinity) } + default: break } } - default: - super.sendEvent(event) + default: super.sendEvent(event) } } func showToolTip() -> Bool { - if !toolTip.isEmpty { - toolTip.show() - return true - } - return false + guard !toolTip.isEmpty else { return false } + toolTip.show() + return true } private func highlightCandidate(_ highlightedCandidate: Int?) { - if highlightedCandidate == nil || self.highlightedCandidate == nil { - return - } + guard let highlightedCandidate = highlightedCandidate, let priorHilitedCandidate = self.highlightedCandidate else { return } let theme: SquirrelTheme = view.theme - let priorHilitedCandidate: Int = self.highlightedCandidate! let priorSectionNum: Int = priorHilitedCandidate / theme.pageSize self.highlightedCandidate = highlightedCandidate - view.sectionNum = highlightedCandidate! / theme.pageSize + view.sectionNum = highlightedCandidate / theme.pageSize // apply new foreground colors for i in 0 ..< theme.pageSize { let priorCandidate: Int = i + priorSectionNum * theme.pageSize @@ -3075,8 +2862,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { view.pagingContents.addAttribute(.foregroundColor, value: theme.preeditForeColor, range: NSRange(location: view.pagingContents.length / 2, length: 1)) case .BackSpaceKey: view.preeditContents.addAttribute(.foregroundColor, value: theme.preeditForeColor, range: NSRange(location: view.preeditContents.length - 1, length: 1)) - default: - break + default: break } self.functionButton = functionButton var newFunctionButton: SquirrelIndex = .VoidSymbol @@ -3084,33 +2870,27 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { case .PageUpKey: view.pagingContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: NSRange(location: 0, length: 1)) newFunctionButton = pageNum == 0 ? .HomeKey : .PageUpKey - toolTip.show(withToolTip: NSLocalizedString(pageNum == 0 ? "home" : "page_up", comment: ""), display: display) + toolTip.show(withToolTip: NSLocalizedString(pageNum == 0 ? "home" : "page_up", tableName: "Tooltips", comment: ""), display: display) case .PageDownKey: view.pagingContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: NSRange(location: view.pagingContents.length - 1, length: 1)) newFunctionButton = isLastPage ? .EndKey : .PageDownKey - toolTip.show(withToolTip: NSLocalizedString(isLastPage ? "end" : "pageDown", comment: ""), display: display) + toolTip.show(withToolTip: NSLocalizedString(isLastPage ? "end" : "page_down", tableName: "Tooltips", comment: ""), display: display) case .ExpandButton: view.pagingContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: NSRange(location: view.pagingContents.length / 2, length: 1)) newFunctionButton = view.isLocked ? .LockButton : view.isExpanded ? .CompressButton : .ExpandButton - toolTip.show(withToolTip: NSLocalizedString(view.isLocked ? "unlock" : view.isExpanded ? "compress" : "expand", comment: ""), display: display) + toolTip.show(withToolTip: NSLocalizedString(view.isLocked ? "unlock" : view.isExpanded ? "compress" : "expand", tableName: "Tooltips", comment: ""), display: display) case .BackSpaceKey: view.preeditContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: NSRange(location: view.preeditContents.length - 1, length: 1)) newFunctionButton = caretPos == nil || caretPos == 0 ? .EscapeKey : .BackSpaceKey - toolTip.show(withToolTip: NSLocalizedString(caretPos == nil || caretPos == 0 ? "escape" : "delete", comment: ""), display: display) - default: - break + toolTip.show(withToolTip: NSLocalizedString(caretPos == nil || caretPos == 0 ? "escape" : "delete", tableName: "Tooltips", comment: ""), display: display) + default: break } view.highlightFunctionButton(newFunctionButton) displayIfNeeded() } private func updateScreen() { - for screen in NSScreen.screens { - if screen.frame.contains(IbeamRect.origin) { - _screen = screen; return - } - } - _screen = .main + _screen = NSScreen.screens.first(where: { $0.frame.contains(IbeamRect.origin) }) ?? .main } // Get the window size, it will be the dirtyRect in SquirrelView.drawRect @@ -3125,30 +2905,33 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let screenRect: NSRect = _screen.visibleFrame // the sweep direction of the client app changes the behavior of adjusting Squirrel panel position - let sweepVertical: Bool = IbeamRect.width > IbeamRect.height + let sweepVertical: Bool = IbeamRect.width > IbeamRect.height.nextUp var contentRect: NSRect = view.contentRect // fixed line length (text width), but not applicable to status message - if theme.lineLength > 0.1 && view.statusView.isHidden { + if theme.lineLength.isNormal && view.statusView.isHidden { contentRect.size.width = textWidthLimit } /* remember panel size (fix the top leading anchor of the panel in screen coordiantes) - but only when the text would expand on the side of upstream (i.e. towards the beginning of text) */ + but only when the text would expand upstreams (towards the leading and/or top edges) */ if theme.rememberSize && view.statusView.isHidden { - if theme.lineLength < 0.1 && theme.isVertical - ? sweepVertical ? (IbeamRect.minY - max(contentRect.width, maxSizeAttained.width) - border.width - floor(theme.fullWidth * 0.5) < screenRect.minY + 0.1) - : (IbeamRect.minY - kOffsetGap - screenRect.height * textWidthRatio - border.width * 2 - theme.fullWidth < screenRect.minY + 0.1) - : sweepVertical ? (IbeamRect.minX - kOffsetGap - screenRect.width * textWidthRatio - border.width * 2 - theme.fullWidth > screenRect.minX + 0.1) - : (IbeamRect.maxX + max(contentRect.width, maxSizeAttained.width) + border.width + floor(theme.fullWidth * 0.5) > screenRect.maxX - 0.1) { - if contentRect.width > maxSizeAttained.width + 0.1 { - maxSizeAttained.width = contentRect.width - } else { - contentRect.size.width = maxSizeAttained.width + if theme.lineLength.isFinite && !theme.lineLength.isNormal { + let attained: Bool = theme.isVertical + ? sweepVertical ? (IbeamRect.minY - max(contentRect.width, maxSizeAttained.width) - border.width - (theme.fullWidth * 0.5).rounded(.down) < screenRect.minY.nextUp) + : (IbeamRect.minY - Self.kOffsetGap - screenRect.height * textWidthRatio - border.width * 2 - theme.fullWidth < screenRect.minY.nextUp) + : sweepVertical ? (IbeamRect.minX - Self.kOffsetGap - screenRect.width * textWidthRatio - border.width * 2 - theme.fullWidth > screenRect.minX.nextUp) + : (IbeamRect.maxX + max(contentRect.width, maxSizeAttained.width) + border.width + (theme.fullWidth * 0.5).rounded(.down) > screenRect.maxX.nextDown) + if attained { + if contentRect.width > maxSizeAttained.width.nextUp { + maxSizeAttained.width = contentRect.width + } else { + contentRect.size.width = maxSizeAttained.width + } } } let textHeight: Double = max(contentRect.height, maxSizeAttained.height) + border.height * 2 - if theme.isVertical ? (IbeamRect.minX - textHeight - (sweepVertical ? kOffsetGap : 0) < screenRect.minX + 0.1) - : (IbeamRect.minY - textHeight - (sweepVertical ? 0 : kOffsetGap) < screenRect.minY + 0.1) { - if contentRect.height > maxSizeAttained.height + 0.1 { + if theme.isVertical ? (IbeamRect.minX - textHeight - (sweepVertical ? Self.kOffsetGap : 0) < screenRect.minX.nextUp) + : (IbeamRect.minY - textHeight - (sweepVertical ? 0 : Self.kOffsetGap) < screenRect.minY.nextUp) { + if contentRect.height > maxSizeAttained.height.nextUp { maxSizeAttained.height = contentRect.height } else { contentRect.size.height = maxSizeAttained.height @@ -3160,53 +2943,47 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { if view.statusView.isHidden { if theme.isVertical { // anchor is the top right corner in screen coordinates (maxX, maxY) - windowRect = NSRect(x: frame.maxX - contentRect.height - border.height * 2, - y: frame.maxY - contentRect.width - border.width * 2 - theme.fullWidth, - width: contentRect.height + border.height * 2, - height: contentRect.width + border.width * 2 + theme.fullWidth) - initPosition = initPosition || windowRect.intersects(IbeamRect) || !screenRect.contains(windowRect) + windowRect = NSRect(x: frame.maxX - contentRect.height - border.height * 2, y: frame.maxY - contentRect.width - border.width * 2 - theme.fullWidth, width: contentRect.height + border.height * 2, height: contentRect.width + border.width * 2 + theme.fullWidth) + initPosition |= windowRect.intersects(IbeamRect) || !screenRect.contains(windowRect) if initPosition { if !sweepVertical { // To avoid jumping up and down while typing, use the lower screen when typing on upper, and vice versa - if IbeamRect.minY - kOffsetGap - screenRect.height * textWidthRatio - border.width * 2 - theme.fullWidth < screenRect.minY + 0.1 { - windowRect.origin.y = IbeamRect.maxY + kOffsetGap + if IbeamRect.minY - Self.kOffsetGap - screenRect.height * textWidthRatio - border.width * 2 - theme.fullWidth < screenRect.minY.nextUp { + windowRect.origin.y = IbeamRect.maxY + Self.kOffsetGap } else { - windowRect.origin.y = IbeamRect.minY - kOffsetGap - windowRect.height + windowRect.origin.y = IbeamRect.minY - Self.kOffsetGap - windowRect.height } // Make the right edge of candidate block fixed at the left of cursor windowRect.origin.x = IbeamRect.minX + border.height - windowRect.width } else { - if IbeamRect.minX - kOffsetGap - windowRect.width < screenRect.minX + 0.1 { - windowRect.origin.x = IbeamRect.maxX + kOffsetGap + if IbeamRect.minX - Self.kOffsetGap - windowRect.width < screenRect.minX.nextUp { + windowRect.origin.x = IbeamRect.maxX + Self.kOffsetGap } else { - windowRect.origin.x = IbeamRect.minX - kOffsetGap - windowRect.width + windowRect.origin.x = IbeamRect.minX - Self.kOffsetGap - windowRect.width } - windowRect.origin.y = IbeamRect.minY + border.width + ceil(theme.fullWidth * 0.5) - windowRect.height + windowRect.origin.y = IbeamRect.minY + border.width + (theme.fullWidth * 0.5).rounded(.up) - windowRect.height } } } else { // anchor is the top left corner in screen coordinates (minX, maxY) - windowRect = NSRect(x: frame.minX, - y: frame.maxY - contentRect.height - border.height * 2, - width: contentRect.width + border.width * 2 + theme.fullWidth, - height: contentRect.height + border.height * 2) - initPosition = initPosition || windowRect.intersects(IbeamRect) || !screenRect.contains(windowRect) + windowRect = NSRect(x: frame.minX, y: frame.maxY - contentRect.height - border.height * 2, width: contentRect.width + border.width * 2 + theme.fullWidth, height: contentRect.height + border.height * 2) + initPosition |= windowRect.intersects(IbeamRect) || !screenRect.contains(windowRect) if initPosition { if sweepVertical { // To avoid jumping left and right while typing, use the lefter screen when typing on righter, and vice versa - if IbeamRect.minX - kOffsetGap - screenRect.width * textWidthRatio - border.width * 2 - theme.fullWidth > screenRect.minX + 0.1 { - windowRect.origin.x = IbeamRect.minX - kOffsetGap - windowRect.width + if IbeamRect.minX - Self.kOffsetGap - screenRect.width * textWidthRatio - border.width * 2 - theme.fullWidth > screenRect.minX.nextUp { + windowRect.origin.x = IbeamRect.minX - Self.kOffsetGap - windowRect.width } else { - windowRect.origin.x = IbeamRect.maxX + kOffsetGap + windowRect.origin.x = IbeamRect.maxX + Self.kOffsetGap } windowRect.origin.y = IbeamRect.minY + border.height - windowRect.height } else { - if IbeamRect.minY - kOffsetGap - windowRect.height < screenRect.minY + 0.1 { - windowRect.origin.y = IbeamRect.maxY + kOffsetGap + if IbeamRect.minY - Self.kOffsetGap - windowRect.height < screenRect.minY.nextUp { + windowRect.origin.y = IbeamRect.maxY + Self.kOffsetGap } else { - windowRect.origin.y = IbeamRect.minY - kOffsetGap - windowRect.height + windowRect.origin.y = IbeamRect.minY - Self.kOffsetGap - windowRect.height } - windowRect.origin.x = IbeamRect.maxX - border.width - ceil(theme.fullWidth * 0.5) + windowRect.origin.x = IbeamRect.maxX - border.width - (theme.fullWidth * 0.5).rounded(.up) } } } @@ -3214,48 +2991,42 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { // following system UI, middle-align status message with cursor initPosition = true if theme.isVertical { - windowRect.size.width = contentRect.height + border.height * 2 - windowRect.size.height = contentRect.width + border.width * 2 + theme.fullWidth + windowRect.size = NSSize(width: contentRect.height + border.height * 2, height: contentRect.width + border.width * 2 + theme.fullWidth) } else { - windowRect.size.width = contentRect.width + border.width * 2 + theme.fullWidth - windowRect.size.height = contentRect.height + border.height * 2 + windowRect.size = NSSize(width: contentRect.width + border.width * 2 + theme.fullWidth, height: contentRect.height + border.height * 2) } if sweepVertical { // vertically centre-align (midY) in screen coordinates - windowRect.origin.x = IbeamRect.minX - kOffsetGap - windowRect.width - windowRect.origin.y = IbeamRect.midY - windowRect.height * 0.5 + windowRect.origin = NSPoint(x: IbeamRect.minX - Self.kOffsetGap - windowRect.width, y: IbeamRect.midY - windowRect.height * 0.5) } else { // horizontally centre-align (midX) in screen coordinates - windowRect.origin.x = IbeamRect.midX - windowRect.width * 0.5 - windowRect.origin.y = IbeamRect.minY - kOffsetGap - windowRect.height + windowRect.origin = NSPoint(x: IbeamRect.midX - windowRect.width * 0.5, y: IbeamRect.minY - Self.kOffsetGap - windowRect.height) } } if !view.preeditView.isHidden { - if initPosition { - anchorOffset = 0 - } + if initPosition { anchorOffset = 0 } if theme.isVertical != sweepVertical { - let anchorOffset: Double = view.preeditRect.height + let offset: Double = view.preeditRect.height if theme.isVertical { - windowRect.origin.x += anchorOffset - self.anchorOffset + windowRect.origin.x += offset - anchorOffset } else { - windowRect.origin.y += anchorOffset - self.anchorOffset + windowRect.origin.y += offset - anchorOffset } - self.anchorOffset = anchorOffset + anchorOffset = offset } } - if windowRect.maxX > screenRect.maxX - 0.1 { - windowRect.origin.x = (initPosition && sweepVertical ? min(IbeamRect.minX - kOffsetGap, screenRect.maxX) : screenRect.maxX) - windowRect.width + if windowRect.maxX > screenRect.maxX.nextDown { + windowRect.origin.x = (initPosition && sweepVertical ? min(IbeamRect.minX - Self.kOffsetGap, screenRect.maxX) : screenRect.maxX) - windowRect.width } - if windowRect.minX < screenRect.minX + 0.1 { - windowRect.origin.x = initPosition && sweepVertical ? max(IbeamRect.maxX + kOffsetGap, screenRect.minX) : screenRect.minX + if windowRect.minX < screenRect.minX.nextUp { + windowRect.origin.x = initPosition && sweepVertical ? max(IbeamRect.maxX + Self.kOffsetGap, screenRect.minX) : screenRect.minX } - if windowRect.minY < screenRect.minY + 0.1 { - windowRect.origin.y = initPosition && !sweepVertical ? max(IbeamRect.maxY + kOffsetGap, screenRect.minY) : screenRect.minY + if windowRect.minY < screenRect.minY.nextUp { + windowRect.origin.y = initPosition && !sweepVertical ? max(IbeamRect.maxY + Self.kOffsetGap, screenRect.minY) : screenRect.minY } - if windowRect.maxY > screenRect.maxY - 0.1 { - windowRect.origin.y = (initPosition && !sweepVertical ? min(IbeamRect.minY - kOffsetGap, screenRect.maxY) : screenRect.maxY) - windowRect.height + if windowRect.maxY > screenRect.maxY.nextDown { + windowRect.origin.y = (initPosition && !sweepVertical ? min(IbeamRect.minY - Self.kOffsetGap, screenRect.maxY) : screenRect.maxY) - windowRect.height } if theme.isVertical { @@ -3268,38 +3039,37 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { windowRect = _screen.backingAlignedRect(windowRect.intersection(screenRect), options: [.alignAllEdgesNearest]) setFrame(windowRect, display: true) - contentView!.setBoundsOrigin(theme.isVertical ? .init(x: -windowRect.width, y: 0.0) : .zero) + contentView!.setBoundsOrigin(theme.isVertical ? NSPoint(x: -windowRect.width, y: .zero) : .zero) let viewRect: NSRect = contentView!.bounds.integral(options: [.alignAllEdgesNearest]) view.frame = viewRect if !view.statusView.isHidden { - view.statusView.frame = .init(x: viewRect.minX + border.width + ceil(theme.fullWidth * 0.5) - view.statusView.textContainerOrigin.x, - y: viewRect.minY + border.height - view.statusView.textContainerOrigin.y, - width: viewRect.width - border.width * 2 - theme.fullWidth, - height: viewRect.height - border.height * 2) + view.statusView.frame = NSRect(x: viewRect.minX + border.width + (theme.fullWidth * 0.5).rounded(.up) - view.statusView.textContainerOrigin.x, + y: viewRect.minY + border.height - view.statusView.textContainerOrigin.y, + width: viewRect.width - border.width * 2 - theme.fullWidth, + height: viewRect.height - border.height * 2) } if !view.preeditView.isHidden { - view.preeditView.frame = .init(x: viewRect.minX + border.width + ceil(theme.fullWidth * 0.5) - view.preeditView.textContainerOrigin.x, - y: viewRect.minY + border.height - view.preeditView.textContainerOrigin.y, - width: viewRect.width - border.width * 2 - theme.fullWidth, - height: view.preeditRect.height) + view.preeditView.frame = NSRect(x: viewRect.minX + border.width + (theme.fullWidth * 0.5).rounded(.up) - view.preeditView.textContainerOrigin.x, + y: viewRect.minY + border.height - view.preeditView.textContainerOrigin.y, + width: viewRect.width - border.width * 2 - theme.fullWidth, + height: view.preeditRect.height) } if !view.pagingView.isHidden { - let leadOrigin: Double = theme.isLinear ? viewRect.maxX - view.pagingRect.width - border.width + ceil(theme.fullWidth * 0.5) : viewRect.minX + border.width + ceil(theme.fullWidth * 0.5) - view.pagingView.frame = .init(x: leadOrigin - view.pagingView.textContainerOrigin.x, - y: viewRect.maxY - border.height - view.pagingRect.height - view.pagingView.textContainerOrigin.y, - width: (theme.isLinear ? view.pagingRect.width : viewRect.width - border.width * 2) - theme.fullWidth, - height: view.pagingRect.height) + let leadOrigin: Double = theme.isLinear ? viewRect.maxX - view.pagingRect.width - border.width + (theme.fullWidth * 0.5).rounded(.up) + : viewRect.minX + border.width + (theme.fullWidth * 0.5).rounded(.up) + view.pagingView.frame = NSRect(x: leadOrigin - view.pagingView.textContainerOrigin.x, + y: viewRect.maxY - border.height - view.pagingRect.height - view.pagingView.textContainerOrigin.y, + width: (theme.isLinear ? view.pagingRect.width : viewRect.width - border.width * 2) - theme.fullWidth, + height: view.pagingRect.height) } if !view.scrollView.isHidden { - view.scrollView.frame = .init(x: viewRect.minX + border.width, - y: viewRect.minY + view.clipRect.minY, - width: viewRect.width - border.width * 2, - height: view.clipRect.height) - view.documentView.frame = .init(x: 0.0, y: 0.0, width: viewRect.width - border.width * 2, height: view.documentRect.height) - view.candidateView.frame = .init(x: ceil(theme.fullWidth * 0.5) - view.candidateView.textContainerOrigin.x, - y: ceil(theme.lineSpacing * 0.5) - view.candidateView.textContainerOrigin.y, - width: viewRect.width - border.width * 2 - theme.fullWidth, - height: view.documentRect.height - theme.lineSpacing) + view.scrollView.frame = NSRect(x: viewRect.minX + border.width, y: viewRect.minY + view.clipRect.minY, + width: viewRect.width - border.width * 2, height: view.clipRect.height) + view.documentView.frame = NSRect(x: .zero, y: .zero, width: viewRect.width - border.width * 2, height: view.documentRect.height) + view.candidateView.frame = NSRect(x: (theme.fullWidth * 0.5).rounded(.up) - view.candidateView.textContainerOrigin.x, + y: (theme.lineSpacing * 0.5).rounded(.down) - view.candidateView.textContainerOrigin.y, + width: viewRect.width - border.width * 2 - theme.fullWidth, + height: view.documentRect.height - theme.lineSpacing) } if !back.isHidden { back.frame = viewRect } orderFront(nil) @@ -3338,8 +3108,8 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { statusTimer = nil } } else { - if !(statusMessage?.isEmpty ?? true) { - showStatus(message: statusMessage!) + if let message = statusMessage, !message.isEmpty { + showStatus(message: message) statusMessage = nil } else if !(statusTimer?.isValid ?? false) { hide() @@ -3355,7 +3125,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { } if updateCandidates { view.candidateContents.deleteCharacters(in: NSRange(location: 0, length: view.candidateContents.length)) - if theme.lineLength > 0.1 { + if theme.lineLength.isNormal { maxSizeAttained.width = min(theme.lineLength, textWidthLimit) } self.indexRange = indexRange @@ -3364,11 +3134,11 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { // preedit if !preedit.isEmpty { - view.preeditContents.setAttributedString(.init(string: preedit, attributes: theme.preeditAttrs)) - view.preeditContents.mutableString.append(rulerAttrsPreedit == nil ? kFullWidthSpace : "\t") + view.preeditContents.setAttributedString(NSAttributedString(string: preedit, attributes: theme.preeditAttrs)) + view.preeditContents.mutableString.append(rulerAttrsPreedit == nil ? "\u{3000}" : "\t") if selRange.length > 0 { view.preeditContents.addAttribute(.foregroundColor, value: theme.hilitedPreeditForeColor, range: selRange) - let padding = NSNumber(value: ceil(theme.preeditParagraphStyle.minimumLineHeight * 0.05)) + let padding = (theme.preeditParagraphStyle.minimumLineHeight * 0.05).rounded(.up) if selRange.location > 0 { view.preeditContents.addAttribute(.kern, value: padding, range: NSRange(location: selRange.location - 1, length: 1)) } @@ -3379,7 +3149,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { view.preeditContents.append(caretPos == nil || caretPos == 0 ? theme.symbolDeleteStroke! : theme.symbolDeleteFill!) // force caret to be rendered sideways, instead of uprights, in vertical orientation if theme.isVertical && caretPos != nil { - view.preeditContents.addAttribute(.verticalGlyphForm, value: NSNumber(value: false), range: NSRange(location: caretPos!, length: 1)) + view.preeditContents.addAttribute(.verticalGlyphForm, value: 0, range: NSRange(location: caretPos!, length: 1)) } if rulerAttrsPreedit != nil { view.preeditContents.addAttribute(.paragraphStyle, value: rulerAttrsPreedit!, range: NSRange(location: 0, length: view.preeditContents.length)) @@ -3399,13 +3169,14 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { highlightCandidate(highlightedCandidate) } let newSize: NSSize = view.contentRect.size - needsRedraw = needsRedraw || priorSize != newSize + needsRedraw |= priorSize != newSize show() return } // candidate items var candidateInfos: [SquirrelCandidateInfo] = [] + candidateInfos.reserveCapacity(indexRange.count) if indexRange.count > 0 { for idx in 0 ..< indexRange.count { let col: Int = idx % theme.pageSize @@ -3418,10 +3189,10 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let text: String = inputController!.candidateTexts[idx + indexRange.lowerBound] candidate.replaceCharacters(in: textRange, with: text) - let commentRange: NSRange = candidate.mutableString.range(of: kTipSpecifier) + let commentRange: NSRange = candidate.mutableString.range(of: "%s") let comment: String = inputController!.candidateComments[idx + indexRange.lowerBound] if !comment.isEmpty { - candidate.replaceCharacters(in: commentRange, with: "\u{00A0}" + comment) + candidate.replaceCharacters(in: commentRange, with: "\u{A0}" + comment) } else { candidate.deleteCharacters(in: commentRange) } @@ -3436,12 +3207,11 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { var isTruncated: Bool = candidateInfos[0].isTruncated var location: Int = candidateInfos[0].location for i in 1 ... idx { - if i == idx || candidateInfos[i].isTruncated != isTruncated { - view.candidateContents.addAttribute(.paragraphStyle, value: isTruncated ? theme.truncatedParagraphStyle! : theme.candidateParagraphStyle, range: NSRange(location: location, length: candidateInfos[i - 1].upperBound - location)) - if i < idx { - isTruncated = candidateInfos[i].isTruncated - location = candidateInfos[i].location - } + guard i == idx || candidateInfos[i].isTruncated != isTruncated else { continue } + view.candidateContents.addAttribute(.paragraphStyle, value: isTruncated ? theme.truncatedParagraphStyle! : theme.candidateParagraphStyle, range: NSRange(location: location, length: candidateInfos[i - 1].upperBound - location)) + if i < idx { + isTruncated = candidateInfos[i].isTruncated + location = candidateInfos[i].location } } } else { @@ -3458,7 +3228,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let candidateStart: Int = view.candidateContents.length view.candidateContents.append(candidate) // for linear layout, middle-truncate candidates that are longer than one line - if theme.isLinear && textWidth(candidate, vertical: theme.isVertical) > textWidthLimit - theme.fullWidth * (theme.isTabular ? 3 : 2) { + if theme.isLinear && view.candidateView.blockRect(for: NSRange(location: candidateStart, length: candidate.length)).width > textWidthLimit - theme.fullWidth * (theme.isTabular ? 3 : 2) { candidateInfos.append(SquirrelCandidateInfo(location: candidateStart, length: view.candidateContents.length - candidateStart, text: textRange.location, comment: textRange.upperBound, idx: idx, col: col, isTruncated: true)) if idx < indexRange.count - 1 || theme.isTabular || theme.showPaging { view.candidateContents.mutableString.append("\n") @@ -3483,8 +3253,8 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { } if theme.showPaging { view.pagingContents.insert(pageNum > 0 ? theme.symbolBackFill! : theme.symbolBackStroke!, at: 0) - view.pagingContents.mutableString.insert(kFullWidthSpace, at: 1) - view.pagingContents.mutableString.append(kFullWidthSpace) + view.pagingContents.mutableString.insert("\u{3000}", at: 1) + view.pagingContents.mutableString.append("\u{3000}") view.pagingContents.append(isLastPage ? theme.symbolForwardStroke! : theme.symbolForwardFill!) } } else if view.pagingContents.length > 0 { @@ -3493,7 +3263,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { } view.estimateBounds(onScreen: _screen.visibleFrame, withPreedit: !preedit.isEmpty, candidates: candidateInfos, paging: !indexRange.isEmpty && (theme.isTabular || theme.showPaging)) - let textWidth: Double = clamp(view.contentRect.width, maxSizeAttained.width, textWidthLimit) + let textWidth: Double = view.contentRect.width.clamp(min: maxSizeAttained.width, max: textWidthLimit) // right-align the backward delete symbol if !preedit.isEmpty && rulerAttrsPreedit == nil { view.preeditContents.replaceCharacters(in: NSRange(location: view.preeditContents.length - 2, length: 1), with: "\t") @@ -3505,8 +3275,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let rulerAttrsPaging = theme.pagingParagraphStyle.mutableCopy() as! NSMutableParagraphStyle view.pagingContents.replaceCharacters(in: NSRange(location: 1, length: 1), with: "\t") view.pagingContents.replaceCharacters(in: NSRange(location: view.pagingContents.length - 2, length: 1), with: "\t") - rulerAttrsPaging.tabStops = [NSTextTab(textAlignment: .center, location: floor(textWidth * 0.5)), - NSTextTab(textAlignment: .right, location: textWidth)] + rulerAttrsPaging.tabStops = [NSTextTab(textAlignment: .center, location: (textWidth * 0.5).rounded()), NSTextTab(textAlignment: .right, location: textWidth)] view.pagingContents.addAttribute(.paragraphStyle, value: rulerAttrsPaging, range: NSRange(location: 0, length: view.pagingContents.length)) } @@ -3515,7 +3284,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { view.drawView(withHilitedCandidate: highlightedCandidate, hilitedPreeditRange: selRange) let newSize: NSSize = view.contentRect.size - needsRedraw = needsRedraw || priorSize != newSize + needsRedraw |= priorSize != newSize show() } @@ -3526,7 +3295,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { case .long: statusMessage = long case .short: - statusMessage = short ?? long == nil ? nil : String(long![long!.rangeOfComposedCharacterSequence(at: long!.startIndex)]) + statusMessage = short ?? (long != nil ? String(long!.first!) : nil) } } @@ -3542,16 +3311,15 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { view.estimateBounds(onScreen: _screen.visibleFrame, withPreedit: false, candidates: [], paging: false) // disable both `remember_size` and fixed lineLength for status messages - initPosition = true maxSizeAttained = .zero statusTimer?.invalidate() animationBehavior = .utilityWindow view.drawView(withHilitedCandidate: nil, hilitedPreeditRange: NSRange(location: NSNotFound, length: 0)) let newSize: NSSize = view.contentRect.size - needsRedraw = needsRedraw || priorSize != newSize + needsRedraw |= priorSize != newSize show() - statusTimer = Timer.scheduledTimer(withTimeInterval: kShowStatusDuration, repeats: false) { _ in self.hide() } + statusTimer = Timer.scheduledTimer(withTimeInterval: Self.kShowStatusDuration, repeats: false) { _ in self.hide() } } private func updateAnnotationHeight(_ height: Double) { @@ -3563,19 +3331,17 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { } func loadLabelConfig(_ config: SquirrelConfig, directUpdate update: Bool) { - SquirrelView.lightTheme.updateLabelsWithConfig(config, directUpdate: update) + SquirrelView.lightTheme.updateLabels(withConfig: config, directUpdate: update) if #available(macOS 10.14, *) { - SquirrelView.darkTheme.updateLabelsWithConfig(config, directUpdate: update) - } - if update { - updateDisplayParameters() + SquirrelView.darkTheme.updateLabels(withConfig: config, directUpdate: update) } + if update { updateDisplayParameters() } } func loadConfig(_ config: SquirrelConfig) { - SquirrelView.lightTheme.updateThemeWithConfig(config, styleOptions: optionSwitcher.optionStates, scriptVariant: optionSwitcher.currentScriptVariant) + SquirrelView.lightTheme.updateTheme(withConfig: config, styleOptions: optionSwitcher.optionStates, scriptVariant: optionSwitcher.currentScriptVariant) if #available(macOS 10.14, *) { - SquirrelView.darkTheme.updateThemeWithConfig(config, styleOptions: optionSwitcher.optionStates, scriptVariant: optionSwitcher.currentScriptVariant) + SquirrelView.darkTheme.updateTheme(withConfig: config, styleOptions: optionSwitcher.optionStates, scriptVariant: optionSwitcher.currentScriptVariant) } getLocked() updateDisplayParameters() @@ -3587,14 +3353,4 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { SquirrelView.darkTheme.updateScriptVariant(optionSwitcher.currentScriptVariant) } } -} // SquirrelPanel - -private func textWidth(_ string: NSAttributedString, vertical: Bool) -> Double { - if vertical { - let verticalString = string.mutableCopy() as! NSMutableAttributedString - verticalString.addAttribute(.verticalGlyphForm, value: NSNumber(value: true), range: NSRange(location: 0, length: verticalString.length)) - return ceil(verticalString.size().width) - } else { - return ceil(string.size().width) - } -} +} // SquirrelPanel