From 0a94d7c6f1866484ac91c82094b877262c96dbda 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 + Sparkle | 2 +- Squirrel.xcodeproj/project.pbxproj | 105 +- .../xcshareddata/xcschemes/Squirrel.xcscheme | 2 +- 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 | 2984 ++++++++-------- sources/SquirrelApplicationDelegate.swift | 360 +- sources/SquirrelConfig.swift | 676 ++-- sources/SquirrelInputController.swift | 786 ++--- sources/SquirrelInputSource.swift | 181 +- sources/SquirrelPanel.swift | 3014 ++++++++--------- 22 files changed, 4556 insertions(+), 5326 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/Sparkle b/Sparkle index 47d3d90ae..41847a58c 160000 --- a/Sparkle +++ b/Sparkle @@ -1 +1 @@ -Subproject commit 47d3d90aee3c52b6f61d04ceae426e607df62347 +Subproject commit 41847a58cdef7506b257591fcca6f9495df591d4 diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 2798739c4..6f92abc0b 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 = ""; @@ -582,7 +588,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1600; TargetAttributes = { 8D1107260486CEB800E47090 = { LastSwiftMigration = 1530; @@ -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; @@ -647,14 +655,13 @@ C01FCF4B08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = AppKit; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = "SwiftUI UIKit AppKit"; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "compiler-default"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = NO; CLANG_ENABLE_OBJC_ARC = YES; CLANG_LINK_OBJC_RUNTIME = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; @@ -664,12 +671,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0.18.0s; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; - ENABLE_HARDENED_RUNTIME = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", @@ -679,9 +681,7 @@ ); FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)\""; FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_2 = "\"$(SRCROOT)\""; - GCC_DYNAMIC_NO_PIC = NO; GCC_MODEL_TUNING = G5; - GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Squirrel Input Method"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -706,16 +706,14 @@ OTHER_LDFLAGS = "-lrime.1"; PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel; PRODUCT_NAME = Squirrel; - PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "sources/Squirrel-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift.h"; - SWIFT_OBJC_INTEROP_MODE = objc; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_OBJC_INTEROP_MODE = objcxx; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Debug; @@ -723,14 +721,13 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = AppKit; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS = "SwiftUI UIKit AppKit"; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "compiler-default"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = NO; CLANG_ENABLE_OBJC_ARC = YES; CLANG_LINK_OBJC_RUNTIME = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; @@ -740,11 +737,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 0.18.0s; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; - ENABLE_HARDENED_RUNTIME = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", @@ -754,7 +747,6 @@ ); FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)\""; FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_2 = "\"$(SRCROOT)\""; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_MODEL_TUNING = G5; INFOPLIST_FILE = resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Squirrel Input Method"; @@ -780,15 +772,14 @@ OTHER_LDFLAGS = "-lrime.1"; PRODUCT_BUNDLE_IDENTIFIER = im.rime.inputmethod.Squirrel; PRODUCT_NAME = Squirrel; - PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "sources/Squirrel-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift.h"; - SWIFT_OBJC_INTEROP_MODE = objc; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_OBJC_INTEROP_MODE = objcxx; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Release; @@ -796,7 +787,6 @@ C01FCF4F08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -806,7 +796,7 @@ CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_ENABLE_MODULES = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_ARC_EXCEPTIONS = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; @@ -839,14 +829,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements; CODE_SIGN_IDENTITY = "-"; - DEAD_CODE_STRIPPING = YES; - DEFINES_MODULE = NO; - ENABLE_HARDENED_RUNTIME = NO; - ENABLE_STRICT_OBJC_MSGSEND = NO; ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_INPUT_FILETYPE = automatic; - GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = NO; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; @@ -870,9 +853,8 @@ INFOPLIST_KEY_CFBundleDisplayName = "Squirrel Input Method"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; - INFOPLIST_KEY_NSMainNibFile = ""; INFOPLIST_KEY_NSPrincipalClass = NSApplication; - INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; + INSTALL_PATH = "/Library/Input Methods/"; LD_RUNPATH_SEARCH_PATHS = ( "@loader_path/../Frameworks", "@loader_path/../Library/OpenSource", @@ -885,15 +867,16 @@ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; MODULE_NAME = Squirrel; ONLY_ACTIVE_ARCH = YES; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME)"; + OTHER_LDFLAGS = ""; SDKROOT = macosx; STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = /Library/Developer/Toolchains; SWIFT_OBJC_BRIDGING_HEADER = "sources/Squirrel-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift.h"; - SWIFT_OBJC_INTEROP_MODE = objc; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_OBJC_INTEROP_MODE = objcxx; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; }; name = Debug; @@ -901,7 +884,6 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -911,7 +893,7 @@ CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_ENABLE_MODULES = NO; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_ARC_EXCEPTIONS = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; @@ -945,13 +927,6 @@ CODE_SIGN_ENTITLEMENTS = resources/Squirrel.entitlements; CODE_SIGN_IDENTITY = "-"; CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release"; - DEAD_CODE_STRIPPING = YES; - DEFINES_MODULE = NO; - ENABLE_HARDENED_RUNTIME = NO; - ENABLE_STRICT_OBJC_MSGSEND = NO; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_INPUT_FILETYPE = automatic; - GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = NO; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; @@ -975,9 +950,8 @@ INFOPLIST_KEY_CFBundleDisplayName = "Squirrel Input Method"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; - INFOPLIST_KEY_NSMainNibFile = ""; INFOPLIST_KEY_NSPrincipalClass = NSApplication; - INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; + INSTALL_PATH = "/Library/Input Methods/"; LD_RUNPATH_SEARCH_PATHS = ( "@loader_path/../Frameworks\n\n@loader_path/../Library/OpenSource\n\n@loader_path/../Library/Frameworks", /usr/lib, @@ -987,16 +961,17 @@ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; MODULE_NAME = Squirrel; ONLY_ACTIVE_ARCH = NO; - PRODUCT_MODULE_NAME = "$(PRODUCT_NAME)"; + OTHER_LDFLAGS = ""; SDKROOT = macosx; STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; - SWIFT_COMPILATION_MODE = singlefile; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = /Library/Developer/Toolchains; SWIFT_OBJC_BRIDGING_HEADER = "sources/Squirrel-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "$(SWIFT_MODULE_NAME)-Swift.h"; - SWIFT_OBJC_INTEROP_MODE = objc; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_VERSION = 5.0; + SWIFT_OBJC_INTEROP_MODE = objcxx; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; }; name = Release; diff --git a/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme b/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme index d8b0e53fd..1f21223f5 100644 --- a/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme +++ b/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme @@ -1,6 +1,6 @@ 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..6016569d0 100644 --- a/sources/RimeKeycode.swift +++ b/sources/RimeKeycode.swift @@ -1,77 +1,54 @@ import AppKit import Carbon -struct RimeModifiers: OptionSet, Sendable { +struct RimeModifiers: OptionSet, Sendable, Hashable { let rawValue: CInt - static let Shift = RimeModifiers(rawValue: 1 << 0) - static let Lock = RimeModifiers(rawValue: 1 << 1) - static let Control = RimeModifiers(rawValue: 1 << 2) - static let Alt = RimeModifiers(rawValue: 1 << 3) - static let Handled = RimeModifiers(rawValue: 1 << 24) - static let Ignored = RimeModifiers(rawValue: 1 << 25) - static let Super = RimeModifiers(rawValue: 1 << 26) - static let Hyper = RimeModifiers(rawValue: 1 << 27) - static let Meta = RimeModifiers(rawValue: 1 << 28) - static let Release = RimeModifiers(rawValue: 1 << 30) - static let ModifierMask = RimeModifiers(rawValue: 0x5F001FFF) + static let Shift: Self = .init(rawValue: 1 << 0) + static let Lock: Self = .init(rawValue: 1 << 1) + static let Control: Self = .init(rawValue: 1 << 2) + static let Alt: Self = .init(rawValue: 1 << 3) + static let Handled: Self = .init(rawValue: 1 << 24) + static let Ignored: Self = .init(rawValue: 1 << 25) + static let Super: Self = .init(rawValue: 1 << 26) + static let Hyper: Self = .init(rawValue: 1 << 27) + static let Meta: Self = .init(rawValue: 1 << 28) + static let Release: Self = .init(rawValue: 1 << 30) + static let ModifierMask: Self = .init(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) - } - self = modifiers + self.init() + if macModifiers.contains(.shift) { insert(.Shift) } + if macModifiers.contains(.capsLock) { insert(.Lock) } + if macModifiers.contains(.control) { insert(.Control) } + if macModifiers.contains(.option) { insert(.Alt) } + if macModifiers.contains(.command) { insert(.Super) } + if macModifiers.contains(.function) { insert(.Hyper) } } 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 +public var kVK_Enter_Powerbook: Int { 0x34 } // pc keyboard -let kVK_PC_Application: Int = 0x6E -let kVK_PC_Power: Int = 0x7F +public var kVK_PC_Application: Int { 0x6E } +public var kVK_PC_Power: Int { 0x7F } -enum RimeKeycode: CInt, Sendable { +@frozen enum RimeKeycode: CInt, Sendable, Strideable, Hashable { case XK_VoidSymbol = 0xFFFFFF case XK_BackSpace = 0xFF08 @@ -454,1488 +431,1463 @@ enum RimeKeycode: CInt, Sendable { case XK_ISO_Enter = 0xFE34 init(macKeycode: Int) { - switch macKeycode { - case kVK_CapsLock: self = .XK_Caps_Lock - case kVK_Command: self = .XK_Super_L // XK_Meta_L? - case kVK_RightCommand: self = .XK_Super_R // XK_Meta_R? - case kVK_Control: self = .XK_Control_L - case kVK_RightControl: self = .XK_Control_R - case kVK_Function: self = .XK_Hyper_L - case kVK_Option: self = .XK_Alt_L - case kVK_RightOption: self = .XK_Alt_R - case kVK_Shift: self = .XK_Shift_L - case kVK_RightShift: self = .XK_Shift_R - // special - case kVK_Delete: self = .XK_BackSpace - case kVK_Enter_Powerbook: self = .XK_ISO_Enter - case kVK_Escape: self = .XK_Escape - case kVK_ForwardDelete: self = .XK_Delete - case kVK_Help: self = .XK_Help - case kVK_Return: self = .XK_Return - case kVK_Space: self = .XK_space - case kVK_Tab: self = .XK_Tab - // function - case kVK_F1: self = .XK_F1 - case kVK_F2: self = .XK_F2 - case kVK_F3: self = .XK_F3 - case kVK_F4: self = .XK_F4 - case kVK_F5: self = .XK_F5 - case kVK_F6: self = .XK_F6 - case kVK_F7: self = .XK_F7 - case kVK_F8: self = .XK_F8 - case kVK_F9: self = .XK_F9 - case kVK_F10: self = .XK_F10 - case kVK_F11: self = .XK_F11 - case kVK_F12: self = .XK_F12 - case kVK_F13: self = .XK_F13 - case kVK_F14: self = .XK_F14 - case kVK_F15: self = .XK_F15 - case kVK_F16: self = .XK_F16 - case kVK_F17: self = .XK_F17 - case kVK_F18: self = .XK_F18 - case kVK_F19: self = .XK_F19 - case kVK_F20: self = .XK_F20 - // cursor - case kVK_UpArrow: self = .XK_Up - case kVK_DownArrow: self = .XK_Down - case kVK_LeftArrow: self = .XK_Left - case kVK_RightArrow: self = .XK_Right - case kVK_PageUp: self = .XK_Page_Up - case kVK_PageDown: self = .XK_Page_Down - case kVK_Home: self = .XK_Home - case kVK_End: self = .XK_End - // keypad - case kVK_ANSI_Keypad0: self = .XK_KP_0 - case kVK_ANSI_Keypad1: self = .XK_KP_1 - case kVK_ANSI_Keypad2: self = .XK_KP_2 - case kVK_ANSI_Keypad3: self = .XK_KP_3 - case kVK_ANSI_Keypad4: self = .XK_KP_4 - case kVK_ANSI_Keypad5: self = .XK_KP_5 - case kVK_ANSI_Keypad6: self = .XK_KP_6 - case kVK_ANSI_Keypad7: self = .XK_KP_7 - case kVK_ANSI_Keypad8: self = .XK_KP_8 - case kVK_ANSI_Keypad9: self = .XK_KP_9 - case kVK_ANSI_KeypadEnter: self = .XK_KP_Enter - case kVK_ANSI_KeypadClear: self = .XK_Clear - case kVK_ANSI_KeypadDecimal: self = .XK_KP_Decimal - case kVK_ANSI_KeypadEquals: self = .XK_KP_Equal - case kVK_ANSI_KeypadMinus: self = .XK_KP_Subtract - case kVK_ANSI_KeypadMultiply: self = .XK_KP_Multiply - case kVK_ANSI_KeypadPlus: self = .XK_KP_Add - case kVK_ANSI_KeypadDivide: self = .XK_KP_Divide - // pc keyboard - case kVK_PC_Application: self = .XK_Menu - // JIS keyboard - case kVK_JIS_KeypadComma: self = .XK_KP_Separator - case kVK_JIS_Eisu: self = .XK_Eisu_toggle - case kVK_JIS_Kana: self = .XK_Kana_Shift + self = switch macKeycode { + case kVK_CapsLock: .XK_Caps_Lock + case kVK_Command: .XK_Super_L // XK_Meta_L? + case kVK_RightCommand: .XK_Super_R // XK_Meta_R? + case kVK_Control: .XK_Control_L + case kVK_RightControl: .XK_Control_R + case kVK_Function: .XK_Hyper_L + case kVK_Option: .XK_Alt_L + case kVK_RightOption: .XK_Alt_R + case kVK_Shift: .XK_Shift_L + case kVK_RightShift: .XK_Shift_R + // special + case kVK_Delete: .XK_BackSpace + case kVK_Enter_Powerbook: .XK_ISO_Enter + case kVK_Escape: .XK_Escape + case kVK_ForwardDelete: .XK_Delete + case kVK_Help: .XK_Help + case kVK_Return: .XK_Return + case kVK_Space: .XK_space + case kVK_Tab: .XK_Tab + // function + case kVK_F1: .XK_F1 + case kVK_F2: .XK_F2 + case kVK_F3: .XK_F3 + case kVK_F4: .XK_F4 + case kVK_F5: .XK_F5 + case kVK_F6: .XK_F6 + case kVK_F7: .XK_F7 + case kVK_F8: .XK_F8 + case kVK_F9: .XK_F9 + case kVK_F10: .XK_F10 + case kVK_F11: .XK_F11 + case kVK_F12: .XK_F12 + case kVK_F13: .XK_F13 + case kVK_F14: .XK_F14 + case kVK_F15: .XK_F15 + case kVK_F16: .XK_F16 + case kVK_F17: .XK_F17 + case kVK_F18: .XK_F18 + case kVK_F19: .XK_F19 + case kVK_F20: .XK_F20 + // cursor + case kVK_UpArrow: .XK_Up + case kVK_DownArrow: .XK_Down + case kVK_LeftArrow: .XK_Left + case kVK_RightArrow: .XK_Right + case kVK_PageUp: .XK_Page_Up + case kVK_PageDown: .XK_Page_Down + case kVK_Home: .XK_Home + case kVK_End: .XK_End + // keypad + case kVK_ANSI_Keypad0: .XK_KP_0 + case kVK_ANSI_Keypad1: .XK_KP_1 + case kVK_ANSI_Keypad2: .XK_KP_2 + case kVK_ANSI_Keypad3: .XK_KP_3 + case kVK_ANSI_Keypad4: .XK_KP_4 + case kVK_ANSI_Keypad5: .XK_KP_5 + case kVK_ANSI_Keypad6: .XK_KP_6 + case kVK_ANSI_Keypad7: .XK_KP_7 + case kVK_ANSI_Keypad8: .XK_KP_8 + case kVK_ANSI_Keypad9: .XK_KP_9 + case kVK_ANSI_KeypadEnter: .XK_KP_Enter + case kVK_ANSI_KeypadClear: .XK_Clear + case kVK_ANSI_KeypadDecimal: .XK_KP_Decimal + case kVK_ANSI_KeypadEquals: .XK_KP_Equal + case kVK_ANSI_KeypadMinus: .XK_KP_Subtract + case kVK_ANSI_KeypadMultiply: .XK_KP_Multiply + case kVK_ANSI_KeypadPlus: .XK_KP_Add + case kVK_ANSI_KeypadDivide: .XK_KP_Divide + // pc keyboard + case kVK_PC_Application: .XK_Menu + // JIS keyboard + case kVK_JIS_KeypadComma: .XK_KP_Separator + case kVK_JIS_Eisu: .XK_Eisu_toggle + case kVK_JIS_Kana: .XK_Kana_Shift - default: self = .XK_VoidSymbol + default: .XK_VoidSymbol } } 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 } - switch NSEvent.SpecialKey(rawValue: Int(keychar)) { - // ASCII control characters - case .newline: self = .XK_Linefeed - case .backTab: self = .XK_ISO_Left_Tab - // Function key characters - case .f21: self = .XK_F21 - case .f22: self = .XK_F22 - case .f23: self = .XK_F23 - case .f24: self = .XK_F24 - case .f25: self = .XK_F25 - case .f26: self = .XK_F26 - case .f27: self = .XK_F27 - case .f28: self = .XK_F28 - case .f29: self = .XK_F29 - case .f30: self = .XK_F30 - case .f31: self = .XK_F31 - case .f32: self = .XK_F32 - case .f33: self = .XK_F33 - case .f34: self = .XK_F34 - case .f35: self = .XK_F35 - // Misc functional key characters - case .insert: self = .XK_Insert - case .begin: self = .XK_Begin - case .scrollLock: self = .XK_Scroll_Lock - case .pause: self = .XK_Pause - case .sysReq: self = .XK_Sys_Req - case .break: self = .XK_Break - case .stop: self = .XK_Cancel - case .print: self = .XK_Print - case .clearLine: self = .XK_Num_Lock - case .prev: self = .XK_Page_Up - case .next: self = .XK_Page_Down - case .select: self = .XK_Select - case .execute: self = .XK_Execute - case .undo: self = .XK_Undo - case .redo: self = .XK_Redo - case .find: self = .XK_Find - case .modeSwitch: self = .XK_Mode_switch + self = switch NSEvent.SpecialKey(rawValue: Int(keychar)) { + // ASCII control characters + case .newline: .XK_Linefeed + case .backTab: .XK_ISO_Left_Tab + // Function key characters + case .f21: .XK_F21 + case .f22: .XK_F22 + case .f23: .XK_F23 + case .f24: .XK_F24 + case .f25: .XK_F25 + case .f26: .XK_F26 + case .f27: .XK_F27 + case .f28: .XK_F28 + case .f29: .XK_F29 + case .f30: .XK_F30 + case .f31: .XK_F31 + case .f32: .XK_F32 + case .f33: .XK_F33 + case .f34: .XK_F34 + case .f35: .XK_F35 + // Misc functional key characters + case .insert: .XK_Insert + case .begin: .XK_Begin + case .scrollLock: .XK_Scroll_Lock + case .pause: .XK_Pause + case .sysReq: .XK_Sys_Req + case .break: .XK_Break + case .stop: .XK_Cancel + case .print: .XK_Print + case .clearLine: .XK_Num_Lock + case .prev: .XK_Page_Up + case .next: .XK_Page_Down + case .select: .XK_Select + case .execute: .XK_Execute + case .undo: .XK_Undo + case .redo: .XK_Redo + case .find: .XK_Find + case .modeSwitch: .XK_Mode_switch - default: self = .XK_VoidSymbol + default: .XK_VoidSymbol } } - 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) -> 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 - } + init(name: String) { self.init(rawValue: Self.nameToRawValue(name))! } - 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 } + + typealias Stride = CInt + func distance(to other: Self) -> Stride { other.rawValue - rawValue } + func advanced(by n: Stride) -> Self { .init(rawValue: rawValue + n) ?? .XK_VoidSymbol } - private static func nameToRawValue(_ name: String) -> CInt { - switch name { + static private func nameToRawValue(_ name: String) -> CInt { + return switch name { // ascii - case "space": return 0x000020 - case "exclam": return 0x000021 - case "quotedbl": return 0x000022 - case "numbersign": return 0x000023 - case "dollar": return 0x000024 - case "percent": return 0x000025 - case "ampersand": return 0x000026 - case "apostrophe": return 0x000027 - case "quoteright": return 0x000027 - case "parenleft": return 0x000028 - case "parenright": return 0x000029 - case "asterisk": return 0x00002A - case "plus": return 0x00002B - case "comma": return 0x00002C - case "minus": return 0x00002D - case "period": return 0x00002E - case "slash": return 0x00002F - case "0": return 0x000030 - case "1": return 0x000031 - case "2": return 0x000032 - case "3": return 0x000033 - case "4": return 0x000034 - case "5": return 0x000035 - case "6": return 0x000036 - case "7": return 0x000037 - case "8": return 0x000038 - case "9": return 0x000039 - case "colon": return 0x00003A - case "semicolon": return 0x00003B - case "less": return 0x00003C - case "equal": return 0x00003D - case "greater": return 0x00003E - case "question": return 0x00003F - case "at": return 0x000040 - case "A": return 0x000041 - case "B": return 0x000042 - case "C": return 0x000043 - case "D": return 0x000044 - case "E": return 0x000045 - case "F": return 0x000046 - case "G": return 0x000047 - case "H": return 0x000048 - case "I": return 0x000049 - case "J": return 0x00004A - case "K": return 0x00004B - case "L": return 0x00004C - case "M": return 0x00004D - case "N": return 0x00004E - case "O": return 0x00004F - case "P": return 0x000050 - case "Q": return 0x000051 - case "R": return 0x000052 - case "S": return 0x000053 - case "T": return 0x000054 - case "U": return 0x000055 - case "V": return 0x000056 - case "W": return 0x000057 - case "X": return 0x000058 - case "Y": return 0x000059 - case "Z": return 0x00005A - case "bracketleft": return 0x00005B - case "backslash": return 0x00005C - case "bracketright": return 0x00005D - case "asciicircum": return 0x00005E - case "underscore": return 0x00005F - case "grave": return 0x000060 - case "quoteleft": return 0x000060 - case "a": return 0x000061 - case "b": return 0x000062 - case "c": return 0x000063 - case "d": return 0x000064 - case "e": return 0x000065 - case "f": return 0x000066 - case "g": return 0x000067 - case "h": return 0x000068 - case "i": return 0x000069 - case "j": return 0x00006A - case "k": return 0x00006B - case "l": return 0x00006C - case "m": return 0x00006D - case "n": return 0x00006E - case "o": return 0x00006F - case "p": return 0x000070 - case "q": return 0x000071 - case "r": return 0x000072 - case "s": return 0x000073 - case "t": return 0x000074 - case "u": return 0x000075 - case "v": return 0x000076 - case "w": return 0x000077 - case "x": return 0x000078 - case "y": return 0x000079 - case "z": return 0x00007A - case "braceleft": return 0x00007B - case "bar": return 0x00007C - case "braceright": return 0x00007D - case "asciitilde": return 0x00007E + case "space": 0x000020 + case "exclam": 0x000021 + case "quotedbl": 0x000022 + case "numbersign": 0x000023 + case "dollar": 0x000024 + case "percent": 0x000025 + case "ampersand": 0x000026 + case "apostrophe": 0x000027 + case "quoteright": 0x000027 + case "parenleft": 0x000028 + case "parenright": 0x000029 + case "asterisk": 0x00002A + case "plus": 0x00002B + case "comma": 0x00002C + case "minus": 0x00002D + case "period": 0x00002E + case "slash": 0x00002F + case "0": 0x000030 + case "1": 0x000031 + case "2": 0x000032 + case "3": 0x000033 + case "4": 0x000034 + case "5": 0x000035 + case "6": 0x000036 + case "7": 0x000037 + case "8": 0x000038 + case "9": 0x000039 + case "colon": 0x00003A + case "semicolon": 0x00003B + case "less": 0x00003C + case "equal": 0x00003D + case "greater": 0x00003E + case "question": 0x00003F + case "at": 0x000040 + case "A": 0x000041 + case "B": 0x000042 + case "C": 0x000043 + case "D": 0x000044 + case "E": 0x000045 + case "F": 0x000046 + case "G": 0x000047 + case "H": 0x000048 + case "I": 0x000049 + case "J": 0x00004A + case "K": 0x00004B + case "L": 0x00004C + case "M": 0x00004D + case "N": 0x00004E + case "O": 0x00004F + case "P": 0x000050 + case "Q": 0x000051 + case "R": 0x000052 + case "S": 0x000053 + case "T": 0x000054 + case "U": 0x000055 + case "V": 0x000056 + case "W": 0x000057 + case "X": 0x000058 + case "Y": 0x000059 + case "Z": 0x00005A + case "bracketleft": 0x00005B + case "backslash": 0x00005C + case "bracketright": 0x00005D + case "asciicircum": 0x00005E + case "underscore": 0x00005F + case "grave": 0x000060 + case "quoteleft": 0x000060 + case "a": 0x000061 + case "b": 0x000062 + case "c": 0x000063 + case "d": 0x000064 + case "e": 0x000065 + case "f": 0x000066 + case "g": 0x000067 + case "h": 0x000068 + case "i": 0x000069 + case "j": 0x00006A + case "k": 0x00006B + case "l": 0x00006C + case "m": 0x00006D + case "n": 0x00006E + case "o": 0x00006F + case "p": 0x000070 + case "q": 0x000071 + case "r": 0x000072 + case "s": 0x000073 + case "t": 0x000074 + case "u": 0x000075 + case "v": 0x000076 + case "w": 0x000077 + case "x": 0x000078 + case "y": 0x000079 + case "z": 0x00007A + case "braceleft": 0x00007B + case "bar": 0x00007C + case "braceright": 0x00007D + case "asciitilde": 0x00007E // latin-1 - case "nobreakspace": return 0x0000A0 - case "exclamdown": return 0x0000A1 - case "cent": return 0x0000A2 - case "sterling": return 0x0000A3 - case "currency": return 0x0000A4 - case "yen": return 0x0000A5 - case "brokenbar": return 0x0000A6 - case "section": return 0x0000A7 - case "diaeresis": return 0x0000A8 - case "copyright": return 0x0000A9 - case "ordfeminine": return 0x0000AA - case "guillemotleft": return 0x0000AB - case "notsign": return 0x0000AC - case "hyphen": return 0x0000AD - case "registered": return 0x0000AE - case "macron": return 0x0000AF - case "degree": return 0x0000B0 - case "plusminus": return 0x0000B1 - case "twosuperior": return 0x0000B2 - case "threesuperior": return 0x0000B3 - case "acute": return 0x0000B4 - case "mu": return 0x0000B5 - case "paragraph": return 0x0000B6 - case "periodcentered": return 0x0000B7 - case "cedilla": return 0x0000B8 - case "onesuperior": return 0x0000B9 - case "masculine": return 0x0000BA - case "guillemotright": return 0x0000BB - case "onequarter": return 0x0000BC - case "onehalf": return 0x0000BD - case "threequarters": return 0x0000BE - case "questiondown": return 0x0000BF - case "Agrave": return 0x0000C0 - case "Aacute": return 0x0000C1 - case "Acircumflex": return 0x0000C2 - case "Atilde": return 0x0000C3 - case "Adiaeresis": return 0x0000C4 - case "Aring": return 0x0000C5 - case "AE": return 0x0000C6 - case "Ccedilla": return 0x0000C7 - case "Egrave": return 0x0000C8 - case "Eacute": return 0x0000C9 - case "Ecircumflex": return 0x0000CA - case "Ediaeresis": return 0x0000CB - case "Igrave": return 0x0000CC - case "Iacute": return 0x0000CD - case "Icircumflex": return 0x0000CE - case "Idiaeresis": return 0x0000CF - case "ETH": return 0x0000D0 - case "Eth": return 0x0000D0 - case "Ntilde": return 0x0000D1 - case "Ograve": return 0x0000D2 - case "Oacute": return 0x0000D3 - case "Ocircumflex": return 0x0000D4 - case "Otilde": return 0x0000D5 - case "Odiaeresis": return 0x0000D6 - case "multiply": return 0x0000D7 - case "Ooblique": return 0x0000D8 - case "Ugrave": return 0x0000D9 - case "Uacute": return 0x0000DA - case "Ucircumflex": return 0x0000DB - case "Udiaeresis": return 0x0000DC - case "Yacute": return 0x0000DD - case "THORN": return 0x0000DE - case "Thorn": return 0x0000DE - case "ssharp": return 0x0000DF - case "agrave": return 0x0000E0 - case "aacute": return 0x0000E1 - case "acircumflex": return 0x0000E2 - case "atilde": return 0x0000E3 - case "adiaeresis": return 0x0000E4 - case "aring": return 0x0000E5 - case "ae": return 0x0000E6 - case "ccedilla": return 0x0000E7 - case "egrave": return 0x0000E8 - case "eacute": return 0x0000E9 - case "ecircumflex": return 0x0000EA - case "ediaeresis": return 0x0000EB - case "igrave": return 0x0000EC - case "iacute": return 0x0000ED - case "icircumflex": return 0x0000EE - case "idiaeresis": return 0x0000EF - case "eth": return 0x0000F0 - case "ntilde": return 0x0000F1 - case "ograve": return 0x0000F2 - case "oacute": return 0x0000F3 - case "ocircumflex": return 0x0000F4 - case "otilde": return 0x0000F5 - case "odiaeresis": return 0x0000F6 - case "division": return 0x0000F7 - case "oslash": return 0x0000F8 - case "ugrave": return 0x0000F9 - case "uacute": return 0x0000FA - case "ucircumflex": return 0x0000FB - case "udiaeresis": return 0x0000FC - case "yacute": return 0x0000FD - case "thorn": return 0x0000FE - case "ydiaeresis": return 0x0000FF - case "Aogonek": return 0x0001A1 - case "breve": return 0x0001A2 - case "Lstroke": return 0x0001A3 - case "Lcaron": return 0x0001A5 - case "Sacute": return 0x0001A6 - case "Scaron": return 0x0001A9 - case "Scedilla": return 0x0001AA - case "Tcaron": return 0x0001AB - case "Zacute": return 0x0001AC - case "Zcaron": return 0x0001AE - case "Zabovedot": return 0x0001AF - case "aogonek": return 0x0001B1 - case "ogonek": return 0x0001B2 - case "lstroke": return 0x0001B3 - case "lcaron": return 0x0001B5 - case "sacute": return 0x0001B6 - case "caron": return 0x0001B7 - case "scaron": return 0x0001B9 - case "scedilla": return 0x0001BA - case "tcaron": return 0x0001BB - case "zacute": return 0x0001BC - case "doubleacute": return 0x0001BD - case "zcaron": return 0x0001BE - case "zabovedot": return 0x0001BF - case "Racute": return 0x0001C0 - case "Abreve": return 0x0001C3 - case "Lacute": return 0x0001C5 - case "Cacute": return 0x0001C6 - case "Ccaron": return 0x0001C8 - case "Eogonek": return 0x0001CA - case "Ecaron": return 0x0001CC - case "Dcaron": return 0x0001CF - case "Dstroke": return 0x0001D0 - case "Nacute": return 0x0001D1 - case "Ncaron": return 0x0001D2 - case "Odoubleacute": return 0x0001D5 - case "Rcaron": return 0x0001D8 - case "Uring": return 0x0001D9 - case "Udoubleacute": return 0x0001DB - case "Tcedilla": return 0x0001DE - case "racute": return 0x0001E0 - case "abreve": return 0x0001E3 - case "lacute": return 0x0001E5 - case "cacute": return 0x0001E6 - case "ccaron": return 0x0001E8 - case "eogonek": return 0x0001EA - case "ecaron": return 0x0001EC - case "dcaron": return 0x0001EF - case "dstroke": return 0x0001F0 - case "nacute": return 0x0001F1 - case "ncaron": return 0x0001F2 - case "odoubleacute": return 0x0001F5 - case "rcaron": return 0x0001F8 - case "uring": return 0x0001F9 - case "udoubleacute": return 0x0001FB - case "tcedilla": return 0x0001FE - case "abovedot": return 0x0001FF + case "nobreakspace": 0x0000A0 + case "exclamdown": 0x0000A1 + case "cent": 0x0000A2 + case "sterling": 0x0000A3 + case "currency": 0x0000A4 + case "yen": 0x0000A5 + case "brokenbar": 0x0000A6 + case "section": 0x0000A7 + case "diaeresis": 0x0000A8 + case "copyright": 0x0000A9 + case "ordfeminine": 0x0000AA + case "guillemotleft": 0x0000AB + case "notsign": 0x0000AC + case "hyphen": 0x0000AD + case "registered": 0x0000AE + case "macron": 0x0000AF + case "degree": 0x0000B0 + case "plusminus": 0x0000B1 + case "twosuperior": 0x0000B2 + case "threesuperior": 0x0000B3 + case "acute": 0x0000B4 + case "mu": 0x0000B5 + case "paragraph": 0x0000B6 + case "periodcentered": 0x0000B7 + case "cedilla": 0x0000B8 + case "onesuperior": 0x0000B9 + case "masculine": 0x0000BA + case "guillemotright": 0x0000BB + case "onequarter": 0x0000BC + case "onehalf": 0x0000BD + case "threequarters": 0x0000BE + case "questiondown": 0x0000BF + case "Agrave": 0x0000C0 + case "Aacute": 0x0000C1 + case "Acircumflex": 0x0000C2 + case "Atilde": 0x0000C3 + case "Adiaeresis": 0x0000C4 + case "Aring": 0x0000C5 + case "AE": 0x0000C6 + case "Ccedilla": 0x0000C7 + case "Egrave": 0x0000C8 + case "Eacute": 0x0000C9 + case "Ecircumflex": 0x0000CA + case "Ediaeresis": 0x0000CB + case "Igrave": 0x0000CC + case "Iacute": 0x0000CD + case "Icircumflex": 0x0000CE + case "Idiaeresis": 0x0000CF + case "ETH": 0x0000D0 + case "Eth": 0x0000D0 + case "Ntilde": 0x0000D1 + case "Ograve": 0x0000D2 + case "Oacute": 0x0000D3 + case "Ocircumflex": 0x0000D4 + case "Otilde": 0x0000D5 + case "Odiaeresis": 0x0000D6 + case "multiply": 0x0000D7 + case "Ooblique": 0x0000D8 + case "Ugrave": 0x0000D9 + case "Uacute": 0x0000DA + case "Ucircumflex": 0x0000DB + case "Udiaeresis": 0x0000DC + case "Yacute": 0x0000DD + case "THORN": 0x0000DE + case "Thorn": 0x0000DE + case "ssharp": 0x0000DF + case "agrave": 0x0000E0 + case "aacute": 0x0000E1 + case "acircumflex": 0x0000E2 + case "atilde": 0x0000E3 + case "adiaeresis": 0x0000E4 + case "aring": 0x0000E5 + case "ae": 0x0000E6 + case "ccedilla": 0x0000E7 + case "egrave": 0x0000E8 + case "eacute": 0x0000E9 + case "ecircumflex": 0x0000EA + case "ediaeresis": 0x0000EB + case "igrave": 0x0000EC + case "iacute": 0x0000ED + case "icircumflex": 0x0000EE + case "idiaeresis": 0x0000EF + case "eth": 0x0000F0 + case "ntilde": 0x0000F1 + case "ograve": 0x0000F2 + case "oacute": 0x0000F3 + case "ocircumflex": 0x0000F4 + case "otilde": 0x0000F5 + case "odiaeresis": 0x0000F6 + case "division": 0x0000F7 + case "oslash": 0x0000F8 + case "ugrave": 0x0000F9 + case "uacute": 0x0000FA + case "ucircumflex": 0x0000FB + case "udiaeresis": 0x0000FC + case "yacute": 0x0000FD + case "thorn": 0x0000FE + case "ydiaeresis": 0x0000FF + case "Aogonek": 0x0001A1 + case "breve": 0x0001A2 + case "Lstroke": 0x0001A3 + case "Lcaron": 0x0001A5 + case "Sacute": 0x0001A6 + case "Scaron": 0x0001A9 + case "Scedilla": 0x0001AA + case "Tcaron": 0x0001AB + case "Zacute": 0x0001AC + case "Zcaron": 0x0001AE + case "Zabovedot": 0x0001AF + case "aogonek": 0x0001B1 + case "ogonek": 0x0001B2 + case "lstroke": 0x0001B3 + case "lcaron": 0x0001B5 + case "sacute": 0x0001B6 + case "caron": 0x0001B7 + case "scaron": 0x0001B9 + case "scedilla": 0x0001BA + case "tcaron": 0x0001BB + case "zacute": 0x0001BC + case "doubleacute": 0x0001BD + case "zcaron": 0x0001BE + case "zabovedot": 0x0001BF + case "Racute": 0x0001C0 + case "Abreve": 0x0001C3 + case "Lacute": 0x0001C5 + case "Cacute": 0x0001C6 + case "Ccaron": 0x0001C8 + case "Eogonek": 0x0001CA + case "Ecaron": 0x0001CC + case "Dcaron": 0x0001CF + case "Dstroke": 0x0001D0 + case "Nacute": 0x0001D1 + case "Ncaron": 0x0001D2 + case "Odoubleacute": 0x0001D5 + case "Rcaron": 0x0001D8 + case "Uring": 0x0001D9 + case "Udoubleacute": 0x0001DB + case "Tcedilla": 0x0001DE + case "racute": 0x0001E0 + case "abreve": 0x0001E3 + case "lacute": 0x0001E5 + case "cacute": 0x0001E6 + case "ccaron": 0x0001E8 + case "eogonek": 0x0001EA + case "ecaron": 0x0001EC + case "dcaron": 0x0001EF + case "dstroke": 0x0001F0 + case "nacute": 0x0001F1 + case "ncaron": 0x0001F2 + case "odoubleacute": 0x0001F5 + case "rcaron": 0x0001F8 + case "uring": 0x0001F9 + case "udoubleacute": 0x0001FB + case "tcedilla": 0x0001FE + case "abovedot": 0x0001FF // others - case "Hstroke": return 0x0002A1 - case "Hcircumflex": return 0x0002A6 - case "Iabovedot": return 0x0002A9 - case "Gbreve": return 0x0002AB - case "Jcircumflex": return 0x0002AC - case "hstroke": return 0x0002B1 - case "hcircumflex": return 0x0002B6 - case "idotless": return 0x0002B9 - case "gbreve": return 0x0002BB - case "jcircumflex": return 0x0002BC - case "Cabovedot": return 0x0002C5 - case "Ccircumflex": return 0x0002C6 - case "Gabovedot": return 0x0002D5 - case "Gcircumflex": return 0x0002D8 - case "Ubreve": return 0x0002DD - case "Scircumflex": return 0x0002DE - case "cabovedot": return 0x0002E5 - case "ccircumflex": return 0x0002E6 - case "gabovedot": return 0x0002F5 - case "gcircumflex": return 0x0002F8 - case "ubreve": return 0x0002FD - case "scircumflex": return 0x0002FE - case "kappa": return 0x0003A2 - case "kra": return 0x0003A2 - case "Rcedilla": return 0x0003A3 - case "Itilde": return 0x0003A5 - case "Lcedilla": return 0x0003A6 - case "Emacron": return 0x0003AA - case "Gcedilla": return 0x0003AB - case "Tslash": return 0x0003AC - case "rcedilla": return 0x0003B3 - case "itilde": return 0x0003B5 - case "lcedilla": return 0x0003B6 - case "emacron": return 0x0003BA - case "gcedilla": return 0x0003BB - case "tslash": return 0x0003BC - case "ENG": return 0x0003BD - case "eng": return 0x0003BF - case "Amacron": return 0x0003C0 - case "Iogonek": return 0x0003C7 - case "Eabovedot": return 0x0003CC - case "Imacron": return 0x0003CF - case "Ncedilla": return 0x0003D1 - case "Omacron": return 0x0003D2 - case "Kcedilla": return 0x0003D3 - case "Uogonek": return 0x0003D9 - case "Utilde": return 0x0003DD - case "Umacron": return 0x0003DE - case "amacron": return 0x0003E0 - case "iogonek": return 0x0003E7 - case "eabovedot": return 0x0003EC - case "imacron": return 0x0003EF - case "ncedilla": return 0x0003F1 - case "omacron": return 0x0003F2 - case "kcedilla": return 0x0003F3 - case "uogonek": return 0x0003F9 - case "utilde": return 0x0003FD - case "umacron": return 0x0003FE - case "overline": return 0x00047E - case "kana_fullstop": return 0x0004A1 - case "kana_openingbracket": return 0x0004A2 - case "kana_closingbracket": return 0x0004A3 - case "kana_comma": return 0x0004A4 - case "kana_conjunctive": return 0x0004A5 - case "kana_middledot": return 0x0004A5 - case "kana_WO": return 0x0004A6 - case "kana_a": return 0x0004A7 - case "kana_i": return 0x0004A8 - case "kana_u": return 0x0004A9 - case "kana_e": return 0x0004AA - case "kana_o": return 0x0004AB - case "kana_ya": return 0x0004AC - case "kana_yu": return 0x0004AD - case "kana_yo": return 0x0004AE - case "kana_tsu": return 0x0004AF - case "kana_tu": return 0x0004AF - case "prolongedsound": return 0x0004B0 - case "kana_A": return 0x0004B1 - case "kana_I": return 0x0004B2 - case "kana_U": return 0x0004B3 - case "kana_E": return 0x0004B4 - case "kana_O": return 0x0004B5 - case "kana_KA": return 0x0004B6 - case "kana_KI": return 0x0004B7 - case "kana_KU": return 0x0004B8 - case "kana_KE": return 0x0004B9 - case "kana_KO": return 0x0004BA - case "kana_SA": return 0x0004BB - case "kana_SHI": return 0x0004BC - case "kana_SU": return 0x0004BD - case "kana_SE": return 0x0004BE - case "kana_SO": return 0x0004BF - case "kana_TA": return 0x0004C0 - case "kana_CHI": return 0x0004C1 - case "kana_TI": return 0x0004C1 - case "kana_TSU": return 0x0004C2 - case "kana_TU": return 0x0004C2 - case "kana_TE": return 0x0004C3 - case "kana_TO": return 0x0004C4 - case "kana_NA": return 0x0004C5 - case "kana_NI": return 0x0004C6 - case "kana_NU": return 0x0004C7 - case "kana_NE": return 0x0004C8 - case "kana_NO": return 0x0004C9 - case "kana_HA": return 0x0004CA - case "kana_HI": return 0x0004CB - case "kana_FU": return 0x0004CC - case "kana_HU": return 0x0004CC - case "kana_HE": return 0x0004CD - case "kana_HO": return 0x0004CE - case "kana_MA": return 0x0004CF - case "kana_MI": return 0x0004D0 - case "kana_MU": return 0x0004D1 - case "kana_ME": return 0x0004D2 - case "kana_MO": return 0x0004D3 - case "kana_YA": return 0x0004D4 - case "kana_YU": return 0x0004D5 - case "kana_YO": return 0x0004D6 - case "kana_RA": return 0x0004D7 - case "kana_RI": return 0x0004D8 - case "kana_RU": return 0x0004D9 - case "kana_RE": return 0x0004DA - case "kana_RO": return 0x0004DB - case "kana_WA": return 0x0004DC - case "kana_N": return 0x0004DD - case "voicedsound": return 0x0004DE - case "semivoicedsound": return 0x0004DF - case "Arabic_comma": return 0x0005AC - case "Arabic_semicolon": return 0x0005BB - case "Arabic_question_mark": return 0x0005BF - case "Arabic_hamza": return 0x0005C1 - case "Arabic_maddaonalef": return 0x0005C2 - case "Arabic_hamzaonalef": return 0x0005C3 - case "Arabic_hamzaonwaw": return 0x0005C4 - case "Arabic_hamzaunderalef": return 0x0005C5 - case "Arabic_hamzaonyeh": return 0x0005C6 - case "Arabic_alef": return 0x0005C7 - case "Arabic_beh": return 0x0005C8 - case "Arabic_tehmarbuta": return 0x0005C9 - case "Arabic_teh": return 0x0005CA - case "Arabic_theh": return 0x0005CB - case "Arabic_jeem": return 0x0005CC - case "Arabic_hah": return 0x0005CD - case "Arabic_khah": return 0x0005CE - case "Arabic_dal": return 0x0005CF - case "Arabic_thal": return 0x0005D0 - case "Arabic_ra": return 0x0005D1 - case "Arabic_zain": return 0x0005D2 - case "Arabic_seen": return 0x0005D3 - case "Arabic_sheen": return 0x0005D4 - case "Arabic_sad": return 0x0005D5 - case "Arabic_dad": return 0x0005D6 - case "Arabic_tah": return 0x0005D7 - case "Arabic_zah": return 0x0005D8 - case "Arabic_ain": return 0x0005D9 - case "Arabic_ghain": return 0x0005DA - case "Arabic_tatweel": return 0x0005E0 - case "Arabic_feh": return 0x0005E1 - case "Arabic_qaf": return 0x0005E2 - case "Arabic_kaf": return 0x0005E3 - case "Arabic_lam": return 0x0005E4 - case "Arabic_meem": return 0x0005E5 - case "Arabic_noon": return 0x0005E6 - case "Arabic_ha": return 0x0005E7 - case "Arabic_heh": return 0x0005E7 - case "Arabic_waw": return 0x0005E8 - case "Arabic_alefmaksura": return 0x0005E9 - case "Arabic_yeh": return 0x0005EA - case "Arabic_fathatan": return 0x0005EB - case "Arabic_dammatan": return 0x0005EC - case "Arabic_kasratan": return 0x0005ED - case "Arabic_fatha": return 0x0005EE - case "Arabic_damma": return 0x0005EF - case "Arabic_kasra": return 0x0005F0 - case "Arabic_shadda": return 0x0005F1 - case "Arabic_sukun": return 0x0005F2 - case "Serbian_dje": return 0x0006A1 - case "Macedonia_gje": return 0x0006A2 - case "Cyrillic_io": return 0x0006A3 - case "Ukrainian_ie": return 0x0006A4 - case "Ukranian_je": return 0x0006A4 - case "Macedonia_dse": return 0x0006A5 - case "Ukrainian_i": return 0x0006A6 - case "Ukranian_i": return 0x0006A6 - case "Ukrainian_yi": return 0x0006A7 - case "Ukranian_yi": return 0x0006A7 - case "Cyrillic_je": return 0x0006A8 - case "Serbian_je": return 0x0006A8 - case "Cyrillic_lje": return 0x0006A9 - case "Serbian_lje": return 0x0006A9 - case "Cyrillic_nje": return 0x0006AA - case "Serbian_nje": return 0x0006AA - case "Serbian_tshe": return 0x0006AB - case "Macedonia_kje": return 0x0006AC - case "Byelorussian_shortu": return 0x0006AE - case "Cyrillic_dzhe": return 0x0006AF - case "Serbian_dze": return 0x0006AF - case "numerosign": return 0x0006B0 - case "Serbian_DJE": return 0x0006B1 - case "Macedonia_GJE": return 0x0006B2 - case "Cyrillic_IO": return 0x0006B3 - case "Ukrainian_IE": return 0x0006B4 - case "Ukranian_JE": return 0x0006B4 - case "Macedonia_DSE": return 0x0006B5 - case "Ukrainian_I": return 0x0006B6 - case "Ukranian_I": return 0x0006B6 - case "Ukrainian_YI": return 0x0006B7 - case "Ukranian_YI": return 0x0006B7 - case "Cyrillic_JE": return 0x0006B8 - case "Serbian_JE": return 0x0006B8 - case "Cyrillic_LJE": return 0x0006B9 - case "Serbian_LJE": return 0x0006B9 - case "Cyrillic_NJE": return 0x0006BA - case "Serbian_NJE": return 0x0006BA - case "Serbian_TSHE": return 0x0006BB - case "Macedonia_KJE": return 0x0006BC - case "Byelorussian_SHORTU": return 0x0006BE - case "Cyrillic_DZHE": return 0x0006BF - case "Serbian_DZE": return 0x0006BF - case "Cyrillic_yu": return 0x0006C0 - case "Cyrillic_a": return 0x0006C1 - case "Cyrillic_be": return 0x0006C2 - case "Cyrillic_tse": return 0x0006C3 - case "Cyrillic_de": return 0x0006C4 - case "Cyrillic_ie": return 0x0006C5 - case "Cyrillic_ef": return 0x0006C6 - case "Cyrillic_ghe": return 0x0006C7 - case "Cyrillic_ha": return 0x0006C8 - case "Cyrillic_i": return 0x0006C9 - case "Cyrillic_shorti": return 0x0006CA - case "Cyrillic_ka": return 0x0006CB - case "Cyrillic_el": return 0x0006CC - case "Cyrillic_em": return 0x0006CD - case "Cyrillic_en": return 0x0006CE - case "Cyrillic_o": return 0x0006CF - case "Cyrillic_pe": return 0x0006D0 - case "Cyrillic_ya": return 0x0006D1 - case "Cyrillic_er": return 0x0006D2 - case "Cyrillic_es": return 0x0006D3 - case "Cyrillic_te": return 0x0006D4 - case "Cyrillic_u": return 0x0006D5 - case "Cyrillic_zhe": return 0x0006D6 - case "Cyrillic_ve": return 0x0006D7 - case "Cyrillic_softsign": return 0x0006D8 - case "Cyrillic_yeru": return 0x0006D9 - case "Cyrillic_ze": return 0x0006DA - case "Cyrillic_sha": return 0x0006DB - case "Cyrillic_e": return 0x0006DC - case "Cyrillic_shcha": return 0x0006DD - case "Cyrillic_che": return 0x0006DE - case "Cyrillic_hardsign": return 0x0006DF - case "Cyrillic_YU": return 0x0006E0 - case "Cyrillic_A": return 0x0006E1 - case "Cyrillic_BE": return 0x0006E2 - case "Cyrillic_TSE": return 0x0006E3 - case "Cyrillic_DE": return 0x0006E4 - case "Cyrillic_IE": return 0x0006E5 - case "Cyrillic_EF": return 0x0006E6 - case "Cyrillic_GHE": return 0x0006E7 - case "Cyrillic_HA": return 0x0006E8 - case "Cyrillic_I": return 0x0006E9 - case "Cyrillic_SHORTI": return 0x0006EA - case "Cyrillic_KA": return 0x0006EB - case "Cyrillic_EL": return 0x0006EC - case "Cyrillic_EM": return 0x0006ED - case "Cyrillic_EN": return 0x0006EE - case "Cyrillic_O": return 0x0006EF - case "Cyrillic_PE": return 0x0006F0 - case "Cyrillic_YA": return 0x0006F1 - case "Cyrillic_ER": return 0x0006F2 - case "Cyrillic_ES": return 0x0006F3 - case "Cyrillic_TE": return 0x0006F4 - case "Cyrillic_U": return 0x0006F5 - case "Cyrillic_ZHE": return 0x0006F6 - case "Cyrillic_VE": return 0x0006F7 - case "Cyrillic_SOFTSIGN": return 0x0006F8 - case "Cyrillic_YERU": return 0x0006F9 - case "Cyrillic_ZE": return 0x0006FA - case "Cyrillic_SHA": return 0x0006FB - case "Cyrillic_E": return 0x0006FC - case "Cyrillic_SHCHA": return 0x0006FD - case "Cyrillic_CHE": return 0x0006FE - case "Cyrillic_HARDSIGN": return 0x0006FF - case "Greek_ALPHAaccent": return 0x0007A1 - case "Greek_EPSILONaccent": return 0x0007A2 - case "Greek_ETAaccent": return 0x0007A3 - case "Greek_IOTAaccent": return 0x0007A4 - case "Greek_IOTAdieresis": return 0x0007A5 - case "Greek_IOTAdiaeresis": return 0x0007A5 - case "Greek_OMICRONaccent": return 0x0007A7 - case "Greek_UPSILONaccent": return 0x0007A8 - case "Greek_UPSILONdieresis": return 0x0007A9 - case "Greek_OMEGAaccent": return 0x0007AB - case "Greek_accentdieresis": return 0x0007AE - case "Greek_horizbar": return 0x0007AF - case "Greek_alphaaccent": return 0x0007B1 - case "Greek_epsilonaccent": return 0x0007B2 - case "Greek_etaaccent": return 0x0007B3 - case "Greek_iotaaccent": return 0x0007B4 - case "Greek_iotadieresis": return 0x0007B5 - case "Greek_iotaaccentdieresis": return 0x0007B6 - case "Greek_omicronaccent": return 0x0007B7 - case "Greek_upsilonaccent": return 0x0007B8 - case "Greek_upsilondieresis": return 0x0007B9 - case "Greek_upsilonaccentdieresis": return 0x0007BA - case "Greek_omegaaccent": return 0x0007BB - case "Greek_ALPHA": return 0x0007C1 - case "Greek_BETA": return 0x0007C2 - case "Greek_GAMMA": return 0x0007C3 - case "Greek_DELTA": return 0x0007C4 - case "Greek_EPSILON": return 0x0007C5 - case "Greek_ZETA": return 0x0007C6 - case "Greek_ETA": return 0x0007C7 - case "Greek_THETA": return 0x0007C8 - case "Greek_IOTA": return 0x0007C9 - case "Greek_KAPPA": return 0x0007CA - case "Greek_LAMBDA": return 0x0007CB - case "Greek_LAMDA": return 0x0007CB - case "Greek_MU": return 0x0007CC - case "Greek_NU": return 0x0007CD - case "Greek_XI": return 0x0007CE - case "Greek_OMICRON": return 0x0007CF - case "Greek_PI": return 0x0007D0 - case "Greek_RHO": return 0x0007D1 - case "Greek_SIGMA": return 0x0007D2 - case "Greek_TAU": return 0x0007D4 - case "Greek_UPSILON": return 0x0007D5 - case "Greek_PHI": return 0x0007D6 - case "Greek_CHI": return 0x0007D7 - case "Greek_PSI": return 0x0007D8 - case "Greek_OMEGA": return 0x0007D9 - case "Greek_alpha": return 0x0007E1 - case "Greek_beta": return 0x0007E2 - case "Greek_gamma": return 0x0007E3 - case "Greek_delta": return 0x0007E4 - case "Greek_epsilon": return 0x0007E5 - case "Greek_zeta": return 0x0007E6 - case "Greek_eta": return 0x0007E7 - case "Greek_theta": return 0x0007E8 - case "Greek_iota": return 0x0007E9 - case "Greek_kappa": return 0x0007EA - case "Greek_lambda": return 0x0007EB - case "Greek_lamda": return 0x0007EB - case "Greek_mu": return 0x0007EC - case "Greek_nu": return 0x0007ED - case "Greek_xi": return 0x0007EE - case "Greek_omicron": return 0x0007EF - case "Greek_pi": return 0x0007F0 - case "Greek_rho": return 0x0007F1 - case "Greek_sigma": return 0x0007F2 - case "Greek_finalsmallsigma": return 0x0007F3 - case "Greek_tau": return 0x0007F4 - case "Greek_upsilon": return 0x0007F5 - case "Greek_phi": return 0x0007F6 - case "Greek_chi": return 0x0007F7 - case "Greek_psi": return 0x0007F8 - case "Greek_omega": return 0x0007F9 - case "leftradical": return 0x0008A1 - case "topleftradical": return 0x0008A2 - case "horizconnector": return 0x0008A3 - case "topintegral": return 0x0008A4 - case "botintegral": return 0x0008A5 - case "vertconnector": return 0x0008A6 - case "topleftsqbracket": return 0x0008A7 - case "botleftsqbracket": return 0x0008A8 - case "toprightsqbracket": return 0x0008A9 - case "botrightsqbracket": return 0x0008AA - case "topleftparens": return 0x0008AB - case "botleftparens": return 0x0008AC - case "toprightparens": return 0x0008AD - case "botrightparens": return 0x0008AE - case "leftmiddlecurlybrace": return 0x0008AF - case "rightmiddlecurlybrace": return 0x0008B0 - case "topleftsummation": return 0x0008B1 - case "botleftsummation": return 0x0008B2 - case "topvertsummationconnector": return 0x0008B3 - case "botvertsummationconnector": return 0x0008B4 - case "toprightsummation": return 0x0008B5 - case "botrightsummation": return 0x0008B6 - case "rightmiddlesummation": return 0x0008B7 - case "lessthanequal": return 0x0008BC - case "notequal": return 0x0008BD - case "greaterthanequal": return 0x0008BE - case "integral": return 0x0008BF - case "therefore": return 0x0008C0 - case "variation": return 0x0008C1 - case "infinity": return 0x0008C2 - case "nabla": return 0x0008C5 - case "approximate": return 0x0008C8 - case "similarequal": return 0x0008C9 - case "ifonlyif": return 0x0008CD - case "implies": return 0x0008CE - case "identical": return 0x0008CF - case "radical": return 0x0008D6 - case "includedin": return 0x0008DA - case "includes": return 0x0008DB - case "intersection": return 0x0008DC - case "union": return 0x0008DD - case "logicaland": return 0x0008DE - case "logicalor": return 0x0008DF - case "partialderivative": return 0x0008EF - case "function": return 0x0008F6 - case "leftarrow": return 0x0008FB - case "uparrow": return 0x0008FC - case "rightarrow": return 0x0008FD - case "downarrow": return 0x0008FE - case "blank": return 0x0009DF - case "soliddiamond": return 0x0009E0 - case "checkerboard": return 0x0009E1 - case "ht": return 0x0009E2 - case "ff": return 0x0009E3 - case "cr": return 0x0009E4 - case "lf": return 0x0009E5 - case "nl": return 0x0009E8 - case "vt": return 0x0009E9 - case "lowrightcorner": return 0x0009EA - case "uprightcorner": return 0x0009EB - case "upleftcorner": return 0x0009EC - case "lowleftcorner": return 0x0009ED - case "crossinglines": return 0x0009EE - case "horizlinescan1": return 0x0009EF - case "horizlinescan3": return 0x0009F0 - case "horizlinescan5": return 0x0009F1 - case "horizlinescan7": return 0x0009F2 - case "horizlinescan9": return 0x0009F3 - case "leftt": return 0x0009F4 - case "rightt": return 0x0009F5 - case "bott": return 0x0009F6 - case "topt": return 0x0009F7 - case "vertbar": return 0x0009F8 - case "emspace": return 0x000AA1 - case "enspace": return 0x000AA2 - case "em3space": return 0x000AA3 - case "em4space": return 0x000AA4 - case "digitspace": return 0x000AA5 - case "punctspace": return 0x000AA6 - case "thinspace": return 0x000AA7 - case "hairspace": return 0x000AA8 - case "emdash": return 0x000AA9 - case "endash": return 0x000AAA - case "signifblank": return 0x000AAC - case "ellipsis": return 0x000AAE - case "doubbaselinedot": return 0x000AAF - case "onethird": return 0x000AB0 - case "twothirds": return 0x000AB1 - case "onefifth": return 0x000AB2 - case "twofifths": return 0x000AB3 - case "threefifths": return 0x000AB4 - case "fourfifths": return 0x000AB5 - case "onesixth": return 0x000AB6 - case "fivesixths": return 0x000AB7 - case "careof": return 0x000AB8 - case "figdash": return 0x000ABB - case "leftanglebracket": return 0x000ABC - case "decimalpoint": return 0x000ABD - case "rightanglebracket": return 0x000ABE - case "marker": return 0x000ABF - case "oneeighth": return 0x000AC3 - case "threeeighths": return 0x000AC4 - case "fiveeighths": return 0x000AC5 - case "seveneighths": return 0x000AC6 - case "trademark": return 0x000AC9 - case "signaturemark": return 0x000ACA - case "trademarkincircle": return 0x000ACB - case "leftopentriangle": return 0x000ACC - case "rightopentriangle": return 0x000ACD - case "emopencircle": return 0x000ACE - case "emopenrectangle": return 0x000ACF - case "leftsinglequotemark": return 0x000AD0 - case "rightsinglequotemark": return 0x000AD1 - case "leftdoublequotemark": return 0x000AD2 - case "rightdoublequotemark": return 0x000AD3 - case "prescription": return 0x000AD4 - case "minutes": return 0x000AD6 - case "seconds": return 0x000AD7 - case "latincross": return 0x000AD9 - case "hexagram": return 0x000ADA - case "filledrectbullet": return 0x000ADB - case "filledlefttribullet": return 0x000ADC - case "filledrighttribullet": return 0x000ADD - case "emfilledcircle": return 0x000ADE - case "emfilledrect": return 0x000ADF - case "enopencircbullet": return 0x000AE0 - case "enopensquarebullet": return 0x000AE1 - case "openrectbullet": return 0x000AE2 - case "opentribulletup": return 0x000AE3 - case "opentribulletdown": return 0x000AE4 - case "openstar": return 0x000AE5 - case "enfilledcircbullet": return 0x000AE6 - case "enfilledsqbullet": return 0x000AE7 - case "filledtribulletup": return 0x000AE8 - case "filledtribulletdown": return 0x000AE9 - case "leftpointer": return 0x000AEA - case "rightpointer": return 0x000AEB - case "club": return 0x000AEC - case "diamond": return 0x000AED - case "heart": return 0x000AEE - case "maltesecross": return 0x000AF0 - case "dagger": return 0x000AF1 - case "doubledagger": return 0x000AF2 - case "checkmark": return 0x000AF3 - case "ballotcross": return 0x000AF4 - case "musicalsharp": return 0x000AF5 - case "musicalflat": return 0x000AF6 - case "malesymbol": return 0x000AF7 - case "femalesymbol": return 0x000AF8 - case "telephone": return 0x000AF9 - case "telephonerecorder": return 0x000AFA - case "phonographcopyright": return 0x000AFB - case "caret": return 0x000AFC - case "singlelowquotemark": return 0x000AFD - case "doublelowquotemark": return 0x000AFE - case "cursor": return 0x000AFF - case "leftcaret": return 0x000BA3 - case "rightcaret": return 0x000BA6 - case "downcaret": return 0x000BA8 - case "upcaret": return 0x000BA9 - case "overbar": return 0x000BC0 - case "downtack": return 0x000BC2 - case "upshoe": return 0x000BC3 - case "downstile": return 0x000BC4 - case "underbar": return 0x000BC6 - case "jot": return 0x000BCA - case "quad": return 0x000BCC - case "uptack": return 0x000BCE - case "circle": return 0x000BCF - case "upstile": return 0x000BD3 - case "downshoe": return 0x000BD6 - case "rightshoe": return 0x000BD8 - case "leftshoe": return 0x000BDA - case "lefttack": return 0x000BDC - case "righttack": return 0x000BFC - case "hebrew_doublelowline": return 0x000CDF - case "hebrew_aleph": return 0x000CE0 - case "hebrew_bet": return 0x000CE1 - case "hebrew_beth": return 0x000CE1 - case "hebrew_gimel": return 0x000CE2 - case "hebrew_gimmel": return 0x000CE2 - case "hebrew_dalet": return 0x000CE3 - case "hebrew_daleth": return 0x000CE3 - case "hebrew_he": return 0x000CE4 - case "hebrew_waw": return 0x000CE5 - case "hebrew_zain": return 0x000CE6 - case "hebrew_zayin": return 0x000CE6 - case "hebrew_chet": return 0x000CE7 - case "hebrew_het": return 0x000CE7 - case "hebrew_tet": return 0x000CE8 - case "hebrew_teth": return 0x000CE8 - case "hebrew_yod": return 0x000CE9 - case "hebrew_finalkaph": return 0x000CEA - case "hebrew_kaph": return 0x000CEB - case "hebrew_lamed": return 0x000CEC - case "hebrew_finalmem": return 0x000CED - case "hebrew_mem": return 0x000CEE - case "hebrew_finalnun": return 0x000CEF - case "hebrew_nun": return 0x000CF0 - case "hebrew_samech": return 0x000CF1 - case "hebrew_samekh": return 0x000CF1 - case "hebrew_ayin": return 0x000CF2 - case "hebrew_finalpe": return 0x000CF3 - case "hebrew_pe": return 0x000CF4 - case "hebrew_finalzade": return 0x000CF5 - case "hebrew_finalzadi": return 0x000CF5 - case "hebrew_zade": return 0x000CF6 - case "hebrew_zadi": return 0x000CF6 - case "hebrew_kuf": return 0x000CF7 - case "hebrew_qoph": return 0x000CF7 - case "hebrew_resh": return 0x000CF8 - case "hebrew_shin": return 0x000CF9 - case "hebrew_taf": return 0x000CFA - case "hebrew_taw": return 0x000CFA - case "Thai_kokai": return 0x000DA1 - case "Thai_khokhai": return 0x000DA2 - case "Thai_khokhuat": return 0x000DA3 - case "Thai_khokhwai": return 0x000DA4 - case "Thai_khokhon": return 0x000DA5 - case "Thai_khorakhang": return 0x000DA6 - case "Thai_ngongu": return 0x000DA7 - case "Thai_chochan": return 0x000DA8 - case "Thai_choching": return 0x000DA9 - case "Thai_chochang": return 0x000DAA - case "Thai_soso": return 0x000DAB - case "Thai_chochoe": return 0x000DAC - case "Thai_yoying": return 0x000DAD - case "Thai_dochada": return 0x000DAE - case "Thai_topatak": return 0x000DAF - case "Thai_thothan": return 0x000DB0 - case "Thai_thonangmontho": return 0x000DB1 - case "Thai_thophuthao": return 0x000DB2 - case "Thai_nonen": return 0x000DB3 - case "Thai_dodek": return 0x000DB4 - case "Thai_totao": return 0x000DB5 - case "Thai_thothung": return 0x000DB6 - case "Thai_thothahan": return 0x000DB7 - case "Thai_thothong": return 0x000DB8 - case "Thai_nonu": return 0x000DB9 - case "Thai_bobaimai": return 0x000DBA - case "Thai_popla": return 0x000DBB - case "Thai_phophung": return 0x000DBC - case "Thai_fofa": return 0x000DBD - case "Thai_phophan": return 0x000DBE - case "Thai_fofan": return 0x000DBF - case "Thai_phosamphao": return 0x000DC0 - case "Thai_moma": return 0x000DC1 - case "Thai_yoyak": return 0x000DC2 - case "Thai_rorua": return 0x000DC3 - case "Thai_ru": return 0x000DC4 - case "Thai_loling": return 0x000DC5 - case "Thai_lu": return 0x000DC6 - case "Thai_wowaen": return 0x000DC7 - case "Thai_sosala": return 0x000DC8 - case "Thai_sorusi": return 0x000DC9 - case "Thai_sosua": return 0x000DCA - case "Thai_hohip": return 0x000DCB - case "Thai_lochula": return 0x000DCC - case "Thai_oang": return 0x000DCD - case "Thai_honokhuk": return 0x000DCE - case "Thai_paiyannoi": return 0x000DCF - case "Thai_saraa": return 0x000DD0 - case "Thai_maihanakat": return 0x000DD1 - case "Thai_saraaa": return 0x000DD2 - case "Thai_saraam": return 0x000DD3 - case "Thai_sarai": return 0x000DD4 - case "Thai_saraii": return 0x000DD5 - case "Thai_saraue": return 0x000DD6 - case "Thai_sarauee": return 0x000DD7 - case "Thai_sarau": return 0x000DD8 - case "Thai_sarauu": return 0x000DD9 - case "Thai_phinthu": return 0x000DDA - case "Thai_maihanakat_maitho": return 0x000DDE - case "Thai_baht": return 0x000DDF - case "Thai_sarae": return 0x000DE0 - case "Thai_saraae": return 0x000DE1 - case "Thai_sarao": return 0x000DE2 - case "Thai_saraaimaimuan": return 0x000DE3 - case "Thai_saraaimaimalai": return 0x000DE4 - case "Thai_lakkhangyao": return 0x000DE5 - case "Thai_maiyamok": return 0x000DE6 - case "Thai_maitaikhu": return 0x000DE7 - case "Thai_maiek": return 0x000DE8 - case "Thai_maitho": return 0x000DE9 - case "Thai_maitri": return 0x000DEA - case "Thai_maichattawa": return 0x000DEB - case "Thai_thanthakhat": return 0x000DEC - case "Thai_nikhahit": return 0x000DED - case "Thai_leksun": return 0x000DF0 - case "Thai_leknung": return 0x000DF1 - case "Thai_leksong": return 0x000DF2 - case "Thai_leksam": return 0x000DF3 - case "Thai_leksi": return 0x000DF4 - case "Thai_lekha": return 0x000DF5 - case "Thai_lekhok": return 0x000DF6 - case "Thai_lekchet": return 0x000DF7 - case "Thai_lekpaet": return 0x000DF8 - case "Thai_lekkao": return 0x000DF9 - case "Hangul_Kiyeog": return 0x000EA1 - case "Hangul_SsangKiyeog": return 0x000EA2 - case "Hangul_KiyeogSios": return 0x000EA3 - case "Hangul_Nieun": return 0x000EA4 - case "Hangul_NieunJieuj": return 0x000EA5 - case "Hangul_NieunHieuh": return 0x000EA6 - case "Hangul_Dikeud": return 0x000EA7 - case "Hangul_SsangDikeud": return 0x000EA8 - case "Hangul_Rieul": return 0x000EA9 - case "Hangul_RieulKiyeog": return 0x000EAA - case "Hangul_RieulMieum": return 0x000EAB - case "Hangul_RieulPieub": return 0x000EAC - case "Hangul_RieulSios": return 0x000EAD - case "Hangul_RieulTieut": return 0x000EAE - case "Hangul_RieulPhieuf": return 0x000EAF - case "Hangul_RieulHieuh": return 0x000EB0 - case "Hangul_Mieum": return 0x000EB1 - case "Hangul_Pieub": return 0x000EB2 - case "Hangul_SsangPieub": return 0x000EB3 - case "Hangul_PieubSios": return 0x000EB4 - case "Hangul_Sios": return 0x000EB5 - case "Hangul_SsangSios": return 0x000EB6 - case "Hangul_Ieung": return 0x000EB7 - case "Hangul_Jieuj": return 0x000EB8 - case "Hangul_SsangJieuj": return 0x000EB9 - case "Hangul_Cieuc": return 0x000EBA - case "Hangul_Khieuq": return 0x000EBB - case "Hangul_Tieut": return 0x000EBC - case "Hangul_Phieuf": return 0x000EBD - case "Hangul_Hieuh": return 0x000EBE - case "Hangul_A": return 0x000EBF - case "Hangul_AE": return 0x000EC0 - case "Hangul_YA": return 0x000EC1 - case "Hangul_YAE": return 0x000EC2 - case "Hangul_EO": return 0x000EC3 - case "Hangul_E": return 0x000EC4 - case "Hangul_YEO": return 0x000EC5 - case "Hangul_YE": return 0x000EC6 - case "Hangul_O": return 0x000EC7 - case "Hangul_WA": return 0x000EC8 - case "Hangul_WAE": return 0x000EC9 - case "Hangul_OE": return 0x000ECA - case "Hangul_YO": return 0x000ECB - case "Hangul_U": return 0x000ECC - case "Hangul_WEO": return 0x000ECD - case "Hangul_WE": return 0x000ECE - case "Hangul_WI": return 0x000ECF - case "Hangul_YU": return 0x000ED0 - case "Hangul_EU": return 0x000ED1 - case "Hangul_YI": return 0x000ED2 - case "Hangul_I": return 0x000ED3 - case "Hangul_J_Kiyeog": return 0x000ED4 - case "Hangul_J_SsangKiyeog": return 0x000ED5 - case "Hangul_J_KiyeogSios": return 0x000ED6 - case "Hangul_J_Nieun": return 0x000ED7 - case "Hangul_J_NieunJieuj": return 0x000ED8 - case "Hangul_J_NieunHieuh": return 0x000ED9 - case "Hangul_J_Dikeud": return 0x000EDA - case "Hangul_J_Rieul": return 0x000EDB - case "Hangul_J_RieulKiyeog": return 0x000EDC - case "Hangul_J_RieulMieum": return 0x000EDD - case "Hangul_J_RieulPieub": return 0x000EDE - case "Hangul_J_RieulSios": return 0x000EDF - case "Hangul_J_RieulTieut": return 0x000EE0 - case "Hangul_J_RieulPhieuf": return 0x000EE1 - case "Hangul_J_RieulHieuh": return 0x000EE2 - case "Hangul_J_Mieum": return 0x000EE3 - case "Hangul_J_Pieub": return 0x000EE4 - case "Hangul_J_PieubSios": return 0x000EE5 - case "Hangul_J_Sios": return 0x000EE6 - case "Hangul_J_SsangSios": return 0x000EE7 - case "Hangul_J_Ieung": return 0x000EE8 - case "Hangul_J_Jieuj": return 0x000EE9 - case "Hangul_J_Cieuc": return 0x000EEA - case "Hangul_J_Khieuq": return 0x000EEB - case "Hangul_J_Tieut": return 0x000EEC - case "Hangul_J_Phieuf": return 0x000EED - case "Hangul_J_Hieuh": return 0x000EEE - case "Hangul_RieulYeorinHieuh": return 0x000EEF - case "Hangul_SunkyeongeumMieum": return 0x000EF0 - case "Hangul_SunkyeongeumPieub": return 0x000EF1 - case "Hangul_PanSios": return 0x000EF2 - case "Hangul_KkogjiDalrinIeung": return 0x000EF3 - case "Hangul_SunkyeongeumPhieuf": return 0x000EF4 - case "Hangul_YeorinHieuh": return 0x000EF5 - case "Hangul_AraeA": return 0x000EF6 - case "Hangul_AraeAE": return 0x000EF7 - case "Hangul_J_PanSios": return 0x000EF8 - case "Hangul_J_KkogjiDalrinIeung": return 0x000EF9 - case "Hangul_J_YeorinHieuh": return 0x000EFA - case "Korean_Won": return 0x000EFF - case "OE": return 0x0013BC - case "oe": return 0x0013BD - case "Ydiaeresis": return 0x0013BE - case "EcuSign": return 0x0020A0 - case "ColonSign": return 0x0020A1 - case "CruzeiroSign": return 0x0020A2 - case "FFrancSign": return 0x0020A3 - case "LiraSign": return 0x0020A4 - case "MillSign": return 0x0020A5 - case "NairaSign": return 0x0020A6 - case "PesetaSign": return 0x0020A7 - case "RupeeSign": return 0x0020A8 - case "WonSign": return 0x0020A9 - case "NewSheqelSign": return 0x0020AA - case "DongSign": return 0x0020AB - case "EuroSign": return 0x0020AC - case "3270_Duplicate": return 0x00FD01 - case "3270_FieldMark": return 0x00FD02 - case "3270_Right2": return 0x00FD03 - case "3270_Left2": return 0x00FD04 - case "3270_BackTab": return 0x00FD05 - case "3270_EraseEOF": return 0x00FD06 - case "3270_EraseInput": return 0x00FD07 - case "3270_Reset": return 0x00FD08 - case "3270_Quit": return 0x00FD09 - case "3270_PA1": return 0x00FD0A - case "3270_PA2": return 0x00FD0B - case "3270_PA3": return 0x00FD0C - case "3270_Test": return 0x00FD0D - case "3270_Attn": return 0x00FD0E - case "3270_CursorBlink": return 0x00FD0F - case "3270_AltCursor": return 0x00FD10 - case "3270_KeyClick": return 0x00FD11 - case "3270_Jump": return 0x00FD12 - case "3270_Ident": return 0x00FD13 - case "3270_Rule": return 0x00FD14 - case "3270_Copy": return 0x00FD15 - case "3270_Play": return 0x00FD16 - case "3270_Setup": return 0x00FD17 - case "3270_Record": return 0x00FD18 - case "3270_ChangeScreen": return 0x00FD19 - case "3270_DeleteWord": return 0x00FD1A - case "3270_ExSelect": return 0x00FD1B - case "3270_CursorSelect": return 0x00FD1C - case "3270_PrintScreen": return 0x00FD1D - case "3270_Enter": return 0x00FD1E - case "ISO_Lock": return 0x00FE01 - case "ISO_Level2_Latch": return 0x00FE02 - case "ISO_Level3_Shift": return 0x00FE03 - case "ISO_Level3_Latch": return 0x00FE04 - case "ISO_Level3_Lock": return 0x00FE05 - case "ISO_Group_Latch": return 0x00FE06 - case "ISO_Group_Lock": return 0x00FE07 - case "ISO_Next_Group": return 0x00FE08 - case "ISO_Next_Group_Lock": return 0x00FE09 - case "ISO_Prev_Group": return 0x00FE0A - case "ISO_Prev_Group_Lock": return 0x00FE0B - case "ISO_First_Group": return 0x00FE0C - case "ISO_First_Group_Lock": return 0x00FE0D - case "ISO_Last_Group": return 0x00FE0E - case "ISO_Last_Group_Lock": return 0x00FE0F - case "ISO_Left_Tab": return 0x00FE20 - case "ISO_Move_Line_Up": return 0x00FE21 - case "ISO_Move_Line_Down": return 0x00FE22 - case "ISO_Partial_Line_Up": return 0x00FE23 - case "ISO_Partial_Line_Down": return 0x00FE24 - case "ISO_Partial_Space_Left": return 0x00FE25 - case "ISO_Partial_Space_Right": return 0x00FE26 - case "ISO_Set_Margin_Left": return 0x00FE27 - case "ISO_Set_Margin_Right": return 0x00FE28 - case "ISO_Release_Margin_Left": return 0x00FE29 - case "ISO_Release_Margin_Right": return 0x00FE2A - case "ISO_Release_Both_Margins": return 0x00FE2B - case "ISO_Fast_Cursor_Left": return 0x00FE2C - case "ISO_Fast_Cursor_Right": return 0x00FE2D - case "ISO_Fast_Cursor_Up": return 0x00FE2E - case "ISO_Fast_Cursor_Down": return 0x00FE2F - case "ISO_Continuous_Underline": return 0x00FE30 - case "ISO_Discontinuous_Underline": return 0x00FE31 - case "ISO_Emphasize": return 0x00FE32 - case "ISO_Center_Object": return 0x00FE33 - case "ISO_Enter": return 0x00FE34 - case "dead_grave": return 0x00FE50 - case "dead_acute": return 0x00FE51 - case "dead_circumflex": return 0x00FE52 - case "dead_tilde": return 0x00FE53 - case "dead_macron": return 0x00FE54 - case "dead_breve": return 0x00FE55 - case "dead_abovedot": return 0x00FE56 - case "dead_diaeresis": return 0x00FE57 - case "dead_abovering": return 0x00FE58 - case "dead_doubleacute": return 0x00FE59 - case "dead_caron": return 0x00FE5A - case "dead_cedilla": return 0x00FE5B - case "dead_ogonek": return 0x00FE5C - case "dead_iota": return 0x00FE5D - case "dead_voiced_sound": return 0x00FE5E - case "dead_semivoiced_sound": return 0x00FE5F - case "dead_belowdot": return 0x00FE60 - case "dead_hook": return 0x00FE61 - case "dead_horn": return 0x00FE62 + case "Hstroke": 0x0002A1 + case "Hcircumflex": 0x0002A6 + case "Iabovedot": 0x0002A9 + case "Gbreve": 0x0002AB + case "Jcircumflex": 0x0002AC + case "hstroke": 0x0002B1 + case "hcircumflex": 0x0002B6 + case "idotless": 0x0002B9 + case "gbreve": 0x0002BB + case "jcircumflex": 0x0002BC + case "Cabovedot": 0x0002C5 + case "Ccircumflex": 0x0002C6 + case "Gabovedot": 0x0002D5 + case "Gcircumflex": 0x0002D8 + case "Ubreve": 0x0002DD + case "Scircumflex": 0x0002DE + case "cabovedot": 0x0002E5 + case "ccircumflex": 0x0002E6 + case "gabovedot": 0x0002F5 + case "gcircumflex": 0x0002F8 + case "ubreve": 0x0002FD + case "scircumflex": 0x0002FE + case "kappa": 0x0003A2 + case "kra": 0x0003A2 + case "Rcedilla": 0x0003A3 + case "Itilde": 0x0003A5 + case "Lcedilla": 0x0003A6 + case "Emacron": 0x0003AA + case "Gcedilla": 0x0003AB + case "Tslash": 0x0003AC + case "rcedilla": 0x0003B3 + case "itilde": 0x0003B5 + case "lcedilla": 0x0003B6 + case "emacron": 0x0003BA + case "gcedilla": 0x0003BB + case "tslash": 0x0003BC + case "ENG": 0x0003BD + case "eng": 0x0003BF + case "Amacron": 0x0003C0 + case "Iogonek": 0x0003C7 + case "Eabovedot": 0x0003CC + case "Imacron": 0x0003CF + case "Ncedilla": 0x0003D1 + case "Omacron": 0x0003D2 + case "Kcedilla": 0x0003D3 + case "Uogonek": 0x0003D9 + case "Utilde": 0x0003DD + case "Umacron": 0x0003DE + case "amacron": 0x0003E0 + case "iogonek": 0x0003E7 + case "eabovedot": 0x0003EC + case "imacron": 0x0003EF + case "ncedilla": 0x0003F1 + case "omacron": 0x0003F2 + case "kcedilla": 0x0003F3 + case "uogonek": 0x0003F9 + case "utilde": 0x0003FD + case "umacron": 0x0003FE + case "overline": 0x00047E + case "kana_fullstop": 0x0004A1 + case "kana_openingbracket": 0x0004A2 + case "kana_closingbracket": 0x0004A3 + case "kana_comma": 0x0004A4 + case "kana_conjunctive": 0x0004A5 + case "kana_middledot": 0x0004A5 + case "kana_WO": 0x0004A6 + case "kana_a": 0x0004A7 + case "kana_i": 0x0004A8 + case "kana_u": 0x0004A9 + case "kana_e": 0x0004AA + case "kana_o": 0x0004AB + case "kana_ya": 0x0004AC + case "kana_yu": 0x0004AD + case "kana_yo": 0x0004AE + case "kana_tsu": 0x0004AF + case "kana_tu": 0x0004AF + case "prolongedsound": 0x0004B0 + case "kana_A": 0x0004B1 + case "kana_I": 0x0004B2 + case "kana_U": 0x0004B3 + case "kana_E": 0x0004B4 + case "kana_O": 0x0004B5 + case "kana_KA": 0x0004B6 + case "kana_KI": 0x0004B7 + case "kana_KU": 0x0004B8 + case "kana_KE": 0x0004B9 + case "kana_KO": 0x0004BA + case "kana_SA": 0x0004BB + case "kana_SHI": 0x0004BC + case "kana_SU": 0x0004BD + case "kana_SE": 0x0004BE + case "kana_SO": 0x0004BF + case "kana_TA": 0x0004C0 + case "kana_CHI": 0x0004C1 + case "kana_TI": 0x0004C1 + case "kana_TSU": 0x0004C2 + case "kana_TU": 0x0004C2 + case "kana_TE": 0x0004C3 + case "kana_TO": 0x0004C4 + case "kana_NA": 0x0004C5 + case "kana_NI": 0x0004C6 + case "kana_NU": 0x0004C7 + case "kana_NE": 0x0004C8 + case "kana_NO": 0x0004C9 + case "kana_HA": 0x0004CA + case "kana_HI": 0x0004CB + case "kana_FU": 0x0004CC + case "kana_HU": 0x0004CC + case "kana_HE": 0x0004CD + case "kana_HO": 0x0004CE + case "kana_MA": 0x0004CF + case "kana_MI": 0x0004D0 + case "kana_MU": 0x0004D1 + case "kana_ME": 0x0004D2 + case "kana_MO": 0x0004D3 + case "kana_YA": 0x0004D4 + case "kana_YU": 0x0004D5 + case "kana_YO": 0x0004D6 + case "kana_RA": 0x0004D7 + case "kana_RI": 0x0004D8 + case "kana_RU": 0x0004D9 + case "kana_RE": 0x0004DA + case "kana_RO": 0x0004DB + case "kana_WA": 0x0004DC + case "kana_N": 0x0004DD + case "voicedsound": 0x0004DE + case "semivoicedsound": 0x0004DF + case "Arabic_comma": 0x0005AC + case "Arabic_semicolon": 0x0005BB + case "Arabic_question_mark": 0x0005BF + case "Arabic_hamza": 0x0005C1 + case "Arabic_maddaonalef": 0x0005C2 + case "Arabic_hamzaonalef": 0x0005C3 + case "Arabic_hamzaonwaw": 0x0005C4 + case "Arabic_hamzaunderalef": 0x0005C5 + case "Arabic_hamzaonyeh": 0x0005C6 + case "Arabic_alef": 0x0005C7 + case "Arabic_beh": 0x0005C8 + case "Arabic_tehmarbuta": 0x0005C9 + case "Arabic_teh": 0x0005CA + case "Arabic_theh": 0x0005CB + case "Arabic_jeem": 0x0005CC + case "Arabic_hah": 0x0005CD + case "Arabic_khah": 0x0005CE + case "Arabic_dal": 0x0005CF + case "Arabic_thal": 0x0005D0 + case "Arabic_ra": 0x0005D1 + case "Arabic_zain": 0x0005D2 + case "Arabic_seen": 0x0005D3 + case "Arabic_sheen": 0x0005D4 + case "Arabic_sad": 0x0005D5 + case "Arabic_dad": 0x0005D6 + case "Arabic_tah": 0x0005D7 + case "Arabic_zah": 0x0005D8 + case "Arabic_ain": 0x0005D9 + case "Arabic_ghain": 0x0005DA + case "Arabic_tatweel": 0x0005E0 + case "Arabic_feh": 0x0005E1 + case "Arabic_qaf": 0x0005E2 + case "Arabic_kaf": 0x0005E3 + case "Arabic_lam": 0x0005E4 + case "Arabic_meem": 0x0005E5 + case "Arabic_noon": 0x0005E6 + case "Arabic_ha": 0x0005E7 + case "Arabic_heh": 0x0005E7 + case "Arabic_waw": 0x0005E8 + case "Arabic_alefmaksura": 0x0005E9 + case "Arabic_yeh": 0x0005EA + case "Arabic_fathatan": 0x0005EB + case "Arabic_dammatan": 0x0005EC + case "Arabic_kasratan": 0x0005ED + case "Arabic_fatha": 0x0005EE + case "Arabic_damma": 0x0005EF + case "Arabic_kasra": 0x0005F0 + case "Arabic_shadda": 0x0005F1 + case "Arabic_sukun": 0x0005F2 + case "Serbian_dje": 0x0006A1 + case "Macedonia_gje": 0x0006A2 + case "Cyrillic_io": 0x0006A3 + case "Ukrainian_ie": 0x0006A4 + case "Ukranian_je": 0x0006A4 + case "Macedonia_dse": 0x0006A5 + case "Ukrainian_i": 0x0006A6 + case "Ukranian_i": 0x0006A6 + case "Ukrainian_yi": 0x0006A7 + case "Ukranian_yi": 0x0006A7 + case "Cyrillic_je": 0x0006A8 + case "Serbian_je": 0x0006A8 + case "Cyrillic_lje": 0x0006A9 + case "Serbian_lje": 0x0006A9 + case "Cyrillic_nje": 0x0006AA + case "Serbian_nje": 0x0006AA + case "Serbian_tshe": 0x0006AB + case "Macedonia_kje": 0x0006AC + case "Byelorussian_shortu": 0x0006AE + case "Cyrillic_dzhe": 0x0006AF + case "Serbian_dze": 0x0006AF + case "numerosign": 0x0006B0 + case "Serbian_DJE": 0x0006B1 + case "Macedonia_GJE": 0x0006B2 + case "Cyrillic_IO": 0x0006B3 + case "Ukrainian_IE": 0x0006B4 + case "Ukranian_JE": 0x0006B4 + case "Macedonia_DSE": 0x0006B5 + case "Ukrainian_I": 0x0006B6 + case "Ukranian_I": 0x0006B6 + case "Ukrainian_YI": 0x0006B7 + case "Ukranian_YI": 0x0006B7 + case "Cyrillic_JE": 0x0006B8 + case "Serbian_JE": 0x0006B8 + case "Cyrillic_LJE": 0x0006B9 + case "Serbian_LJE": 0x0006B9 + case "Cyrillic_NJE": 0x0006BA + case "Serbian_NJE": 0x0006BA + case "Serbian_TSHE": 0x0006BB + case "Macedonia_KJE": 0x0006BC + case "Byelorussian_SHORTU": 0x0006BE + case "Cyrillic_DZHE": 0x0006BF + case "Serbian_DZE": 0x0006BF + case "Cyrillic_yu": 0x0006C0 + case "Cyrillic_a": 0x0006C1 + case "Cyrillic_be": 0x0006C2 + case "Cyrillic_tse": 0x0006C3 + case "Cyrillic_de": 0x0006C4 + case "Cyrillic_ie": 0x0006C5 + case "Cyrillic_ef": 0x0006C6 + case "Cyrillic_ghe": 0x0006C7 + case "Cyrillic_ha": 0x0006C8 + case "Cyrillic_i": 0x0006C9 + case "Cyrillic_shorti": 0x0006CA + case "Cyrillic_ka": 0x0006CB + case "Cyrillic_el": 0x0006CC + case "Cyrillic_em": 0x0006CD + case "Cyrillic_en": 0x0006CE + case "Cyrillic_o": 0x0006CF + case "Cyrillic_pe": 0x0006D0 + case "Cyrillic_ya": 0x0006D1 + case "Cyrillic_er": 0x0006D2 + case "Cyrillic_es": 0x0006D3 + case "Cyrillic_te": 0x0006D4 + case "Cyrillic_u": 0x0006D5 + case "Cyrillic_zhe": 0x0006D6 + case "Cyrillic_ve": 0x0006D7 + case "Cyrillic_softsign": 0x0006D8 + case "Cyrillic_yeru": 0x0006D9 + case "Cyrillic_ze": 0x0006DA + case "Cyrillic_sha": 0x0006DB + case "Cyrillic_e": 0x0006DC + case "Cyrillic_shcha": 0x0006DD + case "Cyrillic_che": 0x0006DE + case "Cyrillic_hardsign": 0x0006DF + case "Cyrillic_YU": 0x0006E0 + case "Cyrillic_A": 0x0006E1 + case "Cyrillic_BE": 0x0006E2 + case "Cyrillic_TSE": 0x0006E3 + case "Cyrillic_DE": 0x0006E4 + case "Cyrillic_IE": 0x0006E5 + case "Cyrillic_EF": 0x0006E6 + case "Cyrillic_GHE": 0x0006E7 + case "Cyrillic_HA": 0x0006E8 + case "Cyrillic_I": 0x0006E9 + case "Cyrillic_SHORTI": 0x0006EA + case "Cyrillic_KA": 0x0006EB + case "Cyrillic_EL": 0x0006EC + case "Cyrillic_EM": 0x0006ED + case "Cyrillic_EN": 0x0006EE + case "Cyrillic_O": 0x0006EF + case "Cyrillic_PE": 0x0006F0 + case "Cyrillic_YA": 0x0006F1 + case "Cyrillic_ER": 0x0006F2 + case "Cyrillic_ES": 0x0006F3 + case "Cyrillic_TE": 0x0006F4 + case "Cyrillic_U": 0x0006F5 + case "Cyrillic_ZHE": 0x0006F6 + case "Cyrillic_VE": 0x0006F7 + case "Cyrillic_SOFTSIGN": 0x0006F8 + case "Cyrillic_YERU": 0x0006F9 + case "Cyrillic_ZE": 0x0006FA + case "Cyrillic_SHA": 0x0006FB + case "Cyrillic_E": 0x0006FC + case "Cyrillic_SHCHA": 0x0006FD + case "Cyrillic_CHE": 0x0006FE + case "Cyrillic_HARDSIGN": 0x0006FF + case "Greek_ALPHAaccent": 0x0007A1 + case "Greek_EPSILONaccent": 0x0007A2 + case "Greek_ETAaccent": 0x0007A3 + case "Greek_IOTAaccent": 0x0007A4 + case "Greek_IOTAdieresis": 0x0007A5 + case "Greek_IOTAdiaeresis": 0x0007A5 + case "Greek_OMICRONaccent": 0x0007A7 + case "Greek_UPSILONaccent": 0x0007A8 + case "Greek_UPSILONdieresis": 0x0007A9 + case "Greek_OMEGAaccent": 0x0007AB + case "Greek_accentdieresis": 0x0007AE + case "Greek_horizbar": 0x0007AF + case "Greek_alphaaccent": 0x0007B1 + case "Greek_epsilonaccent": 0x0007B2 + case "Greek_etaaccent": 0x0007B3 + case "Greek_iotaaccent": 0x0007B4 + case "Greek_iotadieresis": 0x0007B5 + case "Greek_iotaaccentdieresis": 0x0007B6 + case "Greek_omicronaccent": 0x0007B7 + case "Greek_upsilonaccent": 0x0007B8 + case "Greek_upsilondieresis": 0x0007B9 + case "Greek_upsilonaccentdieresis": 0x0007BA + case "Greek_omegaaccent": 0x0007BB + case "Greek_ALPHA": 0x0007C1 + case "Greek_BETA": 0x0007C2 + case "Greek_GAMMA": 0x0007C3 + case "Greek_DELTA": 0x0007C4 + case "Greek_EPSILON": 0x0007C5 + case "Greek_ZETA": 0x0007C6 + case "Greek_ETA": 0x0007C7 + case "Greek_THETA": 0x0007C8 + case "Greek_IOTA": 0x0007C9 + case "Greek_KAPPA": 0x0007CA + case "Greek_LAMBDA": 0x0007CB + case "Greek_LAMDA": 0x0007CB + case "Greek_MU": 0x0007CC + case "Greek_NU": 0x0007CD + case "Greek_XI": 0x0007CE + case "Greek_OMICRON": 0x0007CF + case "Greek_PI": 0x0007D0 + case "Greek_RHO": 0x0007D1 + case "Greek_SIGMA": 0x0007D2 + case "Greek_TAU": 0x0007D4 + case "Greek_UPSILON": 0x0007D5 + case "Greek_PHI": 0x0007D6 + case "Greek_CHI": 0x0007D7 + case "Greek_PSI": 0x0007D8 + case "Greek_OMEGA": 0x0007D9 + case "Greek_alpha": 0x0007E1 + case "Greek_beta": 0x0007E2 + case "Greek_gamma": 0x0007E3 + case "Greek_delta": 0x0007E4 + case "Greek_epsilon": 0x0007E5 + case "Greek_zeta": 0x0007E6 + case "Greek_eta": 0x0007E7 + case "Greek_theta": 0x0007E8 + case "Greek_iota": 0x0007E9 + case "Greek_kappa": 0x0007EA + case "Greek_lambda": 0x0007EB + case "Greek_lamda": 0x0007EB + case "Greek_mu": 0x0007EC + case "Greek_nu": 0x0007ED + case "Greek_xi": 0x0007EE + case "Greek_omicron": 0x0007EF + case "Greek_pi": 0x0007F0 + case "Greek_rho": 0x0007F1 + case "Greek_sigma": 0x0007F2 + case "Greek_finalsmallsigma": 0x0007F3 + case "Greek_tau": 0x0007F4 + case "Greek_upsilon": 0x0007F5 + case "Greek_phi": 0x0007F6 + case "Greek_chi": 0x0007F7 + case "Greek_psi": 0x0007F8 + case "Greek_omega": 0x0007F9 + case "leftradical": 0x0008A1 + case "topleftradical": 0x0008A2 + case "horizconnector": 0x0008A3 + case "topintegral": 0x0008A4 + case "botintegral": 0x0008A5 + case "vertconnector": 0x0008A6 + case "topleftsqbracket": 0x0008A7 + case "botleftsqbracket": 0x0008A8 + case "toprightsqbracket": 0x0008A9 + case "botrightsqbracket": 0x0008AA + case "topleftparens": 0x0008AB + case "botleftparens": 0x0008AC + case "toprightparens": 0x0008AD + case "botrightparens": 0x0008AE + case "leftmiddlecurlybrace": 0x0008AF + case "rightmiddlecurlybrace": 0x0008B0 + case "topleftsummation": 0x0008B1 + case "botleftsummation": 0x0008B2 + case "topvertsummationconnector": 0x0008B3 + case "botvertsummationconnector": 0x0008B4 + case "toprightsummation": 0x0008B5 + case "botrightsummation": 0x0008B6 + case "rightmiddlesummation": 0x0008B7 + case "lessthanequal": 0x0008BC + case "notequal": 0x0008BD + case "greaterthanequal": 0x0008BE + case "integral": 0x0008BF + case "therefore": 0x0008C0 + case "variation": 0x0008C1 + case "infinity": 0x0008C2 + case "nabla": 0x0008C5 + case "approximate": 0x0008C8 + case "similarequal": 0x0008C9 + case "ifonlyif": 0x0008CD + case "implies": 0x0008CE + case "identical": 0x0008CF + case "radical": 0x0008D6 + case "includedin": 0x0008DA + case "includes": 0x0008DB + case "intersection": 0x0008DC + case "union": 0x0008DD + case "logicaland": 0x0008DE + case "logicalor": 0x0008DF + case "partialderivative": 0x0008EF + case "function": 0x0008F6 + case "leftarrow": 0x0008FB + case "uparrow": 0x0008FC + case "rightarrow": 0x0008FD + case "downarrow": 0x0008FE + case "blank": 0x0009DF + case "soliddiamond": 0x0009E0 + case "checkerboard": 0x0009E1 + case "ht": 0x0009E2 + case "ff": 0x0009E3 + case "cr": 0x0009E4 + case "lf": 0x0009E5 + case "nl": 0x0009E8 + case "vt": 0x0009E9 + case "lowrightcorner": 0x0009EA + case "uprightcorner": 0x0009EB + case "upleftcorner": 0x0009EC + case "lowleftcorner": 0x0009ED + case "crossinglines": 0x0009EE + case "horizlinescan1": 0x0009EF + case "horizlinescan3": 0x0009F0 + case "horizlinescan5": 0x0009F1 + case "horizlinescan7": 0x0009F2 + case "horizlinescan9": 0x0009F3 + case "leftt": 0x0009F4 + case "rightt": 0x0009F5 + case "bott": 0x0009F6 + case "topt": 0x0009F7 + case "vertbar": 0x0009F8 + case "emspace": 0x000AA1 + case "enspace": 0x000AA2 + case "em3space": 0x000AA3 + case "em4space": 0x000AA4 + case "digitspace": 0x000AA5 + case "punctspace": 0x000AA6 + case "thinspace": 0x000AA7 + case "hairspace": 0x000AA8 + case "emdash": 0x000AA9 + case "endash": 0x000AAA + case "signifblank": 0x000AAC + case "ellipsis": 0x000AAE + case "doubbaselinedot": 0x000AAF + case "onethird": 0x000AB0 + case "twothirds": 0x000AB1 + case "onefifth": 0x000AB2 + case "twofifths": 0x000AB3 + case "threefifths": 0x000AB4 + case "fourfifths": 0x000AB5 + case "onesixth": 0x000AB6 + case "fivesixths": 0x000AB7 + case "careof": 0x000AB8 + case "figdash": 0x000ABB + case "leftanglebracket": 0x000ABC + case "decimalpoint": 0x000ABD + case "rightanglebracket": 0x000ABE + case "marker": 0x000ABF + case "oneeighth": 0x000AC3 + case "threeeighths": 0x000AC4 + case "fiveeighths": 0x000AC5 + case "seveneighths": 0x000AC6 + case "trademark": 0x000AC9 + case "signaturemark": 0x000ACA + case "trademarkincircle": 0x000ACB + case "leftopentriangle": 0x000ACC + case "rightopentriangle": 0x000ACD + case "emopencircle": 0x000ACE + case "emopenrectangle": 0x000ACF + case "leftsinglequotemark": 0x000AD0 + case "rightsinglequotemark": 0x000AD1 + case "leftdoublequotemark": 0x000AD2 + case "rightdoublequotemark": 0x000AD3 + case "prescription": 0x000AD4 + case "minutes": 0x000AD6 + case "seconds": 0x000AD7 + case "latincross": 0x000AD9 + case "hexagram": 0x000ADA + case "filledrectbullet": 0x000ADB + case "filledlefttribullet": 0x000ADC + case "filledrighttribullet": 0x000ADD + case "emfilledcircle": 0x000ADE + case "emfilledrect": 0x000ADF + case "enopencircbullet": 0x000AE0 + case "enopensquarebullet": 0x000AE1 + case "openrectbullet": 0x000AE2 + case "opentribulletup": 0x000AE3 + case "opentribulletdown": 0x000AE4 + case "openstar": 0x000AE5 + case "enfilledcircbullet": 0x000AE6 + case "enfilledsqbullet": 0x000AE7 + case "filledtribulletup": 0x000AE8 + case "filledtribulletdown": 0x000AE9 + case "leftpointer": 0x000AEA + case "rightpointer": 0x000AEB + case "club": 0x000AEC + case "diamond": 0x000AED + case "heart": 0x000AEE + case "maltesecross": 0x000AF0 + case "dagger": 0x000AF1 + case "doubledagger": 0x000AF2 + case "checkmark": 0x000AF3 + case "ballotcross": 0x000AF4 + case "musicalsharp": 0x000AF5 + case "musicalflat": 0x000AF6 + case "malesymbol": 0x000AF7 + case "femalesymbol": 0x000AF8 + case "telephone": 0x000AF9 + case "telephonerecorder": 0x000AFA + case "phonographcopyright": 0x000AFB + case "caret": 0x000AFC + case "singlelowquotemark": 0x000AFD + case "doublelowquotemark": 0x000AFE + case "cursor": 0x000AFF + case "leftcaret": 0x000BA3 + case "rightcaret": 0x000BA6 + case "downcaret": 0x000BA8 + case "upcaret": 0x000BA9 + case "overbar": 0x000BC0 + case "downtack": 0x000BC2 + case "upshoe": 0x000BC3 + case "downstile": 0x000BC4 + case "underbar": 0x000BC6 + case "jot": 0x000BCA + case "quad": 0x000BCC + case "uptack": 0x000BCE + case "circle": 0x000BCF + case "upstile": 0x000BD3 + case "downshoe": 0x000BD6 + case "rightshoe": 0x000BD8 + case "leftshoe": 0x000BDA + case "lefttack": 0x000BDC + case "righttack": 0x000BFC + case "hebrew_doublelowline": 0x000CDF + case "hebrew_aleph": 0x000CE0 + case "hebrew_bet": 0x000CE1 + case "hebrew_beth": 0x000CE1 + case "hebrew_gimel": 0x000CE2 + case "hebrew_gimmel": 0x000CE2 + case "hebrew_dalet": 0x000CE3 + case "hebrew_daleth": 0x000CE3 + case "hebrew_he": 0x000CE4 + case "hebrew_waw": 0x000CE5 + case "hebrew_zain": 0x000CE6 + case "hebrew_zayin": 0x000CE6 + case "hebrew_chet": 0x000CE7 + case "hebrew_het": 0x000CE7 + case "hebrew_tet": 0x000CE8 + case "hebrew_teth": 0x000CE8 + case "hebrew_yod": 0x000CE9 + case "hebrew_finalkaph": 0x000CEA + case "hebrew_kaph": 0x000CEB + case "hebrew_lamed": 0x000CEC + case "hebrew_finalmem": 0x000CED + case "hebrew_mem": 0x000CEE + case "hebrew_finalnun": 0x000CEF + case "hebrew_nun": 0x000CF0 + case "hebrew_samech": 0x000CF1 + case "hebrew_samekh": 0x000CF1 + case "hebrew_ayin": 0x000CF2 + case "hebrew_finalpe": 0x000CF3 + case "hebrew_pe": 0x000CF4 + case "hebrew_finalzade": 0x000CF5 + case "hebrew_finalzadi": 0x000CF5 + case "hebrew_zade": 0x000CF6 + case "hebrew_zadi": 0x000CF6 + case "hebrew_kuf": 0x000CF7 + case "hebrew_qoph": 0x000CF7 + case "hebrew_resh": 0x000CF8 + case "hebrew_shin": 0x000CF9 + case "hebrew_taf": 0x000CFA + case "hebrew_taw": 0x000CFA + case "Thai_kokai": 0x000DA1 + case "Thai_khokhai": 0x000DA2 + case "Thai_khokhuat": 0x000DA3 + case "Thai_khokhwai": 0x000DA4 + case "Thai_khokhon": 0x000DA5 + case "Thai_khorakhang": 0x000DA6 + case "Thai_ngongu": 0x000DA7 + case "Thai_chochan": 0x000DA8 + case "Thai_choching": 0x000DA9 + case "Thai_chochang": 0x000DAA + case "Thai_soso": 0x000DAB + case "Thai_chochoe": 0x000DAC + case "Thai_yoying": 0x000DAD + case "Thai_dochada": 0x000DAE + case "Thai_topatak": 0x000DAF + case "Thai_thothan": 0x000DB0 + case "Thai_thonangmontho": 0x000DB1 + case "Thai_thophuthao": 0x000DB2 + case "Thai_nonen": 0x000DB3 + case "Thai_dodek": 0x000DB4 + case "Thai_totao": 0x000DB5 + case "Thai_thothung": 0x000DB6 + case "Thai_thothahan": 0x000DB7 + case "Thai_thothong": 0x000DB8 + case "Thai_nonu": 0x000DB9 + case "Thai_bobaimai": 0x000DBA + case "Thai_popla": 0x000DBB + case "Thai_phophung": 0x000DBC + case "Thai_fofa": 0x000DBD + case "Thai_phophan": 0x000DBE + case "Thai_fofan": 0x000DBF + case "Thai_phosamphao": 0x000DC0 + case "Thai_moma": 0x000DC1 + case "Thai_yoyak": 0x000DC2 + case "Thai_rorua": 0x000DC3 + case "Thai_ru": 0x000DC4 + case "Thai_loling": 0x000DC5 + case "Thai_lu": 0x000DC6 + case "Thai_wowaen": 0x000DC7 + case "Thai_sosala": 0x000DC8 + case "Thai_sorusi": 0x000DC9 + case "Thai_sosua": 0x000DCA + case "Thai_hohip": 0x000DCB + case "Thai_lochula": 0x000DCC + case "Thai_oang": 0x000DCD + case "Thai_honokhuk": 0x000DCE + case "Thai_paiyannoi": 0x000DCF + case "Thai_saraa": 0x000DD0 + case "Thai_maihanakat": 0x000DD1 + case "Thai_saraaa": 0x000DD2 + case "Thai_saraam": 0x000DD3 + case "Thai_sarai": 0x000DD4 + case "Thai_saraii": 0x000DD5 + case "Thai_saraue": 0x000DD6 + case "Thai_sarauee": 0x000DD7 + case "Thai_sarau": 0x000DD8 + case "Thai_sarauu": 0x000DD9 + case "Thai_phinthu": 0x000DDA + case "Thai_maihanakat_maitho": 0x000DDE + case "Thai_baht": 0x000DDF + case "Thai_sarae": 0x000DE0 + case "Thai_saraae": 0x000DE1 + case "Thai_sarao": 0x000DE2 + case "Thai_saraaimaimuan": 0x000DE3 + case "Thai_saraaimaimalai": 0x000DE4 + case "Thai_lakkhangyao": 0x000DE5 + case "Thai_maiyamok": 0x000DE6 + case "Thai_maitaikhu": 0x000DE7 + case "Thai_maiek": 0x000DE8 + case "Thai_maitho": 0x000DE9 + case "Thai_maitri": 0x000DEA + case "Thai_maichattawa": 0x000DEB + case "Thai_thanthakhat": 0x000DEC + case "Thai_nikhahit": 0x000DED + case "Thai_leksun": 0x000DF0 + case "Thai_leknung": 0x000DF1 + case "Thai_leksong": 0x000DF2 + case "Thai_leksam": 0x000DF3 + case "Thai_leksi": 0x000DF4 + case "Thai_lekha": 0x000DF5 + case "Thai_lekhok": 0x000DF6 + case "Thai_lekchet": 0x000DF7 + case "Thai_lekpaet": 0x000DF8 + case "Thai_lekkao": 0x000DF9 + case "Hangul_Kiyeog": 0x000EA1 + case "Hangul_SsangKiyeog": 0x000EA2 + case "Hangul_KiyeogSios": 0x000EA3 + case "Hangul_Nieun": 0x000EA4 + case "Hangul_NieunJieuj": 0x000EA5 + case "Hangul_NieunHieuh": 0x000EA6 + case "Hangul_Dikeud": 0x000EA7 + case "Hangul_SsangDikeud": 0x000EA8 + case "Hangul_Rieul": 0x000EA9 + case "Hangul_RieulKiyeog": 0x000EAA + case "Hangul_RieulMieum": 0x000EAB + case "Hangul_RieulPieub": 0x000EAC + case "Hangul_RieulSios": 0x000EAD + case "Hangul_RieulTieut": 0x000EAE + case "Hangul_RieulPhieuf": 0x000EAF + case "Hangul_RieulHieuh": 0x000EB0 + case "Hangul_Mieum": 0x000EB1 + case "Hangul_Pieub": 0x000EB2 + case "Hangul_SsangPieub": 0x000EB3 + case "Hangul_PieubSios": 0x000EB4 + case "Hangul_Sios": 0x000EB5 + case "Hangul_SsangSios": 0x000EB6 + case "Hangul_Ieung": 0x000EB7 + case "Hangul_Jieuj": 0x000EB8 + case "Hangul_SsangJieuj": 0x000EB9 + case "Hangul_Cieuc": 0x000EBA + case "Hangul_Khieuq": 0x000EBB + case "Hangul_Tieut": 0x000EBC + case "Hangul_Phieuf": 0x000EBD + case "Hangul_Hieuh": 0x000EBE + case "Hangul_A": 0x000EBF + case "Hangul_AE": 0x000EC0 + case "Hangul_YA": 0x000EC1 + case "Hangul_YAE": 0x000EC2 + case "Hangul_EO": 0x000EC3 + case "Hangul_E": 0x000EC4 + case "Hangul_YEO": 0x000EC5 + case "Hangul_YE": 0x000EC6 + case "Hangul_O": 0x000EC7 + case "Hangul_WA": 0x000EC8 + case "Hangul_WAE": 0x000EC9 + case "Hangul_OE": 0x000ECA + case "Hangul_YO": 0x000ECB + case "Hangul_U": 0x000ECC + case "Hangul_WEO": 0x000ECD + case "Hangul_WE": 0x000ECE + case "Hangul_WI": 0x000ECF + case "Hangul_YU": 0x000ED0 + case "Hangul_EU": 0x000ED1 + case "Hangul_YI": 0x000ED2 + case "Hangul_I": 0x000ED3 + case "Hangul_J_Kiyeog": 0x000ED4 + case "Hangul_J_SsangKiyeog": 0x000ED5 + case "Hangul_J_KiyeogSios": 0x000ED6 + case "Hangul_J_Nieun": 0x000ED7 + case "Hangul_J_NieunJieuj": 0x000ED8 + case "Hangul_J_NieunHieuh": 0x000ED9 + case "Hangul_J_Dikeud": 0x000EDA + case "Hangul_J_Rieul": 0x000EDB + case "Hangul_J_RieulKiyeog": 0x000EDC + case "Hangul_J_RieulMieum": 0x000EDD + case "Hangul_J_RieulPieub": 0x000EDE + case "Hangul_J_RieulSios": 0x000EDF + case "Hangul_J_RieulTieut": 0x000EE0 + case "Hangul_J_RieulPhieuf": 0x000EE1 + case "Hangul_J_RieulHieuh": 0x000EE2 + case "Hangul_J_Mieum": 0x000EE3 + case "Hangul_J_Pieub": 0x000EE4 + case "Hangul_J_PieubSios": 0x000EE5 + case "Hangul_J_Sios": 0x000EE6 + case "Hangul_J_SsangSios": 0x000EE7 + case "Hangul_J_Ieung": 0x000EE8 + case "Hangul_J_Jieuj": 0x000EE9 + case "Hangul_J_Cieuc": 0x000EEA + case "Hangul_J_Khieuq": 0x000EEB + case "Hangul_J_Tieut": 0x000EEC + case "Hangul_J_Phieuf": 0x000EED + case "Hangul_J_Hieuh": 0x000EEE + case "Hangul_RieulYeorinHieuh": 0x000EEF + case "Hangul_SunkyeongeumMieum": 0x000EF0 + case "Hangul_SunkyeongeumPieub": 0x000EF1 + case "Hangul_PanSios": 0x000EF2 + case "Hangul_KkogjiDalrinIeung": 0x000EF3 + case "Hangul_SunkyeongeumPhieuf": 0x000EF4 + case "Hangul_YeorinHieuh": 0x000EF5 + case "Hangul_AraeA": 0x000EF6 + case "Hangul_AraeAE": 0x000EF7 + case "Hangul_J_PanSios": 0x000EF8 + case "Hangul_J_KkogjiDalrinIeung": 0x000EF9 + case "Hangul_J_YeorinHieuh": 0x000EFA + case "Korean_Won": 0x000EFF + case "OE": 0x0013BC + case "oe": 0x0013BD + case "Ydiaeresis": 0x0013BE + case "EcuSign": 0x0020A0 + case "ColonSign": 0x0020A1 + case "CruzeiroSign": 0x0020A2 + case "FFrancSign": 0x0020A3 + case "LiraSign": 0x0020A4 + case "MillSign": 0x0020A5 + case "NairaSign": 0x0020A6 + case "PesetaSign": 0x0020A7 + case "RupeeSign": 0x0020A8 + case "WonSign": 0x0020A9 + case "NewSheqelSign": 0x0020AA + case "DongSign": 0x0020AB + case "EuroSign": 0x0020AC + case "3270_Duplicate": 0x00FD01 + case "3270_FieldMark": 0x00FD02 + case "3270_Right2": 0x00FD03 + case "3270_Left2": 0x00FD04 + case "3270_BackTab": 0x00FD05 + case "3270_EraseEOF": 0x00FD06 + case "3270_EraseInput": 0x00FD07 + case "3270_Reset": 0x00FD08 + case "3270_Quit": 0x00FD09 + case "3270_PA1": 0x00FD0A + case "3270_PA2": 0x00FD0B + case "3270_PA3": 0x00FD0C + case "3270_Test": 0x00FD0D + case "3270_Attn": 0x00FD0E + case "3270_CursorBlink": 0x00FD0F + case "3270_AltCursor": 0x00FD10 + case "3270_KeyClick": 0x00FD11 + case "3270_Jump": 0x00FD12 + case "3270_Ident": 0x00FD13 + case "3270_Rule": 0x00FD14 + case "3270_Copy": 0x00FD15 + case "3270_Play": 0x00FD16 + case "3270_Setup": 0x00FD17 + case "3270_Record": 0x00FD18 + case "3270_ChangeScreen": 0x00FD19 + case "3270_DeleteWord": 0x00FD1A + case "3270_ExSelect": 0x00FD1B + case "3270_CursorSelect": 0x00FD1C + case "3270_PrintScreen": 0x00FD1D + case "3270_Enter": 0x00FD1E + case "ISO_Lock": 0x00FE01 + case "ISO_Level2_Latch": 0x00FE02 + case "ISO_Level3_Shift": 0x00FE03 + case "ISO_Level3_Latch": 0x00FE04 + case "ISO_Level3_Lock": 0x00FE05 + case "ISO_Group_Latch": 0x00FE06 + case "ISO_Group_Lock": 0x00FE07 + case "ISO_Next_Group": 0x00FE08 + case "ISO_Next_Group_Lock": 0x00FE09 + case "ISO_Prev_Group": 0x00FE0A + case "ISO_Prev_Group_Lock": 0x00FE0B + case "ISO_First_Group": 0x00FE0C + case "ISO_First_Group_Lock": 0x00FE0D + case "ISO_Last_Group": 0x00FE0E + case "ISO_Last_Group_Lock": 0x00FE0F + case "ISO_Left_Tab": 0x00FE20 + case "ISO_Move_Line_Up": 0x00FE21 + case "ISO_Move_Line_Down": 0x00FE22 + case "ISO_Partial_Line_Up": 0x00FE23 + case "ISO_Partial_Line_Down": 0x00FE24 + case "ISO_Partial_Space_Left": 0x00FE25 + case "ISO_Partial_Space_Right": 0x00FE26 + case "ISO_Set_Margin_Left": 0x00FE27 + case "ISO_Set_Margin_Right": 0x00FE28 + case "ISO_Release_Margin_Left": 0x00FE29 + case "ISO_Release_Margin_Right": 0x00FE2A + case "ISO_Release_Both_Margins": 0x00FE2B + case "ISO_Fast_Cursor_Left": 0x00FE2C + case "ISO_Fast_Cursor_Right": 0x00FE2D + case "ISO_Fast_Cursor_Up": 0x00FE2E + case "ISO_Fast_Cursor_Down": 0x00FE2F + case "ISO_Continuous_Underline": 0x00FE30 + case "ISO_Discontinuous_Underline": 0x00FE31 + case "ISO_Emphasize": 0x00FE32 + case "ISO_Center_Object": 0x00FE33 + case "ISO_Enter": 0x00FE34 + case "dead_grave": 0x00FE50 + case "dead_acute": 0x00FE51 + case "dead_circumflex": 0x00FE52 + case "dead_tilde": 0x00FE53 + case "dead_macron": 0x00FE54 + case "dead_breve": 0x00FE55 + case "dead_abovedot": 0x00FE56 + case "dead_diaeresis": 0x00FE57 + case "dead_abovering": 0x00FE58 + case "dead_doubleacute": 0x00FE59 + case "dead_caron": 0x00FE5A + case "dead_cedilla": 0x00FE5B + case "dead_ogonek": 0x00FE5C + case "dead_iota": 0x00FE5D + case "dead_voiced_sound": 0x00FE5E + case "dead_semivoiced_sound": 0x00FE5F + case "dead_belowdot": 0x00FE60 + case "dead_hook": 0x00FE61 + case "dead_horn": 0x00FE62 // auxialiary - case "AccessX_Enable": return 0x00FE70 - case "AccessX_Feedback_Enable": return 0x00FE71 - case "RepeatKeys_Enable": return 0x00FE72 - case "SlowKeys_Enable": return 0x00FE73 - case "BounceKeys_Enable": return 0x00FE74 - case "StickyKeys_Enable": return 0x00FE75 - case "MouseKeys_Enable": return 0x00FE76 - case "MouseKeys_Accel_Enable": return 0x00FE77 - case "Overlay1_Enable": return 0x00FE78 - case "Overlay2_Enable": return 0x00FE79 - case "AudibleBell_Enable": return 0x00FE7A - case "First_Virtual_Screen": return 0x00FED0 - case "Prev_Virtual_Screen": return 0x00FED1 - case "Next_Virtual_Screen": return 0x00FED2 - case "Last_Virtual_Screen": return 0x00FED4 - case "Terminate_Server": return 0x00FED5 - case "Pointer_Left": return 0x00FEE0 - case "Pointer_Right": return 0x00FEE1 - case "Pointer_Up": return 0x00FEE2 - case "Pointer_Down": return 0x00FEE3 - case "Pointer_UpLeft": return 0x00FEE4 - case "Pointer_UpRight": return 0x00FEE5 - case "Pointer_DownLeft": return 0x00FEE6 - case "Pointer_DownRight": return 0x00FEE7 - case "Pointer_Button_Dflt": return 0x00FEE8 - case "Pointer_Button1": return 0x00FEE9 - case "Pointer_Button2": return 0x00FEEA - case "Pointer_Button3": return 0x00FEEB - case "Pointer_Button4": return 0x00FEEC - case "Pointer_Button5": return 0x00FEED - case "Pointer_DblClick_Dflt": return 0x00FEEE - case "Pointer_DblClick1": return 0x00FEEF - case "Pointer_DblClick2": return 0x00FEF0 - case "Pointer_DblClick3": return 0x00FEF1 - case "Pointer_DblClick4": return 0x00FEF2 - case "Pointer_DblClick5": return 0x00FEF3 - case "Pointer_Drag_Dflt": return 0x00FEF4 - case "Pointer_Drag1": return 0x00FEF5 - case "Pointer_Drag2": return 0x00FEF6 - case "Pointer_Drag3": return 0x00FEF7 - case "Pointer_Drag4": return 0x00FEF8 - case "Pointer_EnableKeys": return 0x00FEF9 - case "Pointer_Accelerate": return 0x00FEFA - case "Pointer_DfltBtnNext": return 0x00FEFB - case "Pointer_DfltBtnPrev": return 0x00FEFC - case "Pointer_Drag5": return 0x00FEFD - case "BackSpace": return 0x00FF08 - case "Tab": return 0x00FF09 - case "Linefeed": return 0x00FF0A - case "Clear": return 0x00FF0B - case "Return": return 0x00FF0D - case "Pause": return 0x00FF13 - case "Scroll_Lock": return 0x00FF14 - case "Sys_Req": return 0x00FF15 - case "Escape": return 0x00FF1B - case "Multi_key": return 0x00FF20 - case "Kanji": return 0x00FF21 - case "Muhenkan": return 0x00FF22 - case "Henkan": return 0x00FF23 - case "Henkan_Mode": return 0x00FF23 - case "Romaji": return 0x00FF24 - case "Hiragana": return 0x00FF25 - case "Katakana": return 0x00FF26 - case "Hiragana_Katakana": return 0x00FF27 - case "Zenkaku": return 0x00FF28 - case "Hankaku": return 0x00FF29 - case "Zenkaku_Hankaku": return 0x00FF2A - case "Touroku": return 0x00FF2B - case "Massyo": return 0x00FF2C - case "Kana_Lock": return 0x00FF2D - case "Kana_Shift": return 0x00FF2E - case "Eisu_Shift": return 0x00FF2F - case "Eisu_toggle": return 0x00FF30 - case "Hangul": return 0x00FF31 - case "Hangul_Start": return 0x00FF32 - case "Hangul_End": return 0x00FF33 - case "Hangul_Hanja": return 0x00FF34 - case "Hangul_Jamo": return 0x00FF35 - case "Hangul_Romaja": return 0x00FF36 - case "Codeinput": return 0x00FF37 - case "Hangul_Jeonja": return 0x00FF38 - case "Hangul_Banja": return 0x00FF39 - case "Hangul_PreHanja": return 0x00FF3A - case "Hangul_PostHanja": return 0x00FF3B - case "SingleCandidate": return 0x00FF3C - case "MultipleCandidate": return 0x00FF3D - case "PreviousCandidate": return 0x00FF3E - case "Hangul_Special": return 0x00FF3F - case "Home": return 0x00FF50 - case "Left": return 0x00FF51 - case "Up": return 0x00FF52 - case "Right": return 0x00FF53 - case "Down": return 0x00FF54 - case "Page_Up": return 0x00FF55 - case "Prior": return 0x00FF55 - case "Page_Down": return 0x00FF56 - case "Next": return 0x00FF56 - case "End": return 0x00FF57 - case "Begin": return 0x00FF58 - case "Select": return 0x00FF60 - case "Print": return 0x00FF61 - case "Execute": return 0x00FF62 - case "Insert": return 0x00FF63 - case "Undo": return 0x00FF65 - case "Redo": return 0x00FF66 - case "Menu": return 0x00FF67 - case "Find": return 0x00FF68 - case "Cancel": return 0x00FF69 - case "Help": return 0x00FF6A - case "Break": return 0x00FF6B - case "Arabic_switch": return 0x00FF7E - case "Greek_switch": return 0x00FF7E - case "Hangul_switch": return 0x00FF7E - case "Hebrew_switch": return 0x00FF7E - case "ISO_Group_Shift": return 0x00FF7E - case "Mode_switch": return 0x00FF7E - case "kana_switch": return 0x00FF7E - case "script_switch": return 0x00FF7E - case "Num_Lock": return 0x00FF7F - case "KP_Space": return 0x00FF80 - case "KP_Tab": return 0x00FF89 - case "KP_Enter": return 0x00FF8D - case "KP_F1": return 0x00FF91 - case "KP_F2": return 0x00FF92 - case "KP_F3": return 0x00FF93 - case "KP_F4": return 0x00FF94 - case "KP_Home": return 0x00FF95 - case "KP_Left": return 0x00FF96 - case "KP_Up": return 0x00FF97 - case "KP_Right": return 0x00FF98 - case "KP_Down": return 0x00FF99 - case "KP_Page_Up": return 0x00FF9A - case "KP_Prior": return 0x00FF9A - case "KP_Page_Down": return 0x00FF9B - case "KP_Next": return 0x00FF9B - case "KP_End": return 0x00FF9C - case "KP_Begin": return 0x00FF9D - case "KP_Insert": return 0x00FF9E - case "KP_Delete": return 0x00FF9F - case "KP_Multiply": return 0x00FFAA - case "KP_Add": return 0x00FFAB - case "KP_Separator": return 0x00FFAC - case "KP_Subtract": return 0x00FFAD - case "KP_Decimal": return 0x00FFAE - case "KP_Divide": return 0x00FFAF - case "KP_0": return 0x00FFB0 - case "KP_1": return 0x00FFB1 - case "KP_2": return 0x00FFB2 - case "KP_3": return 0x00FFB3 - case "KP_4": return 0x00FFB4 - case "KP_5": return 0x00FFB5 - case "KP_6": return 0x00FFB6 - case "KP_7": return 0x00FFB7 - case "KP_8": return 0x00FFB8 - case "KP_9": return 0x00FFB9 - case "KP_Equal": return 0x00FFBD - case "F1": return 0x00FFBE - case "F2": return 0x00FFBF - case "F3": return 0x00FFC0 - case "F4": return 0x00FFC1 - case "F5": return 0x00FFC2 - case "F6": return 0x00FFC3 - case "F7": return 0x00FFC4 - case "F8": return 0x00FFC5 - case "F9": return 0x00FFC6 - case "F10": return 0x00FFC7 - case "F11": return 0x00FFC8 - case "F12": return 0x00FFC9 - case "F13": return 0x00FFCA - case "F14": return 0x00FFCB - case "F15": return 0x00FFCC - case "F16": return 0x00FFCD - case "F17": return 0x00FFCE - case "F18": return 0x00FFCF - case "F19": return 0x00FFD0 - case "F20": return 0x00FFD1 - case "F21": return 0x00FFD2 - case "F22": return 0x00FFD3 - case "F23": return 0x00FFD4 - case "F24": return 0x00FFD5 - case "F25": return 0x00FFD6 - case "F26": return 0x00FFD7 - case "F27": return 0x00FFD8 - case "F28": return 0x00FFD9 - case "F29": return 0x00FFDA - case "F30": return 0x00FFDB - case "F31": return 0x00FFDC - case "F32": return 0x00FFDD - case "F33": return 0x00FFDE - case "F34": return 0x00FFDF - case "F35": return 0x00FFE0 - case "Shift_L": return 0x00FFE1 - case "Shift_R": return 0x00FFE2 - case "Control_L": return 0x00FFE3 - case "Control_R": return 0x00FFE4 - case "Caps_Lock": return 0x00FFE5 - case "Shift_Lock": return 0x00FFE6 - case "Meta_L": return 0x00FFE7 - case "Meta_R": return 0x00FFE8 - case "Alt_L": return 0x00FFE9 - case "Alt_R": return 0x00FFEA - case "Super_L": return 0x00FFEB - case "Super_R": return 0x00FFEC - case "Hyper_L": return 0x00FFED - case "Hyper_R": return 0x00FFEE - case "Delete": return 0x00FFFF - default: return 0xFFFFFF + case "AccessX_Enable": 0x00FE70 + case "AccessX_Feedback_Enable": 0x00FE71 + case "RepeatKeys_Enable": 0x00FE72 + case "SlowKeys_Enable": 0x00FE73 + case "BounceKeys_Enable": 0x00FE74 + case "StickyKeys_Enable": 0x00FE75 + case "MouseKeys_Enable": 0x00FE76 + case "MouseKeys_Accel_Enable": 0x00FE77 + case "Overlay1_Enable": 0x00FE78 + case "Overlay2_Enable": 0x00FE79 + case "AudibleBell_Enable": 0x00FE7A + case "First_Virtual_Screen": 0x00FED0 + case "Prev_Virtual_Screen": 0x00FED1 + case "Next_Virtual_Screen": 0x00FED2 + case "Last_Virtual_Screen": 0x00FED4 + case "Terminate_Server": 0x00FED5 + case "Pointer_Left": 0x00FEE0 + case "Pointer_Right": 0x00FEE1 + case "Pointer_Up": 0x00FEE2 + case "Pointer_Down": 0x00FEE3 + case "Pointer_UpLeft": 0x00FEE4 + case "Pointer_UpRight": 0x00FEE5 + case "Pointer_DownLeft": 0x00FEE6 + case "Pointer_DownRight": 0x00FEE7 + case "Pointer_Button_Dflt": 0x00FEE8 + case "Pointer_Button1": 0x00FEE9 + case "Pointer_Button2": 0x00FEEA + case "Pointer_Button3": 0x00FEEB + case "Pointer_Button4": 0x00FEEC + case "Pointer_Button5": 0x00FEED + case "Pointer_DblClick_Dflt": 0x00FEEE + case "Pointer_DblClick1": 0x00FEEF + case "Pointer_DblClick2": 0x00FEF0 + case "Pointer_DblClick3": 0x00FEF1 + case "Pointer_DblClick4": 0x00FEF2 + case "Pointer_DblClick5": 0x00FEF3 + case "Pointer_Drag_Dflt": 0x00FEF4 + case "Pointer_Drag1": 0x00FEF5 + case "Pointer_Drag2": 0x00FEF6 + case "Pointer_Drag3": 0x00FEF7 + case "Pointer_Drag4": 0x00FEF8 + case "Pointer_EnableKeys": 0x00FEF9 + case "Pointer_Accelerate": 0x00FEFA + case "Pointer_DfltBtnNext": 0x00FEFB + case "Pointer_DfltBtnPrev": 0x00FEFC + case "Pointer_Drag5": 0x00FEFD + case "BackSpace": 0x00FF08 + case "Tab": 0x00FF09 + case "Linefeed": 0x00FF0A + case "Clear": 0x00FF0B + case "Return": 0x00FF0D + case "Pause": 0x00FF13 + case "Scroll_Lock": 0x00FF14 + case "Sys_Req": 0x00FF15 + case "Escape": 0x00FF1B + case "Multi_key": 0x00FF20 + case "Kanji": 0x00FF21 + case "Muhenkan": 0x00FF22 + case "Henkan": 0x00FF23 + case "Henkan_Mode": 0x00FF23 + case "Romaji": 0x00FF24 + case "Hiragana": 0x00FF25 + case "Katakana": 0x00FF26 + case "Hiragana_Katakana": 0x00FF27 + case "Zenkaku": 0x00FF28 + case "Hankaku": 0x00FF29 + case "Zenkaku_Hankaku": 0x00FF2A + case "Touroku": 0x00FF2B + case "Massyo": 0x00FF2C + case "Kana_Lock": 0x00FF2D + case "Kana_Shift": 0x00FF2E + case "Eisu_Shift": 0x00FF2F + case "Eisu_toggle": 0x00FF30 + case "Hangul": 0x00FF31 + case "Hangul_Start": 0x00FF32 + case "Hangul_End": 0x00FF33 + case "Hangul_Hanja": 0x00FF34 + case "Hangul_Jamo": 0x00FF35 + case "Hangul_Romaja": 0x00FF36 + case "Codeinput": 0x00FF37 + case "Hangul_Jeonja": 0x00FF38 + case "Hangul_Banja": 0x00FF39 + case "Hangul_PreHanja": 0x00FF3A + case "Hangul_PostHanja": 0x00FF3B + case "SingleCandidate": 0x00FF3C + case "MultipleCandidate": 0x00FF3D + case "PreviousCandidate": 0x00FF3E + case "Hangul_Special": 0x00FF3F + case "Home": 0x00FF50 + case "Left": 0x00FF51 + case "Up": 0x00FF52 + case "Right": 0x00FF53 + case "Down": 0x00FF54 + case "Page_Up": 0x00FF55 + case "Prior": 0x00FF55 + case "Page_Down": 0x00FF56 + case "Next": 0x00FF56 + case "End": 0x00FF57 + case "Begin": 0x00FF58 + case "Select": 0x00FF60 + case "Print": 0x00FF61 + case "Execute": 0x00FF62 + case "Insert": 0x00FF63 + case "Undo": 0x00FF65 + case "Redo": 0x00FF66 + case "Menu": 0x00FF67 + case "Find": 0x00FF68 + case "Cancel": 0x00FF69 + case "Help": 0x00FF6A + case "Break": 0x00FF6B + case "Arabic_switch": 0x00FF7E + case "Greek_switch": 0x00FF7E + case "Hangul_switch": 0x00FF7E + case "Hebrew_switch": 0x00FF7E + case "ISO_Group_Shift": 0x00FF7E + case "Mode_switch": 0x00FF7E + case "kana_switch": 0x00FF7E + case "script_switch": 0x00FF7E + case "Num_Lock": 0x00FF7F + case "KP_Space": 0x00FF80 + case "KP_Tab": 0x00FF89 + case "KP_Enter": 0x00FF8D + case "KP_F1": 0x00FF91 + case "KP_F2": 0x00FF92 + case "KP_F3": 0x00FF93 + case "KP_F4": 0x00FF94 + case "KP_Home": 0x00FF95 + case "KP_Left": 0x00FF96 + case "KP_Up": 0x00FF97 + case "KP_Right": 0x00FF98 + case "KP_Down": 0x00FF99 + case "KP_Page_Up": 0x00FF9A + case "KP_Prior": 0x00FF9A + case "KP_Page_Down": 0x00FF9B + case "KP_Next": 0x00FF9B + case "KP_End": 0x00FF9C + case "KP_Begin": 0x00FF9D + case "KP_Insert": 0x00FF9E + case "KP_Delete": 0x00FF9F + case "KP_Multiply": 0x00FFAA + case "KP_Add": 0x00FFAB + case "KP_Separator": 0x00FFAC + case "KP_Subtract": 0x00FFAD + case "KP_Decimal": 0x00FFAE + case "KP_Divide": 0x00FFAF + case "KP_0": 0x00FFB0 + case "KP_1": 0x00FFB1 + case "KP_2": 0x00FFB2 + case "KP_3": 0x00FFB3 + case "KP_4": 0x00FFB4 + case "KP_5": 0x00FFB5 + case "KP_6": 0x00FFB6 + case "KP_7": 0x00FFB7 + case "KP_8": 0x00FFB8 + case "KP_9": 0x00FFB9 + case "KP_Equal": 0x00FFBD + case "F1": 0x00FFBE + case "F2": 0x00FFBF + case "F3": 0x00FFC0 + case "F4": 0x00FFC1 + case "F5": 0x00FFC2 + case "F6": 0x00FFC3 + case "F7": 0x00FFC4 + case "F8": 0x00FFC5 + case "F9": 0x00FFC6 + case "F10": 0x00FFC7 + case "F11": 0x00FFC8 + case "F12": 0x00FFC9 + case "F13": 0x00FFCA + case "F14": 0x00FFCB + case "F15": 0x00FFCC + case "F16": 0x00FFCD + case "F17": 0x00FFCE + case "F18": 0x00FFCF + case "F19": 0x00FFD0 + case "F20": 0x00FFD1 + case "F21": 0x00FFD2 + case "F22": 0x00FFD3 + case "F23": 0x00FFD4 + case "F24": 0x00FFD5 + case "F25": 0x00FFD6 + case "F26": 0x00FFD7 + case "F27": 0x00FFD8 + case "F28": 0x00FFD9 + case "F29": 0x00FFDA + case "F30": 0x00FFDB + case "F31": 0x00FFDC + case "F32": 0x00FFDD + case "F33": 0x00FFDE + case "F34": 0x00FFDF + case "F35": 0x00FFE0 + case "Shift_L": 0x00FFE1 + case "Shift_R": 0x00FFE2 + case "Control_L": 0x00FFE3 + case "Control_R": 0x00FFE4 + case "Caps_Lock": 0x00FFE5 + case "Shift_Lock": 0x00FFE6 + case "Meta_L": 0x00FFE7 + case "Meta_R": 0x00FFE8 + case "Alt_L": 0x00FFE9 + case "Alt_R": 0x00FFEA + case "Super_L": 0x00FFEB + case "Super_R": 0x00FFEC + case "Hyper_L": 0x00FFED + case "Hyper_R": 0x00FFEE + case "Delete": 0x00FFFF + default: 0xFFFFFF } } -} +} // RimeKeycode diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index 6b0f521c2..8ac4124ab 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -12,38 +12,30 @@ 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) + let inputModes: RimeInputModes = args.dropFirst(2).reduce(RimeInputModes(), { $0.union(RimeInputModes(code: $1) ?? []) }) + 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) + let inputModes: RimeInputModes = args.dropFirst(2).reduce(RimeInputModes(), { $0.union(RimeInputModes(code: $1) ?? []) }) + 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 +49,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() + let delegate: SquirrelApplicationDelegate = .init() 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", Bundle.main.localizedString(forKey: "say_voice", value: nil, table: "Notifications"), Bundle.main.localizedString(forKey: "problematic_launch", value: nil, table: "Notifications")] 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,63 +83,62 @@ import UserNotifications RimeApi.finalize() } } -} +} // SquirrelApp -final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate { - @frozen enum SquirrelNotificationPolicy { +final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, @preconcurrency SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate { + @frozen enum SquirrelNotificationPolicy: Sendable { case never, whenAppropriate, always } private(set) var showNotifications: SquirrelNotificationPolicy = .never private var switcherKeyEquivalent: RimeKeycode = .XK_VoidSymbol private var switcherKeyModifierMask: RimeModifiers = [] var isCurrentInputMethod: Bool = false - lazy var panel = SquirrelPanel() - let menu = NSMenu() - private let updateController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + @MainActor lazy var panel: SquirrelPanel = .init() + @MainActor lazy var menu: NSMenu = getMenu() + private let updateController: SPUStandardUpdaterController = .init(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) 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 let userDataDir: URL = .init(fileURLWithPath: "Library/Rime/", isDirectory: true, relativeTo: FileManager.default.homeDirectoryForCurrentUser).standardizedFileURL + static private let RimeWiki: URL = .init(string: "https://github.com/rime/home/wiki")! + static private let updaterIdentifier: String = "SquirrelUpdateNotification" - /*** updater ***/ - func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) { + /* updater */ + @MainActor func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) { NSApp.setActivationPolicy(.regular) 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) + let content: UNMutableNotificationContent = .init() + content.title = Bundle.main.localizedString(forKey: "new_update", value: nil, table: "Notifications") + content.body = String(format: Bundle.main.localizedString(forKey: "update_version", value: nil, table: "Notifications"), update.displayVersionString) + let request: UNNotificationRequest = .init(identifier: Self.updaterIdentifier, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } } - func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) { + @MainActor func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) { NSApp.dockTile.badgeLabel = "" - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["SquirrelUpdateNotification"]) + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [Self.updaterIdentifier]) } - func standardUserDriverWillFinishUpdateSession() { + @MainActor func standardUserDriverWillFinishUpdateSession() { NSApp.setActivationPolicy(.accessory) } 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() } - /*** launching ***/ + /* launching */ func applicationWillFinishLaunching(_ notification: Notification) { - setupMenu() - let center = NSWorkspace.shared.notificationCenter - center.addObserver(forName: NSWorkspace.willPowerOffNotification, object: nil, queue: nil, using: workspaceWillPowerOff(_:)) - let notifCenter = DistributedNotificationCenter.default() - notifCenter.addObserver(forName: Notification.Name("SquirrelReloadNotification"), object: nil, queue: nil, using: rimeNeedsReload(_:)) - notifCenter.addObserver(forName: Notification.Name("SquirrelSyncNotification"), object: nil, queue: nil, using: rimeNeedsSync(_:)) + let center: NotificationCenter = NSWorkspace.shared.notificationCenter + center.addObserver(self, selector: #selector(workspaceWillPowerOff(_:)), name: NSWorkspace.willPowerOffNotification, object: nil) + let notifCenter: DistributedNotificationCenter = .default() + notifCenter.addObserver(self, selector: #selector(rimeNeedsReload(_:)), name: .init("SquirrelReloadNotification"), object: nil) + notifCenter.addObserver(self, selector: #selector(rimeNeedsSync(_:)), name: .init("SquirrelSyncNotification"), object: nil) isCurrentInputMethod = false - notifCenter.addObserver(forName: kTISNotifySelectedKeyboardInputSourceChanged as Notification.Name, object: nil, queue: nil, using: inputSourceChanged(_:)) + notifCenter.addObserver(self, selector: #selector(inputSourceChanged(_:)), name: kTISNotifySelectedKeyboardInputSourceChanged as Notification.Name, object: nil) } func applicationWillTerminate(_ notification: Notification) { @@ -159,41 +147,42 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta panel.hide() } - private func setupMenu() { - let showSwitcher = NSMenuItem(title: NSLocalizedString("showSwitcher", comment: ""), action: #selector(showSwitcher(_:)), keyEquivalent: "") + @MainActor private func getMenu() -> NSMenu { + let menu: NSMenu = .init() + let showSwitcher: NSMenuItem = .init(title: Bundle.main.localizedString(forKey: "showSwitcher", value: nil, table: "MainMenu"), action: #selector(showSwitcher(_:)), keyEquivalent: "") showSwitcher.target = self menu.addItem(showSwitcher) - let deploy = NSMenuItem(title: NSLocalizedString("deploy", comment: ""), action: #selector(deploy(_:)), keyEquivalent: "`") + let deploy: NSMenuItem = .init(title: Bundle.main.localizedString(forKey: "deploy", value: nil, table: "MainMenu"), 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 = .init(title: Bundle.main.localizedString(forKey: "syncUserData", value: nil, table: "MainMenu"), action: #selector(syncUserData(_:)), keyEquivalent: "") syncUserData.target = self menu.addItem(syncUserData) - let configure = NSMenuItem(title: NSLocalizedString("configure", comment: ""), action: #selector(configure(_:)), keyEquivalent: "") + let configure: NSMenuItem = .init(title: Bundle.main.localizedString(forKey: "configure", value: nil, table: "MainMenu"), action: #selector(configure(_:)), keyEquivalent: "") configure.target = self menu.addItem(configure) - let openWiki = NSMenuItem(title: NSLocalizedString("openWiki", comment: ""), action: #selector(openWiki(_:)), keyEquivalent: "") + let openWiki: NSMenuItem = .init(title: Bundle.main.localizedString(forKey: "openWiki", value: nil, table: "MainMenu"), action: #selector(openWiki(_:)), keyEquivalent: "") openWiki.target = self menu.addItem(openWiki) - let checkForUpdates = NSMenuItem(title: NSLocalizedString("checkForUpdates", comment: ""), action: #selector(checkForUpdates(_:)), keyEquivalent: "") + let checkForUpdates: NSMenuItem = .init(title: Bundle.main.localizedString(forKey: "checkForUpdates", value: nil, table: "MainMenu"), action: #selector(checkForUpdates(_:)), keyEquivalent: "") checkForUpdates.target = self menu.addItem(checkForUpdates) - let openLogFolder = NSMenuItem(title: NSLocalizedString("openLogFolder", comment: ""), action: #selector(openLogFolder(_:)), keyEquivalent: "") + let openLogFolder: NSMenuItem = .init(title: Bundle.main.localizedString(forKey: "openLogFolder", value: nil, table: "MainMenu"), action: #selector(openLogFolder(_:)), keyEquivalent: "") openLogFolder.target = self menu.addItem(openLogFolder) + return menu } - /*** menu selectors ***/ + /* 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: RimeSessionId = sender as? RimeSessionId { _ = RimeApi.process_key(session, switcherKeyEquivalent.rawValue, switcherKeyModifierMask.rawValue) } } - @objc func deploy(_ sender: Any?) { + @MainActor @objc func deploy(_ sender: Any?) { print("Start maintenance...") shutdownRime() startRime(withFullCheck: true) @@ -223,26 +212,26 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } @objc func openLogFolder(_ sender: Any?) { - let infoLog = URL(fileURLWithPath: "rime.squirrel.INFO", isDirectory: false, relativeTo: FileManager.default.temporaryDirectory).standardizedFileURL + let infoLog: URL = .init(fileURLWithPath: "rime.squirrel.INFO", isDirectory: false, relativeTo: FileManager.default.temporaryDirectory).standardizedFileURL NSWorkspace.shared.activateFileViewerSelecting([infoLog]) } - func setupRime() { + @MainActor func setupRime() { if !FileManager.default.fileExists(atPath: Self.userDataDir.path) { 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) } @@ -263,47 +252,39 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta RimeApi.finalize() } - func loadSettings() { + @MainActor func loadSettings() { switcherKeyModifierMask = [] switcherKeyEquivalent = .XK_VoidSymbol - let defaulConfig = SquirrelConfig("default") - if let hotkey = defaulConfig.string(forOption: "switcher/hotkeys/@0") { + let defaultConfig: SquirrelConfig = .init(.default) + SquirrelInputController.goodOldCapsLock = defaultConfig.boolValue(for: "ascii_composer/good_old_caps_lock") + if let hotkey: String = defaultConfig.string(for: "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 = .init(name: $0) { switcherKeyModifierMask.insert(modifier) } } switcherKeyEquivalent = RimeKeycode(name: keys.last!) } - defaulConfig.close() - - let config = SquirrelConfig() - if !config.openBaseConfig() { - return - } + defaultConfig.close() - let showNotificationsWhen = config.string(forOption: "show_notifications_when") - if showNotificationsWhen?.caseInsensitiveCompare("never") == .orderedSame { - showNotifications = .never - } else if showNotificationsWhen?.caseInsensitiveCompare("always") == .orderedSame { - showNotifications = .always - } else { - showNotifications = .whenAppropriate + let baseConfig: SquirrelConfig = .init() + guard baseConfig.openBaseConfig() else { return } + let showNotificationsWhen: String? = baseConfig.string(for: "show_notifications_when") + showNotifications = switch showNotificationsWhen { + case "never": .never + case "always": .always + default: .whenAppropriate } - panel.loadConfig(config) - config.close() + SquirrelInputController.chordDuration = if let duration: Double = baseConfig.optionalDouble(for: "chord_duration"), duration.isNormal { duration } else { 0.1 } + panel.optionSwitcher = SquirrelOptionSwitcher() + panel.loadConfig(baseConfig) + baseConfig.close() } - func loadSchemaSpecificSettings(schemaId: String, withRimeSession sessionId: RimeSessionId) { - if schemaId.isEmpty || schemaId.hasPrefix(".") { - return - } + @MainActor func loadSchemaSpecificSettings(schemaId: String, withRimeSession sessionId: RimeSessionId) { + guard !schemaId.isEmpty, !schemaId.hasPrefix(".") else { return } // update the list of switchers that change styles and color-themes - let baseConfig = SquirrelConfig("squirrel") - let schema = SquirrelConfig() - if schema.open(withSchemaId: schemaId, baseConfig: baseConfig) && schema.hasSection("style") { - panel.optionSwitcher = schema.optionSwitcherForSchema() + let baseConfig: SquirrelConfig = .init(.base) + let schema: SquirrelConfig = .init() + if schema.open(schemaId: schemaId, baseConfig: baseConfig), schema.hasSection("style") { + panel.optionSwitcher = schema.optionSwitcher() panel.optionSwitcher.update(withRimeSession: sessionId) panel.loadConfig(schema) } else { @@ -314,29 +295,28 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta baseConfig.close() } - func loadSchemaSpecificLabels(schemaId: String) { - let defaultConfig = SquirrelConfig("default") + @MainActor func loadSchemaSpecificLabels(schemaId: String) { + let defaultConfig: SquirrelConfig = .init(.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 = .init() + if schema.open(schemaId: schemaId, baseConfig: defaultConfig), schema.hasSection("menu") { + panel.loadLabelConfig(schema, directUpdate: false) + } else { + panel.loadLabelConfig(defaultConfig, directUpdate: false) + } + schema.close() } - schema.close() defaultConfig.close() } // prevent freezing the system func problematicLaunchDetected() -> Bool { var detected: Bool = false - let logfile = URL(fileURLWithPath: "squirrel_launch.dat", isDirectory: false, relativeTo: FileManager.default.temporaryDirectory).standardizedFileURL + let logfile: URL = .init(fileURLWithPath: "squirrel_launch.dat", isDirectory: false, relativeTo: FileManager.default.temporaryDirectory).standardizedFileURL print("[DEBUG] archive: \(logfile)") - if let archive = try? Data(contentsOf: logfile, options: [.uncached]) { + if let archive: Data = try? Data(contentsOf: logfile, options: [.uncached]) { if let previousLaunch: NSDate = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSDate.self, from: archive), previousLaunch.timeIntervalSinceNow >= -2 { detected = true } @@ -347,17 +327,17 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta return detected } - func workspaceWillPowerOff(_ notification: Notification) { + @MainActor @objc private func workspaceWillPowerOff(_ notification: Notification) { print("Finalizing before logging out.") shutdownRime() } - func rimeNeedsReload(_ notification: Notification) { + @MainActor @objc private func rimeNeedsReload(_ notification: Notification) { print("Reloading rime on demand.") deploy(nil) } - func rimeNeedsSync(_ notification: Notification) { + @objc private func rimeNeedsSync(_ notification: Notification) { print("Sync rime on demand.") syncUserData(nil) } @@ -368,101 +348,87 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta return .terminateNow } - func inputSourceChanged(_ notification: Notification) { - let inputSource: TISInputSource = TISCopyCurrentKeyboardInputSource().takeUnretainedValue() - if let inputSourceID: CFString = bridge(ptr: TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)), !(inputSourceID as String).hasPrefix(SquirrelApp.bundleId) { + @MainActor @objc private func inputSourceChanged(_ notification: Notification) { + let inputSource: TISInputSource = TISCopyCurrentKeyboardInputSource().takeRetainedValue() + if let inputSourceID: String = bridge(ptr: TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID), as: CFString.self) as? String, !inputSourceID.hasPrefix(SquirrelApp.bundleId) { isCurrentInputMethod = false } } -} +} // SquirrelApplicationDelegate -private func showNotification(message: String) { +func showNotification(message: String) { if #available(macOS 10.14, *) { - let center = UNUserNotificationCenter.current() + let center: UNUserNotificationCenter = .current() center.requestAuthorization(options: [.alert, .provisional]) { granted, error in if error != nil { print("User notification authorization error: \(error.debugDescription)") } } 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 = .init() + content.title = Bundle.main.localizedString(forKey: "Squirrel", value: nil, table: "Notifications") + content.subtitle = Bundle.main.localizedString(forKey: message, value: nil, table: "Notifications") + if #available(macOS 12.0, *) { content.interruptionLevel = .active } + let request: UNNotificationRequest = .init(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: "") - - let notificationCenter = NSUserNotificationCenter.default + let notification: NSUserNotification = .init() + notification.title = Bundle.main.localizedString(forKey: "Squirrel", value: nil, table: "Notifications") + notification.subtitle = Bundle.main.localizedString(forKey: message, value: nil, table: "Notifications") + let notificationCenter: NSUserNotificationCenter = .default notificationCenter.removeAllDeliveredNotifications() notificationCenter.deliver(notification) } } -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 +@MainActor let notificationHandler: @convention(c) (UnsafeMutableRawPointer?, RimeSessionId, UnsafePointer?, UnsafePointer?) -> Void = { contextObject, sessionId, messageType, messageValue in + guard let messageType = messageType else { return } + switch String(cString: messageType) { + case "deploy": + guard let messageValue = messageValue else { break } + switch String(cString: messageValue) { + 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: contextObject), appDelegate.showNotifications != .never, let messageValue = messageValue else { break } + let schemaName: [String] = String(cString: messageValue).components(separatedBy: "/") + if schemaName.count == 2 { + appDelegate.panel.updateStatus(long: schemaName[1], short: schemaName[1]) + } + case "option": + guard let appDelegate: SquirrelApplicationDelegate = bridge(ptr: contextObject), let messageValue = messageValue else { break } + let optionState: String = .init(cString: messageValue) + let state: Bool = !optionState.hasPrefix("!") + let optionName: String = 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: sessionId) + } + if updateScriptVariant, !updateStyleOptions { + appDelegate.panel.updateScriptVariant() + } + guard appDelegate.showNotifications != .never else { break } + let longLabel: RimeStringSlice = RimeApi.get_state_label_abbreviated(sessionId, optionName, state, false) + let shortLabel: RimeStringSlice = RimeApi.get_state_label_abbreviated(sessionId, optionName, state, true) + if longLabel.str != nil || shortLabel.str != nil { + let long: String? = longLabel.str == nil ? nil : String(cString: longLabel.str) + let short: String? = shortLabel.str == nil || shortLabel.length < strlen(shortLabel.str) ? nil : String(cString: shortLabel.str) + appDelegate.panel.updateStatus(long: long, short: short) } + default: break } } @@ -474,16 +440,16 @@ extension NSApplication { // MARK: Bridging -func bridge(obj: T?) -> UnsafeMutableRawPointer? { - return obj != nil ? Unmanaged.passUnretained(obj!).toOpaque() : nil +func bridge(obj: T!) -> UnsafeMutableRawPointer! { + obj == nil ? nil : Unmanaged.passUnretained(obj).toOpaque() } -func bridge(ptr: UnsafeMutableRawPointer?) -> T? { - return ptr != nil ? Unmanaged.fromOpaque(ptr!).takeUnretainedValue() : nil +func bridge(ptr: UnsafeMutableRawPointer!, as type: T.Type = T.self) -> T! { + ptr == nil ? nil : Unmanaged.fromOpaque(ptr).takeUnretainedValue() } -let RimeApi = rime_get_api_stdbool().pointee typealias RimeSessionId = UInt +let RimeApi: RimeApi_stdbool = rime_get_api_stdbool().pointee protocol RimeStruct { var data_size: CInt { get set } @@ -496,7 +462,7 @@ extension RimeStatus_stdbool: RimeStruct {} extension RimeContext_stdbool: RimeStruct {} func RimeStructInit() -> T { - var rimeStruct = T.init() + var rimeStruct: T = .init() rimeStruct.data_size = CInt(MemoryLayout.size - MemoryLayout.size(ofValue: rimeStruct.data_size)) return rimeStruct } diff --git a/sources/SquirrelConfig.swift b/sources/SquirrelConfig.swift index 630f75a7a..2875e0f6c 100644 --- a/sources/SquirrelConfig.swift +++ b/sources/SquirrelConfig.swift @@ -1,173 +1,95 @@ 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"] +struct SquirrelOptionSwitcher: Sendable { + 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] - - init(schemaId: String?, switcher: [String: String]?, optionGroups: [String: Set]?, defaultScriptVariant: String?, scriptVariantOptions: [String: String]?) { - self.schemaId = schemaId ?? "" - self.switcher = switcher ?? [:] - self.optionGroups = optionGroups ?? [:] - optionNames = switcher == nil ? [] : Set(switcher!.keys) - optionStates = switcher == nil ? [] : Set(switcher!.values) - currentScriptVariant = defaultScriptVariant ?? Bundle.preferredLocalizations(from: Self.Scripts)[0] - self.scriptVariantOptions = scriptVariantOptions ?? [:] - super.init() - } - - convenience init(schemaId: String?) { - self.init(schemaId: schemaId, switcher: [:], optionGroups: [:], defaultScriptVariant: nil, scriptVariantOptions: [:]) - } + private var scriptVariantOptions: [String : String] + private var switcher: [String : String] + private var optionGroups: [String : Set] - override convenience init() { - self.init(schemaId: "", switcher: [:], optionGroups: [:], defaultScriptVariant: nil, scriptVariantOptions: [:]) + init(schemaId: String = "", switcher: [String : String] = [:], optionGroups: [String : Set] = [:], defaultScriptVariant: String? = nil, scriptVariantOptions: [String : String] = [:]) { + self.schemaId = schemaId + self.switcher = switcher + self.optionGroups = optionGroups + self.optionNames = Set(switcher.keys) + self.optionStates = Set(switcher.values) + self.currentScriptVariant = defaultScriptVariant ?? Bundle.preferredLocalizations(from: Self.Scripts)[0] + self.scriptVariantOptions = scriptVariantOptions } // 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 - } + mutating 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 - optionStates = Set(switcher.values) - return true - } - return false + guard optionNames == self.optionNames else { return false } + self.switcher = switcher + optionStates = Set(switcher.values) + return true } - 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 - } + mutating func updateGroupState(_ optionState: String, ofOption optionName: String) -> Bool { + guard let optionGroup: Set = 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 - } + mutating func updateCurrentScriptVariant(_ scriptVariant: String?) -> Bool { + guard let scriptVariant = scriptVariant, !scriptVariantOptions.isEmpty, let scriptVariantCode: String = scriptVariantOptions[scriptVariant] else { return false } + currentScriptVariant = scriptVariantCode + return true } - func update(withRimeSession session: RimeSessionId) { - if switcher.isEmpty || session == 0 { return } + mutating func update(withRimeSession session: RimeSessionId) { + 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] = switcher.filter({ $0.value == state }).map(\.key) + updatedState = optionGroup.first { RimeApi.get_option(session, $0) } 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 - } - } + _ = updateCurrentScriptVariant(scriptVariantOptions.keys.first { $0.hasPrefix("!") ? !RimeApi.get_option(session, String($0.dropFirst())) : RimeApi.get_option(session, $0) }) } -} // SquirrelOptionSwitcher +} // SquirrelOptionSwitcher -struct SquirrelAppOptions { - private var appOptions: [String: Any] +protocol DefaultValueDefined: Sendable { static var `default`: Self { get } } +extension Bool: DefaultValueDefined { static var `default`: Bool { false } } +extension Int: DefaultValueDefined { static var `default`: Int { 0 } } +extension Double: DefaultValueDefined { static var `default`: Double { .zero } } - init() { appOptions = [:] } +struct SquirrelAppOptions: Sendable { + private var appOptions: [String : DefaultValueDefined] = [:] - 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 - } - } - } - - mutating func setValue(_ value: Bool, forKey key: String) { - appOptions[key] = value - } - - mutating func setValue(_ value: Int, forKey key: String) { - appOptions[key] = value - } - - 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 intValue(forKey key: String) -> Int { - if let value = appOptions[key], value is Int.Type { - return value as! Int - } - return 0 + subscript(option: String, as type: T.Type) -> T { + get { appOptions[option] as? T ?? T.default } } - - func doubleValue(forKey key: String) -> Double { - if let value = appOptions[key], value is Double.Type { - return value as! Double - } - return 0.0 - } -} +} // 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 @@ -175,18 +97,10 @@ final class SquirrelConfig: NSObject { private var colorSpaceObject: NSColorSpace private var colorSpaceName: String var colorSpace: String { - get { return colorSpaceName } - set (newValue) { - 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 - } - } - } + get { colorSpaceName } + set { let name: String = newValue.replacingOccurrences(of: "_", with: "") + guard name != colorSpaceName, let (key, value) = Self.colorSpaceMap.first(where: { $0.key ~= name }) else { return } + (colorSpaceName, colorSpaceObject) = (key, value) } } override init() { @@ -197,17 +111,37 @@ final class SquirrelConfig: NSObject { super.init() } - convenience init(_ arg: String) { + @frozen enum RimeConfigType: RawRepresentable, Sendable { + case base, `default`, user, installation, schema(String) + + init?(rawValue: String) { + self = switch rawValue { + case "squirrel": .base + case "default": .default + case "user": .user + case "installation": .installation + default: .schema(rawValue) + } + } + + var rawValue: String { + return switch self { + case .base: "squirrel" + case .default: "default" + case .user: "user" + case .installation: "installation" + case .schema(let id): id + } + } + } + + convenience init(_ type: RimeConfigType) { self.init() - switch arg { - case "squirrel": - _ = openBaseConfig() - case "default": - _ = open(withConfigId: arg) - case "user", "installation": - _ = open(userConfig: arg) - default: - _ = open(withSchemaId: arg, baseConfig: nil) + _ = switch type { + case .base: openBaseConfig() + case .default: open(configId: "default") + case .user, .installation: open(userConfig: type.rawValue) + case .schema(let id): open(schemaId: id, baseConfig: SquirrelConfig(.base)) } } @@ -217,38 +151,34 @@ final class SquirrelConfig: NSObject { return isOpen } - func open(withSchemaId schemaId: String, baseConfig: SquirrelConfig?) -> Bool { + func open(schemaId: String, baseConfig: SquirrelConfig?) -> Bool { close() 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 } - func open(userConfig configId: String) -> Bool { + func open(userConfig: String) -> Bool { close() - isOpen = RimeApi.user_config_open(configId, &config) + isOpen = RimeApi.user_config_open(userConfig, &config) return isOpen } - func open(withConfigId configId: String) -> Bool { + func open(configId: String) -> Bool { close() isOpen = RimeApi.config_open(configId, &config) return isOpen } func close() { - if isOpen && RimeApi.config_close(&config) { - baseConfig = nil - schemaId = nil + if isOpen, RimeApi.config_close(&config) { isOpen = false } + baseConfig = nil + schemaId = nil } deinit { @@ -257,355 +187,291 @@ 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 - } - - func setOption(_ option: String, withBool value: Bool) -> Bool { - return RimeApi.config_set_bool(&config, option, value) - } - - func setOption(_ option: String, withInt value: Int) -> Bool { - return RimeApi.config_set_int(&config, option, CInt(value)) - } - - func setOption(_ option: String, withDouble value: Double) -> Bool { - return RimeApi.config_set_double(&config, option, CDouble(value)) - } - - func setOption(_ option: String, withString value: String) -> Bool { - return RimeApi.config_set_string(&config, option, value) - } - - func boolValue(forOption option: String) -> Bool { - return nullableBool(forOption: option, alias: nil) ?? false + guard isOpen else { return false } + var iterator: RimeConfigIterator = .init() + guard RimeApi.config_begin_map(&iterator, &config, section) else { return false } + RimeApi.config_end(&iterator) + return true } - func intValue(forOption option: String) -> Int { - return nullableInt(forOption: option, alias: nil) ?? 0 - } + func setOption(_ option: String, with value: Bool) -> Bool { RimeApi.config_set_bool(&config, option, value) } + func setOption(_ option: String, with value: Int) -> Bool { RimeApi.config_set_int(&config, option, CInt(value)) } + func setOption(_ option: String, with value: Double) -> Bool { RimeApi.config_set_double(&config, option, value) } + func setOption(_ option: String, with value: String) -> Bool { RimeApi.config_set_string(&config, option, value) } - func doubleValue(forOption option: String) -> Double { - return nullableDouble(forOption: option, alias: nil) ?? 0.0 - } + func boolValue(for option: String) -> Bool { optionalBool(for: option, alias: nil) ?? false } + func intValue(for option: String) -> Int { optionalInt(for: option, alias: nil) ?? 0 } + func doubleValue(for option: String) -> Double { optionalDouble(for: option, alias: nil) ?? .zero } - func doubleValue(forOption option: String, constraint function: (Double) -> Double) -> Double { - return function(nullableDouble(forOption: option, alias: nil) ?? 0.0) + func doubleValue(for option: String, constraint function: (Double) -> Double) -> Double { + function(optionalDouble(for: option, alias: nil) ?? .zero) } - func nullableBool(forOption option: String, alias: String?) -> Bool? { - if let cachedValue = cachedValue(ofType: Bool.self, forKey: option) { + func optionalBool(for option: String, alias: String? = nil) -> Bool? { + if let cachedValue: Bool = cachedValue(ofType: Bool.self, for: option) { return cachedValue } - var value: CBool = false - if isOpen && RimeApi.config_get_bool(&config, option, &value) { - cache[option] = Bool(value) - return Bool(value) + var value: Bool = false + if isOpen, RimeApi.config_get_bool(&config, option, &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 isOpen, let alias = alias, RimeApi.config_get_bool(&config, option.replacingLastPathComponent(with: alias), &value) { + cache[option] = value + return value } - return baseConfig?.nullableBool(forOption: option, alias: alias) + return baseConfig?.optionalBool(for: option, alias: alias) } - func nullableInt(forOption option: String, alias: String?) -> Int? { - if let cachedValue = cachedValue(ofType: Int.self, forKey: option) { + func optionalInt(for option: String, alias: String? = nil) -> Int? { + if let cachedValue: Int = cachedValue(ofType: Int.self, for: option) { return cachedValue } var value: CInt = 0 - if isOpen && RimeApi.config_get_int(&config, option, &value) { + if isOpen, RimeApi.config_get_int(&config, option, &value) { cache[option] = Int(value) return Int(value) } - if let aliasOption = option.replaceLastPathComponent(with: alias), - isOpen && RimeApi.config_get_int(&config, aliasOption, &value) { + if isOpen, let alias = alias, RimeApi.config_get_int(&config, option.replacingLastPathComponent(with: alias), &value) { cache[option] = Int(value) return Int(value) } - return baseConfig?.nullableInt(forOption: option, alias: alias) + return baseConfig?.optionalInt(for: option, alias: alias) } - func nullableDouble(forOption option: String, alias: String?) -> Double? { - if let cachedValue = cachedValue(ofType: Double.self, forKey: option) { + func optionalDouble(for option: String, alias: String? = nil) -> Double? { + if let cachedValue: Double = cachedValue(ofType: Double.self, for: option) { return cachedValue } - var value: CDouble = 0 - if isOpen && RimeApi.config_get_double(&config, option, &value) { - cache[option] = Double(value) - return Double(value) + var value: Double = 0 + if isOpen, RimeApi.config_get_double(&config, option, &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 isOpen, let alias = alias, RimeApi.config_get_double(&config, option.replacingLastPathComponent(with: alias), &value) { + cache[option] = value + return value } - return baseConfig?.nullableDouble(forOption: option, alias: alias) + return baseConfig?.optionalDouble(for: option, alias: alias) } - func nullableDouble(forOption option: String, alias: String?, constraint function: (CDouble) -> CDouble) -> Double? { - if let value = nullableDouble(forOption: option, alias: alias) { - return function(value) - } - return nil + func optionalDouble(for option: String, alias: String? = nil, constraint function: (Double) -> Double) -> Double? { + guard let value: Double = optionalDouble(for: option, alias: alias) else { return nil } + return function(value) } - func nullableBool(forOption option: String) -> Bool? { - return nullableBool(forOption: option, alias: nil) - } - - func nullableInt(forOption option: String) -> Int? { - return nullableInt(forOption: option, alias: nil) - } - - func nullableDouble(forOption option: String) -> Double? { - return nullableDouble(forOption: option, alias: nil) - } - - func nullableDouble(forOption option: String, - constraint function: (CDouble) -> CDouble) -> Double? { - if let value = nullableDouble(forOption: option, alias: nil) { - return function(value) - } - return nil - } - - func string(forOption option: String, alias: String?) -> String? { - if let cachedValue = cachedValue(ofType: String.self, forKey: option) { + func string(for option: String, alias: String? = nil) -> String? { + if let cachedValue: String = cachedValue(ofType: String.self, for: option) { 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 + if isOpen, let value: UnsafePointer = RimeApi.config_get_cstring(&config, option) { + let string: String = .init(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 isOpen, let alias = alias, let value: UnsafePointer = RimeApi.config_get_cstring(&config, option.replacingLastPathComponent(with: alias)) { + let string: String = .init(cString: value).trimmingCharacters(in: .whitespaces) + cache[option] = string + return string } - return baseConfig?.string(forOption: option, alias: alias) + return baseConfig?.string(for: option, alias: alias) } - func color(forOption option: String, alias: String?) -> NSColor? { - if let cachedValue = cachedValue(ofType: NSColor.self, forKey: option) { + func color(for option: String, alias: String? = nil) -> NSColor? { + if let cachedValue: NSColor = cachedValue(ofType: NSColor.self, for: option) { return cachedValue } - if let hexCode = string(forOption: option, alias: alias), let color = color(hexCode: hexCode) { + if let hexCode: String = string(for: option, alias: alias), let color: NSColor = color(hexCode: hexCode) { cache[option] = color return color } - return baseConfig?.color(forOption: option, alias: alias) + return baseConfig?.color(for: option, alias: alias) } - func image(forOption option: String, alias: String?) -> NSImage? { - if let cachedValue = cachedValue(ofType: NSImage.self, forKey: option) { + func image(for option: String, alias: String? = nil) -> NSImage? { + if let cachedValue: NSImage = cachedValue(ofType: NSImage.self, for: option) { return cachedValue } - if let file = string(forOption: option, alias: alias), let image = image(filePath: file) { + if let file: String = string(for: option, alias: alias), let image: NSImage = image(filePath: file) { cache[option] = image return image } - return baseConfig?.image(forOption: option, alias: alias) - } - - func string(forOption option: String) -> String? { - return string(forOption: option, alias: nil) - } - - func color(forOption option: String) -> NSColor? { - return color(forOption: option, alias: nil) - } - - func image(forOption option: String) -> NSImage? { - return image(forOption: option, alias: nil) + return baseConfig?.image(for: option, alias: alias) } - func listSize(forOption option: String) -> Int { - return RimeApi.config_list_size(&config, option) - } + func listSize(for option: String) -> Int { RimeApi.config_list_size(&config, option) } - func list(forOption option: String) -> [String]? { - var iterator = RimeConfigIterator() - if !RimeApi.config_begin_list(&iterator, &config, option) { - return nil - } + func list(for option: String) -> [String]? { + var iterator: RimeConfigIterator = .init() + 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!))!) + strList.append(string(for: String(cString: iterator.path))!) } RimeApi.config_end(&iterator) - 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"] + return strList.isEmpty ? nil : strList + } + + 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" + localeScript.first(where: { $0.key ~= scriptVariant })?.value ?? localeRegion.first(where: { scriptVariant.range(of: $0.key, options: [.caseInsensitive, .diacriticInsensitive]) != nil })?.value ?? "zh" } - func optionSwitcherForSchema() -> SquirrelOptionSwitcher { - if schemaId == nil || schemaId!.isEmpty || schemaId == "." { - return SquirrelOptionSwitcher() - } - var switchIter = RimeConfigIterator() - if !RimeApi.config_begin_list(&switchIter, &config, "switches") { - return SquirrelOptionSwitcher(schemaId: schemaId) - } - var switcher: [String: String] = [:] - var optionGroups: [String: Set] = [:] + func optionSwitcher() -> SquirrelOptionSwitcher { + guard let schemaId = schemaId, !schemaId.isEmpty, schemaId != "." else { return .init() } + var switchIter: RimeConfigIterator = .init() + guard RimeApi.config_begin_list(&switchIter, &config, "switches") else { return .init(schemaId: schemaId) } + 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") { + let reset: Int = intValue(for: String(cString: switchIter.path) + "/reset") + if let name: String = string(for: String(cString: switchIter.path) + "/name") { if hasSection("style/!" + name) || hasSection("style/" + name) { 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, Set(["simplification", "simplified", "traditional"]).contains(where: { name ~= $0 }) { 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 - } + var optionIter: RimeConfigIterator = .init() + 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 + var hasScriptVariant: Bool = defaultScriptVariant != nil while RimeApi.config_next(&optionIter) { - let option: String = string(forOption: String(cString: optionIter.path!))! + let option: String = string(for: 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 |= Set(["simplification", "simplified", "traditional"]).contains(where: { option ~= $0 }) } 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) - } + if defaultScriptVariant == nil, hasScriptVariant { + optGroup.forEach { scriptVariantOptions[$0] = Self.code(scriptVariant: $0) } defaultScriptVariant = scriptVariantOptions[optGroup[reset]] } } } RimeApi.config_end(&switchIter) - return SquirrelOptionSwitcher(schemaId: schemaId, switcher: switcher, optionGroups: optionGroups, defaultScriptVariant: defaultScriptVariant, scriptVariantOptions: scriptVariantOptions) + return .init(schemaId: schemaId, switcher: switcher, optionGroups: optionGroups, defaultScriptVariant: defaultScriptVariant, scriptVariantOptions: scriptVariantOptions) } - func appOptions(forApp bundleId: String) -> SquirrelAppOptions { - if let cachedValue = cachedValue(ofType: SquirrelAppOptions.self, forKey: bundleId) { + func appOptions(for bundleId: String) -> SquirrelAppOptions { + let rootKey: String = "app_options/" + bundleId + if let cachedValue: SquirrelAppOptions = cachedValue(ofType: SquirrelAppOptions.self, for: rootKey) { return cachedValue } - let rootKey = "app_options/" + bundleId - var appOptions = SquirrelAppOptions() - var iterator = RimeConfigIterator() + var appOptions: SquirrelAppOptions = .init() + var iterator: RimeConfigIterator = .init() 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 = .init(cString: iterator.path), key: String = .init(cString: iterator.key) + if let boolValue: Bool = optionalBool(for: path) { + appOptions[key] = boolValue + } else if let intValue: Int = optionalInt(for: path) { + appOptions[key] = intValue + } else if let doubleValue: Double = optionalDouble(for: 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 - } + private func cachedValue(ofType: T.Type, for key: String) -> T? { cache[key] as? T } 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, [8, 10].contains(hexCode.count), ["0x", "0X"].contains(where: { hexCode.hasPrefix($0) }) else { return nil } + let hexScanner: Scanner = .init(string: hexCode) + var hex: CUnsignedLongLong = 0x0 + guard hexScanner.scanHexInt64(&hex), hexScanner.isAtEnd else { return nil } + let r: CGFloat = .init(hex % 0x100) + let g: CGFloat = .init(hex / 0x100 % 0x100) + let b: CGFloat = .init(hex / 0x10000 % 0x100) + // 0xaaBBGGRR or 0xBBGGRR + let a: CGFloat = hexCode.count == 10 ? .init(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 = .init(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) + static let FullWidthSpace: Self = " " + + // UTF16/UniChar length and index + 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)]) } - func replaceLastPathComponent(with replacement: String?) -> String? { - if let replacement = replacement, let sep = range(of: "/", options: .backwards) { - return String(self[.. 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 replacingLastPathComponent(with replacement: String) -> String { + guard let sep: Range = range(of: "/", options: [.backwards]) else { return replacement } + return replacingCharacters(in: sep.upperBound..., with: replacement) + } +} + +extension StringProtocol { + static func ~= (lhs: Self, rhs: Self) -> Bool { lhs.compare(rhs, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame } } diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index a4818582e..519fc8baa 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -3,9 +3,10 @@ import IOKit final class SquirrelInputController: IMKInputController { // class variables - weak static var currentController: SquirrelInputController? - private static var currentApp: String = "" - private static var asciiMode: Bool? = nil + @MainActor static private var currentApp: String = "" + @MainActor static private var asciiMode: Bool? = nil + @MainActor static var goodOldCapsLock: Bool = false + static private let kStatusDelay: TimeInterval = 0.2 // private private var inlineString: NSMutableAttributedString? private var originalString: String? @@ -18,135 +19,111 @@ final class SquirrelInputController: IMKInputController { private var converted: Int = 0 private var currentIndex: Int? private var lastModifiers: NSEvent.ModifierFlags = [] - private var lastEventCount: UInt32 = 0 + private var lastEventCount: CUnsignedInt = 0 private var session: RimeSessionId = 0 private var inlinePreedit: Bool = false private var inlineCandidate: Bool = false - private var goodOldCapsLock: Bool = false private var showingSwitcherMenu: Bool = false - // app-specific bug fix - private var appOptions = SquirrelAppOptions() + private var showingInitialStatus: Bool = false + // app-specific options + private var appOptions: SquirrelAppOptions = .init() private var inlinePlaceholder: Bool = false private var panellessCommitFix: Bool = false - private var inlineOffset: Int = 0 + private var inlineOffset: Double = .zero // for chord-typing + static private let kNumKeyRollOver: Int = 50 + @MainActor static var chordDuration: TimeInterval = 0.1 private var chordTimer: Timer? - private var chordDuration: TimeInterval = 0 - private var chordKeyCodes: [RimeKeycode] = [] - private var chordModifiers: [RimeModifiers] = [] - private var chordKeyCount: Int = 0 - // public + private var chordKeyCombos: [(keycode: RimeKeycode, modifiers: RimeModifiers)] = [] + // caching candidates private(set) var candidateTexts: [String] = [] private(set) var candidateComments: [String] = [] - // KVO - @objc dynamic var viewEffectiveAppearance: NSAppearance { - let sel: Selector = NSSelectorFromString("viewEffectiveAppearance") - let sourceAppearance: NSAppearance? = client().perform(sel)?.takeUnretainedValue() as? NSAppearance - return sourceAppearance ?? NSApp.effectiveAppearance + // appearance (dark mode) + @available(macOS 10.14, *) @MainActor private var style: SquirrelStyle { + let clientAppearance: NSAppearance = client().perform(NSSelectorFromString("viewEffectiveAppearance"))?.takeUnretainedValue() as? NSAppearance ?? NSApp.effectiveAppearance + return clientAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? .dark : .light } - private var observation: NSKeyValueObservation? - // constants - private let kFullWidthSpace: String = " " - private let kNumKeyRollOver: Int = 50 - - static func updateCurrentController(_ controller: SquirrelInputController) { - currentController = controller - NSApp.SquirrelAppDelegate.panel.inputController = controller - NSApp.SquirrelAppDelegate.panel.IbeamRect = .zero - let appearanceName: NSAppearance.Name = controller.viewEffectiveAppearance.bestMatch(from: [.aqua, .darkAqua])! - let style: SquirrelStyle = appearanceName == .darkAqua ? .dark : .light - NSApp.SquirrelAppDelegate.panel.style = style - } - - override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { + @MainActor override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { // print("init(server:delegate:client:)") super.init(server: server, delegate: delegate, client: inputClient) - observation = observe(\.viewEffectiveAppearance, options: [.new, .initial], changeHandler: { object, change in - let appearanceName: NSAppearance.Name = change.newValue!.bestMatch(from: [.aqua, .darkAqua])! - let style: SquirrelStyle = appearanceName == .darkAqua ? .dark : .light - NSApp.SquirrelAppDelegate.panel.style = style - }) createSession() } - override func activateServer(_ sender: Any!) { + @MainActor override func activateServer(_ sender: Any!) { // print("activateServer:") - Self.updateCurrentController(self) - let baseConfig = SquirrelConfig("squirrel") - if let keyboardLayout: String = baseConfig.string(forOption: "keyboard_layout") { - if keyboardLayout.caseInsensitiveCompare("last") == .orderedSame || keyboardLayout.isEmpty { - // do nothing - } else if keyboardLayout.caseInsensitiveCompare("default") == .orderedSame { - client().overrideKeyboard(withKeyboardNamed: "com.apple.keylayout.ABC") - } else if !keyboardLayout.hasPrefix("com.apple.keylayout.") { - client().overrideKeyboard(withKeyboardNamed: "com.apple.keylayout." + keyboardLayout) + super.activateServer(sender) + lastModifiers = [] + lastEventCount = 0 + candidateTexts = [] + candidateComments = [] + NSApp.SquirrelAppDelegate.panel.inputController = self + NSApp.SquirrelAppDelegate.panel.IbeamRect = .zero + + let baseConfig: SquirrelConfig = .init(.base) + if let keyboardLayout: String = baseConfig.string(for: "keyboard_layout") { + switch keyboardLayout { + case "last", "": break + case "default": client().overrideKeyboard(withKeyboardNamed: "com.apple.keylayout.ABC") + case let k where k.hasPrefix("com.apple.keylayout."): client().overrideKeyboard(withKeyboardNamed: k) + default: client().overrideKeyboard(withKeyboardNamed: "com.apple.keylayout." + keyboardLayout) } } baseConfig.close() - let defaultConfig = SquirrelConfig("default") - if defaultConfig.hasSection("ascii_composer") { - goodOldCapsLock = defaultConfig.boolValue(forOption: "ascii_composer/good_old_caps_lock") - } - defaultConfig.close() if !NSApp.SquirrelAppDelegate.isCurrentInputMethod { NSApp.SquirrelAppDelegate.isCurrentInputMethod = true if NSApp.SquirrelAppDelegate.showNotifications == .always { showInitialStatus() } } - lastModifiers = [] - lastEventCount = 0 - super.activateServer(sender) } - override func deactivateServer(_ sender: Any!) { + @MainActor override func deactivateServer(_ sender: Any!) { // print("deactivateServer:") Self.asciiMode = RimeApi.get_option(session, "ascii_mode") commitComposition(sender) super.deactivateServer(sender) } - /* Receive incoming event - Return `true` to indicate the the key input was received and dealt with. - Key processing will not continue in that case. In other words the - system will not deliver a key down event to the application. - Returning `false` means the original key down will be passed on to the client. */ - override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { +/** - Receive incoming event: + - Return `true` to indicate the the key input was received and dealt with. + Key processing will not continue in that case. In other words, + the system will not deliver a key-down event to the application. + - Returning `false` means the original key down will be passed on to the client. */ + @MainActor override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { autoreleasepool { if session == 0 || !RimeApi.find_session(session) { createSession() - if session == 0 { - return false - } + guard session != 0 else { return false } } var handled: Bool = false let modifiers: NSEvent.ModifierFlags = event.modifierFlags - var rimeModifiers = RimeModifiers(macModifiers: modifiers) - let keyCode = Int(event.cgEvent!.getIntegerValueField(.keyboardEventKeycode)) + var rimeModifiers: RimeModifiers = .init(macModifiers: modifiers) switch event.type { case .flagsChanged: - if lastModifiers == modifiers { - return true - } + guard lastModifiers != modifiers else { return true } // print("FLAGSCHANGED client: \(sender!), modifiers: 0x\(modifiers.rawValue)") - let rimeKeycode = RimeKeycode(macKeycode: keyCode) + let keyCode: Int = Int(event.cgEvent!.getIntegerValueField(.keyboardEventKeycode)) + let rimeKeycode: RimeKeycode = .init(macKeycode: keyCode) let eventCountTypes: [CGEventType] = [.flagsChanged, .keyDown, .leftMouseDown, .rightMouseDown, .otherMouseDown] - let eventCount = eventCountTypes.reduce(0) { $0 + CGEventSource.counterForEventType(.combinedSessionState, eventType: $1) } + let eventCount: CUnsignedInt = eventCountTypes.reduce(0, { $0 + CGEventSource.counterForEventType(.combinedSessionState, eventType: $1) }) lastModifiers = modifiers switch keyCode { case kVK_CapsLock: - if !goodOldCapsLock { - set_CapsLock_LED_state(target_state: false) + if !Self.goodOldCapsLock { + updateCapsLockLEDState(targetState: false) if RimeApi.get_option(session, "ascii_mode") { rimeModifiers.insert(.Lock) } else { - rimeModifiers.subtract(.Lock) + rimeModifiers.remove(.Lock) } } else { rimeModifiers.formSymmetricDifference(.Lock) + if #available(macOS 14.0, *) { // avoid overlapping with capslock accessory view + NSApp.SquirrelAppDelegate.panel.IbeamRect = .zero + } } handled = processKey(rimeKeycode, modifiers: rimeModifiers) case kVK_Shift, kVK_RightShift: @@ -158,7 +135,7 @@ final class SquirrelInputController: IMKInputController { if eventCount - lastEventCount != 1 { rimeModifiers.insert(.Ignored) } handled = processKey(rimeKeycode, modifiers: rimeModifiers) case kVK_Option, kVK_RightOption: - if modifiers == .option && NSApp.SquirrelAppDelegate.panel.showToolTip() { + if modifiers == .option, NSApp.SquirrelAppDelegate.panel.showToolTip() { lastEventCount = eventCount return true } @@ -176,102 +153,96 @@ final class SquirrelInputController: IMKInputController { default: return false } - if NSApp.SquirrelAppDelegate.panel.hasStatusMessage || handled { + if NSApp.SquirrelAppDelegate.panel.statusMessage != nil || handled { rimeUpdate() handled = true } lastEventCount = eventCount case .keyDown: + let keyCode: Int = Int(event.keyCode) // print("KEYDOWN client: \(sender), modifiers: \(modifiers), keyCode: \(keyCode)") // translate osx keyevents to rime keyevents - var rime_keycode = RimeKeycode(macKeycode: keyCode) + var rime_keycode: RimeKeycode = .init(macKeycode: keyCode) if rime_keycode == .XK_VoidSymbol { let keyChars: String = (modifiers.contains(.shift) && modifiers.isDisjoint(with: [.control, .option]) ? event.characters! : event.charactersIgnoringModifiers!).precomposedStringWithCanonicalMapping - rime_keycode = RimeKeycode(keychar: keyChars.utf16[keyChars.utf16.startIndex], shift: modifiers.contains(.shift), caps: modifiers.contains(.capsLock)) - } else if (0x60 <= keyCode && keyCode <= 0xFF) || keyCode == 0x50 || keyCode == 0x4F || keyCode == 0x47 || keyCode == 0x40 { + rime_keycode = RimeKeycode(keychar: keyChars[0], shift: modifiers.contains(.shift), caps: modifiers.contains(.capsLock)) + } else if Set(0x60 ... 0xFF).union([0x40, 0x47, 0x4F, 0x50]).contains(keyCode) { // revert non-modifier function keys' FunctionKeyMask (FwdDel, Navigations, F1..F19) - rimeModifiers.subtract(.Hyper) + rimeModifiers.remove(.Hyper) } if rime_keycode != .XK_VoidSymbol { handled = processKey(rime_keycode, modifiers: rimeModifiers) if handled { rimeUpdate() - } else if panellessCommitFix && client().markedRange().length > 0 { - if rime_keycode == .XK_Delete || (rime_keycode >= .XK_Home && rime_keycode <= .XK_KP_Delete) || - (rime_keycode >= .XK_BackSpace && rime_keycode <= .XK_Escape) { + } else if panellessCommitFix, client().markedRange().length > 0 { + if Set(.XK_Home ... .XK_KP_Delete).union(.XK_BackSpace ... .XK_Escape).union([.XK_Delete]).contains(rime_keycode) { showPlaceholder("") - } else if modifiers.isDisjoint(with: [.control, .command]) && !event.characters!.isEmpty { + } else if modifiers.isDisjoint(with: [.control, .command]), !event.characters!.isEmpty { showPlaceholder(nil) client().insertText(event.characters, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) return true } } } - default: - break + default: break } return handled } } - override func mouseDown(onCharacterIndex index: Int, coordinate point: NSPoint, withModifier flags: Int, continueTracking keepTracking: UnsafeMutablePointer!, client sender: Any!) -> Bool { + @MainActor 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 composedString = composedString, !composedString.isEmpty, inlineCaretPos != index, NSEvent.ModifierFlags.KeyEventFlags(flags).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: NSPoint = client().attributes(forCharacterIndex: 0, lineHeightRectangle: nil)["IMKBaseline"] as! NSPoint + let tail: NSPoint = 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) } return true } - private func processKey(_ keycode: RimeKeycode, modifiers: RimeModifiers) -> Bool { - let panel = NSApp.SquirrelAppDelegate.panel + @MainActor private func processKey(_ keycode: RimeKeycode, modifiers: RimeModifiers) -> Bool { + let panel: SquirrelPanel = NSApp.SquirrelAppDelegate.panel // with linear candidate list, arrow keys may behave differently. - let isLinear = panel.isLinear + let isLinear: Bool = panel.isLinear if isLinear != RimeApi.get_option(session, "_linear") { RimeApi.set_option(session, "_linear", isLinear) } // with vertical text, arrow keys may behave differently. - let isVertical = panel.isVertical + let isVertical: Bool = panel.isVertical if isVertical != RimeApi.get_option(session, "_vertical") { RimeApi.set_option(session, "_vertical", isVertical) } - 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) + let isNavigatorInTabular: Bool = panel.isTabular && modifiers.isEmpty && panel.isVisible && (isVertical ? Set([.XK_Left, .XK_KP_Left, .XK_Right, .XK_KP_Right]).contains(keycode) : Set([.XK_Up, .XK_KP_Up, .XK_Down, .XK_KP_Down]).contains(keycode)) 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))!) { - if !panel.isLocked && !panel.isExpanded && keycode == (isVertical ? .XK_Left : .XK_Down) { + if let newIndex: Int = panel.candidateIndex(onDirection: SquirrelIndex(rawValue: Int(keycode.rawValue))!) { + if !panel.isLocked, !panel.isExpanded, keycode == (isVertical ? .XK_Left : .XK_Down) { panel.isExpanded = true } _ = RimeApi.highlight_candidate(session, newIndex) return true - } else if !panel.isLocked && panel.isExpanded && panel.sectionNum == 0 && keycode == (isVertical ? .XK_Right : .XK_Up) { + } else if !panel.isLocked, panel.isExpanded, panel.sectionNum == 0, keycode == (isVertical ? .XK_Right : .XK_Up) { panel.isExpanded = false return true } } - let handled = RimeApi.process_key(session, keycode.rawValue, modifiers.rawValue) + let handled: Bool = RimeApi.process_key(session, keycode.rawValue, modifiers.rawValue) // print("rime_keycode: \(rime_keycode), rime_modifiers: \(rime_modifiers), handled = \(handled)") if !handled { - let isVimBackInCommandMode: Bool = keycode == .XK_Escape || (modifiers.contains(.Control) && (keycode == .XK_c || keycode == .XK_C || keycode == .XK_bracketleft)) - if isVimBackInCommandMode && RimeApi.get_option(session, "vim_mode") && - !RimeApi.get_option(session, "ascii_mode") { + let isVimBackInCommandMode: Bool = keycode == .XK_Escape || (modifiers.contains(.Control) && Set([.XK_c, .XK_C, .XK_bracketleft]).contains(keycode)) + if isVimBackInCommandMode, RimeApi.get_option(session, "vim_mode"), !RimeApi.get_option(session, "ascii_mode") { cancelComposition() RimeApi.set_option(session, "ascii_mode", true) // print("turned Chinese mode off in vim-like editor's command mode") @@ -281,11 +252,10 @@ 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 - if isChordingKey && RimeApi.get_option(session, "_chord_typing") { + let isChordingKey: Bool = Set(.XK_space ... .XK_asciitilde).union([.XK_Control_L, .XK_Control_R, .XK_Alt_L, .XK_Alt_R, .XK_Shift_L, .XK_Shift_R]).contains(keycode) + if isChordingKey, RimeApi.get_option(session, "_chord_typing") { updateChord(keycode, modifiers: modifiers) - } else if modifiers.isDisjoint(with: .Release) { - // non-chording key pressed + } else if modifiers.isDisjoint(with: .Release) { // non-chording key pressed clearChord() } } @@ -293,41 +263,35 @@ final class SquirrelInputController: IMKInputController { return handled } - func moveCursor(_ cursorPosition: Int, to targetPosition: Int, inlinePreedit: Bool, inlineCandidate: Bool) { + @MainActor func moveCursor(_ cursorPosition: Int, to targetPosition: Int, inlinePreedit: Bool, inlineCandidate: Bool) { let isVertical: Bool = NSApp.SquirrelAppDelegate.panel.isVertical autoreleasepool { 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 .BackSpaceKey ... .EndKey ~= index { 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 { @@ -359,85 +323,80 @@ final class SquirrelInputController: IMKInputController { } } - private func onChordTimer() { + @MainActor @objc 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 processedKeyCount: Int = 0 + if !chordKeyCombos.isEmpty, session != 0 { + chordKeyCombos.forEach { if RimeApi.process_key(session, $0.keycode.rawValue, $0.modifiers.union(.Release).rawValue) { processedKeyCount += 1 } } } clearChord() - if processed_keys > 0 { + if processedKeyCount > 0 { rimeUpdate() } } - private func updateChord(_ keycode: RimeKeycode, modifiers: RimeModifiers) { + @MainActor private func updateChord(_ keycode: RimeKeycode, modifiers: RimeModifiers) { // print("update chord: {\(_chord)} << \(keycode)") - for i in 0 ..< chordKeyCount { - if chordKeyCodes[i] == keycode { return } - } - if chordKeyCount >= kNumKeyRollOver { - // you are cheating. only one human typist (fingers <= 10) is supported. - return + for (k, _) in chordKeyCombos { + if k == keycode { 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(timeInterval: Self.chordDuration, target: self, selector: #selector(onChordTimer), userInfo: nil, repeats: false) } 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() { + @MainActor 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 = .init(cString: status.schema_name ?? status.schema_id) + var options: [String] = [] + if let asciiMode: String = getOptionLabel(session: session, option: "ascii_mode", state: status.is_ascii_mode) { + options.append(asciiMode) + } + if let fullShape: String = getOptionLabel(session: session, option: "full_shape", state: status.is_full_shape) { + options.append(fullShape) + } + if let asciiPunct: String = getOptionLabel(session: session, option: "ascii_punct", state: status.is_ascii_punct) { + options.append(asciiPunct) + } + _ = RimeApi.free_status(&status) + let foldedOptions: String = 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, *) { + showingInitialStatus = true } + Timer.scheduledTimer(timeInterval: Self.kStatusDelay, target: self, selector: #selector(onDelayedStatus), userInfo: nil, repeats: false) } - override func commitComposition(_ sender: Any!) { + @MainActor @objc func onDelayedStatus() { + rimeUpdate() + if #available(macOS 14.0, *) { + showingInitialStatus = false + NSApp.SquirrelAppDelegate.panel.IbeamRect = .zero + } + } + + @MainActor 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() } - private func clearBuffer() { + @MainActor private func clearBuffer() { NSApp.SquirrelAppDelegate.panel.IbeamRect = .zero inlineString = nil originalString = nil @@ -446,36 +405,36 @@ final class SquirrelInputController: IMKInputController { // Though we specify AppDelegate as the menu action receiver, Inputcontroller // is the one that actually receives the event. Here we relay these messages. - @objc private func showSwitcher(_ sender: Any?) { + @MainActor @objc private func showSwitcher(_ sender: Any?) { NSApp.SquirrelAppDelegate.showSwitcher(session) rimeUpdate() } - @objc private func deploy(_ sender: Any?) { + @MainActor @objc private func deploy(_ sender: Any?) { NSApp.SquirrelAppDelegate.deploy(sender) } - @objc private func syncUserData(_ sender: Any?) { + @MainActor @objc private func syncUserData(_ sender: Any?) { NSApp.SquirrelAppDelegate.syncUserData(sender) } - @objc private func configure(_ sender: Any?) { + @MainActor @objc private func configure(_ sender: Any?) { NSApp.SquirrelAppDelegate.configure(sender) } - @objc private func checkForUpdates(_ sender: Any?) { + @MainActor @objc private func checkForUpdates(_ sender: Any?) { NSApp.SquirrelAppDelegate.checkForUpdates(sender) } - @objc private func openWiki(_ sender: Any?) { + @MainActor @objc private func openWiki(_ sender: Any?) { NSApp.SquirrelAppDelegate.openWiki(sender) } - @objc private func openLogFolder(_ sender: Any?) { + @MainActor @objc private func openLogFolder(_ sender: Any?) { NSApp.SquirrelAppDelegate.openLogFolder(sender) } - override func menu() -> NSMenu { + @MainActor override func menu() -> NSMenu { return NSApp.SquirrelAppDelegate.menu } @@ -491,7 +450,7 @@ final class SquirrelInputController: IMKInputController { return Array(candidateTexts[candidateIndices]) } - override func hidePalettes() { + @MainActor override func hidePalettes() { NSApp.SquirrelAppDelegate.panel.hide() super.hidePalettes() } @@ -499,29 +458,22 @@ final class SquirrelInputController: IMKInputController { deinit { // print("deinit") destroySession() - clearBuffer() } - override func selectionRange() -> NSRange { - return NSRange(location: inlineCaretPos, length: 0) - } + override func selectionRange() -> NSRange { NSRange(location: inlineCaretPos, length: 0) } - override func replacementRange() -> NSRange { - return NSRange(location: NSNotFound, length: NSNotFound) - } + override func replacementRange() -> NSRange { NSRange(location: NSNotFound, length: NSNotFound) } - private func commitString(_ string: Any!) { + @MainActor private func commitString(_ string: Any!) { // print("commitString:") client().insertText(string, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) clearBuffer() } - override func cancelComposition() { + @MainActor 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 +481,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: [NSAttributedString.Key : Any] = mark(forStyle: kTSMHiliteSelectedRawText, at: NSRange(location: 0, length: placeholder?.length ?? 1)) as! [NSAttributedString.Key : Any] inlineString = NSMutableAttributedString(string: placeholder ?? "█", attributes: attrs) inlineCaretPos = 0 updateComposition() @@ -537,108 +489,111 @@ final class SquirrelInputController: IMKInputController { private func showInlineString(_ string: String, withSelRange selRange: Range, caretPos: Int) { // print("showPreeditString: '\(preedit)'") - if caretPos == inlineCaretPos && selRange == inlineSelRange && string == inlineString?.string { - return - } + if caretPos == inlineCaretPos, selRange == inlineSelRange, string == inlineString?.string { return } 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: [NSAttributedString.Key : Any] = 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() } - private func getIbeamRect() -> NSRect { + @MainActor private func getIbeamRect() -> NSRect { var IbeamRect: NSRect = .zero client().attributes(forCharacterIndex: 0, lineHeightRectangle: &IbeamRect) - if IbeamRect.isEmpty && inlineString?.length == 0 { - if client().selectedRange().length == 0 { + if IbeamRect.isEmpty { + let selectedRange: NSRange = client().selectedRange() + if selectedRange.length == 0 { // activate inline session, in e.g. table cells, by fake inputs client().setMarkedText(" ", selectionRange: NSRange(location: 0, length: 0), replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) client().attributes(forCharacterIndex: 0, lineHeightRectangle: &IbeamRect) client().setMarkedText("", selectionRange: NSRange(location: 0, length: 0), replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) } else { - client().attributes(forCharacterIndex: client().selectedRange().location, lineHeightRectangle: &IbeamRect) + IbeamRect = client().firstRect(forCharacterRange: selectedRange, actualRange: nil) } } if IbeamRect.isEmpty { - return .init(origin: NSEvent.mouseLocation, size: .zero) + return NSRect(origin: NSEvent.mouseLocation, size: .zero) + } + let sweepVertical: Bool = IbeamRect.width > IbeamRect.height + if inlineOffset.isNormal { + IbeamRect = IbeamRect.offsetBy(dx: sweepVertical ? inlineOffset : .zero, dy: sweepVertical ? .zero : inlineOffset) } - if IbeamRect.width > IbeamRect.height { - IbeamRect.origin.x += CGFloat(inlineOffset) + // avoid overlapping with cursor effects view + guard #available(macOS 14.0, *), (Self.goodOldCapsLock && lastModifiers.contains(.capsLock)) || showingInitialStatus else { return IbeamRect } + let screenRect: NSRect = NSScreen.main!.visibleFrame + if sweepVertical { + var capslockAccessory: NSRect = .init(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 { - 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) - } - } + var capslockAccessory: NSRect = .init(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 } - private func showPanel(withPreedit preedit: String, selRange: NSRange, caretPos: Int?, candidateIndices: Range, highlightedCandidate: Int?, pageNum: Int, isLastPage: Bool, didCompose: Bool) { + @MainActor private func showPanel(withPreedit preedit: String, selRange: NSRange, caretPos: Int?, candidateIndices: Range, highlightedCandidate: Int?, pageNum: Int, isLastPage: Bool, didCompose: Bool) { // print("showPanelWithPreedit:...:") - let panel: SquirrelPanel! = NSApp.SquirrelAppDelegate.panel - panel.IbeamRect = getIbeamRect() - if panel.IbeamRect.isEmpty && panel.hasStatusMessage { + let panel: SquirrelPanel = NSApp.SquirrelAppDelegate.panel + if panel.IbeamRect == .zero { + panel.IbeamRect = getIbeamRect() + if #available(macOS 10.14, *) { panel.style = style } + } + if panel.IbeamRect.isEmpty, panel.statusMessage != nil { panel.updateStatus(long: nil, short: nil) } else { panel.showPanel(withPreedit: preedit, selRange: selRange, caretPos: caretPos, candidateIndices: candidateIndices, highlightedCandidate: highlightedCandidate, pageNum: pageNum, isLastPage: isLastPage, didCompose: didCompose) } } - // MARK: Private functions + // MARK: Functions communicating with Rime - private func createSession() { + @MainActor private func createSession() { let app: String = client().bundleIdentifier() // print("createSession: \(app)") + let panel: SquirrelPanel = NSApp.SquirrelAppDelegate.panel + schemaId = panel.optionSwitcher.schemaId 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() + guard session != 0 else { return } + // retrieve app-specific options + let config: SquirrelConfig = .init(.base) + appOptions = config.appOptions(for: app) + config.close() + inlinePreedit = (panel.inlinePreedit && !appOptions["no_inline", as: Bool.self]) || appOptions["inline", as: Bool.self] + inlineCandidate = panel.inlineCandidate && !appOptions["no_inline", as: Bool.self] + RimeApi.set_option(session, "soft_cursor", !inlinePreedit) + panellessCommitFix = appOptions["panelless_commit_fix", as: Bool.self] + inlinePlaceholder = appOptions["inline_placeholder", as: Bool.self] + inlineOffset = appOptions["inline_offset", as: Double.self] + // restore ascii mode if client app has not changed + if let asciiMode = Self.asciiMode, app == Self.currentApp, asciiMode != RimeApi.get_option(session, "ascii_mode") { + RimeApi.set_option(session, "ascii_mode", asciiMode) + } + Self.currentApp = app + Self.asciiMode = nil + if !inlinePreedit, !inlineCandidate, !inlinePlaceholder, client().selectedRange().length == 0 { + showInlineString(" ", withSelRange: 1 ..< 1, caretPos: 1) + showInlineString("", withSelRange: 0 ..< 0, caretPos: 0) } + rimeUpdate() } private func destroySession() { @@ -650,42 +605,40 @@ final class SquirrelInputController: IMKInputController { clearChord() } - private func rimeConsumeCommittedText() -> Bool { + @MainActor private func rimeConsumeCommittedText() -> Bool { var commit: RimeCommit = RimeStructInit() - if RimeApi.get_commit(session, &commit) { - let commitText = String(cString: commit.text!) - if panellessCommitFix { - showPlaceholder(commitText) - commitString(commitText) - showPlaceholder(commitText.utf8.count == 1 ? "" : nil) - } else { - commitString(commitText) - showPlaceholder("") - } - var _ = RimeApi.free_commit(&commit) - return true + guard RimeApi.get_commit(session, &commit) else { return false } + let commitText: String = .init(cString: commit.text) + if panellessCommitFix { + showPlaceholder(commitText) + commitString(commitText) + showPlaceholder(commitText.utf8.count == 1 ? "" : nil) + } else { + commitString(commitText) + showPlaceholder("") } - return false + _ = RimeApi.free_commit(&commit) + return true } - private func rimeUpdate() { + @MainActor @objc private func rimeUpdate() { // print("rimeUpdate") let didCommit: Bool = rimeConsumeCommittedText() var didCompose: Bool = didCommit - let panel = NSApp.SquirrelAppDelegate.panel + let panel: SquirrelPanel = NSApp.SquirrelAppDelegate.panel 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["no_inline", as: Bool.self]) || appOptions["inline", as: Bool.self] + inlineCandidate = panel.inlineCandidate && !appOptions["no_inline", as: Bool.self] // if not inline, embed soft cursor in preedit string RimeApi.set_option(session, "soft_cursor", !inlinePreedit) } else { @@ -698,32 +651,30 @@ final class SquirrelInputController: IMKInputController { var ctx: RimeContext_stdbool = RimeStructInit() if RimeApi.get_context(session, &ctx) { - let showingStatus: Bool = panel.hasStatusMessage + let showingStatus: Bool = panel.statusMessage != nil // update preedit text - let preedit: UnsafeMutablePointer? = ctx.composition.preedit - let preeditText = preedit == nil ? "" : String(cString: preedit!) + let preedit: UnsafeMutablePointer! = ctx.composition.preedit + let preeditText: String = preedit == nil ? "" : String(cString: preedit) // 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 + let originalString: String = raw_input == nil ? "" : String(cString: raw_input!) + 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: Int = preeditText[start...].replacingOccurrences(of: " ", with: "").length + let selLength: Int = 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 - if panel.isTabular && !showingStatus { + // already processed the action if `currentIndex` == nil + if panel.isTabular, !showingStatus { if numCandidates == 0 || didCompose { panel.sectionNum = 0 } else if currentIndex != nil { let currentPageNum: Int = currentIndex! / pageSize - if !panel.isLocked && panel.isExpanded && panel.isFirstLine && pageNum == 0 && hilitedCandidate == 0 && currentIndex == 0 { + if !panel.isLocked, panel.isExpanded, panel.isFirstLine, pageNum == 0, hilitedCandidate == 0, currentIndex == 0 { panel.isExpanded = false - } else if !panel.isLocked && !panel.isExpanded && pageNum > currentPageNum { + } else if !panel.isLocked, !panel.isExpanded, pageNum > currentPageNum { panel.isExpanded = true } - if panel.isExpanded && pageNum > currentPageNum && panel.sectionNum < (panel.isVertical ? 2 : 4) { + if panel.isExpanded, pageNum > currentPageNum, panel.sectionNum < (panel.isVertical ? 2 : 4) { panel.sectionNum = min(panel.sectionNum + pageNum - currentPageNum, (isLastPage ? 4 : 3) - (panel.isVertical ? 2 : 0)) - } else if panel.isExpanded && pageNum < currentPageNum && panel.sectionNum > 0 { + } else if panel.isExpanded, pageNum < currentPageNum, panel.sectionNum > 0 { panel.sectionNum = max(panel.sectionNum + pageNum - currentPageNum, pageNum == 0 ? 0 : 1) } } - hilitedCandidate = hilitedCandidate == nil ? nil : hilitedCandidate! + pageSize * panel.sectionNum + hilitedCandidate? += pageSize * panel.sectionNum } let extraCandidates: Int = panel.isExpanded ? (isLastPage ? panel.sectionNum : (panel.isVertical ? 2 : 4)) * pageSize : 0 let indexStart: Int = (pageNum - panel.sectionNum) * pageSize 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!) + let candidatePreview: UnsafeMutablePointer! = ctx.commit_text_preview + var candidatePreviewText: String = candidatePreview == nil ? "" : String(cString: candidatePreview) if inlinePreedit { - if end <= caretPos && caretPos < length { - candidatePreviewText += String(preeditText[String.Index(utf16Offset: caretPos, in: preeditText)...]) + if end <= caretPos, caretPos < length { + 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) + if inlinePlaceholder, preeditText.isEmpty, numCandidates > 0 { + showPlaceholder(String.FullWidthSpace) } else if !didCommit || !preeditText.isEmpty { showInlineString(preeditText, withSelRange: start ..< end, caretPos: caretPos) } } else { - if inlinePlaceholder && preedit != nil { - showPlaceholder(kFullWidthSpace) + if inlinePlaceholder, preedit != nil { + showPlaceholder(String.FullWidthSpace) } else if !didCommit || preedit != nil { showInlineString("", withSelRange: 0 ..< 0, caretPos: 0) } @@ -816,9 +761,9 @@ final class SquirrelInputController: IMKInputController { var endIndex: Int = pageSize * pageNum // cache candidates if index < endIndex { - var iterator = RimeCandidateListIterator() + var iterator: RimeCandidateListIterator = .init() if RimeApi.candidate_list_from_index(session, &iterator, CInt(index)) { - while index < endIndex && RimeApi.candidate_list_next(&iterator) { + while index < endIndex, RimeApi.candidate_list_next(&iterator) { updateCandidate(iterator.candidate, at: index) index += 1 } @@ -833,9 +778,9 @@ final class SquirrelInputController: IMKInputController { } endIndex = candidateIndices.upperBound if index < endIndex { - var iterator = RimeCandidateListIterator() + var iterator: RimeCandidateListIterator = .init() if RimeApi.candidate_list_from_index(session, &iterator, CInt(index)) { - while index < endIndex && RimeApi.candidate_list_next(&iterator) { + while index < endIndex, RimeApi.candidate_list_next(&iterator) { updateCandidate(iterator.candidate, at: index) index += 1 } @@ -852,58 +797,59 @@ final class SquirrelInputController: IMKInputController { } private func updateCandidate(_ candidate: RimeCandidate?, at index: Int) { - if candidate == nil || index > candidateTexts.count { - if index < candidateTexts.count { - let remove: Range = index ..< candidateTexts.count - candidateTexts.removeSubrange(remove) - candidateComments.removeSubrange(remove) + guard let candidate = candidate, 0 ... candidateTexts.count ~= index else { + if 0 ..< candidateTexts.count ~= index { + candidateTexts.removeSubrange(index...) + candidateComments.removeSubrange(index...) } return } - let text = String(cString: candidate!.text) - let comment = candidate!.comment == nil ? "" : String(cString: candidate!.comment!) if index == candidateTexts.count { - candidateTexts.append(text) - candidateComments.append(comment) + candidateTexts.append(String(cString: candidate.text)) + candidateComments.append(candidate.comment == nil ? "" : String(cString: candidate.comment)) } else { - if text != candidateTexts[index] { - candidateTexts[index] = text + if strcmp(candidate.text, candidateTexts[index]) != 0 { + candidateTexts[index] = String(cString: candidate.text) } - if comment != candidateComments[index] { - candidateComments[index] = comment + if strcmp(candidate.comment, candidateComments[index]) != 0 { + candidateComments[index] = candidate.comment == nil ? "" : String(cString: candidate.comment) } } } -} +} // SquirrelInputController -private func set_CapsLock_LED_state(target_state: CBool) { - 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) +private func updateCapsLockLEDState(targetState: Bool) { + let ioService: IOAlignment = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching(kIOHIDSystemClass)) + var ioConnect: IOAlignment = 0 + IOServiceOpen(ioService, mach_host_self(), CUnsignedInt(kIOHIDParamConnectType), &ioConnect) + 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) { - return String(cString: 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)]) + let label: String? = labelLong.str == nil ? nil : String(cString: labelLong.str) + return label == nil ? nil : String(label!.first!) } } +extension NSEvent.ModifierFlags { + static func KeyEventFlags(_ flags: Int) -> Self { .init(rawValue: UInt(flags)).intersection(.deviceIndependentFlagsMask) } +} + @frozen enum SquirrelAction: Sendable { - case PROCESS, SELECT, HIGHLIGHT, DELETE + case Process, Select, Highlight, Delete } -enum SquirrelIndex: RawRepresentable, Sendable { +@frozen enum SquirrelIndex: RawRepresentable, Sendable, Strideable, Hashable { // 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 +871,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,53 +886,56 @@ 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 .BackSpaceKey: return 0xFF08 - case .EscapeKey: return 0xFF1B - case .CodeInputArea: return 0xFF37 - case .HomeKey: return 0xFF50 - case .LeftKey: return 0xFF51 - case .UpKey: return 0xFF52 - case .RightKey: return 0xFF53 - case .DownKey: return 0xFF54 - case .PageUpKey: return 0xFF55 - case .PageDownKey: return 0xFF56 - case .EndKey: return 0xFF57 - case .ExpandButton: return 0xFF04 - case .CompressButton: return 0xFF05 - case .LockButton: return 0xFF06 - 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)! - } - } - return .VoidSymbol - } + return switch self { + case .Ordinal(let num): num + case .BackSpaceKey: 0xFF08 + case .EscapeKey: 0xFF1B + case .CodeInputArea: 0xFF37 + case .HomeKey: 0xFF50 + case .LeftKey: 0xFF51 + case .UpKey: 0xFF52 + case .RightKey: 0xFF53 + case .DownKey: 0xFF54 + case .PageUpKey: 0xFF55 + case .PageDownKey: 0xFF56 + case .EndKey: 0xFF57 + case .ExpandButton: 0xFF04 + case .CompressButton: 0xFF05 + case .LockButton: 0xFF06 + case .VoidSymbol: 0xFFFFFF + } + } + var isOrdinal: Bool { 0x0 ... 0xFFF ~= rawValue } + + 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) -> Bool { lhs.rawValue <= rhs.rawValue } + 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) -> Bool { lhs.rawValue != rhs.rawValue } + static func < (lhs: Self, rhs: Int) -> Bool { lhs.rawValue < rhs } + static func > (lhs: Self, rhs: Int) -> Bool { lhs.rawValue > rhs } + static func <= (lhs: Self, rhs: Int) -> Bool { lhs.rawValue <= rhs } + static func >= (lhs: Self, rhs: Int) -> Bool { lhs.rawValue >= rhs } + static func == (lhs: Self, rhs: Int!) -> Bool { lhs.rawValue == (rhs ?? 0xFFFFFF) } + static func != (lhs: Self, rhs: Int!) -> Bool { lhs.rawValue != (rhs ?? 0xFFFFFF) } + static func + (lhs: Self, rhs: Int) -> Self { + guard lhs.isOrdinal, let result: Self = .init(rawValue: lhs.rawValue + rhs), result.isOrdinal else { return .VoidSymbol } + return result + } + + typealias Stride = Int + func distance(to other: SquirrelIndex) -> Int { other.rawValue - rawValue } + func advanced(by n: Int) -> SquirrelIndex { .init(rawValue: rawValue + n) ?? .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..6ae7aa08f 100644 --- a/sources/SquirrelInputSource.swift +++ b/sources/SquirrelInputSource.swift @@ -1,158 +1,143 @@ import Carbon import Foundation -struct RimeInputModes: OptionSet, Sendable { +struct RimeInputModes: OptionSet, Sendable, Hashable { let rawValue: CInt - static let DEFAULT = RimeInputModes(rawValue: 1 << 0) - static let HANS = RimeInputModes(rawValue: 1 << 0) - static let HANT = RimeInputModes(rawValue: 1 << 1) - static let CANT = RimeInputModes(rawValue: 1 << 2) - init(rawValue: CInt) { - self.rawValue = rawValue - } + static let DEFAULT: Self = .init(rawValue: 1 << 0) + static let HANS: Self = .init(rawValue: 1 << 0) + static let HANT: Self = .init(rawValue: 1 << 1) + static let CANT: Self = .init(rawValue: 1 << 2) + + init(rawValue: CInt) { self.rawValue = rawValue } 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: Sendable { + static private let property: CFDictionary = [kTISPropertyBundleID : bundleId] as CFDictionary + static private let InputModeIDHans: String = "\(bundleId).Hans" + static private let InputModeIDHant: String = "\(bundleId).Hant" + static private let InputModeIDCant: String = "\(bundleId).Cant" + static private let preferences: [String] = 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 registerError = TISRegisterInputSource(bundlePath) + let bundlePath: NSURL = .init(fileURLWithPath: "/Library/Input Methods/Squirrel.App", isDirectory: false) + let registerError: OSStatus = 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 if inputModesToEnable.isEmpty { if !preferences.isEmpty { - if preferences[0].caseInsensitiveCompare("zh-Hans") == .orderedSame { - inputModesToEnable.insert(.HANS) - } else if preferences[0].caseInsensitiveCompare("zh-Hant") == .orderedSame { - inputModesToEnable.insert(.HANT) - } else if preferences[0].caseInsensitiveCompare("zh-HK") == .orderedSame { - inputModesToEnable.insert(.CANT) + inputModesToEnable = switch preferences[0] { + case "zh-Hans": [.HANS] + case "zh-Hant": [.HANT] + case "zh-HK": [.CANT] + default: [] } } else { inputModesToEnable = [.HANS] } } - let sourceList = TISCreateInputSourceList(property, true).takeUnretainedValue() as! [TISInputSource] + let inputModeIDsToEnable: [String] = [(InputModeIDHans, RimeInputModes.HANS), (InputModeIDHant, RimeInputModes.HANT), (InputModeIDCant, RimeInputModes.CANT)].filter({ inputModesToEnable.contains($0.1) }).map(\.0) + let sourceList: [TISInputSource] = TISCreateInputSourceList(property, true).takeRetainedValue() 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: String = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID), as: CFString.self) as? String, inputModeIDsToEnable.contains(sourceID) 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(_ modes: RimeInputModes) { + let enabledInputModes: RimeInputModes = GetEnabledInputModes(includeAllInstalled: false) + var inputModeToSelect: RimeInputModes = modes.intersection(enabledInputModes) + if inputModeToSelect.isEmpty { for language in preferences { - if language.caseInsensitiveCompare("zh-Hans") == .orderedSame && enabledInputModes.contains(.HANS) { - inputModeToSelect = .HANS; break - } - if language.caseInsensitiveCompare("zh-Hant") == .orderedSame && enabledInputModes.contains(.HANT) { - inputModeToSelect = .HANT; break - } - if language.caseInsensitiveCompare("zh-HK") == .orderedSame && enabledInputModes.contains(.CANT) { - inputModeToSelect = .CANT; break + switch language { + case "zh-Hans": if enabledInputModes.contains(.HANS) { inputModeToSelect = .HANS; break } + case "zh-Hant": if enabledInputModes.contains(.HANT) { inputModeToSelect = .HANT; break } + case "zh-HK": if enabledInputModes.contains(.CANT) { inputModeToSelect = .CANT; break } + default: break } } } - if inputModeToSelect == nil { + if inputModeToSelect.isEmpty { print("No enabled input sources."); return } - let sourceList = TISCreateInputSourceList(property, false).takeUnretainedValue() as! [TISInputSource] + let inputModeIDToSelect: [String] = [(InputModeIDHans, RimeInputModes.HANS), (InputModeIDHant, RimeInputModes.HANT), (InputModeIDCant, RimeInputModes.CANT)].filter({ inputModeToSelect.contains($0.1) }).map(\.0) + let sourceList: [TISInputSource] = TISCreateInputSourceList(property, false).takeRetainedValue() 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: String = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID), as: CFString.self) as? String, inputModeIDToSelect.contains(sourceID) 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))") } } } static func DisableInputSource() { - let sourceList = TISCreateInputSourceList(property, false).takeUnretainedValue() as! [TISInputSource] + let sourceList: [TISInputSource] = TISCreateInputSourceList(property, false).takeRetainedValue() 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: String = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID), as: CFString.self) as? String, Set([InputModeIDHans, InputModeIDHant, InputModeIDCant]).contains(sourceID) 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: [TISInputSource] = TISCreateInputSourceList(property, includeAllInstalled).takeRetainedValue() 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: String = bridge(ptr: TISGetInputSourceProperty(source, kTISPropertyInputSourceID), as: CFString.self) as? String 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 { + var length: Int { CFStringGetLength(self) } } diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift index 21d199696..656c98010 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 =? (lhs: inout T?, rhs: T?) { if rhs != nil { lhs = rhs } } +func =? (lhs: inout T, rhs: T?) { if rhs != nil { lhs = rhs! } } -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 - } - - static func != (left: CFString, right: CFString) -> Bool { - return CFStringCompare(left, right, []) != .compareEqualTo - } +extension CharacterSet { + static let fullWidthDigits: Self = .init(charactersIn: UnicodeScalar(0xFF10)! ... UnicodeScalar(0xFF19)!) + static let fullWidthLatinCapitals: Self = .init(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 { .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } + static public func - (lhs: Self, rhs: Self) -> Self { .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) -> Self { NSIntegralRectWithOptions(self, options) } - func integral(options: AlignmentOptions) -> NSRect { return NSIntegralRectWithOptions(self, options) } + func squirclePath(cornerRadius: Double) -> CGPath? { + 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]] + return switch (head.vertices, body.vertices, tail.vertices) { + case let (h, [], []): h + case let ([], [], t): t + case let ([], b, []): b + case let (h, b, []): [h[0], h[1], b[0], b[1], b[2], h[3]] + case let ([], b, t): [b[0], t[1], t[2], t[3], b[2], b[3]] + case let (h, [], t): [h[0], h[1], t[0], t[1], t[2], t[3], h[2], h[3]] + case let (h, b, t): [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)) + [body, head, tail].contains(where: { !$0.isEmpty && NSMouseInRect(point, $0, 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) } @@ -126,253 +104,224 @@ extension SquirrelCandidateInfo { extension CGPath { static func combinePaths(_ x: CGPath?, _ y: CGPath?) -> CGPath? { - if x == nil { return y?.copy() } - if y == nil { return x?.copy() } - let path: CGMutablePath? = x!.mutableCopy() - path?.addPath(y!) + guard let x = x, let y = y else { return y?.copy() ?? x?.copy() } + let path: CGMutablePath? = x.mutableCopy() + 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 } - let path = CGMutablePath() + static func squirclePath(vertices: [CGPoint], cornerRadius: Double) -> CGMutablePath? { + guard [4, 6, 8].contains(vertices.count) else { return nil } + let path: CGMutablePath = .init() var vertex: CGPoint = vertices.last! var nextVertex: CGPoint = vertices.first! - var nextDiff = CGVector(dx: nextVertex.x - vertex.x, dy: nextVertex.y - vertex.y) + var nextDiff: CGVector = .init(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 + var endPoint: CGPoint = .init(x: vertex.x + nextDiff.dx * 0.5, y: nextVertex.y) 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) +} + +protocol ContrastingMutability { + associatedtype ImmutableType + associatedtype MutableType + func copy() -> ImmutableType + func mutableCopy() -> MutableType +} + +extension NSParagraphStyle: ContrastingMutability { + typealias ImmutableType = NSParagraphStyle + typealias MutableType = NSMutableParagraphStyle + func mutableCopy() -> MutableType { mutableCopy(with: nil) as! MutableType } + func copy() -> ImmutableType { copy(with: nil) as! ImmutableType } +} + +extension NSAttributedString: ContrastingMutability { + typealias ImmutableType = NSAttributedString + typealias MutableType = NSMutableAttributedString + func mutableCopy() -> MutableType { mutableCopy(with: nil) as! MutableType } + func copy() -> ImmutableType { copy(with: nil) as! ImmutableType } +} 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: NSFont = value as? NSFont else { return } + let newFont: NSFont = .init(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: NSFont = value as? NSFont else { return } + let newFont: NSFont = .init(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: String = "((\\*{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: NSRegularExpression = try? .init(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: NSTextCheckingResult = 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: String = "(\\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 - 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) - + guard let regex: NSRegularExpression = try? .init(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 + let baseString: NSString = 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: NSString = 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: CTRubyAnnotation = 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: NSSize = attributedSubstring(from: baseRange).size() + let rubyWidth: Double = 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) + var attrs: [NSAttributedString.Key : Any] = attributes(at: 0, effectiveRange: nil) + let font: NSFont = attrs[.font] as! NSFont + let attrString: NSAttributedString = .init(string: string, attributes: fontAttributes(in: NSRange(location: 0, length: length))) + let stringWidth: Double = 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 = .init(size: NSSize(width: height, height: height), flipped: true) { dstRect in NSGraphicsContext.saveGraphicsState() - let transform = NSAffineTransform() + let transform: NSAffineTransform = .init() + 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() + } + let attm: NSTextAttachment = .init() 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 = { let whitePoint: [CGFloat] = [0.950489, 1.0, 1.088840] let blackPoint: [CGFloat] = [0.0, 0.0, 0.0] let range: [CGFloat] = [-127.0, 127.0, -127.0, 127.0] - let colorSpaceLab = CGColorSpace(labWhitePoint: whitePoint, blackPoint: blackPoint, range: range) - return NSColorSpace(cgColorSpace: colorSpaceLab!)! + let colorSpaceLab: CGColorSpace = .init(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 components: [CGFloat] = [lum, green_red, blue_yellow, opaque] + let lum: CGFloat = lStar.clamp(min: 0.0, max: 100.0) + let greenRed: CGFloat = aStar.clamp(min: -127.0, max: 127.0) + let blueYellow: CGFloat = bStar.clamp(min: -127.0, max: 127.0) + let opaque: CGFloat = alpha.clamp(min: 0.0, max: 1.0) + let components: [CGFloat] = [lum, greenRed, blueYellow, 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: NSColor = 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] } @@ -386,28 +335,21 @@ extension NSColor { if alpha != nil { alpha = LABComponents[3] } } - @frozen enum ColorInversionExtent: Int { + @frozen enum ColorInversionExtent: Int, Sendable { case standard = 0, augmented = 1, moderate = -1 } 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: NSColor = usingType(.componentBased), let labColor: NSColor = componentBased.usingColorSpace(.labColorSpace) else { return self } + var components: [CGFloat] = [0.0, 0.0, 0.0, 1.0] + labColor.getComponents(&components) + components[0] = switch extent { + case .augmented: 100.0 - components[0] + case .moderate: 80.0 - components[0] * 0.6 + case .standard: 90.0 - components[0] * 0.8 } + let invertedColor: NSColor = .init(colorSpace: .labColorSpace, components: components, count: 4) + return invertedColor.usingColorSpace(componentBased.colorSpace)! } var hooverColor: NSColor { @@ -426,8 +368,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 +378,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,85 +391,72 @@ 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 = .init(name: name.trimmingCharacters(in: .whitespaces), size: 0.0) else { return nil } + let fontDescriptor: NSFontDescriptor = font.fontDescriptor.addingAttributes([.featureSettings : features]) + let UIFontDescriptor: NSFontDescriptor = fontDescriptor.withSymbolicTraits([.UIOptimized]) + return NSFont(descriptor: UIFontDescriptor, size: 0.0) == nil ? fontDescriptor : UIFontDescriptor + } + guard let fontDescriptor: NSFontDescriptor = 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: [NSFontDescriptor] = 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 = .init(descriptor: fallback, size: pointSize) else { continue } + let fallbackHeight: Double = asVertical ? fallbackFont.vertical.ascender - fallbackFont.vertical.descender : fallbackFont.ascender - fallbackFont.descender + lineHeight = max(lineHeight, fallbackHeight.rounded(.up)) } return lineHeight } } private func updateCandidateListLayout(isLinear: inout Bool, isTabular: inout Bool, config: SquirrelConfig, prefix: String) { - if let candidateListLayout = config.string(forOption: "\(prefix)/candidate_list_layout") { - if candidateListLayout.caseInsensitiveCompare("stacked") == .orderedSame { - isLinear = false - isTabular = false - } else if candidateListLayout.caseInsensitiveCompare("linear") == .orderedSame { - isLinear = true - isTabular = false - } else if candidateListLayout.caseInsensitiveCompare("tabular") == .orderedSame { - // `isTabular` is a derived layout of `isLinear`; isTabular implies isLinear - isLinear = true - isTabular = true - } - } else if let horizontal = config.nullableBool(forOption: "\(prefix)/horizontal") { + if let candidateListLayout: String = config.string(for: "\(prefix)/candidate_list_layout") { + switch candidateListLayout { + case "stacked": (isLinear, isTabular) = (false, false) + case "linear": (isLinear, isTabular) = (true, false) + // `isTabular` is a derived layout of `isLinear`; isTabular implies isLinear + case "tabular": (isLinear, isTabular) = (true, true) + default: break + } + } else if let horizontal: Bool = config.optionalBool(for: "\(prefix)/horizontal") { // Deprecated. Not to be confused with text_orientation: horizontal - isLinear = horizontal - isTabular = false + (isLinear, isTabular) = (horizontal, false) } } private func updateTextOrientation(isVertical: inout Bool, config: SquirrelConfig, prefix: String) { - if let textOrientation = config.string(forOption: "\(prefix)/text_orientation") { - if textOrientation.caseInsensitiveCompare("horizontal") == .orderedSame { - isVertical = false - } else if textOrientation.caseInsensitiveCompare("vertical") == .orderedSame { - isVertical = true + if let textOrientation: String = config.string(for: "\(prefix)/text_orientation") { + switch textOrientation { + case "horizontal": isVertical = false + case "vertical": isVertical = true + default: break } - } else if let vertical = config.nullableBool(forOption: "\(prefix)/vertical") { + } else if let vertical: Bool = config.optionalBool(for: "\(prefix)/vertical") { isVertical = vertical } } // 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 { param < .zero ? .zero : param } +@inlinable func positiveRound(param: Double) -> Double { param < .zero ? .zero : param.rounded() } +@inlinable func positiveCeil(param: Double) -> Double { param < .zero ? .zero : param.rounded(.up) } +@inlinable func clampUniform(param: Double) -> Double { 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 +475,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 +494,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,51 +518,56 @@ 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 private(set) var style: SquirrelStyle - init(style: SquirrelStyle) { + @MainActor init(style: SquirrelStyle) { self.style = style - let candidateParagraphStyle = NSMutableParagraphStyle() + let candidateParagraphStyle: NSMutableParagraphStyle = .init() 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 */ + // 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 candidateParagraphStyle.baseWritingDirection = .leftToRight - let preeditParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle - let pagingParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle - let statusParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + let preeditParagraphStyle: NSMutableParagraphStyle = candidateParagraphStyle.mutableCopy() + let pagingParagraphStyle: NSMutableParagraphStyle = candidateParagraphStyle.mutableCopy() + let statusParagraphStyle: NSMutableParagraphStyle = candidateParagraphStyle.mutableCopy() preeditParagraphStyle.lineBreakMode = .byWordWrapping statusParagraphStyle.lineBreakMode = .byTruncatingTail - self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle - self.preeditParagraphStyle = preeditParagraphStyle.copy() as! NSParagraphStyle - self.pagingParagraphStyle = pagingParagraphStyle.copy() as! NSParagraphStyle - self.statusParagraphStyle = statusParagraphStyle.copy() as! NSParagraphStyle + self.candidateParagraphStyle = candidateParagraphStyle.copy() + self.preeditParagraphStyle = preeditParagraphStyle.copy() + self.pagingParagraphStyle = pagingParagraphStyle.copy() + self.statusParagraphStyle = statusParagraphStyle.copy() - 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,56 +576,51 @@ 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 - + fullWidth = NSString(string: .FullWidthSpace).size(withAttributes: [.font : userFont]).width.rounded(.up) super.init() - updateCandidateTemplates(forAttributesOnly: false) + updateCandidateTemplates() updateSeperatorAndSymbolAttrs() } - override convenience init() { + @MainActor override convenience init() { self.init(style: .light) } 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 = .init(UnicodeScalar(NSTextAttachment.character)!) - let attmDeleteFill = NSTextAttachment() + let attmDeleteFill: NSTextAttachment = .init() 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() + let attmDeleteStroke: NSTextAttachment = .init() 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() + let attmCompress: NSTextAttachment = .init() 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() + let attmExpand: NSTextAttachment = .init() 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() + let attmLock: NSTextAttachment = .init() 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 { @@ -700,24 +630,24 @@ final class SquirrelTheme: NSObject { } if showPaging { - let attmBackFill = NSTextAttachment() + let attmBackFill: NSTextAttachment = .init() 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() + let attmBackStroke: NSTextAttachment = .init() 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() + let attmForwardFill: NSTextAttachment = .init() 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() + let attmForwardStroke: NSTextAttachment = .init() 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,272 +658,203 @@ final class SquirrelTheme: NSObject { } } - func updateLabelsWithConfig(_ config: SquirrelConfig, directUpdate update: 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) + @MainActor func updateLabels(withConfig config: SquirrelConfig, directUpdate: Bool) { + let menuSize: Int = config.optionalInt(for: "menu/page_size") ?? 5 + let selectKeys: String = String((config.string(for: "menu/alternative_select_keys") ?? "1234567890").prefix(menuSize)) + let selectLabels: [String]? = config.list(for: "menu/alternative_select_labels") + let rawLabels: [String] = selectLabels == nil ? selectKeys.map { $0.uppercased().applyingTransform(.fullwidthToHalfwidth, reverse: true)! } : Array(selectLabels!.prefix(menuSize)) + updateSelectKeys(selectKeys, labels: rawLabels, directUpdate: directUpdate) } - func updateSelectKeys(_ selectKeys: String, labels: [String], directUpdate update: Bool) { + @MainActor private func updateSelectKeys(_ selectKeys: String, labels rawLabels: [String], directUpdate: Bool) { + guard self.selectKeys != selectKeys, self.rawLabels != rawLabels else { 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 + @MainActor 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 { + @MainActor 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) } - var labels: [String] = self.labels - var enumRange: Range? - let labelCharacters: CharacterSet = CharacterSet(charactersIn: labels.joined()) + if !isLinear { + candidateFormat.insert("\t", at: textRange!.lowerBound) + } + + 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}" - } - } 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)!)) - } - } 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)!)) - } - } 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)!)) - } - } 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)!)) - } + if let range: Range = candidateFormat.range(of: "%c\u{20E3}", options: [.literal]) { // 1︎⃣...9︎⃣0︎⃣ + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF10 + 0x0030)!) + "\u{FE0E}\u{20E3}" } + } else if let range: Range = candidateFormat.range(of: "%c\u{20DD}", options: [.literal]) { // ①...⑨⓪ + 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: Range = candidateFormat.range(of: "(%c)", options: [.literal]) { // ⑴...⑼⑽ + 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: Range = candidateFormat.range(of: "%c.", options: [.literal]) { // ⒈...⒐🄀 + 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: Range = candidateFormat.range(of: "%c,", options: [.literal]) { // 🄂...🄊🄁 + 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)!)) - } - } 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)!)) - } - } 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)!)) - } + if let range: Range = candidateFormat.range(of: "%c\u{20DD}", options: [.literal]) { // Ⓐ...Ⓩ + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF21 + 0x24B6)!) } + } else if let range: Range = candidateFormat.range(of: "(%c)", options: [.literal]) { // 🄐...🄩 + candidateFormat.replaceSubrange(range, with: "%c") + labels = rawLabels.map { String(UnicodeScalar($0.unicodeScalars.first!.value - 0xFF21 + 0x1F110)!) } + } else if let range: Range = candidateFormat.range(of: "%c\u{20DE}", options: [.literal]) { // 🄰...🅉 + 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]) - 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) + let labelFont: NSFont = labelAttrs[.font] as! NSFont + let labelString: CFString = 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 - 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 - } - } + let candidateTemplate: NSMutableAttributedString = .init(string: candidateFormat) + var textRange: NSRange = candidateTemplate.mutableString.range(of: "(\\x{FFF9})?%@", options: [.regularExpression]) + var labelRange: NSRange = .init(location: 0, length: textRange.location) + var commentRange: NSRange = .init(location: textRange.upperBound, length: candidateTemplate.length - textRange.upperBound) + candidateTemplate.setAttributes(labelAttrs, range: labelRange) + candidateTemplate.setAttributes(textAttrs, range: textRange) + 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 + let candidateParagraphStyle: NSMutableParagraphStyle = self.candidateParagraphStyle.mutableCopy() 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 = .init() + let textView: SquirrelTextView = .init(contentBlock: .stackedCandidate, 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: NSMutableAttributedString = candidateTemplate.attributedSubstring(from: NSRange(location: 0, length: labelRange.length - 1)).mutableCopy() + 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 + self.candidateParagraphStyle = candidateParagraphStyle.copy() truncatedParagraphStyle = nil } else { candidateParagraphStyle.tabStops = [] - candidateParagraphStyle.headIndent = 0.0 - self.candidateParagraphStyle = candidateParagraphStyle.copy() as! NSParagraphStyle - let truncatedParagraphStyle = candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + candidateParagraphStyle.headIndent = .zero + self.candidateParagraphStyle = candidateParagraphStyle.copy() + let truncatedParagraphStyle: NSMutableParagraphStyle = candidateParagraphStyle.mutableCopy() truncatedParagraphStyle.lineBreakMode = .byTruncatingMiddle - truncatedParagraphStyle.tighteningFactorForTruncation = 0.0 - self.truncatedParagraphStyle = truncatedParagraphStyle.copy() as? NSParagraphStyle + truncatedParagraphStyle.tighteningFactorForTruncation = .zero + self.truncatedParagraphStyle = truncatedParagraphStyle.copy() } textAttrs[.paragraphStyle] = candidateParagraphStyle commentAttrs[.paragraphStyle] = candidateParagraphStyle labelAttrs[.paragraphStyle] = candidateParagraphStyle candidateTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateTemplate.length)) - self.candidateTemplate = candidateTemplate.copy() as! NSAttributedString + self.candidateTemplate = candidateTemplate.copy() - let candidateHilitedTemplate = candidateTemplate.mutableCopy() as! NSMutableAttributedString + let candidateHilitedTemplate: NSMutableAttributedString = candidateTemplate.mutableCopy() candidateHilitedTemplate.addAttribute(.foregroundColor, value: hilitedLabelForeColor, range: labelRange) candidateHilitedTemplate.addAttribute(.foregroundColor, value: hilitedTextForeColor, range: textRange) candidateHilitedTemplate.addAttribute(.foregroundColor, value: hilitedCommentForeColor, range: commentRange) - self.candidateHilitedTemplate = candidateHilitedTemplate.copy() as! NSAttributedString + self.candidateHilitedTemplate = candidateHilitedTemplate.copy() if isTabular { - let candidateDimmedTemplate = candidateTemplate.mutableCopy() as! NSMutableAttributedString + let candidateDimmedTemplate: NSMutableAttributedString = candidateTemplate.mutableCopy() candidateDimmedTemplate.addAttribute(.foregroundColor, value: dimmedLabelForeColor!, range: labelRange) - self.candidateDimmedTemplate = candidateDimmedTemplate.copy() as? NSAttributedString + self.candidateDimmedTemplate = candidateDimmedTemplate.copy() } else { candidateDimmedTemplate = nil } } - func updateStatusMessageType(_ type: String?) { - if type?.caseInsensitiveCompare("long") == .orderedSame { - statusMessageType = .long - } else if type?.caseInsensitiveCompare("short") == .orderedSame { - statusMessageType = .short - } else { - statusMessageType = .mixed + private func updateStatusMessageType(_ type: String?) { + statusMessageType = switch type { + case "long": .long + case "short": .short + default: .mixed } } - func updateThemeWithConfig(_ config: SquirrelConfig, styleOptions: Set, scriptVariant: String) { - /*** INTERFACE ***/ + static private let monoDigitFeatures: [[NSFontDescriptor.FeatureKey : Int]] = [[.typeIdentifier : kNumberSpacingType, .selectorIdentifier : kMonospacedNumbersSelector], [.typeIdentifier : kTextSpacingType, .selectorIdentifier : kHalfWidthTextSelector], [.typeIdentifier : kCJKRomanSpacingType, .selectorIdentifier : kHalfWidthCJKRomanSelector]] + + @MainActor func updateTheme(withConfig config: SquirrelConfig, styleOptions: Set, scriptVariant: String) { + /* INTERFACE */ var isLinear: Bool = false var isTabular: Bool = false var isVertical: Bool = false updateCandidateListLayout(isLinear: &isLinear, isTabular: &isTabular, config: config, prefix: "style") updateTextOrientation(isVertical: &isVertical, config: config, prefix: "style") - var inlinePreedit: Bool? = config.nullableBool(forOption: "style/inline_preedit") - var inlineCandidate: Bool? = config.nullableBool(forOption: "style/inline_candidate") - var showPaging: Bool? = config.nullableBool(forOption: "style/show_paging") - var rememberSize: Bool? = config.nullableBool(forOption: "style/remember_size", alias: "memorize_size") - var statusMessageType: String? = config.string(forOption: "style/status_message_type") - var candidateFormat: String? = config.string(forOption: "style/candidate_format") - /*** TYPOGRAPHY ***/ - var fontName: String? = config.string(forOption: "style/font_face") - var fontSize: Double? = config.nullableDouble(forOption: "style/font_point", constraint: pos_round) - var labelFontName: String? = config.string(forOption: "style/label_font_face") - var labelFontSize: Double? = config.nullableDouble(forOption: "style/label_font_point", constraint: pos_round) - var commentFontName: String? = config.string(forOption: "style/comment_font_face") - var commentFontSize: Double? = config.nullableDouble(forOption: "style/comment_font_point", constraint: pos_round) - var opacity: Double? = config.nullableDouble(forOption: "style/opacity", alias: "alpha", constraint: clamp_uni) - var translucency: Double? = config.nullableDouble(forOption: "style/translucency", constraint: clamp_uni) - var stackColors: Bool? = config.nullableBool(forOption: "style/stack_colors", alias: "mutual_exclusive") - var cornerRadius: Double? = config.nullableDouble(forOption: "style/corner_radius", constraint: positive) - var hilitedCornerRadius: Double? = config.nullableDouble(forOption: "style/hilited_corner_radius", constraint: positive) - var borderHeight: Double? = config.nullableDouble(forOption: "style/border_height", constraint: pos_ceil) - var borderWidth: Double? = config.nullableDouble(forOption: "style/border_width", constraint: pos_ceil) - var lineSpacing: Double? = config.nullableDouble(forOption: "style/line_spacing", constraint: pos_round) - var preeditSpacing: Double? = config.nullableDouble(forOption: "style/spacing", constraint: pos_round) - var baseOffset: Double? = config.nullableDouble(forOption: "style/base_offset") - var lineLength: Double? = config.nullableDouble(forOption: "style/line_length") - var shadowSize: Double? = config.nullableDouble(forOption: "style/shadow_size", constraint: positive) - /*** CHROMATICS ***/ + var inlinePreedit: Bool? = config.optionalBool(for: "style/inline_preedit") + var inlineCandidate: Bool? = config.optionalBool(for: "style/inline_candidate") + var showPaging: Bool? = config.optionalBool(for: "style/show_paging") + var rememberSize: Bool? = config.optionalBool(for: "style/remember_size", alias: "memorize_size") + var statusMessageType: String? = config.string(for: "style/status_message_type") + var rawCandidateFormat: String? = config.string(for: "style/candidate_format") + /* TYPOGRAPHY */ + var fontName: String? = config.string(for: "style/font_face") + var fontSize: Double? = config.optionalDouble(for: "style/font_point", constraint: positiveRound) + var labelFontName: String? = config.string(for: "style/label_font_face") + var labelFontSize: Double? = config.optionalDouble(for: "style/label_font_point", constraint: positiveRound) + var commentFontName: String? = config.string(for: "style/comment_font_face") + var commentFontSize: Double? = config.optionalDouble(for: "style/comment_font_point", constraint: positiveRound) + var opacity: Double? = config.optionalDouble(for: "style/opacity", alias: "alpha", constraint: clampUniform) + var translucency: Double? = config.optionalDouble(for: "style/translucency", constraint: clampUniform) + var stackColors: Bool? = config.optionalBool(for: "style/stack_colors", alias: "mutual_exclusive") + var cornerRadius: Double? = config.optionalDouble(for: "style/corner_radius", constraint: positive) + var hilitedCornerRadius: Double? = config.optionalDouble(for: "style/hilited_corner_radius", constraint: positive) + var borderHeight: Double? = config.optionalDouble(for: "style/border_height", constraint: positiveCeil) + var borderWidth: Double? = config.optionalDouble(for: "style/border_width", constraint: positiveCeil) + var lineSpacing: Double? = config.optionalDouble(for: "style/line_spacing", constraint: positiveRound) + var preeditSpacing: Double? = config.optionalDouble(for: "style/spacing", constraint: positiveRound) + var baseOffset: Double? = config.optionalDouble(for: "style/base_offset") + var lineLength: Double? = config.optionalDouble(for: "style/line_length") + var shadowSize: Double? = config.optionalDouble(for: "style/shadow_size", constraint: positive) + /* CHROMATICS */ var backColor: NSColor? var borderColor: NSColor? var preeditBackColor: NSColor? @@ -1012,20 +873,14 @@ 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 - } - } - colorScheme ?= config.string(forOption: "style/color_scheme_dark") + _ = styleOptions.first { guard let value: String = config.string(for: "style/\($0)/color_scheme_dark") else { return false } + colorScheme = value; return true } + colorScheme ?= config.string(for: "style/color_scheme_dark") } if colorScheme == nil { - for option in styleOptions { - if let value = config.string(forOption: "style/\(option)/color_scheme") { - colorScheme = value; break - } - } - colorScheme ?= config.string(forOption: "style/color_scheme") + _ = styleOptions.first { guard let value: String = config.string(for: "style/\($0)/color_scheme") else { return false } + colorScheme = value; return true } + colorScheme ?= config.string(for: "style/color_scheme") } let isNative: Bool = (colorScheme == nil) || (colorScheme! == "native") var configPrefixes: [String] = styleOptions.map { "style/" + $0 } @@ -1035,110 +890,109 @@ final class SquirrelTheme: NSObject { // get color scheme and then check possible overrides from styleSwitcher for prefix in configPrefixes { - /*** CHROMATICS override ***/ - config.colorSpace =? config.string(forOption: prefix + "/color_space") - backColor =? config.color(forOption: prefix + "/back_color") - borderColor =? config.color(forOption: prefix + "/border_color") - preeditBackColor =? config.color(forOption: prefix + "/preedit_back_color") - preeditForeColor =? config.color(forOption: prefix + "/text_color") - candidateBackColor =? config.color(forOption: prefix + "/candidate_back_color") - textForeColor =? config.color(forOption: prefix + "/candidate_text_color") - commentForeColor =? config.color(forOption: prefix + "/comment_text_color") - labelForeColor =? config.color(forOption: prefix + "/label_color") - hilitedPreeditBackColor =? config.color(forOption: prefix + "/hilited_back_color") - hilitedPreeditForeColor =? config.color(forOption: prefix + "/hilited_text_color") - hilitedCandidateBackColor =? config.color(forOption: prefix + "/hilited_candidate_back_color") - hilitedTextForeColor =? config.color(forOption: prefix + "/hilited_candidate_text_color") - hilitedCommentForeColor =? config.color(forOption: prefix + "/hilited_comment_text_color") + /* CHROMATICS override */ + config.colorSpace =? config.string(for: prefix + "/color_space") + backColor =? config.color(for: prefix + "/back_color") + borderColor =? config.color(for: prefix + "/border_color") + preeditBackColor =? config.color(for: prefix + "/preedit_back_color") + preeditForeColor =? config.color(for: prefix + "/text_color") + candidateBackColor =? config.color(for: prefix + "/candidate_back_color") + textForeColor =? config.color(for: prefix + "/candidate_text_color") + commentForeColor =? config.color(for: prefix + "/comment_text_color") + labelForeColor =? config.color(for: prefix + "/label_color") + hilitedPreeditBackColor =? config.color(for: prefix + "/hilited_back_color") + hilitedPreeditForeColor =? config.color(for: prefix + "/hilited_text_color") + hilitedCandidateBackColor =? config.color(for: prefix + "/hilited_candidate_back_color") + hilitedTextForeColor =? config.color(for: prefix + "/hilited_candidate_text_color") + hilitedCommentForeColor =? config.color(for: prefix + "/hilited_comment_text_color") // for backward compatibility, `labelHilited_color` and `hilited_candidateLabel_color` are both valid - hilitedLabelForeColor =? config.color(forOption: prefix + "/label_hilited_color", alias: "hilited_candidate_label_color") - backImage =? config.image(forOption: prefix + "/back_image") + hilitedLabelForeColor =? config.color(for: prefix + "/label_hilited_color", alias: "hilited_candidate_label_color") + backImage =? config.image(for: prefix + "/back_image") - /* the following per-color-scheme configurations, if exist, will - override configurations with the same name under the global 'style' section */ - /*** INTERFACE override ***/ + // the following per-color-scheme configurations, if exist, will override + // configurations with the same name under the global 'style' section + /* INTERFACE override */ updateCandidateListLayout(isLinear: &isLinear, isTabular: &isTabular, config: config, prefix: prefix) updateTextOrientation(isVertical: &isVertical, config: config, prefix: prefix) - inlinePreedit =? config.nullableBool(forOption: prefix + "/inline_preedit") - inlineCandidate =? config.nullableBool(forOption: prefix + "/inline_candidate") - showPaging =? config.nullableBool(forOption: prefix + "/show_paging") - rememberSize =? config.nullableBool(forOption: prefix + "/remember_size", alias: "memorize_size") - statusMessageType =? config.string(forOption: prefix + "/status_message_type") - candidateFormat =? config.string(forOption: prefix + "/candidate_format") - /*** TYPOGRAPHY override ***/ - fontName =? config.string(forOption: prefix + "/font_face") - fontSize =? config.nullableDouble(forOption: prefix + "/font_point", constraint: pos_round) - labelFontName =? config.string(forOption: prefix + "/label_font_face") - labelFontSize =? config.nullableDouble(forOption: prefix + "/label_font_point", constraint: pos_round) - commentFontName =? config.string(forOption: prefix + "/comment_font_face") - commentFontSize =? config.nullableDouble(forOption: prefix + "/comment_font_point", constraint: pos_round) - opacity =? config.nullableDouble(forOption: prefix + "/opacity", alias: "alpha", constraint: clamp_uni) - translucency =? config.nullableDouble(forOption: prefix + "/translucency", constraint: clamp_uni) - stackColors =? config.nullableBool(forOption: prefix + "/stack_colors", alias: "mutual_exclusive") - cornerRadius =? config.nullableDouble(forOption: prefix + "/corner_radius", constraint: positive) - hilitedCornerRadius =? config.nullableDouble(forOption: prefix + "/hilited_corner_radius", constraint: positive) - borderHeight =? config.nullableDouble(forOption: prefix + "/border_height", constraint: pos_ceil) - borderWidth =? config.nullableDouble(forOption: prefix + "/border_width", constraint: pos_ceil) - lineSpacing =? config.nullableDouble(forOption: prefix + "/line_spacing", constraint: pos_round) - preeditSpacing =? config.nullableDouble(forOption: prefix + "/spacing", constraint: pos_round) - baseOffset =? config.nullableDouble(forOption: prefix + "/base_offset") - lineLength =? config.nullableDouble(forOption: prefix + "/line_length") - shadowSize =? config.nullableDouble(forOption: prefix + "/shadow_size", constraint: positive) - } - - /*** TYPOGRAPHY refinement ***/ - fontSize ?= kDefaultFontSize + inlinePreedit =? config.optionalBool(for: prefix + "/inline_preedit") + inlineCandidate =? config.optionalBool(for: prefix + "/inline_candidate") + showPaging =? config.optionalBool(for: prefix + "/show_paging") + rememberSize =? config.optionalBool(for: prefix + "/remember_size", alias: "memorize_size") + statusMessageType =? config.string(for: prefix + "/status_message_type") + rawCandidateFormat =? config.string(for: prefix + "/candidate_format") + /* TYPOGRAPHY override */ + fontName =? config.string(for: prefix + "/font_face") + fontSize =? config.optionalDouble(for: prefix + "/font_point", constraint: positiveRound) + labelFontName =? config.string(for: prefix + "/label_font_face") + labelFontSize =? config.optionalDouble(for: prefix + "/label_font_point", constraint: positiveRound) + commentFontName =? config.string(for: prefix + "/comment_font_face") + commentFontSize =? config.optionalDouble(for: prefix + "/comment_font_point", constraint: positiveRound) + opacity =? config.optionalDouble(for: prefix + "/opacity", alias: "alpha", constraint: clampUniform) + translucency =? config.optionalDouble(for: prefix + "/translucency", constraint: clampUniform) + stackColors =? config.optionalBool(for: prefix + "/stack_colors", alias: "mutual_exclusive") + cornerRadius =? config.optionalDouble(for: prefix + "/corner_radius", constraint: positive) + hilitedCornerRadius =? config.optionalDouble(for: prefix + "/hilited_corner_radius", constraint: positive) + borderHeight =? config.optionalDouble(for: prefix + "/border_height", constraint: positiveCeil) + borderWidth =? config.optionalDouble(for: prefix + "/border_width", constraint: positiveCeil) + lineSpacing =? config.optionalDouble(for: prefix + "/line_spacing", constraint: positiveRound) + preeditSpacing =? config.optionalDouble(for: prefix + "/spacing", constraint: positiveRound) + baseOffset =? config.optionalDouble(for: prefix + "/base_offset") + lineLength =? config.optionalDouble(for: prefix + "/line_length") + shadowSize =? config.optionalDouble(for: prefix + "/shadow_size", constraint: positive) + } + + /* FORMAT reset */ + rawCandidateFormat ?= Self.kDefaultCandidateFormat + if self.isLinear != isLinear { + candidateFormat = "" // reset format after switching between linear and stacked + } + + /* TYPOGRAPHY refinement */ + 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 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 fontDescriptor: NSFontDescriptor = .create(fullname: fontName) ?? .create(fullname: NSFont.userFont(ofSize: .zero)?.fontName)! + let font: NSFont = .init(descriptor: fontDescriptor, size: fontSize!)! + let labelFont: NSFont = .init(descriptor: (.create(fullname: labelFontName) ?? fontDescriptor).addingAttributes([.featureSettings : Self.monoDigitFeatures]), size: labelFontSize!)! + let commentFont: NSFont = .init(descriptor: .create(fullname: commentFontName) ?? fontDescriptor, size: commentFontSize!)! + let systemFont: NSFont = .systemFont(ofSize: labelFontSize!) + let pagingFont: NSFont = .init(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 fullWidth: Double = NSString(string: .FullWidthSpace).size(withAttributes: [.font : commentFont]).width.rounded(.up) + preeditSpacing ?= .zero + lineSpacing ?= .zero - let candidateParagraphStyle = self.candidateParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + let candidateParagraphStyle: NSMutableParagraphStyle = self.candidateParagraphStyle.mutableCopy() 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 + self.candidateParagraphStyle = candidateParagraphStyle.copy() - let preeditParagraphStyle = self.preeditParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + let preeditParagraphStyle: NSMutableParagraphStyle = self.preeditParagraphStyle.mutableCopy() preeditParagraphStyle.minimumLineHeight = fontHeight preeditParagraphStyle.maximumLineHeight = fontHeight - preeditParagraphStyle.paragraphSpacing = preeditSpacing! preeditParagraphStyle.tabStops = [] - self.preeditParagraphStyle = preeditParagraphStyle.copy() as! NSParagraphStyle + self.preeditParagraphStyle = preeditParagraphStyle.copy() - let pagingParagraphStyle = self.pagingParagraphStyle.mutableCopy() as! NSMutableParagraphStyle - pagingParagraphStyle.minimumLineHeight = ceil(pagingFont.ascender - pagingFont.descender) - pagingParagraphStyle.maximumLineHeight = ceil(pagingFont.ascender - pagingFont.descender) + let pagingParagraphStyle: NSMutableParagraphStyle = self.pagingParagraphStyle.mutableCopy() + pagingParagraphStyle.minimumLineHeight = pagingFontHeight + pagingParagraphStyle.maximumLineHeight = pagingFontHeight pagingParagraphStyle.tabStops = [] - self.pagingParagraphStyle = pagingParagraphStyle.copy() as! NSParagraphStyle + self.pagingParagraphStyle = pagingParagraphStyle.copy() - let statusParagraphStyle = self.statusParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + let statusParagraphStyle: NSMutableParagraphStyle = self.statusParagraphStyle.mutableCopy() statusParagraphStyle.minimumLineHeight = commentFontHeight statusParagraphStyle.maximumLineHeight = commentFontHeight - self.statusParagraphStyle = statusParagraphStyle.copy() as! NSParagraphStyle + self.statusParagraphStyle = statusParagraphStyle.copy() textAttrs[.font] = font labelAttrs[.font] = labelFont @@ -1146,77 +1000,72 @@ 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 zhCommentFont = NSFont(descriptor: zhFont.fontDescriptor, size: commentFontSize!)! + var zhFont: NSFont = CTFont(.system, size: fontSize!, language: scriptVariant as CFString) + var zhCommentFont: NSFont = .init(descriptor: zhFont.fontDescriptor, size: commentFontSize!)! let maxFontSize: Double = max(fontSize!, commentFontSize!, labelFontSize!) - var refFont = NSFont(descriptor: zhFont.fontDescriptor, size: maxFontSize)! + var refFont: NSFont = .init(descriptor: zhFont.fontDescriptor, size: maxFontSize)! if isVertical { zhFont = zhFont.vertical 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 @@ -1234,7 +1083,7 @@ final class SquirrelTheme: NSObject { self.hilitedTextForeColor = hilitedTextForeColor ?? .selectedMenuItemTextColor self.hilitedCommentForeColor = hilitedCommentForeColor ?? .alternateSelectedControlTextColor self.hilitedLabelForeColor = hilitedLabelForeColor ?? (isNative ? .alternateSelectedControlTextColor : self.hilitedTextForeColor.blend(background: self.hilitedCandidateBackColor)) - dimmedLabelForeColor = isTabular ? self.labelForeColor.withAlphaComponent(self.labelForeColor.alphaComponent * 0.2) : nil + self.dimmedLabelForeColor = isTabular ? self.labelForeColor.withAlphaComponent(self.labelForeColor.alphaComponent * 0.2) : nil textAttrs[.foregroundColor] = self.textForeColor commentAttrs[.foregroundColor] = self.commentForeColor @@ -1243,16 +1092,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) + self.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 +1110,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(rawCandidateFormat!) + 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: NSMutableParagraphStyle = self.candidateParagraphStyle.mutableCopy() + if isLinear { + candidateParagraphStyle.lineSpacing = height * 2 + let truncatedParagraphStyle: NSMutableParagraphStyle = candidateParagraphStyle.mutableCopy() + truncatedParagraphStyle.lineBreakMode = .byTruncatingMiddle + truncatedParagraphStyle.tighteningFactorForTruncation = .zero + self.truncatedParagraphStyle = truncatedParagraphStyle.copy() + } else { + candidateParagraphStyle.paragraphSpacingBefore = height + candidateParagraphStyle.paragraphSpacing = height + } + self.candidateParagraphStyle = candidateParagraphStyle.copy() - 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: NSMutableAttributedString = self.candidateTemplate.mutableCopy() + candidateTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateTemplate.length)) + self.candidateTemplate = candidateTemplate.copy() + let candidateHilitedTemplate: NSMutableAttributedString = self.candidateHilitedTemplate.mutableCopy() + candidateHilitedTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateHilitedTemplate.length)) + self.candidateHilitedTemplate = candidateHilitedTemplate.copy() + if isTabular { + let candidateDimmedTemplate: NSMutableAttributedString = self.candidateDimmedTemplate!.mutableCopy() + candidateDimmedTemplate.addAttribute(.paragraphStyle, value: candidateParagraphStyle, range: NSRange(location: 0, length: candidateDimmedTemplate.length)) + self.candidateDimmedTemplate = candidateDimmedTemplate.copy() } } @@ -1308,159 +1157,141 @@ 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 zhCommentFont = NSFont(descriptor: zhFont.fontDescriptor, size: commentFontSize)! + var zhFont: NSFont = CTFont(.system, size: textFontSize, language: scriptVariant as CFString) + var zhCommentFont: NSFont = .init(descriptor: zhFont.fontDescriptor, size: commentFontSize)! let maxFontSize: Double = max(textFontSize, commentFontSize, labelFontSize) - var refFont = NSFont(descriptor: zhFont.fontDescriptor, size: maxFontSize)! + var refFont: NSFont = .init(descriptor: zhFont.fontDescriptor, size: maxFontSize)! if isVertical { zhFont = zhFont.vertical 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 - - 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 - - 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) - 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) - self.candidateHilitedTemplate = candidateHilitedTemplate.copy() as! NSAttributedString + preeditAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : zhFont] + statusAttrs[.baselineReferenceInfo] = [kCTBaselineReferenceFont : zhCommentFont] + + textAttrs[.language] = scriptVariant + labelAttrs[.language] = scriptVariant + commentAttrs[.language] = scriptVariant + preeditAttrs[.language] = scriptVariant + statusAttrs[.language] = scriptVariant + + let candidateTemplate: NSMutableAttributedString = self.candidateTemplate.mutableCopy() + let templateRange: NSRange = .init(location: 0, length: candidateTemplate.length) + candidateTemplate.addAttribute(.baselineReferenceInfo, value: baselineRefInfo, range: templateRange) + candidateTemplate.addAttribute(.language, value: scriptVariant, range: templateRange) + self.candidateTemplate = candidateTemplate.copy() + + let candidateHilitedTemplate: NSMutableAttributedString = self.candidateHilitedTemplate.mutableCopy() + candidateHilitedTemplate.addAttribute(.baselineReferenceInfo, value: baselineRefInfo, range: templateRange) + candidateHilitedTemplate.addAttribute(.language, value: scriptVariant, range: templateRange) + self.candidateHilitedTemplate = candidateHilitedTemplate.copy() if isTabular { - let candidateDimmedTemplate = candidateTemplate.mutableCopy() as! NSMutableAttributedString - candidateDimmedTemplate.addAttribute(.foregroundColor, value: dimmedLabelForeColor!, range: labelRange) - self.candidateDimmedTemplate = candidateDimmedTemplate.copy() as? NSAttributedString + let candidateDimmedTemplate: NSMutableAttributedString? = self.candidateDimmedTemplate?.mutableCopy() + candidateDimmedTemplate?.addAttribute(.baselineReferenceInfo, value: baselineRefInfo, range: templateRange) + candidateDimmedTemplate?.addAttribute(.language, value: scriptVariant, range: templateRange) + self.candidateDimmedTemplate = candidateDimmedTemplate?.copy() } } -} // SquirrelTheme +} // SquirrelTheme // MARK: Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) -@frozen enum SquirrelContentBlock: Int, Sendable { - case preedit, linearCandidates, stackedCandidates, paging, status +@frozen enum SquirrelContentBlock: Sendable { + case preedit, linearCandidate, stackedCandidate, paging, status + var isCandidate: Bool { [.linearCandidate, .stackedCandidate].contains(self) } } -final class SquirrelLayoutManager: NSLayoutManager, NSLayoutManagerDelegate { - var contentBlock: SquirrelContentBlock? { (firstTextView as? SquirrelTextView)?.contentBlock } - +final class SquirrelLayoutManager: NSLayoutManager, @preconcurrency NSLayoutManagerDelegate { override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: NSPoint) { - let textContainer = textContainer(forGlyphAt: glyphsToShow.location, effectiveRange: nil, withoutAdditionalLayout: true)! + let textContainer: NSTextContainer = textContainer(forGlyphAt: glyphsToShow.location, effectiveRange: nil, withoutAdditionalLayout: true)! let verticalOrientation: Bool = textContainer.layoutOrientation == .vertical - let context = NSGraphicsContext.current!.cgContext + let context: CGContext = NSGraphicsContext.current!.cgContext context.resetClip() enumerateLineFragments(forGlyphRange: glyphsToShow) { lineRect, lineUsedRect, container, lineRange, flag in 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 runGlyphRange: NSRange = self.glyphRange(forCharacterRange: runRange, actualCharacterRange: nil) + let runFont: NSFont = 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)) - 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) + var position: NSPoint = self.location(forGlyphAt: runGlyphRange.location) + lineRect.origin + origin + var line: CTLine + if attrs[.rubyAnnotation] == nil { + let subString: NSMutableAttributedString = self.textStorage!.attributedSubstring(from: runRange).mutableCopy() + subString.addAttribute(.verticalGlyphForm, value: 1, range: NSRange(location: 0, length: runRange.length)) + line = CTLineCreateWithAttributedString(subString) + if let superscript: Int = attrs[.superscript] as? Int { + position.y -= runFont.descender * Double(superscript) * 0.5 + } + } else { + line = CTLineCreateWithAttributedString(self.textStorage!.attributedSubstring(from: runRange)) + } + let glyphRuns: [CTRun] = CTLineGetGlyphRuns(line) as! [CTRun] + for (i, run) in glyphRuns.enumerated() { 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 = context.convertToDeviceSpace(position) + glyphOrigin = context.convertToUserSpace(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 < glyphRuns.count - 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) { - 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)) + var glyphOrigin: NSPoint = origin + if !verticalOrientation { + let refFont: NSFont = (attrs[.baselineReferenceInfo] as! NSDictionary)[kCTBaselineReferenceFont] as! NSFont + 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 = context.convertToDeviceSpace(glyphOrigin) + glyphOrigin = context.convertToUserSpace(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 { + @MainActor func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer, lineFragmentUsedRect: UnsafeMutablePointer, baselineOffset: UnsafeMutablePointer, in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) -> Bool { + guard let rulerAttrs: NSParagraphStyle = 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 + let refFont: NSFont = (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 } return didModify } - func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool { + @MainActor func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool { if charIndex <= 1 { return true } else { let charBeforeIndex: unichar = layoutManager.textStorage!.mutableString.character(at: charIndex - 1) - return contentBlock == .linearCandidates ? charBeforeIndex == 0x1D : charBeforeIndex != UInt8(ascii: "\t") + let contentBlock: SquirrelContentBlock? = (firstTextView as? SquirrelTextView)?.contentBlock + return contentBlock == .linearCandidate ? charBeforeIndex == 0x1D : charBeforeIndex != UInt8(ascii: "\t") } } 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 +1299,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 = .init(origin: glyphPosition, size: .zero) + if layoutManager.textStorage!.mutableString.character(at: charIndex) == 0x8B, let controlCharacterSize: NSSize = 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 } + + @MainActor override var renderingSurfaceBounds: CGRect { + var bounds: CGRect = super.renderingSurfaceBounds + guard state == .layoutAvailable, let contentBlock: SquirrelContentBlock = (textLayoutManager?.textContainer?.textView as? SquirrelTextView)?.contentBlock, contentBlock.isCandidate, let documentRange: NSTextRange = textLayoutManager?.documentRange, let rulerStyle: NSParagraphStyle = textLayoutManager?.textContainer?.textView?.defaultParagraphStyle else { return bounds } + if rangeInElement.location.isEqual(documentRange.location) { + let spacing: Double = contentBlock == .stackedCandidate ? 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 == .stackedCandidate ? 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,65 +1333,62 @@ 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 { - let refFont = (lineFrag.attributedString.attribute(.baselineReferenceInfo, at: lineFrag.characterRange.location, effectiveRange: nil) as! NSDictionary)[kCTBaselineReferenceFont] as! NSFont + if layoutOrientation == .horizontal { + let refFont: NSFont = (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 = .init(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 - -@available(macOS 12.0, *) -final class SquirrelTextLayoutManager: NSTextLayoutManager, NSTextLayoutManagerDelegate { - var contentBlock: SquirrelContentBlock? { (textContainer?.textView as? SquirrelTextView)?.contentBlock } +} // SquirrelTextLayoutFragment - func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, shouldBreakLineBefore location: any NSTextLocation, hyphenating: Bool) -> Bool { - let contentStorage = textLayoutManager.textContentManager as! NSTextContentStorage +@available(macOS 12.0, *) final class SquirrelTextLayoutManager: NSTextLayoutManager, @preconcurrency NSTextLayoutManagerDelegate { + @MainActor func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, shouldBreakLineBefore location: any NSTextLocation, hyphenating: Bool) -> Bool { + let contentStorage: NSTextContentStorage = textLayoutManager.textContentManager as! NSTextContentStorage let charIndex: Int = contentStorage.offset(from: contentStorage.documentRange.location, to: location) if charIndex <= 1 { return true } else { let charBeforeIndex: unichar = contentStorage.textStorage!.mutableString.character(at: charIndex - 1) - return contentBlock == .linearCandidates ? charBeforeIndex == 0x1D : charBeforeIndex != UInt8(ascii: "\t") + let contentBlock: SquirrelContentBlock? = (textContainer?.textView as? SquirrelTextView)?.contentBlock + return contentBlock == .linearCandidate ? charBeforeIndex == 0x1D : charBeforeIndex != UInt8(ascii: "\t") } } func textLayoutManager(_: NSTextLayoutManager, textLayoutFragmentFor location: any NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment { - let textRange = NSTextRange(location: location, end: textElement.elementRange!.endLocation) + let textRange: NSTextRange? = .init(location: location, end: textElement.elementRange!.endLocation) return SquirrelTextLayoutFragment(textElement: textElement, range: textRange) } -} // SquirrelTextLayoutManager +} // SquirrelTextLayoutManager -final class NSFlippedView: NSView { +final class NSFlippedView: NSView, Sendable { override var isFlipped: Bool { true } } -final class SquirrelTextView: NSTextView { +final class SquirrelTextView: NSTextView, Sendable { var contentBlock: SquirrelContentBlock init(contentBlock: SquirrelContentBlock, textStorage: NSTextStorage) { self.contentBlock = contentBlock - let textContainer = NSTextContainer(size: .zero) + let textContainer: NSTextContainer = .init(size: .zero) textContainer.lineFragmentPadding = 0 if #available(macOS 12.0, *) { - let textLayoutManager = SquirrelTextLayoutManager() + let textLayoutManager: SquirrelTextLayoutManager = .init() textLayoutManager.usesFontLeading = false textLayoutManager.usesHyphenation = false textLayoutManager.delegate = textLayoutManager textLayoutManager.textContainer = textContainer - let contentStorage = NSTextContentStorage() + let contentStorage: NSTextContentStorage = .init() contentStorage.addTextLayoutManager(textLayoutManager) contentStorage.textStorage = textStorage } else { - let layoutManager = SquirrelLayoutManager() + let layoutManager: SquirrelLayoutManager = .init() layoutManager.backgroundLayoutEnabled = true layoutManager.usesFontLeading = false layoutManager.typesetterBehavior = .latestBehavior @@ -1566,31 +1403,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: NSTextLocation = textContentStorage!.location(textContentStorage!.documentRange.location, offsetBy: charRange.location)! + let end: NSTextLocation = 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: Int = textContentStorage!.offset(from: textContentStorage!.documentRange.location, to: textRange.location) + let length: Int = textContentStorage!.offset(from: textRange.location, to: textRange.endLocation) + return NSRange(location: location, length: length) } func layoutText() -> NSRect { @@ -1607,24 +1435,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 == .linearCandidate, let lineSpacing: CGFloat = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { firstLineRect.size.height += lineSpacing if !finalLineRect.isEmpty { finalLineRect.size.height += lineSpacing @@ -1635,50 +1460,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) + var firstLineRange: NSRange = .init(location: NSNotFound, length: 0) let firstLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: &firstLineRange) 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 == .linearCandidate, firstLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing: CGFloat = 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 = .init(location: NSNotFound, length: 0) + let finalLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: &finalLineRange) 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 == .linearCandidate, finalLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing: CGFloat = 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 */ + /** Calculate 3 rectangles enclosing 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 */ func textPolygon(forRange charRange: NSRange) -> SquirrelTextPolygon { var textPolygon: SquirrelTextPolygon = .init(head: .zero, body: .zero, tail: .zero) - if charRange.location == NSNotFound { - return textPolygon - } + 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, let segRange = segRange 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 == .linearCandidate, let lineSpacing: CGFloat = defaultParagraphStyle?.lineSpacing, lineSpacing.isNormal { headLineRect.size.height += lineSpacing if !tailLineRect.isEmpty { tailLineRect.size.height += lineSpacing @@ -1690,53 +1521,61 @@ 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) } } } } } 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 headLineRange: NSRange = .init(location: NSNotFound, length: 0) + var headLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.location, effectiveRange: &headLineRange) 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 == .linearCandidate, headLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing: CGFloat = 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 - var tailLineRange = NSRange(location: NSNotFound, length: 0) - let tailLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: &tailLineRange) + headLineRect.size.width = containerWidth - headLineRect.minX + var tailLineRange: NSRange = .init(location: NSNotFound, length: 0) + var tailLineRect: NSRect = layoutManager!.lineFragmentUsedRect(forGlyphAt: glyphRange.upperBound - 1, effectiveRange: &tailLineRange) + if contentBlock == .linearCandidate, tailLineRange.upperBound == layoutManager!.numberOfGlyphs, let lineSpacing: CGFloat = 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,11 +1583,11 @@ final class SquirrelTextView: NSTextView { } return textPolygon } -} // SquirrelTextView +} // SquirrelTextView // MARK: View behind text, containing drawings of backgrounds and highlights -final class SquirrelView: NSView { +final class SquirrelView: NSView, Sendable { static var lightTheme: SquirrelTheme = SquirrelTheme(style: .light) @available(macOS 10.14, *) static var darkTheme: SquirrelTheme = SquirrelTheme(style: .dark) private(set) var theme: SquirrelTheme @@ -1758,23 +1597,23 @@ final class SquirrelView: NSView { let statusView: SquirrelTextView let scrollView: NSScrollView let documentView: NSFlippedView - let candidateContents = NSTextStorage() - let preeditContents = NSTextStorage() - let pagingContents = NSTextStorage() - let statusContents = NSTextStorage() - @available(macOS 10.14, *) let shape = CAShapeLayer() - let logoLayer = CAShapeLayer() - private let backImageLayer = CAShapeLayer() - private let backColorLayer = CAShapeLayer() - private let borderLayer = CAShapeLayer() - private let hilitedPreeditLayer = CAShapeLayer() - private let functionButtonLayer = CAShapeLayer() - private let documentLayer = CAShapeLayer() - private let activePageLayer = CAShapeLayer() - private let gridLayer = CAShapeLayer() - private let nonHilitedCandidateLayer = CAShapeLayer() - private let hilitedCandidateLayer = CAShapeLayer() - private let clipLayer = CAShapeLayer() + let candidateContents: NSTextStorage = .init() + let preeditContents: NSTextStorage = .init() + let pagingContents: NSTextStorage = .init() + let statusContents: NSTextStorage = .init() + @available(macOS 10.14, *) let shape: CAShapeLayer = .init() + let logoLayer: CAShapeLayer = .init() + private let backImageLayer: CAShapeLayer = .init() + private let backColorLayer: CAShapeLayer = .init() + private let borderLayer: CAShapeLayer = .init() + private let hilitedPreeditLayer: CAShapeLayer = .init() + private let functionButtonLayer: CAShapeLayer = .init() + private let documentLayer: CAShapeLayer = .init() + private let activePageLayer: CAShapeLayer = .init() + private let gridLayer: CAShapeLayer = .init() + private let nonHilitedCandidateLayer: CAShapeLayer = .init() + private let hilitedCandidateLayer: CAShapeLayer = .init() + private let clipLayer: CAShapeLayer = .init() private(set) var tabularIndices: [SquirrelTabularIndex] = [] private(set) var candidatePolygons: [SquirrelTextPolygon] = [] private(set) var sectionRects: [NSRect] = [] @@ -1788,35 +1627,23 @@ 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) + private(set) var hilitedPreeditRange: NSRange = .init(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() - } - } - } + didSet { theme = style == .dark ? Self.darkTheme : Self.lightTheme + scrollView.scrollerKnobStyle = style == .dark ? .light : .dark + updateColors() } } + override var isFlipped: Bool { true } + override var wantsUpdateLayer: Bool { true } override init(frame frameRect: NSRect) { - candidateView = SquirrelTextView(contentBlock: .stackedCandidates, textStorage: candidateContents) + candidateView = SquirrelTextView(contentBlock: .stackedCandidate, textStorage: candidateContents) preeditView = SquirrelTextView(contentBlock: .preedit, textStorage: preeditContents) pagingView = SquirrelTextView(contentBlock: .paging, textStorage: pagingContents) statusView = SquirrelTextView(contentBlock: .status, textStorage: statusContents) @@ -1834,44 +1661,44 @@ 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 + layer?.isGeometryFlipped = true layerContentsRedrawPolicy = .onSetNeedsDisplay - backImageLayer.actions = ["transform": NSNull()] + backImageLayer.actions = ["transform" : NSNull()] backColorLayer.fillRule = .evenOdd borderLayer.fillRule = .evenOdd - layer!.addSublayer(backImageLayer) - layer!.addSublayer(backColorLayer) - layer!.addSublayer(hilitedPreeditLayer) - layer!.addSublayer(functionButtonLayer) - layer!.addSublayer(logoLayer) - layer!.addSublayer(borderLayer) + layer?.addSublayer(backImageLayer) + layer?.addSublayer(backColorLayer) + layer?.addSublayer(hilitedPreeditLayer) + layer?.addSublayer(functionButtonLayer) + layer?.addSublayer(logoLayer) + layer?.addSublayer(borderLayer) documentLayer.fillRule = .evenOdd documentLayer.allowsGroupOpacity = true activePageLayer.fillRule = .evenOdd + gridLayer.lineCap = .round gridLayer.lineWidth = 1.0 - clipLayer.fillColor = CGColor.white - documentView.layer!.addSublayer(documentLayer) + 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 + documentView.layer?.addSublayer(gridLayer) + documentView.layer?.addSublayer(nonHilitedCandidateLayer) + documentView.layer?.addSublayer(hilitedCandidateLayer) + scrollView.layer?.mask = clipLayer } - @available(*, unavailable) - required init?(coder _: NSCoder) { + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -1883,29 +1710,33 @@ final class SquirrelView: NSView { backColorLayer.fillColor = (theme.preeditBackColor ?? theme.backColor).cgColor borderLayer.fillColor = (theme.borderColor ?? theme.backColor).cgColor documentLayer.fillColor = theme.backColor.cgColor - if let backImage = theme.backImage, backImage.isValid { + if let backImage: NSImage = theme.backImage, backImage.isValid { backImageLayer.fillColor = NSColor(patternImage: theme.backImage!).cgColor backImageLayer.isHidden = false } else { backImageLayer.isHidden = true } - if let hilitedPreeditBackColor = theme.hilitedPreeditBackColor { + if let hilitedPreeditBackColor: NSColor = theme.hilitedPreeditBackColor { hilitedPreeditLayer.fillColor = hilitedPreeditBackColor.cgColor } else { hilitedPreeditLayer.isHidden = true } - if let candidateBackColor = theme.candidateBackColor { + if let candidateBackColor: NSColor = theme.candidateBackColor { nonHilitedCandidateLayer.fillColor = candidateBackColor.cgColor } else { nonHilitedCandidateLayer.isHidden = true } - if let hilitedCandidateBackColor = theme.hilitedCandidateBackColor { + if let hilitedCandidateBackColor: NSColor = 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 +1759,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,41 +1767,36 @@ final class SquirrelView: NSView { documentRect = .zero // in textView's own coordinates clipRect = .zero pagingRect = .zero - clippedHeight = 0.0 - if !hasPreedit && candidateInfos.isEmpty { // status + clippedHeight = .zero + if !hasPreedit, candidateInfos.isEmpty { // status contentRect = statusView.layoutText(); return } if hasPreedit { 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.contains(where: \.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 @@ -1978,9 +1804,9 @@ final class SquirrelView: NSView { // Get the rectangles enclosing each part and the entire panel func layoutContents() { - let origin = NSPoint(x: theme.borderInsets.width, y: theme.borderInsets.height) + let origin: NSPoint = .init(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 +1816,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 +1831,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 +1868,10 @@ final class SquirrelView: NSView { } func highlightCandidate(_ hilitedCandidate: Int?) { - if hilitedCandidate == nil || self.hilitedCandidate == nil { return } + guard let hilitedCandidate = hilitedCandidate, let priorHilitedCandidate: Int = 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 +1890,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 { - var origin = scrollView.contentView.bounds.origin + let activePage: Int = hilitedCandidate / theme.pageSize + if sectionRects[activePage].minY < scrollView.documentVisibleRect.minY.nextDown { + var origin: NSPoint = 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 { - var origin = scrollView.contentView.bounds.origin + } else if sectionRects[activePage].maxY > scrollView.documentVisibleRect.maxY.nextUp { + var origin: NSPoint = 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 { - var origin = scrollView.contentView.bounds.origin - origin.y -= scrollView.documentVisibleRect.minY - candidatePolygons[hilitedCandidate!].minY + if scrollView.documentVisibleRect.minY > candidatePolygons[hilitedCandidate].minY.nextUp { + var origin: NSPoint = scrollView.contentView.bounds.origin + 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 { - var origin = scrollView.contentView.bounds.origin - origin.y += candidatePolygons[hilitedCandidate!].maxY - scrollView.documentVisibleRect.maxY + } else if scrollView.documentVisibleRect.maxY < candidatePolygons[hilitedCandidate].maxY.nextDown { + var origin: NSPoint = scrollView.contentView.bounds.origin + origin.y += candidatePolygons[hilitedCandidate].maxY - scrollView.documentVisibleRect.maxY scrollView.contentView.scroll(to: origin) scrollView.verticalScroller?.doubleValue = scrollView.documentVisibleRect.minY / clippedHeight } @@ -2110,56 +1934,33 @@ 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 - } - var buttonColor: NSColor? - var buttonRect: NSRect = .zero - switch functionButton { - case .PageUpKey: - buttonColor = theme.hilitedPreeditBackColor?.hooverColor - buttonRect = pageUpRect - case .HomeKey: - buttonColor = theme.hilitedPreeditBackColor?.disabledColor - buttonRect = pageUpRect - case .PageDownKey: - buttonColor = theme.hilitedPreeditBackColor?.hooverColor - buttonRect = pageDownRect - case .EndKey: - buttonColor = theme.hilitedPreeditBackColor?.disabledColor - buttonRect = pageDownRect - case .ExpandButton, .CompressButton, .LockButton: - buttonColor = theme.hilitedPreeditBackColor?.hooverColor - buttonRect = expanderRect - case .BackSpaceKey: - buttonColor = theme.hilitedPreeditBackColor?.hooverColor - buttonRect = deleteBackRect - 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 - } + guard functionButton != .VoidSymbol else { return nil } + let (buttonColor, buttonRect): (NSColor?, NSRect) = switch functionButton { + case .PageUpKey: (theme.hilitedPreeditBackColor?.hooverColor, pageUpRect) + case .HomeKey: (theme.hilitedPreeditBackColor?.disabledColor, pageUpRect) + case .PageDownKey: (theme.hilitedPreeditBackColor?.hooverColor, pageDownRect) + case .EndKey: (theme.hilitedPreeditBackColor?.disabledColor, pageDownRect) + case .ExpandButton, .CompressButton, .LockButton: (theme.hilitedPreeditBackColor?.hooverColor, expanderRect) + case .BackSpaceKey: (theme.hilitedPreeditBackColor?.hooverColor, deleteBackRect) + case .EscapeKey: (theme.hilitedPreeditBackColor?.disabledColor, deleteBackRect) + default: (nil, .zero) + } + 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 @@ -2168,65 +1969,64 @@ final class SquirrelView: NSView { let backgroundRect: NSRect = backingAlignedRect(panelRect.insetBy(dx: theme.borderInsets.width, dy: theme.borderInsets.height), options: [.alignAllEdgesNearest]) let hilitedCornerRadius: Double = min(theme.hilitedCornerRadius, theme.candidateParagraphStyle.minimumLineHeight * 0.5) - /*** Preedit Rects **/ + /* Preedit */ deleteBackRect = .zero var hilitedPreeditPath: CGPath? if !preeditView.isHidden { + preeditRect.origin = backgroundRect.origin preeditRect.size.width = backgroundRect.width 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) + if hilitedPreeditRange.length > 0, theme.hilitedPreeditBackColor != nil { + 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) + var textPolygon: SquirrelTextPolygon = preeditView.textPolygon(forRange: hilitedPreeditRange) if !textPolygon.head.isEmpty { - textPolygon.head.origin.x += theme.borderInsets.width + ceil(theme.fullWidth * 0.5) - padding - textPolygon.head.origin.y += theme.borderInsets.height - textPolygon.head.size.width += padding * 2 + textPolygon.head = textPolygon.head.offsetBy(dx: theme.borderInsets.width + (theme.fullWidth * 0.5).rounded(.up), dy: theme.borderInsets.height).insetBy(dx: -padding, dy: 0) 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.y += theme.borderInsets.height + textPolygon.body = textPolygon.body.offsetBy(dx: theme.borderInsets.width + (theme.fullWidth * 0.5).rounded(.up) - padding, dy: 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.y += theme.borderInsets.height + textPolygon.tail = textPolygon.tail.offsetBy(dx: theme.borderInsets.width + (theme.fullWidth * 0.5).rounded(.up) - padding, dy: theme.borderInsets.height) textPolygon.tail.size.width += padding if hilitedPreeditRange.upperBound + 2 == preeditContents.length { textPolygon.tail.size.width += padding } 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 - deleteBackRect.origin.x = backgroundRect.maxX - deleteBackRect.width - deleteBackRect.origin.y += theme.borderInsets.height + deleteBackRect.origin = NSPoint(x: preeditRect.maxX - deleteBackRect.width, y: preeditRect.maxY - deleteBackRect.height) deleteBackRect = backingAlignedRect(deleteBackRect.intersection(preeditRect), options: [.alignAllEdgesNearest]) } - /*** Candidates Rects, all in documentView coordinates (except for `candidatesRect`) ***/ + /* Candidates (in documentView coordinates, except for `clipRect`) */ candidatePolygons = [] 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) @@ -2244,7 +2044,7 @@ final class SquirrelView: NSView { } } for candInfo in candidateInfos { - var candidatePolygon = candidateView.textPolygon(forRange: candInfo.candidateRange) + var candidatePolygon: SquirrelTextPolygon = candidateView.textPolygon(forRange: candInfo.candidateRange) if !candidatePolygon.head.isEmpty { candidatePolygon.head.size.width += theme.fullWidth candidatePolygon.head = documentView.backingAlignedRect(candidatePolygon.head.intersection(documentRect), options: [.alignAllEdgesNearest]) @@ -2257,37 +2057,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 = 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,12 +2099,12 @@ 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)) } } } - /*** Paging Rects ***/ + /* Paging */ pageUpRect = .zero pageDownRect = .zero expanderRect = .zero @@ -2314,77 +2116,67 @@ final class SquirrelView: NSView { } pagingRect = backingAlignedRect(pagingRect.intersection(backgroundRect), options: [.alignAllEdgesNearest]) if theme.showPaging { - pageUpRect = pagingView.blockRect(for: NSRange(location: 0, length: 1)) - pageDownRect = pagingView.blockRect(for: NSRange(location: pagingContents.length - 1, length: 1)) - pageDownRect.origin.x += pagingRect.minX + pageUpRect = pagingView.blockRect(for: NSRange(location: 0, length: 1)).offsetBy(dx: pagingRect.minX, dy: pagingRect.minY) + pageDownRect = pagingView.blockRect(for: NSRange(location: pagingContents.length - 1, length: 1)).offsetBy(dx: pagingRect.minX, dy: pagingRect.minY) pageDownRect.size.width += theme.fullWidth - pageDownRect.origin.y += pagingRect.minY - pageUpRect.origin.x += pagingRect.minX // bypass the bug of getting wrong glyph position when tab is presented - pageUpRect.size.width = pageDownRect.width - pageUpRect.origin.y += pagingRect.minY + pageUpRect.size = pageDownRect.size pageUpRect = backingAlignedRect(pageUpRect.intersection(pagingRect), options: [.alignAllEdgesNearest]) pageDownRect = backingAlignedRect(pageDownRect.intersection(pagingRect), options: [.alignAllEdgesNearest]) } if theme.isTabular { - expanderRect = pagingView.blockRect(for: NSRange(location: pagingContents.length / 2, length: 1)) - expanderRect.origin.x += pagingRect.minX + expanderRect = pagingView.blockRect(for: NSRange(location: pagingContents.length / 2, length: 1)).offsetBy(dx: pagingRect.minX, dy: pagingRect.minY) expanderRect.size.width += theme.fullWidth - expanderRect.origin.y += pagingRect.minY expanderRect = backingAlignedRect(expanderRect.intersection(pagingRect), options: [.alignAllEdgesNearest]) } } - /*** Border Rects ***/ + /* Border */ 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 = 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) + let tailPanelRect: NSRect = pagingRect.offsetBy(dx: 0, dy: theme.borderInsets.height).insetBy(dx: -theme.borderInsets.width, dy: 0) + 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) - flip = flip.scaledBy(x: 1, y: -1) - let shapePath: CGPath? = panelPath?.copy(using: &flip)! + var flip: CGAffineTransform = .init(translationX: 0, y: panelRect.height).scaledBy(x: 1, y: -1) + let shapePath: CGPath? = panelPath?.copy(using: &flip) - /*** Draw into layers ***/ + /* Draw into layers */ if #available(macOS 10.14, *) { shape.path = shapePath } // highlighted preedit layer - if hilitedPreeditPath != nil && theme.hilitedPreeditBackColor != nil { - hilitedPreeditLayer.path = hilitedPreeditPath! + if hilitedPreeditPath != nil, theme.hilitedPreeditBackColor != nil { + hilitedPreeditLayer.path = hilitedPreeditPath hilitedPreeditLayer.isHidden = false } else { hilitedPreeditLayer.isHidden = true } // 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 nonHilitedCandidatePath: CGMutablePath = .init() + 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: CGPath = 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 +2188,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: CGPath = 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 +2212,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 @@ -2437,17 +2225,17 @@ 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) + var transform: CGAffineTransform = theme.isVertical ? .init(rotationAngle: .pi / 2) : .identity + transform = transform.translatedBy(x: -backgroundRect.minX, y: -backgroundRect.minY) backImageLayer.path = backgroundPath?.copy(using: &transform) backImageLayer.setAffineTransform(transform.inverted()) } // background color layer if !preeditRect.isEmpty || !pagingRect.isEmpty { - if clipPath != nil { - let nonCandidatePath = backgroundPath?.mutableCopy() - nonCandidatePath?.addPath(clipPath!) - if theme.stackColors && theme.hilitedPreeditBackColor != nil && theme.hilitedPreeditBackColor!.alphaComponent < 0.999 { + if let clipPath = clipPath { + let nonCandidatePath: CGMutablePath? = backgroundPath?.mutableCopy() + nonCandidatePath?.addPath(clipPath) + if theme.stackColors, theme.hilitedPreeditBackColor != nil, theme.hilitedPreeditBackColor!.alphaComponent < 1.0.nextDown { if hilitedPreeditPath != nil { nonCandidatePath?.addPath(hilitedPreeditPath!) } @@ -2469,45 +2257,41 @@ final class SquirrelView: NSView { unclipHighlightedCandidate() } - 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)! - } - } - } + func index(mouseSpot spot: NSPoint) -> SquirrelIndex? { + var point: NSPoint = convert(spot, from: nil) + 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 } - return .VoidSymbol + if NSMouseInRect(point, pageDownRect, true) { + return .PageDownKey + } + guard NSMouseInRect(point, clipRect, true) else { return nil } + point = convert(point, to: documentView) + if let idx: Int = candidatePolygons.firstIndex(where: { $0.mouseInPolygon(point: point, flipped: true) }) { + return .Ordinal(idx) + } + 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 */ -final class SquirrelToolTip: NSPanel { - private let backView = NSVisualEffectView() - private let textView = NSTextField() +/** 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 */ +final class SquirrelToolTip: NSPanel, Sendable { + private let backView: NSVisualEffectView = .init() + private let textView: NSTextField = .init() private var showTimer: Timer? private var hideTimer: Timer? private(set) var isEmpty: Bool = true @@ -2517,18 +2301,20 @@ final class SquirrelToolTip: NSPanel { backgroundColor = .clear isOpaque = true hasShadow = true - let contentView = NSView() + let contentView: NSView = .init() backView.material = .toolTip contentView.addSubview(backView) textView.isBezeled = true textView.bezelStyle = .squareBezel textView.isBordered = true textView.isSelectable = false + textView.usesSingleLineMode = false + textView.lineBreakMode = .byWordWrapping contentView.addSubview(textView) self.contentView = contentView } - func show(withToolTip toolTip: String!, display: SquirrelTooltipDisplay) { + func showToolTip(_ toolTip: String!, display: SquirrelTooltipDisplay) { if display == .none || toolTip.isEmpty { clear(); return } @@ -2537,22 +2323,25 @@ 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 + let cursor: NSCursor = .currentSystem! spot.x += cursor.image.size.width - cursor.hotSpot.x spot.y -= cursor.image.size.height - cursor.hotSpot.y - var windowRect = NSRect(x: spot.x, y: spot.y - contentSize.height, width: contentSize.width, height: contentSize.height) + var windowRect: NSRect = .init(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]) @@ -2563,33 +2352,28 @@ final class SquirrelToolTip: NSPanel { showTimer?.invalidate() showTimer = nil switch display { - case .now: - show() - case .delayed: - showTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in self.show() } - default: - break + case .now: show() + case .delayed: showTimer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(show), userInfo: nil, repeats: false) + default: break } } - func show() { + @objc func show() { if isEmpty { return } showTimer?.invalidate() showTimer = nil display() orderFrontRegardless() hideTimer?.invalidate() - hideTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in self.hide() } + hideTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(hide), userInfo: nil, repeats: false) } - func hide() { + @objc func hide() { showTimer?.invalidate() showTimer = nil hideTimer?.invalidate() hideTimer = nil - if isVisible { - orderOut(nil) - } + if isVisible { orderOut(nil) } } func clear() { @@ -2597,19 +2381,21 @@ final class SquirrelToolTip: NSPanel { textView.stringValue = "" hide() } -} // SquirrelToolTipView +} // SquirrelToolTipView // MARK: Panel window, dealing with text content and mouse interactions -final class SquirrelPanel: NSPanel, NSWindowDelegate { +final class SquirrelPanel: NSPanel, NSWindowDelegate, Sendable { + 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() - private let view = SquirrelView() + @available(macOS 10.14, *) private let back: NSVisualEffectView = .init() + private let toolTip: SquirrelToolTip = .init() + private let view: SquirrelView = .init() 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 @@ -2622,107 +2408,82 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { private var caretPos: Int? private var pageNum: Int = 0 private var isLastPage: Bool = false - // Show preedit text inline. + /// Show preedit text inline. var inlinePreedit: Bool { view.theme.inlinePreedit } - // Show primary candidate inline + /// Show primary candidate inline. var inlineCandidate: Bool { view.theme.inlineCandidate } - // Vertical text orientation, as opposed to horizontal text orientation. + /// Vertical text orientation, as opposed to horizontal text orientation. var isVertical: Bool { view.theme.isVertical } - // Linear candidate list layout, as opposed to stacked candidate list layout. + /// 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 */ + /// Tabular candidate list layout, initializes as tab-aligned linear layout, + /// 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() - } - } + get { view.isLocked } + set { guard view.theme.isTabular, view.isLocked != newValue else { return } + view.isLocked = newValue + let userConfig: SquirrelConfig = .init(.user) + _ = userConfig.setOption("var/option/_isLockedTabular", with: newValue) + if newValue { _ = userConfig.setOption("var/option/_isExpandedTabular", with: 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 - } - } - 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 - } - } + get { view.isExpanded } + 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) - } - } + get { view.sectionNum } + 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. + /// 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 == .zero { + initPosition = true + } else if !_screen.frame.contains(IbeamRect), !_screen.frame.intersects(IbeamRect) { + willChangeValue(for: \.screen) + updateScreen() + didChangeValue(for: \.screen) + updateDisplayParameters() } } } - - private var _screen: NSScreen! = .main + private var _screen: NSScreen = .main! override var screen: NSScreen? { _screen } weak var inputController: SquirrelInputController? var style: SquirrelStyle { - didSet { - if oldValue != style { - view.style = style - appearance = NSAppearance(named: style == .dark ? .darkAqua : .aqua) - } - } - } - - // Status message when pop-up is about to be displayed; nil when normal panel is about to be displayed - private var statusMessage: String? - var hasStatusMessage: Bool { statusMessage != nil } - // Store switch options that change style (color theme) settings - var optionSwitcher = SquirrelOptionSwitcher() + get { view.style } + set { guard #available(macOS 10.14, *), view.style != newValue else { return } + view.style = newValue + appearance = NSAppearance(named: newValue == .dark ? .darkAqua : .aqua) + view.needsDisplay = true + display() } + } + /// Status message when pop-up is about to be displayed; nil when normal panel is about to be displayed. + private(set) var statusMessage: String? + /// Stores switch options that change style (color theme) settings. + var optionSwitcher: SquirrelOptionSwitcher = .init() 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 + displaysWhenScreenProfileChanges = true + worksWhenModal = true - let contentView = NSFlippedView() + let contentView: NSFlippedView = .init() contentView.autoresizesSubviews = false if #available(macOS 10.14, *) { back.blendingMode = .behindWindow @@ -2730,7 +2491,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { back.state = .active back.isEmphasized = true back.wantsLayer = true - back.layer!.mask = view.shape + back.layer?.mask = view.shape contentView.addSubview(back) } contentView.addSubview(view) @@ -2746,7 +2507,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { } func windowDidChangeBackingProperties(_ notification: Notification) { - if let panel = notification.object as? SquirrelPanel { + if let panel: SquirrelPanel = notification.object as? SquirrelPanel { panel.updateDisplayParameters() } } @@ -2757,87 +2518,86 @@ 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 ? .linearCandidate : .stackedCandidate + 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 = .init(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)! + let logoImage: NSImage = .init(named: NSImage.applicationIconName)! logoImage.size = logoRect.size view.logoLayer.contents = logoImage - view.logoLayer.setAffineTransform(view.theme.isVertical ? CGAffineTransform(rotationAngle: -.pi / 2) : CGAffineTransformIdentity) - if let lightBackImage = SquirrelView.lightTheme.backImage, lightBackImage.isValid { + view.logoLayer.setAffineTransform(theme.isVertical ? .init(rotationAngle: -.pi / 2) : .identity) + if let lightBackImage: NSImage = 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 - if let darkBackImage = SquirrelView.darkTheme.backImage, darkBackImage.isValid { + back.isHidden = view.theme.translucency.isFinite && !view.theme.translucency.isNormal + if let darkBackImage: NSImage = 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() + if isVisible { + showPanel(withPreedit: view.preeditContents.mutableString.substring(to: max(0, view.preeditContents.length - 2)), selRange: view.hilitedPreeditRange, caretPos: caretPos, candidateIndices: indexRange, highlightedCandidate: highlightedCandidate, pageNum: pageNum, isLastPage: isLastPage, didCompose: true) + } } 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 { + if highlightedCandidate == indexRange.count - 1, isLastPage { return nil } - if currentLine == finalLine && !isLastPage { + 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 +2608,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 +2622,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 { break } + 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 { break } + 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 { break } + 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 } + if event.modifierFlags.intersection(.deviceIndependentFlagsMask) == [.control] { break } let noDelay: Bool = event.modifierFlags.intersection(.deviceIndependentFlagsMask) == [.option] cursorIndex = view.index(mouseSpot: mouseLocationOutsideOfEventStream) - if cursorIndex == .VoidSymbol { - toolTip.clear() - highlightFunctionButton(.VoidSymbol, displayToolTip: .none) - } else { - if cursorIndex != highlightedCandidate && cursorIndex != functionButton { + 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 theme.isLinear && view.candidateInfos[cursorIndex.rawValue].isTruncated { - toolTip.show(withToolTip: view.candidateContents.mutableString.substring(with: view.candidateInfos[cursorIndex.rawValue].candidateRange), display: .now) + if 0 ..< indexRange.count ~= cursorIndex.rawValue, cursorIndex != highlightedCandidate { + if functionButton != .VoidSymbol { + highlightFunctionButton(.VoidSymbol, displayToolTip: .none) + } + if theme.isLinear, view.candidateInfos[cursorIndex.rawValue].isTruncated { + toolTip.showToolTip(view.candidateContents.mutableString.substring(with: view.candidateInfos[cursorIndex.rawValue].candidateRange), display: .now) } else { - toolTip.show(withToolTip: NSLocalizedString("candidate", comment: ""), display: .onRequest ) + toolTip.showToolTip(Bundle.main.localizedString(forKey: "candidate", value: nil, table: "Tooltips"), 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 Set([.PageUpKey, .PageDownKey, .ExpandButton, .BackSpaceKey]).contains(cursorIndex), functionButton != cursorIndex { highlightFunctionButton(cursorIndex, displayToolTip: noDelay ? .now : .delayed) } + } else { + toolTip.clear() + if functionButton != .VoidSymbol { + highlightFunctionButton(.VoidSymbol, displayToolTip: .none) + } } case .mouseExited: cursorIndex = .VoidSymbol @@ -2957,94 +2714,86 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { if event.phase == .began { scrollLocus = .zero scrollByLine = false - } else if event.phase == .changed && scrollLocus.x.isFinite && scrollLocus.y.isFinite { - var scrollDistance: Double = 0.0 + } else if event.phase == .changed, scrollLocus.x.isFinite, scrollLocus.y.isFinite { + 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: Int = 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 - if (view.sectionNum != priorSectionNum || priorCandidate == priorHilitedCandidate) && priorCandidate < indexRange.count { - let labelColor = priorCandidate == priorHilitedCandidate && view.sectionNum == priorSectionNum ? theme.labelForeColor : theme.dimmedLabelForeColor! + if view.sectionNum != priorSectionNum || priorCandidate == priorHilitedCandidate, priorCandidate < indexRange.count { + let labelColor: NSColor = priorCandidate == priorHilitedCandidate && view.sectionNum == priorSectionNum ? theme.labelForeColor : theme.dimmedLabelForeColor! view.candidateContents.addAttribute(.foregroundColor, value: labelColor, range: view.candidateInfos[priorCandidate].labelRange) if priorCandidate == priorHilitedCandidate { view.candidateContents.addAttribute(.foregroundColor, value: theme.textForeColor, range: view.candidateInfos[priorCandidate].textRange) @@ -3052,7 +2801,7 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { } } let newCandidate: Int = i + view.sectionNum * theme.pageSize - if (view.sectionNum != priorSectionNum || newCandidate == highlightedCandidate) && newCandidate < indexRange.count { + if view.sectionNum != priorSectionNum || newCandidate == highlightedCandidate, newCandidate < indexRange.count { view.candidateContents.addAttribute(.foregroundColor, value: newCandidate == highlightedCandidate ? theme.hilitedLabelForeColor : theme.labelForeColor, range: view.candidateInfos[newCandidate].labelRange) if newCandidate == highlightedCandidate { view.candidateContents.addAttribute(.foregroundColor, value: theme.hilitedTextForeColor, range: view.candidateInfos[newCandidate].textRange) @@ -3063,6 +2812,8 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { view.highlightCandidate(highlightedCandidate) } + static private let buttonToolTip: [SquirrelIndex : String] = [.HomeKey: "home", .PageUpKey: "page_up", .EndKey: "end", .PageDownKey: "page_down", .LockButton: "unlock", .CompressButton: "compress", .ExpandButton: "expand", .EscapeKey: "escape", .BackSpaceKey: "delete"] + private func highlightFunctionButton(_ functionButton: SquirrelIndex, displayToolTip display: SquirrelTooltipDisplay) { if self.functionButton == functionButton { return } let theme: SquirrelTheme = view.theme @@ -3075,8 +2826,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,38 +2834,31 @@ 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) 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) 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) 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 + default: break + } + if newFunctionButton != .VoidSymbol, let toolTipKey: String = Self.buttonToolTip[newFunctionButton] { + toolTip.showToolTip(Bundle.main.localizedString(forKey: toolTipKey, value: nil, table: "Tooltips"), display: display) } 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 { $0.frame.contains(IbeamRect.origin) } ?? .main! } // Get the window size, it will be the dirtyRect in SquirrelView.drawRect private func show() { - if !needsRedraw && !initPosition { + if !needsRedraw, !initPosition { isVisible ? displayIfNeeded() : orderFront(nil); return } // Break line if the text is too long, based on screen size. @@ -3128,27 +2871,30 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let sweepVertical: Bool = IbeamRect.width > IbeamRect.height 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) */ - 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 + // remember panel size (fix the top leading anchor of the panel in screen coordiantes) + // but only when the text would expand upstreams (towards the leading and/or top edges) + if theme.rememberSize, view.statusView.isHidden { + 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,102 +2906,65 @@ 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) || windowRect.contains(IbeamRect) || (!screenRect.contains(windowRect) && !screenRect.intersects(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 - } else { - windowRect.origin.y = IbeamRect.minY - kOffsetGap - windowRect.height - } + windowRect.origin.y = IbeamRect.minY - Self.kOffsetGap - screenRect.height * textWidthRatio - border.width * 2 - theme.fullWidth < screenRect.minY.nextUp ? IbeamRect.maxY + Self.kOffsetGap : 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 - } else { - windowRect.origin.x = IbeamRect.minX - kOffsetGap - windowRect.width - } - windowRect.origin.y = IbeamRect.minY + border.width + ceil(theme.fullWidth * 0.5) - windowRect.height + windowRect.origin.x = IbeamRect.minX - Self.kOffsetGap - windowRect.width < screenRect.minX.nextUp ? IbeamRect.maxX + Self.kOffsetGap : IbeamRect.minX - Self.kOffsetGap - windowRect.width + 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) || windowRect.contains(IbeamRect) || (!screenRect.contains(windowRect) && !screenRect.intersects(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 - } else { - windowRect.origin.x = IbeamRect.maxX + kOffsetGap - } + windowRect.origin.x = IbeamRect.minX - Self.kOffsetGap - screenRect.width * textWidthRatio - border.width * 2 - theme.fullWidth > screenRect.minX.nextUp ? IbeamRect.minX - Self.kOffsetGap - windowRect.width : 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 - } else { - windowRect.origin.y = IbeamRect.minY - kOffsetGap - windowRect.height - } - windowRect.origin.x = IbeamRect.maxX - border.width - ceil(theme.fullWidth * 0.5) + windowRect.origin.y = IbeamRect.minY - Self.kOffsetGap - windowRect.height < screenRect.minY.nextUp ? IbeamRect.maxY + Self.kOffsetGap : IbeamRect.minY - Self.kOffsetGap - windowRect.height + windowRect.origin.x = IbeamRect.maxX - border.width - (theme.fullWidth * 0.5).rounded(.up) } } } } else { // 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 - } else { - windowRect.size.width = contentRect.width + border.width * 2 + theme.fullWidth - windowRect.size.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 - } 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.size = theme.isVertical ? NSSize(width: contentRect.height + border.height * 2, height: contentRect.width + border.width * 2 + theme.fullWidth) : NSSize(width: contentRect.width + border.width * 2 + theme.fullWidth, height: contentRect.height + border.height * 2) + // vertically/horizontally centre-align (midY/midX) in screen coordinates + windowRect.origin = sweepVertical ? NSPoint(x: IbeamRect.minX - Self.kOffsetGap - windowRect.width, y: IbeamRect.midY - windowRect.height * 0.5) : 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 +2977,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) @@ -3309,13 +3017,13 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { // voila ! } - func hide() { + @objc func hide() { statusTimer?.invalidate() statusTimer = nil toolTip.hide() orderOut(nil) maxSizeAttained = .zero - initPosition = true + IbeamRect = .zero isExpanded = false sectionNum = 0 } @@ -3333,13 +3041,13 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { if view.statusContents.length > 0 { view.statusContents.deleteCharacters(in: NSRange(location: 0, length: view.statusContents.length)) } - if statusTimer?.isValid ?? false { - statusTimer!.invalidate() + if let timer: Timer = statusTimer, timer.isValid { + timer.invalidate() statusTimer = nil } } else { - if !(statusMessage?.isEmpty ?? true) { - showStatus(message: statusMessage!) + if let message: String = statusMessage, !message.isEmpty { + showStatus(message: message) statusMessage = nil } else if !(statusTimer?.isValid ?? false) { hide() @@ -3350,12 +3058,12 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { let theme: SquirrelTheme = view.theme var rulerAttrsPreedit: NSParagraphStyle? let priorSize: NSSize = !view.candidateInfos.isEmpty || !view.preeditView.isHidden ? view.contentRect.size : .zero - if (indexRange.isEmpty || !updateCandidates) && !preedit.isEmpty && view.preeditContents.length > 0 { + if indexRange.isEmpty || !updateCandidates, !preedit.isEmpty, view.preeditContents.length > 0 { rulerAttrsPreedit = view.preeditContents.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle } 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,28 +3072,28 @@ 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 ? .FullWidthSpace : "\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: Double = (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)) } - if selRange.upperBound < view.preeditContents.length { + if selRange.upperBound < view.preeditContents.length - 1 { view.preeditContents.addAttribute(.kern, value: padding, range: NSRange(location: selRange.upperBound - 1, length: 1)) } } 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)) + if theme.isVertical, let caretPos = caretPos { + 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)) + if let rulerAttrsPreedit = rulerAttrsPreedit { + view.preeditContents.addAttribute(.paragraphStyle, value: rulerAttrsPreedit, range: NSRange(location: 0, length: view.preeditContents.length)) } - if updateCandidates && indexRange.isEmpty { + if updateCandidates, indexRange.isEmpty { sectionNum = 0 } else { view.setPreedit(hilitedPreeditRange: selRange) @@ -3399,17 +3107,18 @@ 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 - let candidate = (idx / theme.pageSize != view.sectionNum ? theme.candidateDimmedTemplate! : idx == highlightedCandidate ? theme.candidateHilitedTemplate : theme.candidateTemplate).mutableCopy() as! NSMutableAttributedString + let candidate: NSMutableAttributedString = (idx / theme.pageSize != view.sectionNum ? theme.candidateDimmedTemplate! : idx == highlightedCandidate ? theme.candidateHilitedTemplate : theme.candidateTemplate).mutableCopy() // plug in enumerator, candidate text and comment into the template let enumRange: NSRange = candidate.mutableString.range(of: "%c") candidate.replaceCharacters(in: enumRange, with: theme.labels[col]) @@ -3418,10 +3127,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 +3145,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 { @@ -3452,15 +3160,19 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { // store final in-candidate locations of label, text, and comment textRange = candidate.mutableString.range(of: text) - if idx > 0 && col == 0 && theme.isLinear && !candidateInfos[idx - 1].isTruncated { + if idx > 0, col == 0, theme.isLinear, !candidateInfos[idx - 1].isTruncated { view.candidateContents.mutableString.append("\n") } - let candidateStart: Int = view.candidateContents.length + var 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) { + if col > 0, !candidateInfos[idx - 1].isTruncated { + view.candidateContents.mutableString.insert("\n", at: candidateStart) + candidateStart += 1 + } 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 { + if idx < indexRange.count - 1 { view.candidateContents.mutableString.append("\n") } view.candidateContents.addAttribute(.paragraphStyle, value: theme.truncatedParagraphStyle!, range: NSRange(location: candidateStart, length: view.candidateContents.length - candidateStart)) @@ -3478,13 +3190,13 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { if theme.isTabular { view.pagingContents.setAttributedString(view.isLocked ? theme.symbolLock! : view.isExpanded ? theme.symbolCompress! : theme.symbolExpand!) } else { - let pageNumString = NSAttributedString(string: "\(pageNum + 1)", attributes: theme.pagingAttrs) + let pageNumString: NSAttributedString = .init(string: "\(pageNum + 1)", attributes: theme.pagingAttrs) view.pagingContents.setAttributedString(theme.isVertical ? pageNumString.horizontalInVerticalForms() : pageNumString) } 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(.FullWidthSpace, at: 1) + view.pagingContents.mutableString.append(.FullWidthSpace) view.pagingContents.append(isLastPage ? theme.symbolForwardStroke! : theme.symbolForwardFill!) } } else if view.pagingContents.length > 0 { @@ -3493,20 +3205,21 @@ 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") - let rulerAttrs = theme.preeditParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + if !preedit.isEmpty, rulerAttrsPreedit == nil || rulerAttrsPreedit!.tabStops[0].location < textWidth.nextDown { + if rulerAttrsPreedit == nil { + view.preeditContents.replaceCharacters(in: NSRange(location: view.preeditContents.length - 2, length: 1), with: "\t") + } + let rulerAttrs: NSMutableParagraphStyle = theme.preeditParagraphStyle.mutableCopy() rulerAttrs.tabStops = [NSTextTab(textAlignment: .right, location: textWidth)] view.preeditContents.addAttribute(.paragraphStyle, value: rulerAttrs, range: NSRange(location: 0, length: view.preeditContents.length)) } - if !theme.isLinear && theme.showPaging { - let rulerAttrsPaging = theme.pagingParagraphStyle.mutableCopy() as! NSMutableParagraphStyle + if !theme.isLinear, theme.showPaging { + let rulerAttrsPaging: NSMutableParagraphStyle = theme.pagingParagraphStyle.mutableCopy() 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,18 +3228,15 @@ 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() } func updateStatus(long: String?, short: String?) { - switch view.theme.statusMessageType { - case .mixed: - statusMessage = short ?? long - case .long: - statusMessage = long - case .short: - statusMessage = short ?? long == nil ? nil : String(long![long!.rangeOfComposedCharacterSequence(at: long!.startIndex)]) + statusMessage = switch view.theme.statusMessageType { + case .mixed: short ?? long + case .long: long + case .short: short ?? (long == nil ? nil : String(long!.first!)) } } @@ -3537,21 +3247,20 @@ final class SquirrelPanel: NSPanel, NSWindowDelegate { view.preeditContents.deleteCharacters(in: NSRange(location: 0, length: view.preeditContents.length)) view.pagingContents.deleteCharacters(in: NSRange(location: 0, length: view.pagingContents.length)) - let attrString = NSAttributedString(string: "\u{3000}\u{2002}" + message, attributes: view.theme.statusAttrs) + let attrString: NSAttributedString = .init(string: "\u{3000}\u{2002}" + message, attributes: view.theme.statusAttrs) view.statusContents.setAttributedString(attrString) 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(timeInterval: Self.kShowStatusDuration, target: self, selector: #selector(hide), userInfo: nil, repeats: false) } private func updateAnnotationHeight(_ height: Double) { @@ -3563,19 +3272,26 @@ 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() } + } + + private func getLocked() { + guard view.theme.isTabular else { return } + let userConfig: SquirrelConfig = .init(.user) + view.isLocked = userConfig.boolValue(for: "var/option/_isLockedTabular") + if view.isLocked { view.isExpanded = userConfig.boolValue(for: "var/option/_isExpandedTabular") } + userConfig.close() + view.sectionNum = 0 } 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 +3303,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