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 04c665697..000000000
Binary files a/Assets.xcassets/rime.imageset/rime.pdf and /dev/null differ
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 @@
+
+
\ 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