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/Info.plist b/Info.plist index c168cbf5f..731795dab 100644 --- a/Info.plist +++ b/Info.plist @@ -7,13 +7,15 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIconFile - RimeIcon.icns + $(ASSETCATALOG_COMPILER_APPICON_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} + CFBundleDisplayName + ${INFOPLIST_KEY_CFBundleDisplayName} CFBundlePackageType APPL CFBundleSignature @@ -121,6 +123,8 @@ LSUIElement 1 + NSSupportsSuddenTermination + NSMainNibFile MainMenu NSPrincipalClass diff --git a/InfoPlist.xcstrings b/InfoPlist.xcstrings index f8d6aa418..85300a162 100644 --- a/InfoPlist.xcstrings +++ b/InfoPlist.xcstrings @@ -2,36 +2,36 @@ "sourceLanguage" : "en", "strings" : { "CFBundleDisplayName" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "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" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -60,7 +60,7 @@ } }, "im.rime.inputmethod.Squirrel" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -89,7 +89,7 @@ } }, "im.rime.inputmethod.Squirrel.Cant" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -118,7 +118,7 @@ } }, "im.rime.inputmethod.Squirrel.Hans" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -147,7 +147,7 @@ } }, "im.rime.inputmethod.Squirrel.Hant" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -176,7 +176,7 @@ } }, "NSHumanReadableCopyright" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -206,4 +206,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Notifications.xcstrings b/Notifications.xcstrings new file mode 100644 index 000000000..dc18f0729 --- /dev/null +++ b/Notifications.xcstrings @@ -0,0 +1,209 @@ +{ + "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" : "更新輸入法引擎⋯" + } + } + } + }, + "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" : "鼠鬚筆" + } + } + } + } + }, + "version" : "1.0" +} \ 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 19fb287c4..9e831bebc 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -80,7 +80,6 @@ A4B8E1B30F645B870094E08B /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4B8E1B20F645B870094E08B /* Carbon.framework */; }; E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; F42760AE2C07A2F60050B08A /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F42760AD2C07A2F60050B08A /* InfoPlist.xcstrings */; }; - F42760B02C07A2F60050B08A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F42760AF2C07A2F60050B08A /* Localizable.xcstrings */; }; F48CFB6B2B327A2E00DB9CF9 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447765C725C30E6B002415AF /* Sparkle.framework */; }; F493BF7B2B76F28A008BD7D0 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */; }; F49FEC632B8FA4E0009DDC32 /* EmojiCategoryEN.ocd2 in Copy opencc Files */ = {isa = PBXBuildFile; fileRef = F49FEC412B8FA3FB009DDC32 /* EmojiCategoryEN.ocd2 */; }; @@ -105,6 +104,8 @@ F4EC47A32B323223004862A4 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97324FDCFA39411CA2CEA /* AppKit.framework */; }; F4EC47A42B32322F004862A4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97325FDCFA39411CA2CEA /* Foundation.framework */; }; F4EC47A82B3233D4004862A4 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4EC47A72B3233D0004862A4 /* IOKit.framework */; }; + F4FDF9262C25653200A8E629 /* Notifications.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F4FDF9242C25653100A8E629 /* Notifications.xcstrings */; }; + F4FDF9272C25653200A8E629 /* Tooltips.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = F4FDF9252C25653100A8E629 /* Tooltips.xcstrings */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -317,7 +318,6 @@ A4B8E1B20F645B870094E08B /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; F42760AD2C07A2F60050B08A /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; - F42760AF2C07A2F60050B08A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; F42760B12C07A2F60050B08A /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/MainMenu.xcstrings; sourceTree = ""; }; F493BF7A2B76F27E008BD7D0 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; F49FEC412B8FA3FB009DDC32 /* EmojiCategoryEN.ocd2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = EmojiCategoryEN.ocd2; sourceTree = ""; }; @@ -340,6 +340,8 @@ F4EC479F2B323203004862A4 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; F4EC47A12B32320B004862A4 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; F4EC47A72B3233D0004862A4 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; + F4FDF9242C25653100A8E629 /* Notifications.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = Notifications.xcstrings; sourceTree = ""; }; + F4FDF9252C25653100A8E629 /* Tooltips.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = Tooltips.xcstrings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -434,7 +436,8 @@ 44986A93184B421700B3278D /* LICENSE.txt */, 44986A94184B421700B3278D /* README.md */, 44F7708E152B3334005CF491 /* dsa_pub.pem */, - F42760AF2C07A2F60050B08A /* Localizable.xcstrings */, + F4FDF9242C25653100A8E629 /* Notifications.xcstrings */, + F4FDF9252C25653100A8E629 /* Tooltips.xcstrings */, 8D1107310486CEB800E47090 /* Info.plist */, F42760AD2C07A2F60050B08A /* InfoPlist.xcstrings */, A45578F41146A75200592C6E /* MainMenu.xib */, @@ -599,7 +602,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1600; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Squirrel" */; compatibilityVersion = "Xcode 15.0"; @@ -630,11 +633,12 @@ files = ( A45578F51146A75200592C6E /* MainMenu.xib in Resources */, 446C01D71F767BD400A6C23E /* Assets.xcassets in Resources */, + F4FDF9272C25653200A8E629 /* Tooltips.xcstrings in Resources */, 44986A95184B421700B3278D /* LICENSE.txt in Resources */, 44986A96184B421700B3278D /* README.md in Resources */, + F4FDF9262C25653200A8E629 /* Notifications.xcstrings in Resources */, F42760AE2C07A2F60050B08A /* InfoPlist.xcstrings in Resources */, 44F7708F152B3334005CF491 /* dsa_pub.pem in Resources */, - F42760B02C07A2F60050B08A /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -677,19 +681,17 @@ CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; - CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0.18.0t; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", @@ -725,8 +727,8 @@ 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; WRAPPER_EXTENSION = app; }; @@ -739,18 +741,16 @@ CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; - CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 0.18.0t; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", @@ -785,8 +785,8 @@ 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; WRAPPER_EXTENSION = app; }; @@ -795,13 +795,14 @@ C01FCF4F08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; + ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_ARC_EXCEPTIONS = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; @@ -835,7 +836,6 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; 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_NEWLINE = YES; @@ -866,6 +866,7 @@ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; }; name = Debug; @@ -873,13 +874,14 @@ C01FCF5008A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; + ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_APPICON_NAME = RimeIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_ARC_EXCEPTIONS = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; @@ -913,7 +915,6 @@ DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = 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_NEWLINE = YES; @@ -941,6 +942,7 @@ MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; + STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; 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 36681daf7..40582026c 100644 --- a/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme +++ b/Squirrel.xcodeproj/xcshareddata/xcschemes/Squirrel.xcscheme @@ -1,6 +1,6 @@ -- (bool)boolValueForKey:(NSString* _Nonnull)key; -- (int)intValueForKey:(NSString* _Nonnull)key; -- (double)doubleValueForKey:(NSString* _Nonnull)key; +- (bool)boolValueForOption:(NSString* _Nonnull)option; +- (int)intValueForOption:(NSString* _Nonnull)option; +- (double)doubleValueForOption:(NSString* _Nonnull)option; @end // SquirrelAppOptions @@ -46,7 +46,7 @@ __attribute__((objc_direct_members)) @property(nonatomic, strong, readonly, nullable) NSString* schemaId; @property(nonatomic, strong, nonnull) NSString* colorSpace; -- (instancetype _Nonnull)initWithArg:(NSString* _Nonnull)arg; +- (instancetype _Nonnull)initWithType:(NSString* _Nonnull)arg; - (BOOL)openBaseConfig; - (BOOL)openWithSchemaId:(NSString* _Nonnull)schemaId baseConfig:(SquirrelConfig* _Nullable)config; diff --git a/SquirrelConfig.mm b/SquirrelConfig.mm index df2ca582a..94d5e55ed 100644 --- a/SquirrelConfig.mm +++ b/SquirrelConfig.mm @@ -121,24 +121,24 @@ - (void)updateWithRimeSession:(RimeSessionId)session { @implementation SquirrelAppOptions -- (bool)boolValueForKey:(NSString*)key { - if (NSNumber* value = self[key]; +- (bool)boolValueForOption:(NSString*)option { + if (NSNumber* value = self[option]; value != nil && strcmp(value.objCType, @encode(BOOL)) == 0) { return value.boolValue; } return NO; } -- (int)intValueForKey:(NSString*)key { - if (NSNumber* value = self[key]; +- (int)intValueForOption:(NSString*)option { + if (NSNumber* value = self[option]; value != nil && strcmp(value.objCType, @encode(int)) == 0) { return value.intValue; } return 0; } -- (double)doubleValueForKey:(NSString*)key { - if (NSNumber* value = self[key]; +- (double)doubleValueForOption:(NSString*)option { + if (NSNumber* value = self[option]; value != nil && strcmp(value.objCType, @encode(double)) == 0) { return value.doubleValue; } @@ -192,19 +192,19 @@ - (instancetype)init { return self; } -- (instancetype)initWithArg:(NSString*)arg { +- (instancetype)initWithType:(NSString*)type { if (self = [super init]) { _cache = NSCache.alloc.init; _colorSpace = NSColorSpace.sRGBColorSpace; _colorSpaceName = @"sRGB"; - if ([arg isEqualToString:@"squirrel"]) { + if ([type isEqualToString:@".squirrel"] || [type isEqualToString:@".base"]) { [self openBaseConfig]; - } else if ([arg isEqualToString:@"default"]) { - [self openWithConfigId:arg]; - } else if ([arg isEqualToString:@"user"] || [arg isEqualToString:@"installation"]) { - [self openUserConfig:arg]; + } else if ([type isEqualToString:@".default"]) { + [self openWithConfigId:@"default"]; + } else if ([type isEqualToString:@".user"] || [type isEqualToString:@".installation"]) { + [self openUserConfig:[type substringFromIndex:1]]; } else { - [self openWithSchemaId:arg baseConfig:nil]; + [self openWithSchemaId:type baseConfig:[SquirrelConfig.alloc initWithType:@".base"]]; } } return self; @@ -222,11 +222,7 @@ - (BOOL)openWithSchemaId:(NSString*)schemaId _isOpen = rime_get_api_stdbool()->schema_open(schemaId.UTF8String, &_config); if (_isOpen) { _schemaId = schemaId; - if (baseConfig == nil) { - _baseConfig = [SquirrelConfig.alloc initWithArg:@"squirrel"]; - } else { - _baseConfig = baseConfig; - } + _baseConfig = baseConfig; } return _isOpen; } @@ -245,10 +241,10 @@ - (BOOL)openWithConfigId:(NSString*)configId { - (void)close { if (_isOpen && rime_get_api_stdbool()->config_close(&_config)) { - _baseConfig = nil; - _schemaId = nil; _isOpen = NO; } + _baseConfig = nil; + _schemaId = nil; } - (void)dealloc { @@ -572,13 +568,14 @@ - (SquirrelOptionSwitcher*)optionSwitcherForSchema { } - (SquirrelAppOptions*)appOptionsForApp:(NSString*)bundleId { - if (SquirrelAppOptions* cachedValue = [self cachedValueOfClass:SquirrelAppOptions.class forKey:bundleId]) { + NSString* rootKey = [@"app_options/" append:bundleId]; + if (SquirrelAppOptions* cachedValue = [self cachedValueOfClass:SquirrelAppOptions.class forKey:rootKey]) { return cachedValue; } - NSString* rootKey = [@"app_options/" append:bundleId]; NSMutableDictionary* appOptions = NSMutableDictionary.alloc.init; RimeConfigIterator iterator; if (!rime_get_api_stdbool()->config_begin_map(&iterator, &_config, rootKey.UTF8String)) { + [_cache setObject:appOptions forKey:rootKey]; return appOptions.copy; } while (rime_get_api_stdbool()->config_next(&iterator)) { @@ -590,7 +587,7 @@ - (SquirrelAppOptions*)appOptionsForApp:(NSString*)bundleId { } } rime_get_api_stdbool()->config_end(&iterator); - [_cache setObject:appOptions forKey:bundleId]; + [_cache setObject:appOptions forKey:rootKey]; return appOptions.copy; } diff --git a/SquirrelInputController.hh b/SquirrelInputController.hh index 13c19966a..67959e73e 100644 --- a/SquirrelInputController.hh +++ b/SquirrelInputController.hh @@ -32,6 +32,7 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { }; @property(nonatomic, readonly, weak, nullable, direct, class) SquirrelInputController* currentController; +@property(nonatomic, direct, class) NSTimeInterval chordDuration; @property(nonatomic, readonly, strong, nonnull) NSAppearance* viewEffectiveAppearance API_AVAILABLE(macos(10.14)); @property(nonatomic, readonly, strong, nonnull, direct) NSMutableArray* candidateTexts; @property(nonatomic, readonly, strong, nonnull, direct) NSMutableArray* candidateComments; diff --git a/SquirrelInputController.mm b/SquirrelInputController.mm index 891a9601e..2ec88ae7a 100644 --- a/SquirrelInputController.mm +++ b/SquirrelInputController.mm @@ -12,6 +12,7 @@ static NSString* const kFullWidthSpace = @" "; static const int N_KEY_ROLL_OVER = 50; +static const NSTimeInterval kStatusDelay = 0.2; @implementation SquirrelInputController { NSMutableAttributedString* _inlineString; @@ -30,14 +31,15 @@ @implementation SquirrelInputController { BOOL _inlineCandidate; BOOL _goodOldCapsLock; BOOL _showingSwitcherMenu; + BOOL _showingInitialStatus; // app-specific options and bug fix SquirrelAppOptions* _appOptions; BOOL _inlinePlaceholder; BOOL _panellessCommitFix; - int _inlineOffset; + double _inlineOffset; // for chord-typing NSTimer* _chordTimer; - NSTimeInterval _chordDuration; + int _chordKeyCodes[N_KEY_ROLL_OVER]; int _chordModifiers[N_KEY_ROLL_OVER]; int _chordKeyCount; @@ -45,6 +47,7 @@ @implementation SquirrelInputController { static SquirrelInputController* __weak _currentController = nil; static NSString* _currentApp; +static NSTimeInterval _chordDuration = 0.1; static int _asciiMode = -1; + (void)setCurrentController:(SquirrelInputController*)controller { @@ -56,6 +59,14 @@ + (SquirrelInputController*)currentController { return _currentController; } ++ (void)setChordDuration:(NSTimeInterval)chordDuration { + _chordDuration = chordDuration; +} + ++ (NSTimeInterval)chordDuration { + return _chordDuration; +} + - (NSAppearance*)viewEffectiveAppearance API_AVAILABLE(macos(10.14)) { return [self.client performSelector: @selector(viewEffectiveAppearance)] ? : NSApp.effectiveAppearance; @@ -65,17 +76,13 @@ - (NSAppearance*)viewEffectiveAppearance API_AVAILABLE(macos(10.14)) { return [NSSet setWithObjects:@"client.viewEffectiveAppearance", nil]; } -/*! - @method - @abstract Receive incoming event - @discussion This method receives key events from the client application. - */ +/** - Receive incoming event: + - Return `YES` 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 `NO` means the original key down will be passed on to the client. */ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender { - // Return YES 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 NO means the original key down will be passed on to the client. BOOL handled = NO; @autoreleasepool { @@ -211,12 +218,12 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index lineHeightRectangle:NULL][@"IMKBaseline"] pointValue]; NSPoint tail = [[sender attributesForCharacterIndex:markedRange.length - 1 lineHeightRectangle:NULL][@"IMKBaseline"] pointValue]; - if (point.x > tail.x || index >= markedRange.length) { + if (point.x > nexttoward(tail.x, INFINITY) || index >= markedRange.length) { if (_inlineCandidate && !_inlinePreedit) { return NO; } [self performAction:kPROCESS onIndex:kEndKey]; - } else if (point.x < head.x || index <= 0) { + } else if (point.x < nexttoward(head.x, -INFINITY) || index <= 0) { [self performAction:kPROCESS onIndex:kHomeKey]; } else { [self moveCursor:_inlineCaretPos @@ -403,18 +410,18 @@ - (void)performAction:(SquirrelAction)action - (void)onChordTimer:(NSTimer*)timer { // chord release triggered by timer - int processed_keys = 0; + int processedKeyCount = 0; if (_chordKeyCount > 0 && _session != 0) { // simulate key-ups for (int i = 0; i < _chordKeyCount; ++i) { if (rime_get_api_stdbool()->process_key(_session, _chordKeyCodes[i], - (_chordModifiers[i] | kReleaseMask))) { - ++processed_keys; + _chordModifiers[i] | kReleaseMask)) { + ++processedKeyCount; } } } [self clearChord]; - if (processed_keys > 0) { + if (processedKeyCount > 0) { [self rimeUpdate]; } } @@ -474,8 +481,7 @@ - (NSUInteger)recognizedEvents:(id)sender { - (void)showInitialStatus __attribute__((objc_direct)) { RIME_STRUCT(RimeStatus_stdbool, status); if (_session != 0 && rime_get_api_stdbool()->get_status(_session, &status)) { - _schemaId = @(status.schema_id); - NSString* schemaName = status.schema_name ? @(status.schema_name) : @(status.schema_id); + NSString* schemaName = @(status.schema_name ? : status.schema_id); NSMutableArray* options = [NSMutableArray.alloc initWithCapacity:3]; if (NSString* asciiMode = getOptionLabel(_session, "ascii_mode", status.is_ascii_mode)) { [options addObject:asciiMode]; @@ -488,12 +494,14 @@ - (void)showInitialStatus __attribute__((objc_direct)) { } rime_get_api_stdbool()->free_status(&status); NSString* foldedOptions = options.count == 0 ? schemaName : - [NSString stringWithFormat:@"%@|%@", schemaName, [options componentsJoinedByString:@" "]]; + [NSString stringWithFormat:@"%@ │ %@", schemaName, [options componentsJoinedByString:@" "]]; [NSApp.squirrelAppDelegate.panel updateStatusLong:foldedOptions statusShort:schemaName]; if (@available(macOS 14.0, *)) { - _lastModifiers |= NSEventModifierFlagHelp; + _showingInitialStatus = YES; } - [self rimeUpdate]; + [NSTimer scheduledTimerWithTimeInterval:kStatusDelay repeats:NO block:^(NSTimer * _Nonnull timer) { + [self rimeUpdate]; + }]; } } @@ -505,7 +513,7 @@ - (void)activateServer:(id)sender { options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:nil]; - SquirrelConfig* baseConfig = [SquirrelConfig.alloc initWithArg:@"squirrel"]; + SquirrelConfig* baseConfig = [SquirrelConfig.alloc initWithType:@".base"]; NSString* keyboardLayout = [baseConfig stringForOption:@"keyboard_layout"]; if ([@"last" caseInsensitiveCompare:keyboardLayout] == NSOrderedSame || [keyboardLayout isEqualToString:@""]) { @@ -520,7 +528,7 @@ - (void)activateServer:(id)sender { } [baseConfig close]; - SquirrelConfig* defaultConfig = [SquirrelConfig.alloc initWithArg:@"default"]; + SquirrelConfig* defaultConfig = [SquirrelConfig.alloc initWithType:@".default"]; if ([defaultConfig hasSection:@"ascii_composer"]) { _goodOldCapsLock = [defaultConfig boolValueForOption: @"ascii_composer/good_old_caps_lock"]; @@ -646,7 +654,6 @@ - (void)hidePalettes { - (void)dealloc { // NSLog(@"dealloc"); [self destroySession]; - [self clearBuffer]; } - (NSRange)selectionRange { @@ -718,11 +725,18 @@ - (void)showInlineString:(NSString*)inlineString [self updateComposition]; } -- (CGRect)getIbeamRect __attribute__((objc_direct)) { +NS_INLINE NSRect NSMakeRect(NSPoint origin, NSSize size) { + NSRect r; + r.origin = origin; + r.size = size; + return r; +} + +- (NSRect)getIbeamRect __attribute__((objc_direct)) { NSRect IbeamRect = NSZeroRect; [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect]; - if (NSEqualRects(IbeamRect, NSZeroRect) && _inlineString.length == 0) { + if (NSIsEmptyRect(IbeamRect) && _inlineString.length == 0) { if (self.client.selectedRange.length == 0) { // activate inline session, in e.g. table cells, by fake inputs [self.client setMarkedText:@" " @@ -739,38 +753,39 @@ - (CGRect)getIbeamRect __attribute__((objc_direct)) { } } if (NSIsEmptyRect(IbeamRect)) { - return IbeamRect; + return NSMakeRect(NSEvent.mouseLocation, NSZeroSize); + } + BOOL sweepVertical = NSWidth(IbeamRect) > NSHeight(IbeamRect); + if (isnormal(_inlineOffset)) { + IbeamRect = NSOffsetRect(IbeamRect, sweepVertical ? _inlineOffset : 0.0, sweepVertical ? 0.0 : _inlineOffset); } - NSWidth(IbeamRect) > NSHeight(IbeamRect) ? IbeamRect.origin.x += _inlineOffset - : IbeamRect.origin.y += _inlineOffset; - if (@available(macOS 14.0, *)) { // avoid overlapping with cursor effects view + if (@available(macOS 14.0, *)) { + // avoid overlapping with cursor effects view if ((_goodOldCapsLock && (_lastModifiers & NSEventModifierFlagCapsLock) != 0) || (_lastModifiers & NSEventModifierFlagHelp) != 0) { - _lastModifiers &= ~NSEventModifierFlagHelp; - NSRect screenRect = NSScreen.mainScreen.frame; - if (NSIntersectsRect(IbeamRect, screenRect)) { - screenRect = NSScreen.mainScreen.visibleFrame; - if (NSWidth(IbeamRect) > NSHeight(IbeamRect)) { - NSRect capslockAccessory = NSMakeRect(NSMinX(IbeamRect) - 30, NSMinY(IbeamRect), - 27, NSHeight(IbeamRect)); - if (NSMinX(capslockAccessory) < NSMinX(screenRect)) { - capslockAccessory.origin.x = NSMinX(screenRect); - } - if (NSMaxX(capslockAccessory) > NSMaxX(screenRect)) { - capslockAccessory.origin.x = NSMaxX(screenRect) - NSWidth(capslockAccessory); - } - IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); - } else { - NSRect capslockAccessory = NSMakeRect(NSMinX(IbeamRect), NSMinY(IbeamRect) - 26, - NSWidth(IbeamRect), 23); - if (NSMinY(capslockAccessory) < NSMinY(screenRect)) { - capslockAccessory.origin.y = NSMaxY(screenRect) + 3; - } - if (NSMaxY(capslockAccessory) > NSMaxY(screenRect)) { - capslockAccessory.origin.y = NSMaxY(screenRect) - NSHeight(capslockAccessory); - } - IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); + if (_showingInitialStatus) + _showingInitialStatus = NO; + NSRect screenRect = NSScreen.mainScreen.visibleFrame; + if (sweepVertical) { + NSRect capslockAccessory = NSMakeRect(NSMinX(IbeamRect) - 30, NSMinY(IbeamRect), + 27, NSHeight(IbeamRect)); + if (NSMinX(capslockAccessory) < nexttoward(NSMinX(screenRect), INFINITY)) { + capslockAccessory.origin.x = NSMinX(screenRect); + } + if (NSMaxX(capslockAccessory) > nexttoward(NSMaxX(screenRect), -INFINITY)) { + capslockAccessory.origin.x = NSMaxX(screenRect) - NSWidth(capslockAccessory); + } + IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); + } else { + NSRect capslockAccessory = NSMakeRect(NSMinX(IbeamRect), NSMinY(IbeamRect) - 26, + NSWidth(IbeamRect), 23); + if (NSMinY(capslockAccessory) < nexttoward(NSMinY(screenRect), INFINITY)) { + capslockAccessory.origin.y = NSMaxY(screenRect) + 3; } + if (NSMaxY(capslockAccessory) > nexttoward(NSMaxY(screenRect), -INFINITY)) { + capslockAccessory.origin.y = NSMaxY(screenRect) - NSHeight(capslockAccessory); + } + IbeamRect = NSUnionRect(IbeamRect, capslockAccessory); } } } @@ -787,8 +802,10 @@ - (void)showPanelWithPreedit:(NSString*)preedit didCompose:(BOOL)didCompose __attribute__((objc_direct)) { // NSLog(@"showPanelWithPreedit:...:"); SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; - panel.IbeamRect = [self getIbeamRect]; - if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) { + if (NSEqualRects(panel.IbeamRect, NSZeroRect)) { + panel.IbeamRect = self.getIbeamRect; + } + if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage != nil) { [panel updateStatusLong:nil statusShort:nil]; } else { [panel showPreedit:preedit @@ -808,16 +825,18 @@ - (void)createSession __attribute__((objc_direct)) { NSString* app = self.client.bundleIdentifier; // NSLog(@"createSession: %@", app); _session = rime_get_api_stdbool()->create_session(); - _schemaId = nil; + SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; + _schemaId = panel.optionSwitcher.schemaId.copy; if (_session != 0) { - SquirrelConfig* config = [SquirrelConfig.alloc initWithArg:@"squirrel"]; + SquirrelConfig* config = [SquirrelConfig.alloc initWithType:@".base"]; _appOptions = [config appOptionsForApp:app]; - CGFloat chordDuration = [[config nullableDoubleForOption:@"chord_duration"] doubleValue]; - _chordDuration = chordDuration > 0 ? chordDuration : 0.1; [config close]; - _panellessCommitFix = [_appOptions boolValueForKey:@"panelless_commit_fix"]; - _inlinePlaceholder = [_appOptions boolValueForKey:@"inline_placeholder"]; - _inlineOffset = [_appOptions intValueForKey:@"inline_offset"]; + _inlinePreedit = (panel.inlinePreedit && ![_appOptions boolValueForOption:@"no_inline"]) || [_appOptions boolValueForOption:@"inline"]; + _inlineCandidate = panel.inlineCandidate && ![_appOptions boolValueForOption:@"no_inline"]; + rime_get_api_stdbool()->set_option(_session, "soft_cursor", !_inlinePreedit); + _panellessCommitFix = [_appOptions boolValueForOption:@"panelless_commit_fix"]; + _inlinePlaceholder = [_appOptions boolValueForOption:@"inline_placeholder"]; + _inlineOffset = [_appOptions intValueForOption:@"inline_offset"]; if ([app isEqualToString:_currentApp] && _asciiMode >= 0) { rime_get_api_stdbool()->set_option(_session, "ascii_mode", _asciiMode); } @@ -860,14 +879,6 @@ static inline NSUInteger UnicharCount(const char* cString, int length) { encoding:NSUTF8StringEncoding].length; } -static inline NSUInteger fmin(NSUInteger x, NSUInteger y) { - return x < y ? x : y; -} - -static inline NSUInteger fmax(NSUInteger x, NSUInteger y) { - return x < y ? y : x; -} - - (void)rimeUpdate __attribute__((objc_direct)) { // NSLog(@"rimeUpdate"); BOOL didCommit = self.rimeConsumeCommittedText; @@ -877,7 +888,7 @@ - (void)rimeUpdate __attribute__((objc_direct)) { RIME_STRUCT(RimeStatus_stdbool, status); if (rime_get_api_stdbool()->get_status(_session, &status)) { // enable schema specific ui style - if (_schemaId == nil || strcmp(_schemaId.UTF8String, status.schema_id) != 0) { + if (strcmp(_schemaId.UTF8String, status.schema_id) != 0) { _schemaId = @(status.schema_id); _showingSwitcherMenu = rime_get_api_stdbool()->get_option(_session, "dumb"); if (!_showingSwitcherMenu) { @@ -885,9 +896,9 @@ - (void)rimeUpdate __attribute__((objc_direct)) { [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId withRimeSession:_session]; // inline preedit - _inlinePreedit = (panel.inlinePreedit && ![_appOptions boolValueForKey:@"no_inline"]) || - [_appOptions boolValueForKey:@"inline"]; - _inlineCandidate = panel.inlineCandidate && ![_appOptions boolValueForKey:@"no_inline"]; + _inlinePreedit = (panel.inlinePreedit && ![_appOptions boolValueForOption:@"no_inline"]) || + [_appOptions boolValueForOption:@"inline"]; + _inlineCandidate = panel.inlineCandidate && ![_appOptions boolValueForOption:@"no_inline"]; // if not inline, embed soft cursor in preedit string rime_get_api_stdbool()->set_option(_session, "soft_cursor", !_inlinePreedit); } else { @@ -900,7 +911,7 @@ - (void)rimeUpdate __attribute__((objc_direct)) { RIME_STRUCT(RimeContext_stdbool, ctx); if (rime_get_api_stdbool()->get_context(_session, &ctx)) { - BOOL showingStatus = panel.statusMessage.length > 0; + BOOL showingStatus = panel.statusMessage != nil; // update preedit text const char* preedit = ctx.composition.preedit; NSString* preeditText = @(preedit ? : ""); @@ -975,9 +986,7 @@ - (void)rimeUpdate __attribute__((objc_direct)) { numCandidates + extraCandidates); _currentIndex = hilitedCandidate + _candidateIndices.location; - if (showingStatus) { - [self clearBuffer]; - } else if (_showingSwitcherMenu) { + if (_showingSwitcherMenu) { if (_inlinePlaceholder) { [self updateComposition]; } diff --git a/SquirrelPanel.hh b/SquirrelPanel.hh index 5dfff5de5..62d6b2d0f 100644 --- a/SquirrelPanel.hh +++ b/SquirrelPanel.hh @@ -5,27 +5,27 @@ @interface SquirrelPanel : NSPanel -// Show preedit text inline. +/// Show preedit text inline. @property(nonatomic, readonly, direct) BOOL inlinePreedit; -// Show primary candidate inline +/// Show primary candidate inline. @property(nonatomic, readonly, direct) BOOL inlineCandidate; -// Vertical text orientation, as opposed to horizontal text orientation. +/// Vertical text orientation, as opposed to horizontal text orientation. @property(nonatomic, readonly, direct) BOOL vertical; -// Linear candidate list layout, as opposed to stacked candidate list layout. +/// Linear candidate list layout, as opposed to stacked candidate list layout. @property(nonatomic, readonly, direct) BOOL linear; -// 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. @property(nonatomic, readonly, direct) BOOL tabular; @property(nonatomic, readonly, direct) BOOL locked; @property(nonatomic, readonly, direct) BOOL firstLine; @property(nonatomic, direct) BOOL expanded; @property(nonatomic, direct) NSUInteger sectionNum; -// position of the text input I-beam cursor on screen. +/// Position of the text input I-beam cursor on screen. @property(nonatomic, direct) NSRect IbeamRect; @property(nonatomic, readonly, strong, nullable) NSScreen* screen; -// Status message before pop-up is displayed; nil before normal panel is displayed +/// Status message before pop-up is displayed; nil before normal panel is displayed. @property(nonatomic, readonly, strong, nullable, direct) NSString* statusMessage; -// Store switch options that change style (color theme) settings +/// Stores switch options that change style (color theme) settings. @property(nonatomic, strong, nonnull, direct) SquirrelOptionSwitcher* optionSwitcher; // query @@ -51,3 +51,10 @@ - (void)updateScriptVariant __attribute__((objc_direct)); @end // SquirrelPanel + +extern inline NSUInteger fmin(NSUInteger x, NSUInteger y) { + return x < y ? x : y; +} +extern inline NSUInteger fmax(NSUInteger x, NSUInteger y) { + return x < y ? y : x; +} diff --git a/SquirrelPanel.mm b/SquirrelPanel.mm index be8ea4de9..5945cdcf0 100644 --- a/SquirrelPanel.mm +++ b/SquirrelPanel.mm @@ -4,9 +4,9 @@ #import "SquirrelConfig.hh" #import -static NSString* const kDefaultCandidateFormat = @"%c. %@"; -static NSString* const kTipSpecifier = @"%s"; +static NSString* const kDefaultCandidateFormat = @"%c. %@ %s"; static NSString* const kFullWidthSpace = @" "; +static NSString* const kControlCharacterSizeAttributeName = @"ControlCharacterSize"; static const NSTimeInterval kShowStatusDuration = 2.0; static const CGFloat kBlendedBackgroundColorFraction = 0.2; static const CGFloat kDefaultFontSize = 24; @@ -25,9 +25,8 @@ static void rectVertices(NSRect rect, NSPointArray vertices) { } typedef struct SquirrelTextPolygon { - NSRect head; - NSRect body; - NSRect tail; + NSRect head, body, tail; + inline NSPoint origin() { return (NSIsEmptyRect(head) ? body : head).origin; } @@ -39,17 +38,15 @@ inline CGFloat maxY() { } inline BOOL separated() { return !NSIsEmptyRect(head) && NSIsEmptyRect(body) && - !NSIsEmptyRect(tail) && NSMaxX(tail) < NSMinX(head) - 0.1; + !NSIsEmptyRect(tail) && NSMaxX(tail) < nexttoward(NSMinX(head), -INFINITY); } inline BOOL mouseInPolygon(NSPoint point, BOOL flipped) { return (!NSIsEmptyRect(body) && NSMouseInRect(point, body, flipped)) || - (!NSIsEmptyRect(head) && NSMouseInRect(point, head, flipped)) || - (!NSIsEmptyRect(tail) && NSMouseInRect(point, tail, flipped)); + (!NSIsEmptyRect(head) && NSMouseInRect(point, head, flipped)) || + (!NSIsEmptyRect(tail) && NSMouseInRect(point, tail, flipped)); } void getVertices(NSPointArray vertices) { - switch ((NSIsEmptyRect(head) << 2) | - (NSIsEmptyRect(body) << 1) | - (NSIsEmptyRect(tail) << 0)) { + switch ((NSIsEmptyRect(head) << 2) | (NSIsEmptyRect(body) << 1) | (NSIsEmptyRect(tail) << 0)) { case 0b011: rectVertices(head, vertices); break; @@ -120,6 +117,27 @@ void getVertices(NSPointArray vertices) { } SquirrelTextPolygon; +__attribute__((objc_direct_members)) +@interface NSCharacterSet (FullWidthCharacterSets) + +@property(nonatomic, readonly, copy, nonnull, class) NSCharacterSet *fullWidthDigitCharacterSet; +@property(nonatomic, readonly, copy, nonnull, class) NSCharacterSet *fullWidthLatinCapitalCharacterSet; + +@end + +@implementation NSCharacterSet (FullWidthCharacterSets) + ++ (NSCharacterSet *)fullWidthDigitCharacterSet { + return [NSCharacterSet characterSetWithRange:NSMakeRange(0xFF10, 10)]; +} + ++ (NSCharacterSet *)fullWidthLatinCapitalCharacterSet { + return [NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)]; +} + +@end // NSCharacterSet (FullWidthCharacterSets) + + __attribute__((objc_direct_members)) @interface NSAffineTransform (NSCGAffinTransformConversion) @@ -150,8 +168,6 @@ - (CGPathRef)quartzPath { if (@available(macOS 14.0, *)) { return self.CGPath; } - // Need to begin a path here. - CGPathRef immutablePath = NULL; // Then draw the path elements. if (NSInteger numElements = self.elementCount; numElements > 0) { CGMutablePathRef path = CGPathCreateMutable(); @@ -165,92 +181,56 @@ - (CGPathRef)quartzPath { CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); break; case NSBezierPathElementCurveTo: - CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, - points[1].x, points[1].y, points[2].x, points[2].y); + CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y); break; case NSBezierPathElementQuadraticCurveTo: - CGPathAddQuadCurveToPoint(path, NULL, points[0].x, points[0].y, - points[1].x, points[1].y); + CGPathAddQuadCurveToPoint(path, NULL, points[0].x, points[0].y, points[1].x, points[1].y); break; case NSBezierPathElementClosePath: CGPathCloseSubpath(path); break; } } - immutablePath = (CGPathRef)CFAutorelease(CGPathCreateCopy(path)); + CGPathRef immutablePath = CGPathCreateCopy(path); CGPathRelease(path); + return (CGPathRef)CFAutorelease(immutablePath); } - return immutablePath; + return NULL; } // Bezier squircle curves, whose rounded corners are smooth (continously differentiable) + (NSBezierPath*)squirclePathWithVertices:(NSPointArray)vertices - count:(NSUInteger)numVert - cornerRadius:(CGFloat)radius { - if (vertices == NULL || numVert < 4) { + count:(NSUInteger)numVertex + cornerRadius:(CGFloat)cornerRadius { + if (vertices == NULL || numVertex < 4) { return nil; } NSBezierPath* path = NSBezierPath.bezierPath; // Always start from the topleft origin going along y axis - NSPoint point = vertices[numVert - 1]; - NSPoint nextPoint = vertices[0]; - CGVector nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + NSPoint vertex = vertices[numVertex - 1]; + NSPoint nextVertex = vertices[0]; + CGVector nextDiff = CGVectorMake(nextVertex.x - vertex.x, nextVertex.y - vertex.y); CGVector lastDiff; - CGFloat arcRadius = fmin(radius, fabs(nextDiff.dx) * 0.3); + CGFloat arcRadius; NSPoint startPoint; - NSPoint relayPointA, relayPointB; - NSPoint controlPointA1, controlPointA2, controlPointB1, controlPointB2; - NSPoint controlPoint1, controlPoint2; - NSPoint endPoint = NSMakePoint(point.x + copysign(arcRadius * 1.528664, nextDiff.dx), nextPoint.y); + NSPoint endPoint = NSMakePoint(vertex.x + nextDiff.dx * 0.5, vertex.y); [path moveToPoint:endPoint]; - for (NSUInteger i = 0; i < numVert; ++i) { + for (NSUInteger i = 0; i < numVertex; ++i) { lastDiff = nextDiff; - point = nextPoint; - nextPoint = vertices[(i + 1) % numVert]; - nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + vertex = nextVertex; + nextVertex = vertices[(i + 1) % numVertex]; + nextDiff = CGVectorMake(nextVertex.x - vertex.x, nextVertex.y - vertex.y); if (fabs(nextDiff.dx) >= fabs(nextDiff.dy)) { - arcRadius = fmin(radius, fmin(fabs(nextDiff.dx), fabs(lastDiff.dy)) * 0.3); - startPoint = NSMakePoint(point.x, fma(copysign(arcRadius, lastDiff.dy), -1.528664, nextPoint.y)); - relayPointA = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.074911, point.x), - fma(copysign(arcRadius, lastDiff.dy), -0.631494, nextPoint.y)); - controlPointA1 = NSMakePoint(point.x, fma(copysign(arcRadius, lastDiff.dy), -1.088493, nextPoint.y)); - controlPointA2 = NSMakePoint(point.x, fma(copysign(arcRadius, lastDiff.dy), -0.868407, nextPoint.y)); - relayPointB = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.631494, point.x), - fma(copysign(arcRadius, lastDiff.dy), -0.074911, nextPoint.y)); - controlPointB1 = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.372824, point.x), - fma(copysign(arcRadius, lastDiff.dy), -0.169060, nextPoint.y)); - controlPointB2 = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.169060, point.x), - fma(copysign(arcRadius, lastDiff.dy), -0.372824, nextPoint.y)); - endPoint = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 1.528664, point.x), nextPoint.y); - controlPoint1 = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 0.868407, point.x), nextPoint.y); - controlPoint2 = NSMakePoint(fma(copysign(arcRadius, nextDiff.dx), 1.088493, point.x), nextPoint.y); + arcRadius = floor(fmin(fabs(cornerRadius), fmin(fabs(nextDiff.dx), fabs(lastDiff.dy)) * 0.5)); + startPoint = NSMakePoint(vertex.x, vertex.y - copysign(arcRadius, lastDiff.dy)); + endPoint = NSMakePoint(vertex.x + copysign(arcRadius, nextDiff.dx), vertex.y); } else { - arcRadius = fmin(radius, fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.3); - startPoint = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -1.528664, nextPoint.x), point.y); - relayPointA = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -0.631494, nextPoint.x), - fma(copysign(arcRadius, nextDiff.dy), 0.074911, point.y)); - controlPointA1 = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -1.088493, nextPoint.x), point.y); - controlPointA2 = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -0.868407, nextPoint.x), point.y); - relayPointB = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -0.074911, nextPoint.x), - fma(copysign(arcRadius, nextDiff.dy), 0.631494, point.y)); - controlPointB1 = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -0.169060, nextPoint.x), - fma(copysign(arcRadius, nextDiff.dy), 0.372824, point.y)); - controlPointB2 = NSMakePoint(fma(copysign(arcRadius, lastDiff.dx), -0.372824, nextPoint.x), - fma(copysign(arcRadius, nextDiff.dy), 0.169060, point.y)); - endPoint = NSMakePoint(nextPoint.x, fma(copysign(arcRadius, nextDiff.dy), 1.528664, point.y)); - controlPoint1 = NSMakePoint(nextPoint.x, fma(copysign(arcRadius, nextDiff.dy), 0.868407, point.y)); - controlPoint2 = NSMakePoint(nextPoint.x, fma(copysign(arcRadius, nextDiff.dy), 1.088493, point.y)); + arcRadius = floor(fmin(fabs(cornerRadius), fmin(fabs(nextDiff.dy), fabs(lastDiff.dx)) * 0.5)); + startPoint = NSMakePoint(vertex.x - copysign(arcRadius, lastDiff.dx), vertex.y); + endPoint = NSMakePoint(vertex.x, vertex.y + copysign(arcRadius, nextDiff.dy)); } [path lineToPoint:startPoint]; - [path curveToPoint:relayPointA - controlPoint1:controlPointA1 - controlPoint2:controlPointA2]; - [path curveToPoint:relayPointB - controlPoint1:controlPointB1 - controlPoint2:controlPointB2]; - [path curveToPoint:endPoint - controlPoint1:controlPoint1 - controlPoint2:controlPoint2]; + [path curveToPoint:endPoint controlPoint1:vertex controlPoint2:vertex]; } [path closePath]; return path; @@ -260,8 +240,7 @@ + (NSBezierPath*)squirclePathForRect:(NSRect)rect cornerRadius:(CGFloat)cornerRadius { NSPoint vertices[4]; rectVertices(rect, vertices); - return [NSBezierPath squirclePathWithVertices:vertices - count:4 + return [NSBezierPath squirclePathWithVertices:vertices count:4 cornerRadius:cornerRadius]; } @@ -272,21 +251,18 @@ + (NSBezierPath*)squirclePathForPolygon:(SquirrelTextPolygon)polygon NSPoint headVertices[4], tailVertices[4]; rectVertices(polygon.head, headVertices); rectVertices(polygon.tail, tailVertices); - path = [NSBezierPath squirclePathWithVertices:headVertices - count:4 + path = [NSBezierPath squirclePathWithVertices:headVertices count:4 cornerRadius:cornerRadius]; [path appendBezierPath: - [NSBezierPath squirclePathWithVertices:tailVertices - count:4 + [NSBezierPath squirclePathWithVertices:tailVertices count:4 cornerRadius:cornerRadius]]; } else { - NSUInteger numVert = clamp((NSIsEmptyRect(polygon.head) ? 0 : 4UL) + - (NSIsEmptyRect(polygon.body) ? 0 : 2UL) + - (NSIsEmptyRect(polygon.tail) ? 0 : 4UL), 4UL, 8UL); - NSPoint vertices[numVert]; + NSUInteger numVertex = clamp((NSIsEmptyRect(polygon.head) ? 0 : 4UL) + + (NSIsEmptyRect(polygon.body) ? 0 : 2UL) + + (NSIsEmptyRect(polygon.tail) ? 0 : 4UL), 4UL, 8UL); + NSPoint vertices[numVertex]; polygon.getVertices(vertices); - path = [NSBezierPath squirclePathWithVertices:vertices - count:numVert + path = [NSBezierPath squirclePathWithVertices:vertices count:numVertex cornerRadius:cornerRadius]; } return path; @@ -295,6 +271,78 @@ + (NSBezierPath*)squirclePathForPolygon:(SquirrelTextPolygon)polygon @end // NSBezierPath (BezierPathQuartzUtilities) +__attribute__((objc_direct_members)) +@implementation NSFontDescriptor (NSFontDescriptorWithFallbackFonts) + +static NSArray*>* const features = + @[@{NSFontFeatureTypeIdentifierKey: @(kVerticalSubstitutionType), + NSFontFeatureSelectorIdentifierKey: @(kSubstituteVerticalFormsOnSelector)}, + @{NSFontFeatureTypeIdentifierKey: @(kCJKVerticalRomanPlacementType), + NSFontFeatureSelectorIdentifierKey: @(kCJKVerticalRomanCenteredSelector)}, + @{NSFontFeatureTypeIdentifierKey: @(kRubyKanaType), + NSFontFeatureSelectorIdentifierKey: @(kRubyKanaOffSelector)}]; + ++ (NSFontDescriptor*)createWithFullname:(NSString*)fullname { + if (fullname.length == 0) { + return nil; + } + NSArray* fontNames = [fullname componentsSeparatedByString:@","]; + NSMutableArray* validFontDescriptors = + [NSMutableArray.alloc initWithCapacity:fontNames.count]; + for (NSString* fontName in fontNames) { + if (NSFont* font = [NSFont fontWithName:[fontName stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceAndNewlineCharacterSet] + 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. */ + NSFontDescriptor* fontDescriptor = [font.fontDescriptor fontDescriptorByAddingAttributes: + @{NSFontFeatureSettingsAttribute: features}]; + NSFontDescriptor* UIFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits: + NSFontDescriptorTraitUIOptimized]; + [validFontDescriptors addObject:[NSFont fontWithDescriptor:UIFontDescriptor size:0.0] != nil ? + UIFontDescriptor : fontDescriptor]; + } + } + if (validFontDescriptors.count == 0) { + return nil; + } + NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0]; + NSFontDescriptor* emojiFontDescriptor = + [[NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0] + fontDescriptorByAddingAttributes:@{NSFontFeatureSettingsAttribute: features}]; + NSArray* fallbackDescriptors = + [[validFontDescriptors subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)] + arrayByAddingObject:emojiFontDescriptor]; + return [initialFontDescriptor fontDescriptorByAddingAttributes: + @{NSFontCascadeListAttribute: fallbackDescriptors}]; +} + +@end // NSFontDescriptor (NSFontDescriptorWithFallbackFonts) + + +__attribute__((objc_direct_members)) +@implementation NSFont (NSFontGetLineHeight) + +- (CGFloat)lineHeightAsVerticalFont:(BOOL)vertical { + NSFont* font = vertical ? self.verticalFont : self; + CGFloat lineHeight = ceil(font.ascender - font.descender); + NSArray* fallbackList = + [font.fontDescriptor objectForKey:NSFontCascadeListAttribute]; + for (NSFontDescriptor* fallback in fallbackList) { + NSFont* fallbackFont = [NSFont fontWithDescriptor:fallback + size:font.pointSize]; + if (vertical) { + fallbackFont = fallbackFont.verticalFont; + } + lineHeight = fmax(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); + } + return lineHeight; +} + +@end // NSFont (NSFontGetLineHeight) + + __attribute__((objc_direct_members)) @implementation NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) @@ -306,7 +354,6 @@ - (void)superscriptionRange:(NSRange)range { NSFont* font = [NSFont fontWithDescriptor:value.fontDescriptor size:floor(value.pointSize * 0.55)]; [self addAttributes:@{NSFontAttributeName: font, - (id)kCTBaselineClassAttributeName: (id)kCTBaselineClassIdeographicCentered, NSSuperscriptAttributeName: @1} range:subRange]; }]; @@ -320,7 +367,6 @@ - (void)subscriptionRange:(NSRange)range { NSFont* font = [NSFont fontWithDescriptor:value.fontDescriptor size:floor(value.pointSize * 0.55)]; [self addAttributes:@{NSFontAttributeName: font, - (id)kCTBaselineClassAttributeName: (id)kCTBaselineClassIdeographicCentered, NSSuperscriptAttributeName: @-1} range:subRange]; }]; @@ -335,8 +381,7 @@ - (void)formatMarkDown { options:NSRegularExpressionUseUnicodeWordBoundaries error:nil]; NSInteger __block offset = 0; - [regex enumerateMatchesInString:self.mutableString - options:0 + [regex enumerateMatchesInString:self.mutableString options:0 range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult* _Nullable result, NSMatchingFlags flags, BOOL* _Nonnull stop) { @@ -372,7 +417,7 @@ - (void)formatMarkDown { } } -static NSString* const kRubyPattern = @"(\uFFF9\\s*)(\\S+?)(\\s*\uFFFA(.+?)\uFFFB)"; +static NSString* const kRubyPattern = @"(\\x{FFF9}\\s*)(\\S+?)(\\s*\\x{FFFA}(.+?)\\x{FFFB})"; - (CGFloat)annotateRubyInRange:(NSRange)range verticalOrientation:(BOOL)isVertical @@ -381,50 +426,56 @@ - (CGFloat)annotateRubyInRange:(NSRange)range NSRegularExpression* regex = [NSRegularExpression.alloc initWithPattern:kRubyPattern options:0 error:nil]; CGFloat __block rubyLineHeight; - [regex enumerateMatchesInString:self.mutableString - options:0 - range:range + [regex enumerateMatchesInString:self.mutableString options:0 range:range usingBlock:^(NSTextCheckingResult* _Nullable result, NSMatchingFlags flags, BOOL* _Nonnull stop) { NSRange baseRange = [result rangeAtIndex:2]; // no ruby annotation if the base string includes line breaks - if ([self attributedSubstringFromRange:NSMakeRange(0, NSMaxRange(baseRange))].size.width > maxLength - 0.1) { + if ([self attributedSubstringFromRange:NSMakeRange(0, NSMaxRange(baseRange))].size.width > nexttoward(maxLength, -INFINITY)) { [self deleteCharactersInRange:NSMakeRange(NSMaxRange(result.range) - 1, 1)]; [self deleteCharactersInRange:NSMakeRange([result rangeAtIndex:3].location, 1)]; [self deleteCharactersInRange:NSMakeRange([result rangeAtIndex:1].location, 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 */ + // base string must use only one font so that all fall within one glyph run + // and the ruby annotation is aligned with no duplicates NSFont* baseFont = [self attribute:NSFontAttributeName atIndex:baseRange.location effectiveRange:NULL]; + NSString* baseString = [self.mutableString substringWithRange:baseRange]; baseFont = CFBridgingRelease(CTFontCreateForStringWithLanguage - ((CTFontRef)baseFont, (CFStringRef)self.mutableString, - CFRangeMake((CFIndex)baseRange.location, (CFIndex)baseRange.length), + ((CTFontRef)baseFont, (CFStringRef)baseString, + CFRangeMake(0, (CFIndex)baseRange.length), (CFStringRef)scriptVariant)); - CFStringRef rubyString = (__bridge CFStringRef)[self.mutableString substringWithRange: - [result rangeAtIndex:4]]; - NSFont* rubyFont = [self attribute:NSFontAttributeName atIndex:[result rangeAtIndex:4].location effectiveRange:NULL]; - rubyFont = [NSFont fontWithDescriptor:rubyFont.fontDescriptor size:ceil(rubyFont.pointSize * 0.5)]; - rubyLineHeight = isVertical ? rubyFont.verticalFont.ascender - rubyFont.verticalFont.descender + 1.0 : rubyFont.ascender - rubyFont.descender + 1.0; - CFDictionaryRef rubyAttrs = CFDictionaryCreate(NULL, (CFTypeRef[]){kCTFontAttributeName}, (CFTypeRef[]){(__bridge CTFontRef)rubyFont}, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - CTRubyAnnotationRef rubyAnnotation = CTRubyAnnotationCreateWithAttributes(kCTRubyAlignmentDistributeSpace, kCTRubyOverhangNone, kCTRubyPositionBefore, rubyString, rubyAttrs); - - [self deleteCharactersInRange:[result rangeAtIndex:3]]; + NSString* rubyString = [self.mutableString substringWithRange:[result rangeAtIndex:4]]; + NSFont* rubyFont = [NSFont fontWithDescriptor:baseFont.fontDescriptor size:baseFont.pointSize * 0.5]; + rubyLineHeight = [rubyFont lineHeightAsVerticalFont:isVertical]; + CFStringRef rubyText[kCTRubyPositionCount] = {(__bridge CFStringRef)rubyString, NULL, NULL, NULL}; + CTRubyAnnotationRef rubyAnnotation = CTRubyAnnotationCreate(kCTRubyAlignmentDistributeSpace, + kCTRubyOverhangNone, 0.5, rubyText); + [self addAttributes:@{NSFontAttributeName: baseFont, + NSVerticalGlyphFormAttributeName: @(isVertical)} + range:result.range]; + if (@available(macOS 12.0, *)) { + [self deleteCharactersInRange:[result rangeAtIndex:3]]; } else { // use U+008B as placeholder for line-forward spaces in case ruby is wider than base + NSSize baseSize = [self attributedSubstringFromRange:baseRange].size; + CGFloat rubyWidth = [self attributedSubstringFromRange:[result rangeAtIndex:4]].size.width * 0.5; + [self deleteCharactersInRange:[result rangeAtIndex:3]]; [self replaceCharactersInRange:NSMakeRange(NSMaxRange(baseRange), 0) withString:[NSString stringWithFormat:@"%C", 0x8B]]; + [self addAttribute:kControlCharacterSizeAttributeName + value:[NSValue valueWithSize:NSMakeSize(fdim(ceil(rubyWidth), floor(baseSize.width)), baseSize.height)] + range:NSMakeRange(NSMaxRange(baseRange), 1)]; } - [self addAttributes:@{(id)kCTRubyAnnotationAttributeName: CFBridgingRelease(rubyAnnotation), - NSFontAttributeName: baseFont, - NSVerticalGlyphFormAttributeName: @(isVertical)} - range:baseRange]; + [self addAttribute:(id)kCTRubyAnnotationAttributeName + value:CFBridgingRelease(rubyAnnotation) + range:baseRange]; [self deleteCharactersInRange:[result rangeAtIndex:1]]; } }]; - [self.mutableString replaceOccurrencesOfString:@"[\uFFF9-\uFFFB]" - withString:@"" + [self.mutableString replaceOccurrencesOfString:@"(.)?[\\x{FFF9}-\\x{FFFB}]" + withString:@"$1" options:NSRegularExpressionSearch range:NSMakeRange(0, self.length)]; return ceil(rubyLineHeight); @@ -440,26 +491,28 @@ - (NSAttributedString*)attributedStringHorizontalInVerticalForms { NSMutableDictionary* attrs = [[self attributesAtIndex:0 effectiveRange:NULL] mutableCopy]; NSFont* font = attrs[NSFontAttributeName]; - CGFloat stringWidth = floor(self.size.width); - CGFloat height = floor(font.ascender - font.descender); + NSAttributedString* attrString = [NSAttributedString.alloc + initWithString:self.string + attributes:[self fontAttributesInRange:NSMakeRange(0, self.length)]]; + CGFloat stringWidth = ceil(attrString.size.width); + CGFloat height = ceil(attrString.size.height); CGFloat width = fmax(height, stringWidth); - NSImage* image = [NSImage imageWithSize:NSMakeSize(height, width) - flipped:YES + NSImage* image = [NSImage imageWithSize:NSMakeSize(height, height) flipped:YES drawingHandler:^BOOL(NSRect dstRect) { [NSGraphicsContext saveGraphicsState]; NSAffineTransform* transform = NSAffineTransform.transform; + [transform scaleXBy:1.0 yBy:height / width]; + [transform translateXBy:ceil(height * 0.5) yBy:ceil(width * 0.5)]; [transform rotateByDegrees:-90.0]; [transform concat]; - CGPoint origin = CGPointMake(floor((width - stringWidth) * 0.5 - dstRect.size.height), 0); - [self drawAtPoint:origin]; + [attrString drawWithRect:NSMakeRect(-ceil(stringWidth * 0.5), -ceil(height * 0.5), stringWidth, height) + options:NSStringDrawingUsesLineFragmentOrigin]; [NSGraphicsContext restoreGraphicsState]; return YES; }]; - image.resizingMode = NSImageResizingModeStretch; - image.size = NSMakeSize(height, height); NSTextAttachment* attm = NSTextAttachment.alloc.init; attm.image = image; - attm.bounds = NSMakeRect(0, floor(font.descender), height, height); + attm.bounds = NSMakeRect(0, ceil(font.descender), height, height); attrs[NSAttachmentAttributeName] = attm; return [NSAttributedString.alloc initWithString: [NSString stringWithCharacters:(unichar[]){NSAttachmentCharacter} length:1] @@ -544,9 +597,9 @@ + (NSColor*)colorWithLabLStar:(CGFloat)lStar bStar:(CGFloat)bStar alpha:(CGFloat)alpha { CGFloat components[4]; - components[0] = clamp(lStar, 0.0, 100.0); - components[1] = clamp(aStar, -127.0, 127.0); - components[2] = clamp(bStar, -127.0, 127.0); + components[0] = clamp(lStar, 0.0, 100.0); // luminance + components[1] = clamp(aStar, -127.0, 127.0); // green-red + components[2] = clamp(bStar, -127.0, 127.0); // blue-yellow components[3] = clamp(alpha, 0.0, 1.0); return [NSColor colorWithColorSpace:NSColorSpace.labColorSpace components:components count:4]; @@ -616,67 +669,6 @@ - (NSColor*)colorByInvertingLuminanceToExtent:(ColorInversionExtent)extent { @end // NSColor (colorWithLabColorSpace) -@implementation NSFontDescriptor (NSFontDescriptorWithFallbackFonts) - -+ (NSFontDescriptor*)createWithFullname:(NSString*)fullname { - if (fullname.length == 0) { - return nil; - } - NSArray* fontNames = [fullname componentsSeparatedByString:@","]; - NSMutableArray* validFontDescriptors = - [NSMutableArray.alloc initWithCapacity:fontNames.count]; - for (NSString* fontName in fontNames) { - if (NSFont* font = [NSFont fontWithName:[fontName stringByTrimmingCharactersInSet: - NSCharacterSet.whitespaceAndNewlineCharacterSet] - 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. */ - NSFontDescriptor* fontDescriptor = font.fontDescriptor; - NSFontDescriptor* UIFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits: - NSFontDescriptorTraitUIOptimized]; - [validFontDescriptors addObject:[NSFont fontWithDescriptor:UIFontDescriptor - size:0.0] != nil ? - UIFontDescriptor : fontDescriptor]; - } - } - if (validFontDescriptors.count == 0) { - return nil; - } - NSFontDescriptor* initialFontDescriptor = validFontDescriptors[0]; - NSFontDescriptor* emojiFontDescriptor = - [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0]; - NSArray* fallbackDescriptors = - [[validFontDescriptors subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)] - arrayByAddingObject:emojiFontDescriptor]; - return [initialFontDescriptor fontDescriptorByAddingAttributes: - @{NSFontCascadeListAttribute: fallbackDescriptors}]; -} - -@end // NSFontDescriptor (NSFontDescriptorWithFallbackFonts) - - -@implementation NSFont (NSFontGetLineHeight) - -- (CGFloat)lineHeightAsVerticalFont:(BOOL)vertical { - NSFont* font = vertical ? self.verticalFont : self; - CGFloat lineHeight = ceil(font.ascender - font.descender); - NSArray* fallbackList = - [font.fontDescriptor objectForKey:NSFontCascadeListAttribute]; - for (NSFontDescriptor* fallback in fallbackList) { - NSFont* fallbackFont = [NSFont fontWithDescriptor:fallback - size:font.pointSize]; - if (vertical) { - fallbackFont = fallbackFont.verticalFont; - } - lineHeight = fmax(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); - } - return lineHeight; -} - -@end // NSFont (NSFontGetLineHeight) - - #pragma mark - Color scheme and other user configurations typedef NS_CLOSED_ENUM(BOOL, SquirrelStyle) { @@ -691,6 +683,34 @@ typedef NS_CLOSED_ENUM(NSUInteger, SquirrelStatusMessageType) { kStatusMessageTypeLong = 2 }; +typedef NS_CLOSED_ENUM(NSUInteger, SquirrelContentBlock) { + kPreeditBlock, + kLinearCandidatesBlock, + kStackedCandidatesBlock, + kPagingBlock, + kStatusBlock +}; + +__attribute__((objc_direct_members)) +@interface NSFlippedView : NSView +@end + +__attribute__((objc_direct_members)) +@interface SquirrelTextView : NSTextView + +@property(nonatomic) SquirrelContentBlock contentBlock; + +- (instancetype)initWithContentBlock:(SquirrelContentBlock)contentBlock + storage:(NSTextStorage*)textStorage; +- (NSTextRange*)textRangeFromCharRange:(NSRange)charRange API_AVAILABLE(macos(12.0)); +- (NSRange)charRangeFromTextRange:(NSTextRange*)textRange API_AVAILABLE(macos(12.0)); +- (NSRect)layoutText; +- (NSRect)blockRectForRange:(NSRange)charRange; +- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange; + +@end + + __attribute__((objc_direct_members)) @interface SquirrelTheme : NSObject @@ -753,12 +773,14 @@ @interface SquirrelTheme : NSObject @property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolExpand; @property(nonatomic, readonly, strong, nullable) NSAttributedString* symbolLock; -@property(nonatomic, readonly, strong, nonnull) NSArray* labels; +@property(nonatomic, readonly, strong, nonnull) NSArray* rawLabels; +@property(nonatomic, readonly, strong, nullable) NSArray* labels; @property(nonatomic, readonly, strong, nonnull) NSAttributedString* candidateTemplate; @property(nonatomic, readonly, strong, nonnull) NSAttributedString* candidateHilitedTemplate; @property(nonatomic, readonly, strong, nullable) NSAttributedString* candidateDimmedTemplate; @property(nonatomic, readonly, strong, nonnull) NSString* selectKeys; -@property(nonatomic, readonly, strong, nonnull) NSString* candidateFormat; +@property(nonatomic, readonly, strong, nonnull) NSString* rawCandidateFormat; +@property(nonatomic, readonly, strong, nullable) NSString* candidateFormat; @property(nonatomic, readonly, strong, nonnull) NSString* scriptVariant; @property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; @property(nonatomic, readonly) NSUInteger pageSize; @@ -767,16 +789,16 @@ @interface SquirrelTheme : NSObject - (instancetype)initWithStyle:(SquirrelStyle)style NS_DESIGNATED_INITIALIZER; - (void)updateLabelsWithConfig:(SquirrelConfig* _Nonnull)config directUpdate:(BOOL)update; -- (void)setSelectKeys:(NSString* _Nonnull)selectKeys - labels:(NSArray* _Nonnull)labels - directUpdate:(BOOL)update; -- (void)setCandidateFormat:(NSString* _Nonnull)candidateFormat; -- (void)setStatusMessageType:(NSString* _Nullable)type; +- (void)updateSelectKeys:(NSString* _Nonnull)selectKeys + labels:(NSArray* _Nonnull)rawLabels + directUpdate:(BOOL)update; +- (void)updateCandidateFormat:(NSString* _Nonnull)rawCandidateFormat; +- (void)updateStatusMessageType:(NSString* _Nullable)type; - (void)updateWithConfig:(SquirrelConfig* _Nonnull)config styleOptions:(NSSet* _Nonnull)styleOptions scriptVariant:(NSString* _Nonnull)scriptVariant; - (void)setAnnotationHeight:(CGFloat)height; -- (void)setScriptVariant:(NSString* _Nonnull)scriptVariant; +- (void)updateScriptVariant:(NSString* _Nonnull)scriptVariant; @end @@ -792,16 +814,16 @@ - (instancetype)initWithStyle:(SquirrelStyle)style { if (self = [super init]) { _style = style; _selectKeys = @"12345"; - _labels = @[@"1", @"2", @"3", @"4", @"5"]; + _rawLabels = @[@"1", @"2", @"3", @"4", @"5"]; _pageSize = 5UL; - _candidateFormat = kDefaultCandidateFormat; + _rawCandidateFormat = kDefaultCandidateFormat; _scriptVariant = @"zh"; NSMutableParagraphStyle* candidateParagraphStyle = NSMutableParagraphStyle.alloc.init; candidateParagraphStyle.alignment = NSTextAlignmentLeft; candidateParagraphStyle.lineBreakStrategy = NSLineBreakStrategyNone; - /* 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 = NSWritingDirectionLeftToRight; NSMutableParagraphStyle* preeditParagraphStyle = candidateParagraphStyle.mutableCopy; NSMutableParagraphStyle* pagingParagraphStyle = candidateParagraphStyle.mutableCopy; @@ -810,8 +832,10 @@ - (instancetype)initWithStyle:(SquirrelStyle)style { preeditParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; statusParagraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; - NSFontDescriptor* userFontDesc = [NSFontDescriptor createWithFullname:[NSFont userFontOfSize:0.0].fontName]; - NSFontDescriptor* monoFontDesc = [NSFontDescriptor createWithFullname:[NSFont userFixedPitchFontOfSize:0.0].fontName]; + NSFontDescriptor* userFontDesc = [NSFontDescriptor createWithFullname: + [NSFont userFontOfSize:0.0].fontName]; + NSFontDescriptor* monoFontDesc = [NSFontDescriptor createWithFullname: + [NSFont userFixedPitchFontOfSize:0.0].fontName]; NSFont* userFont = [NSFont fontWithDescriptor:userFontDesc size:kDefaultFontSize]; NSFont* userMonoFont = [NSFont fontWithDescriptor:monoFontDesc size:kDefaultFontSize]; NSFont* monoDigitFont = [NSFont monospacedDigitSystemFontOfSize:kDefaultFontSize @@ -820,6 +844,7 @@ - (instancetype)initWithStyle:(SquirrelStyle)style { NSMutableDictionary* textAttrs = NSMutableDictionary.alloc.init; textAttrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; textAttrs[NSFontAttributeName] = userFont; + textAttrs[NSKernAttributeName] = @0; // Use left-to-right embedding to prevent right-to-left text from changing the layout of the candidate. textAttrs[NSWritingDirectionAttributeName] = @[@0]; textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; @@ -827,12 +852,14 @@ - (instancetype)initWithStyle:(SquirrelStyle)style { NSMutableDictionary* labelAttrs = textAttrs.mutableCopy; labelAttrs[NSForegroundColorAttributeName] = NSColor.secondaryLabelColor; labelAttrs[NSFontAttributeName] = userMonoFont; + labelAttrs[NSKernAttributeName] = @0; labelAttrs[NSStrokeWidthAttributeName] = @(-2.0 / kDefaultFontSize); labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; NSMutableDictionary* commentAttrs = NSMutableDictionary.alloc.init; commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryLabelColor; commentAttrs[NSFontAttributeName] = userFont; + commentAttrs[NSKernAttributeName] = @0; commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; NSMutableDictionary* preeditAttrs = NSMutableDictionary.alloc.init; @@ -870,7 +897,10 @@ - (instancetype)initWithStyle:(SquirrelStyle)style { _hilitedCommentForeColor = NSColor.alternateSelectedControlTextColor; _hilitedLabelForeColor = NSColor.alternateSelectedControlTextColor; - [self updateCandidateFormatForAttributesOnly:NO]; + CGGlyph glyphs[1]; + CTFontGetGlyphsForCharacters((__bridge CTFontRef)userFont, (unichar[1]){0x3000}, glyphs, 1); + _fullWidth = ceil([kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName: userFont}].width); + [self updateCandidatetemplates]; [self updateSeperatorAndSymbolAttrs]; } return self; @@ -975,79 +1005,80 @@ - (void)updateSeperatorAndSymbolAttrs { - (void)updateLabelsWithConfig:(SquirrelConfig*)config directUpdate:(BOOL)update { NSUInteger menuSize = (NSUInteger)[config intValueForOption:@"menu/page_size"] ? : 5; - NSMutableArray* labels = [NSMutableArray.alloc initWithCapacity:menuSize]; - NSString* selectKeys = [config stringForOption:@"menu/alternative_select_keys"]; + NSString* selectKeys = [([config stringForOption:@"menu/alternative_select_keys"] ? : @"1234567890") substringToIndex:menuSize]; NSArray* selectLabels = [config listForOption:@"menu/alternative_select_labels"]; - if (selectLabels.count > 0) { - [labels addObjectsFromArray: - [selectLabels subarrayWithRange:NSMakeRange(0, menuSize)]]; - } - if (selectKeys != nil) { - if (selectLabels.count == 0) { - NSString* keyCaps = [selectKeys.uppercaseString stringByApplyingTransform: - NSStringTransformFullwidthToHalfwidth reverse:YES]; - for (NSUInteger i = 0; i < menuSize; ++i) { - labels[i] = [keyCaps substringWithRange:NSMakeRange(i, 1)]; - } + NSMutableArray* rawLabels = [NSMutableArray.alloc initWithCapacity:menuSize]; + if (selectLabels == nil) { + NSString* labelString = [selectKeys.uppercaseString + stringByApplyingTransform:NSStringTransformFullwidthToHalfwidth + reverse:YES]; + for (NSUInteger i = 0; i < menuSize; ++i) { + rawLabels[i] = [labelString substringWithRange:NSMakeRange(i, 1)]; } } else { - selectKeys = [@"1234567890" substringToIndex:menuSize]; - if (selectLabels.count == 0) { - NSString* numerals = [selectKeys stringByApplyingTransform: - NSStringTransformFullwidthToHalfwidth reverse:YES]; - for (NSUInteger i = 0; i < menuSize; ++i) { - labels[i] = [numerals substringWithRange:NSMakeRange(i, 1)]; - } + for (NSUInteger i = 0; i < menuSize; ++i) { + rawLabels[i] = selectLabels[i]; } } - [self setSelectKeys:selectKeys - labels:labels - directUpdate:update]; + [self updateSelectKeys:selectKeys labels:rawLabels directUpdate:update]; } -- (void)setSelectKeys:(NSString*)selectKeys - labels:(NSArray*)labels - directUpdate:(BOOL)update { +- (void)updateSelectKeys:(NSString*)selectKeys + labels:(NSArray*)rawLabels + directUpdate:(BOOL)update { + if ([_selectKeys isEqualToString:selectKeys] && [_rawLabels isEqualToArray:rawLabels]) { + return; + } _selectKeys = selectKeys; - _labels = labels; - _pageSize = labels.count; + _rawLabels = rawLabels; + _pageSize = rawLabels.count; + _labels = nil; if (update) { - [self updateCandidateFormatForAttributesOnly:YES]; + [self updateCandidatetemplates]; } } -- (void)setCandidateFormat:(NSString*)candidateFormat { - BOOL attrsOnly = [candidateFormat isEqualToString:_candidateFormat]; - if (!attrsOnly) { - _candidateFormat = candidateFormat; +- (void)updateCandidateFormat:(NSString*)rawCandidateFormat { + if (![_rawCandidateFormat isEqualToString:rawCandidateFormat]) { + _rawCandidateFormat = rawCandidateFormat; + _candidateFormat = nil; } - [self updateCandidateFormatForAttributesOnly:attrsOnly]; + [self updateCandidatetemplates]; [self updateSeperatorAndSymbolAttrs]; } -- (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { - NSMutableAttributedString* candidateTemplate; - if (!attrsOnly) { +- (void)updateCandidatetemplates { + if (_candidateFormat.length == 0 || _labels.count == 0) { // validate candidate format: must have enumerator '%c' before candidate '%@' - NSMutableString* candidateFormat = _candidateFormat.mutableCopy; - if (![candidateFormat containsString:@"%@"]) { + NSMutableString* candidateFormat = _rawCandidateFormat.mutableCopy; + NSRange textRange = [candidateFormat rangeOfString:@"%@" options:NSLiteralSearch]; + if (textRange.length == 0) { [candidateFormat appendString:@"%@"]; } NSRange labelRange = [candidateFormat rangeOfString:@"%c" options:NSLiteralSearch]; if (labelRange.length == 0) { [candidateFormat insertString:@"%c" atIndex:0]; + labelRange = [candidateFormat rangeOfString:@"%c" options:NSLiteralSearch]; } - NSRange textRange = [candidateFormat rangeOfString:@"%@" options:NSLiteralSearch]; + textRange = [candidateFormat rangeOfString:@"%@" options:NSLiteralSearch]; if (labelRange.location > textRange.location) { candidateFormat.string = kDefaultCandidateFormat; } + textRange = [candidateFormat rangeOfString:@"(\\x{FFF9})?%@" options:NSRegularExpressionSearch]; + NSRange commentRange = NSMakeRange(NSMaxRange(textRange), candidateFormat.length - NSMaxRange(textRange)); + if (commentRange.length == 0 || ![[candidateFormat substringWithRange:commentRange] containsString:@"%s"]) { + [candidateFormat insertString:@"%s" atIndex:commentRange.location]; + } + if (!_linear) { + [candidateFormat insertString:@"\t" atIndex:textRange.location]; + } + _candidateFormat = candidateFormat; - NSMutableArray* labels = _labels.mutableCopy; + NSMutableArray* labels = _rawLabels.mutableCopy; NSRange enumRange = NSMakeRange(0, 0); NSCharacterSet* labelCharacters = [NSCharacterSet characterSetWithCharactersInString: [labels componentsJoinedByString:@""]]; - if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF10, 10)] - isSupersetOfSet:labelCharacters]) { // 01..9 + if ([NSCharacterSet.fullWidthDigitCharacterSet isSupersetOfSet:labelCharacters]) { // 01..9 if ((enumRange = [candidateFormat rangeOfString:@"%c\u20E3" options:NSLiteralSearch]).length > 0) { // 1︎⃣...9︎⃣0︎⃣ for (NSUInteger i = 0; i < labels.count; ++i) { @@ -1058,15 +1089,15 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { options:NSLiteralSearch]).length > 0) { // ①...⑨⓪ for (NSUInteger i = 0; i < labels.count; ++i) { labels[i] = [NSString stringWithFormat:@"%C", - (unichar)([labels[i] characterAtIndex:0] == 0xFF10 ? 0x24EA : - [labels[i] characterAtIndex:0] - 0xFF11 + 0x2460)]; + (unichar)([labels[i] characterAtIndex:0] == 0xFF10 ? 0x24EA : + [labels[i] characterAtIndex:0] - 0xFF11 + 0x2460)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"(%c)" options:NSLiteralSearch]).length > 0) { // ⑴...⑼⑽ for (NSUInteger i = 0; i < labels.count; ++i) { labels[i] = [NSString stringWithFormat:@"%C", - (unichar)([labels[i] characterAtIndex:0] == 0xFF10 ? 0x247D : - [labels[i] characterAtIndex:0] - 0xFF11 + 0x2474)]; + (unichar)([labels[i] characterAtIndex:0] == 0xFF10 ? 0x247D : + [labels[i] characterAtIndex:0] - 0xFF11 + 0x2474)]; } } else if ((enumRange = [candidateFormat rangeOfString:@"%c." options:NSLiteralSearch]).length > 0) { // ⒈...⒐🄀 @@ -1081,8 +1112,7 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { (const unichar[2]){0xD83C, (unichar)([labels[i] characterAtIndex:0] - 0xFF10 + 0xDD01)}]; } } - } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF21, 26)] - isSupersetOfSet:labelCharacters]) { // A..Z + } else if ([NSCharacterSet.fullWidthLatinCapitalCharacterSet isSupersetOfSet:labelCharacters]) { // A..Z if ((enumRange = [candidateFormat rangeOfString:@"%c\u20DD" options:NSLiteralSearch]).length > 0) { // Ⓐ...Ⓩ for (NSUInteger i = 0; i < labels.count; ++i) { @@ -1105,85 +1135,53 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { } if (enumRange.length > 0) { [candidateFormat replaceCharactersInRange:enumRange withString:@"%c"]; - _labels = labels; } - candidateTemplate = [NSMutableAttributedString.alloc initWithString:candidateFormat]; - } else { - candidateTemplate = _candidateTemplate.mutableCopy; + _labels = labels; } + // make sure label font can render all label strings - NSString* labelString = [_labels componentsJoinedByString:@""]; + NSMutableDictionary* textAttrs = _textAttrs.mutableCopy; + NSMutableDictionary* commentAttrs = _commentAttrs.mutableCopy; NSMutableDictionary* labelAttrs = _labelAttrs.mutableCopy; + NSString* labelString = [_rawLabels componentsJoinedByString:@""]; NSFont* labelFont = labelAttrs[NSFontAttributeName]; NSFont* substituteFont = CFBridgingRelease(CTFontCreateForString((CTFontRef)labelFont, (CFStringRef)labelString, CFRangeMake(0, (CFIndex)labelString.length))); if ([substituteFont isNotEqualTo:labelFont]) { - NSDictionary* monoDigitAttrs = - @{NSFontFeatureSettingsAttribute: @[@{NSFontFeatureTypeIdentifierKey: @(kNumberSpacingType), - NSFontFeatureSelectorIdentifierKey: @(kMonospacedNumbersSelector)}, - @{NSFontFeatureTypeIdentifierKey: @(kTextSpacingType), - NSFontFeatureSelectorIdentifierKey: @(kHalfWidthTextSelector)}]}; - NSFontDescriptor* substituteFontDescriptor = [substituteFont.fontDescriptor - fontDescriptorByAddingAttributes:monoDigitAttrs]; - substituteFont = [NSFont fontWithDescriptor:substituteFontDescriptor size:labelFont.pointSize]; - labelAttrs[NSFontAttributeName] = substituteFont; - } - - NSRange textRange = [candidateTemplate.mutableString rangeOfString:@"%@" options:NSLiteralSearch]; - NSRange labelRange = NSMakeRange(0, textRange.location); - NSRange commentRange = NSMakeRange(NSMaxRange(textRange), - candidateTemplate.length - NSMaxRange(textRange)); - [candidateTemplate setAttributes:_labelAttrs range:labelRange]; - [candidateTemplate setAttributes:_textAttrs range:textRange]; - if (commentRange.length > 0) { - [candidateTemplate setAttributes:_commentAttrs range:commentRange]; + labelAttrs[NSFontAttributeName] = CFBridgingRelease(CTFontCreateForString((CTFontRef)substituteFont, + (CFStringRef)labelString, CFRangeMake(0, (CFIndex)labelString.length))); } + // parse markdown formats - if (!attrsOnly) { - [candidateTemplate formatMarkDown]; - // add placeholder for comment '%s' - textRange = [candidateTemplate.mutableString rangeOfString:@"%@" options:NSLiteralSearch]; - labelRange = NSMakeRange(0, textRange.location); - commentRange = NSMakeRange(NSMaxRange(textRange), - candidateTemplate.length - NSMaxRange(textRange)); - if (commentRange.length > 0) { - [candidateTemplate replaceCharactersInRange:commentRange - withString:[kTipSpecifier append:[candidateTemplate.mutableString - substringWithRange:commentRange]]]; - } else { - [candidateTemplate appendAttributedString: - [NSAttributedString.alloc initWithString:kTipSpecifier - attributes:_commentAttrs]]; - } - commentRange.length += kTipSpecifier.length; - if (!_linear) { - [candidateTemplate replaceCharactersInRange:NSMakeRange(textRange.location, 0) - withString:@"\t"]; - labelRange.length += 1; - textRange.location += 1; - commentRange.location += 1; - } - } + NSMutableAttributedString* candidateTemplate = [NSMutableAttributedString.alloc initWithString:_candidateFormat]; + NSRange textRange = [candidateTemplate.mutableString rangeOfString:@"(\\x{FFF9})?%@" options:NSRegularExpressionSearch]; + NSRange labelRange = NSMakeRange(0, textRange.location); + NSRange commentRange = NSMakeRange(NSMaxRange(textRange), candidateTemplate.length - NSMaxRange(textRange)); + [candidateTemplate setAttributes:labelAttrs range:labelRange]; + [candidateTemplate setAttributes:textAttrs range:textRange]; + [candidateTemplate setAttributes:commentAttrs range:commentRange]; + [candidateTemplate formatMarkDown]; + textRange = [candidateTemplate.mutableString rangeOfString:@"(\\x{FFF9})?%@" options:NSRegularExpressionSearch]; + labelRange = NSMakeRange(0, textRange.location); + commentRange = NSMakeRange(NSMaxRange(textRange), candidateTemplate.length - NSMaxRange(textRange)); + // for stacked layout, calculate head indent NSMutableParagraphStyle* candidateParagraphStyle = _candidateParagraphStyle.mutableCopy; if (!_linear) { - CGFloat indent = 0.0; - NSAttributedString* labelFormat = [candidateTemplate attributedSubstringFromRange: - NSMakeRange(0, labelRange.length - 1)]; + NSRange enumRange = [candidateTemplate.mutableString rangeOfString:@"%c" options:NSLiteralSearch]; + NSTextStorage* textStorage = NSTextStorage.alloc.init; + SquirrelTextView* textView = [SquirrelTextView.alloc initWithContentBlock:kStackedCandidatesBlock storage:textStorage]; + textView.layoutOrientation = _vertical ? NSTextLayoutOrientationVertical : NSTextLayoutOrientationHorizontal; for (NSString* label in _labels) { - NSMutableAttributedString* enumString = labelFormat.mutableCopy; - NSRange enumRange = [enumString.mutableString rangeOfString:@"%c" options:NSLiteralSearch]; - [enumString.mutableString replaceCharactersInRange:enumRange withString:label]; - [enumString addAttribute:NSVerticalGlyphFormAttributeName - value:@(_vertical) - range:NSMakeRange(enumRange.location, label.length)]; - indent = fmax(indent, enumString.size.width); + NSMutableAttributedString* labelString = [candidateTemplate attributedSubstringFromRange:NSMakeRange(0, labelRange.length - 1)].mutableCopy; + [labelString replaceCharactersInRange:enumRange withString:label]; + [textStorage appendAttributedString:labelString]; + [textStorage appendAttributedString:[NSAttributedString.alloc initWithString:@"\n"]]; } - indent = floor(indent) + 1.0; - candidateParagraphStyle.tabStops = @[[NSTextTab.alloc - initWithTextAlignment:NSTextAlignmentLeft - location:indent - options:@{}]]; + CGFloat indent = floor(NSMaxX(textView.layoutText)) + 1.0; + candidateParagraphStyle.tabStops = @[[NSTextTab.alloc initWithTextAlignment:NSTextAlignmentLeft + location:indent + options:@{}]]; candidateParagraphStyle.headIndent = indent; _candidateParagraphStyle = candidateParagraphStyle; _truncatedParagraphStyle = nil; @@ -1197,8 +1195,6 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { _truncatedParagraphStyle = truncatedParagraphStyle; } - NSMutableDictionary* textAttrs = _textAttrs.mutableCopy; - NSMutableDictionary* commentAttrs = _commentAttrs.mutableCopy; textAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; commentAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; labelAttrs[NSParagraphStyleAttributeName] = candidateParagraphStyle; @@ -1210,6 +1206,7 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { value:candidateParagraphStyle range:NSMakeRange(0, candidateTemplate.length)]; _candidateTemplate = candidateTemplate; + NSMutableAttributedString* candidateHilitedTemplate = candidateTemplate.mutableCopy; [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName value:_hilitedLabelForeColor @@ -1221,6 +1218,7 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { value:_hilitedCommentForeColor range:commentRange]; _candidateHilitedTemplate = candidateHilitedTemplate; + if (_tabular) { NSMutableAttributedString* candidateDimmedTemplate = candidateTemplate.mutableCopy; [candidateDimmedTemplate addAttribute:NSForegroundColorAttributeName @@ -1232,7 +1230,7 @@ - (void)updateCandidateFormatForAttributesOnly:(BOOL)attrsOnly { } } -- (void)setStatusMessageType:(NSString*)type { +- (void)updateStatusMessageType:(NSString*)type { if ([@"long" caseInsensitiveCompare:type] == NSOrderedSame) { _statusMessageType = kStatusMessageTypeLong; } else if ([@"short" caseInsensitiveCompare:type] == NSOrderedSame) { @@ -1283,10 +1281,16 @@ static void updateTextOrientation(BOOL* isVertical, SquirrelConfig* config, NSSt if (newValue != nil) *existing = newValue; } +static NSArray*>* monoDigitFeatures = + @[@{NSFontFeatureTypeIdentifierKey: @(kNumberSpacingType), + NSFontFeatureSelectorIdentifierKey: @(kMonospacedNumbersSelector)}, + @{NSFontFeatureTypeIdentifierKey: @(kTextSpacingType), + NSFontFeatureSelectorIdentifierKey: @(kHalfWidthTextSelector)}]; + - (void)updateWithConfig:(SquirrelConfig*)config styleOptions:(NSSet*)styleOptions scriptVariant:(NSString*)scriptVariant { - /*** INTERFACE ***/ + /* INTERFACE */ BOOL linear = NO; BOOL tabular = NO; BOOL vertical = NO; @@ -1297,8 +1301,8 @@ - (void)updateWithConfig:(SquirrelConfig*)config NSNumber* showPaging = [config nullableBoolForOption:@"style/show_paging"]; NSNumber* rememberSize = [config nullableBoolForOption:@"style/remember_size" alias:@"memorize_size"]; NSString* statusMessageType = [config stringForOption:@"style/status_message_type"]; - NSString* candidateFormat = [config stringForOption:@"style/candidate_format"]; - /*** TYPOGRAPHY ***/ + NSString* rawCandidateFormat = [config stringForOption:@"style/candidate_format"]; + /* TYPOGRAPHY */ NSString* fontName = [config stringForOption:@"style/font_face"]; NSNumber* fontSize = [config nullableDoubleForOption:@"style/font_point" constraint:pos_round]; NSString* labelFontName = [config stringForOption:@"style/label_font_face"]; @@ -1317,7 +1321,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config NSNumber* baseOffset = [config nullableDoubleForOption:@"style/base_offset"]; NSNumber* lineLength = [config nullableDoubleForOption:@"style/line_length"]; NSNumber* shadowSize = [config nullableDoubleForOption:@"style/shadow_size" constraint:positive]; - /*** CHROMATICS ***/ + /* CHROMATICS */ NSColor* backColor; NSColor* borderColor; NSColor* preeditBackColor; @@ -1359,7 +1363,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config } // get color scheme and then check possible overrides from styleSwitcher for (NSString* prefix in configPrefixes) { - /*** CHROMATICS override ***/ + /* CHROMATICS override */ if (NSString* colorSpace = [config stringForOption:[prefix append:@"/color_space"]]) { config.colorSpace = colorSpace; } @@ -1380,9 +1384,9 @@ - (void)updateWithConfig:(SquirrelConfig*)config update(&hilitedLabelForeColor, [config colorForOption:[prefix append:@"/label_hilited_color"] alias:@"hilited_candidate_label_color"]); update(&backImage, [config imageForOption:[prefix append:@"/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(&linear, &tabular, config, prefix); updateTextOrientation(&vertical, config, prefix); update(&inlinePreedit, [config nullableBoolForOption:[prefix append:@"/inline_preedit"]]); @@ -1390,8 +1394,8 @@ - (void)updateWithConfig:(SquirrelConfig*)config update(&showPaging, [config nullableBoolForOption:[prefix append:@"/show_paging"]]); update(&rememberSize, [config nullableBoolForOption:[prefix append:@"/remember_size"] alias:@"memorize_size"]); update(&statusMessageType, [config stringForOption:[prefix append:@"/status_message_type"]]); - update(&candidateFormat, [config stringForOption:[prefix append:@"/candidate_format"]]); - /*** TYPOGRAPHY override ***/ + update(&rawCandidateFormat, [config stringForOption:[prefix append:@"/candidate_format"]]); + /* TYPOGRAPHY override */ update(&fontName, [config stringForOption:[prefix append:@"/font_face"]]); update(&fontSize, [config nullableDoubleForOption:[prefix append:@"/font_point"] constraint:pos_round]); update(&labelFontName, [config stringForOption:[prefix append:@"/label_font_face"]]); @@ -1412,46 +1416,45 @@ - (void)updateWithConfig:(SquirrelConfig*)config update(&shadowSize, [config nullableDoubleForOption:[prefix append:@"/shadow_size"] constraint:positive]); } - /*** TYPOGRAPHY refinement ***/ + /* FORMAT reset */ + rawCandidateFormat = rawCandidateFormat ? : kDefaultCandidateFormat; + if (_linear != linear) { + _candidateFormat = @""; // reset format after switching between linear and stacked + } + + /* TYPOGRAPHY refinement */ fontSize = fontSize ? : @(kDefaultFontSize); labelFontSize = labelFontSize ? : fontSize; commentFontSize = commentFontSize ? : fontSize; - NSDictionary* monoDigitAttrs = - @{NSFontFeatureSettingsAttribute: @[@{NSFontFeatureTypeIdentifierKey: @(kNumberSpacingType), - NSFontFeatureSelectorIdentifierKey: @(kMonospacedNumbersSelector)}, - @{NSFontFeatureTypeIdentifierKey: @(kTextSpacingType), - NSFontFeatureSelectorIdentifierKey: @(kHalfWidthTextSelector)}]}; - - NSFontDescriptor* fontDescriptor = [NSFontDescriptor createWithFullname:fontName]; - NSFont* font = [NSFont fontWithDescriptor:fontDescriptor ? : [NSFontDescriptor createWithFullname:[NSFont userFontOfSize:0].fontName] - size:fontSize.doubleValue]; + NSFontDescriptor* fontDescriptor = [NSFontDescriptor createWithFullname:fontName] ? : + [NSFontDescriptor createWithFullname:[NSFont userFontOfSize:0].fontName]; + NSFont* font = [NSFont fontWithDescriptor:fontDescriptor + size:fontSize.doubleValue]; NSFontDescriptor* labelFontDescriptor = [([NSFontDescriptor createWithFullname:labelFontName] ? : fontDescriptor) - fontDescriptorByAddingAttributes:monoDigitAttrs]; - NSFont* labelFont = labelFontDescriptor ? [NSFont fontWithDescriptor:labelFontDescriptor - size:labelFontSize.doubleValue] - : [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue - weight:NSFontWeightRegular]; - - NSFontDescriptor* commentFontDescriptor = [NSFontDescriptor createWithFullname:commentFontName]; - NSFont* commentFont = [NSFont fontWithDescriptor:commentFontDescriptor ? : fontDescriptor + fontDescriptorByAddingAttributes:@{NSFontFeatureSettingsAttribute: monoDigitFeatures}]; + NSFont* labelFont = [NSFont fontWithDescriptor:labelFontDescriptor + size:labelFontSize.doubleValue]; + NSFont* commentFont = [NSFont fontWithDescriptor:[NSFontDescriptor createWithFullname:commentFontName] ? : fontDescriptor size:commentFontSize.doubleValue]; - - NSFont* pagingFont = [NSFont monospacedDigitSystemFontOfSize:labelFontSize.doubleValue - weight:NSFontWeightRegular]; + NSFont* systemFont = [NSFont systemFontOfSize:labelFontSize.doubleValue]; + NSFontDescriptor* pagingFontDescriptor = [labelFont.fontDescriptor fontDescriptorByAddingAttributes: + @{NSFontCascadeListAttribute: @[systemFont.fontDescriptor]}]; + NSFont* pagingFont = [NSFont fontWithDescriptor:pagingFontDescriptor + size:labelFontSize.doubleValue]; CGFloat fontHeight = [font lineHeightAsVerticalFont:vertical]; CGFloat labelFontHeight = [labelFont lineHeightAsVerticalFont:vertical]; CGFloat commentFontHeight = [commentFont lineHeightAsVerticalFont:vertical]; + CGFloat pagingFontHeight = [pagingFont lineHeightAsVerticalFont:NO]; CGFloat lineHeight = fmax(fontHeight, fmax(labelFontHeight, commentFontHeight)); - CGFloat fullWidth = ceil([kFullWidthSpace sizeWithAttributes: - @{NSFontAttributeName : commentFont}].width); + CGFloat fullWidth = ceil([kFullWidthSpace sizeWithAttributes:@{NSFontAttributeName: commentFont}].width); NSMutableParagraphStyle* candidateParagraphStyle = _candidateParagraphStyle.mutableCopy; candidateParagraphStyle.minimumLineHeight = lineHeight; candidateParagraphStyle.maximumLineHeight = lineHeight; - candidateParagraphStyle.paragraphSpacingBefore = linear ? 0.0 : ceil(lineSpacing.doubleValue * 0.5); - candidateParagraphStyle.paragraphSpacing = linear ? 0.0 : floor(lineSpacing.doubleValue * 0.5); + candidateParagraphStyle.paragraphSpacingBefore = linear ? 0.0 : floor(lineSpacing.doubleValue * 0.5); + candidateParagraphStyle.paragraphSpacing = linear ? 0.0 : ceil(lineSpacing.doubleValue * 0.5); candidateParagraphStyle.lineSpacing = linear ? lineSpacing.doubleValue : 0.0; candidateParagraphStyle.tabStops = @[]; candidateParagraphStyle.defaultTabInterval = fullWidth * 2; @@ -1459,12 +1462,11 @@ - (void)updateWithConfig:(SquirrelConfig*)config NSMutableParagraphStyle* preeditParagraphStyle = _preeditParagraphStyle.mutableCopy; preeditParagraphStyle.minimumLineHeight = fontHeight; preeditParagraphStyle.maximumLineHeight = fontHeight; - preeditParagraphStyle.paragraphSpacing = spacing.doubleValue; preeditParagraphStyle.tabStops = @[]; NSMutableParagraphStyle* pagingParagraphStyle = _pagingParagraphStyle.mutableCopy; - pagingParagraphStyle.minimumLineHeight = ceil(pagingFont.ascender - pagingFont.descender); - pagingParagraphStyle.maximumLineHeight = ceil(pagingFont.ascender - pagingFont.descender); + pagingParagraphStyle.minimumLineHeight = pagingFontHeight; + pagingParagraphStyle.maximumLineHeight = pagingFontHeight; pagingParagraphStyle.tabStops = @[]; NSMutableParagraphStyle* statusParagraphStyle = _statusParagraphStyle.mutableCopy; @@ -1485,6 +1487,9 @@ - (void)updateWithConfig:(SquirrelConfig*)config pagingAttrs[NSFontAttributeName] = pagingFont; statusAttrs[NSFontAttributeName] = commentFont; labelAttrs[NSStrokeWidthAttributeName] = @(-2.0 / labelFontSize.doubleValue); + textAttrs[NSKernAttributeName] = vertical ? @(0.1 * fontSize.doubleValue) : @(0.0); + labelAttrs[NSKernAttributeName] = vertical ? @(0.1 * labelFontSize.doubleValue) : @(0.0); + commentAttrs[NSKernAttributeName] = vertical ? @(0.1 * commentFontSize.doubleValue) : @(0.0); NSFont* zhFont = CFBridgingRelease(CTFontCreateUIFontForLanguage (kCTFontUIFontSystem, fontSize.doubleValue, (CFStringRef)scriptVariant)); @@ -1509,7 +1514,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config labelAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; commentAttrs[(id)kCTBaselineReferenceInfoAttributeName] = baselineRefInfo; preeditAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{(id)kCTBaselineReferenceFont : zhFont}; - pagingAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{(id)kCTBaselineReferenceFont : pagingFont}; + pagingAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{(id)kCTBaselineReferenceFont : systemFont}; statusAttrs[(id)kCTBaselineReferenceInfoAttributeName] = @{(id)kCTBaselineReferenceFont : zhCommentFont}; textAttrs[(id)kCTBaselineClassAttributeName] = @@ -1521,7 +1526,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config vertical ? (id)kCTBaselineClassIdeographicCentered : (id)kCTBaselineClassRoman; statusAttrs[(id)kCTBaselineClassAttributeName] = vertical ? (id)kCTBaselineClassIdeographicCentered : (id)kCTBaselineClassRoman; - pagingAttrs[(id)kCTBaselineClassAttributeName] = (id)kCTBaselineClassIdeographicCentered; + pagingAttrs[(id)kCTBaselineClassAttributeName] = (id)kCTBaselineClassRoman; textAttrs[(id)kCTLanguageAttributeName] = scriptVariant; labelAttrs[(id)kCTLanguageAttributeName] = scriptVariant; @@ -1539,15 +1544,12 @@ - (void)updateWithConfig:(SquirrelConfig*)config preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; pagingAttrs[NSParagraphStyleAttributeName] = pagingParagraphStyle; statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; - - labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); pagingAttrs[NSVerticalGlyphFormAttributeName] = @NO; /*** CHROMATICS refinement ***/ if (@available(macOS 10.14, *)) { - if (translucency.floatValue > 0.001f && !isNative && backColor != nil && - (_style == kDarkStyle ? backColor.lStarComponent > 0.6 - : backColor.lStarComponent < 0.4)) { + if (isnormal(translucency.floatValue) && !isNative && backColor != nil && + (_style == kDarkStyle ? backColor.lStarComponent > 0.6 : backColor.lStarComponent < 0.4)) { backColor = [backColor colorByInvertingLuminanceToExtent:kStandardColorInversion]; borderColor = [borderColor colorByInvertingLuminanceToExtent:kStandardColorInversion]; preeditBackColor = [preeditBackColor colorByInvertingLuminanceToExtent:kStandardColorInversion]; @@ -1595,7 +1597,7 @@ - (void)updateWithConfig:(SquirrelConfig*)config _lineSpacing = lineSpacing.doubleValue; _preeditSpacing = spacing.doubleValue; _opacity = opacity ? opacity.doubleValue : 1.0; - _lineLength = lineLength.doubleValue > 0.1 ? fmax(ceil(lineLength.doubleValue), fullWidth * 5) : 0.0; + _lineLength = isnormal(lineLength.doubleValue) ? fmax(ceil(lineLength.doubleValue), fullWidth * 5) : 0.0; _shadowSize = shadowSize.doubleValue; _translucency = translucency.floatValue; _stackColors = stackColors.boolValue; @@ -1636,14 +1638,16 @@ - (void)updateWithConfig:(SquirrelConfig*)config _hilitedLabelForeColor = hilitedLabelForeColor; _dimmedLabelForeColor = tabular ? [labelForeColor colorWithAlphaComponent: labelForeColor.alphaComponent * 0.2] : nil; - _scriptVariant = scriptVariant; - [self setCandidateFormat:candidateFormat ? : kDefaultCandidateFormat]; - [self setStatusMessageType:statusMessageType]; + + [self updateStatusMessageType:statusMessageType]; + [self updateCandidateFormat:rawCandidateFormat]; + [self updateSeperatorAndSymbolAttrs]; + } - (void)setAnnotationHeight:(CGFloat)height { - if (height > 0.1 && _lineSpacing < height * 2) { + if (isnormal(height) && _lineSpacing < height * 2) { _lineSpacing = height * 2; NSMutableParagraphStyle* candidateParagraphStyle = _candidateParagraphStyle.mutableCopy; if (_linear) { @@ -1688,7 +1692,7 @@ - (void)setAnnotationHeight:(CGFloat)height { } } -- (void)setScriptVariant:(NSString*)scriptVariant { +- (void)updateScriptVariant:(NSString*)scriptVariant { if ([scriptVariant isEqualToString:_scriptVariant]) { return; } @@ -1740,30 +1744,20 @@ - (void)setScriptVariant:(NSString*)scriptVariant { _statusAttrs = statusAttrs; NSMutableAttributedString* candidateTemplate = _candidateTemplate.mutableCopy; - NSRange textRange = [candidateTemplate.mutableString rangeOfString:@"%@" options:NSLiteralSearch]; - NSRange labelRange = NSMakeRange(0, textRange.location); - NSRange commentRange = NSMakeRange(NSMaxRange(textRange), - candidateTemplate.length - NSMaxRange(textRange)); - [candidateTemplate addAttributes:labelAttrs range:labelRange]; - [candidateTemplate addAttributes:textAttrs range:textRange]; - [candidateTemplate addAttributes:commentAttrs range:commentRange]; + NSRange templateRange = NSMakeRange(0, candidateTemplate.length); + [candidateTemplate addAttribute:(id)kCTBaselineReferenceInfoAttributeName value:baselineRefInfo range:templateRange]; + [candidateTemplate addAttribute:(id)kCTLanguageAttributeName value:scriptVariant range:templateRange]; _candidateTemplate = candidateTemplate; + NSMutableAttributedString* candidateHilitedTemplate = candidateTemplate.mutableCopy; - [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName - value:_hilitedLabelForeColor - range:labelRange]; - [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName - value:_hilitedTextForeColor - range:textRange]; - [candidateHilitedTemplate addAttribute:NSForegroundColorAttributeName - value:_hilitedCommentForeColor - range:commentRange]; + [candidateHilitedTemplate addAttribute:(id)kCTBaselineReferenceInfoAttributeName value:baselineRefInfo range:templateRange]; + [candidateHilitedTemplate addAttribute:(id)kCTLanguageAttributeName value:scriptVariant range:templateRange]; _candidateHilitedTemplate = candidateHilitedTemplate; + if (_tabular) { NSMutableAttributedString* candidateDimmedTemplate = candidateTemplate.mutableCopy; - [candidateDimmedTemplate addAttribute:NSForegroundColorAttributeName - value:_dimmedLabelForeColor - range:labelRange]; + [candidateDimmedTemplate addAttribute:(id)kCTBaselineReferenceInfoAttributeName value:baselineRefInfo range:templateRange]; + [candidateDimmedTemplate addAttribute:(id)kCTLanguageAttributeName value:scriptVariant range:templateRange]; _candidateDimmedTemplate = candidateDimmedTemplate; } } @@ -1773,14 +1767,6 @@ - (void)setScriptVariant:(NSString*)scriptVariant { #pragma mark - Auxiliary structs and views -typedef NS_CLOSED_ENUM(NSUInteger, SquirrelContentBlock) { - kPreeditBlock, - kLinearCandidatesBlock, - kStackedCandidatesBlock, - kPagingBlock, - kStatusBlock -}; - typedef struct SquirrelTabularIndex { NSUInteger index; NSUInteger lineNum; @@ -1814,41 +1800,15 @@ inline NSRange commentRange() { } } SquirrelCandidateInfo; -__attribute__((objc_direct_members)) -@interface NSFlippedView : NSView -@end - -__attribute__((objc_direct_members)) -@interface SquirrelTextView : NSTextView - -@property(nonatomic) SquirrelContentBlock contentBlock; - -- (instancetype)initWithContentBlock:(SquirrelContentBlock)contentBlock - storage:(NSTextStorage*)textStorage; -- (NSTextRange*)textRangeFromCharRange:(NSRange)charRange API_AVAILABLE(macos(12.0)); -- (NSRange)charRangeFromTextRange:(NSTextRange*)textRange API_AVAILABLE(macos(12.0)); -- (NSRect)layoutText; -- (NSRect)blockRectForRange:(NSRange)charRange; -- (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange; - -@end - #pragma mark - Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) __attribute__((objc_direct_members)) @interface SquirrelLayoutManager : NSLayoutManager - -@property(nonatomic, readonly) SquirrelContentBlock contentBlock; - @end @implementation SquirrelLayoutManager -- (SquirrelContentBlock)contentBlock { - return ((SquirrelTextView*)self.firstTextView).contentBlock; -} - - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin { NSTextContainer* textContainer = [self textContainerForGlyphAtIndex:glyphsToShow.location @@ -1862,60 +1822,52 @@ - (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow NSRange charRange = [self characterRangeForGlyphRange:lineRange actualGlyphRange:NULL]; [self.textStorage enumerateAttributesInRange:charRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSDictionary * _Nonnull attrs, NSRange runRange, BOOL * _Nonnull stop) { NSRange runGlyphRange = [self glyphRangeForCharacterRange:runRange actualCharacterRange:NULL]; - if (attrs[(id)kCTRubyAnnotationAttributeName] != nil) { + NSFont* runFont = attrs[NSFontAttributeName]; + if (attrs[(id)kCTRubyAnnotationAttributeName] != nil || (verticalOrientation && [runFont.fontName isEqualToString:@"AppleColorEmoji"] && runFont.pointSize < 24)) { CGContextSaveGState(context); CGContextScaleCTM(context, 1.0, -1.0); - NSUInteger glyphIndex = runGlyphRange.location; - CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef) - [self.textStorage attributedSubstringFromRange:runRange]); + CGPoint position = [self locationForGlyphAtIndex:runGlyphRange.location]; + position.x += lineRect.origin.x + origin.x; + position.y += lineRect.origin.y + origin.y; + CTLineRef line; + if (attrs[(id)kCTRubyAnnotationAttributeName] == nil) { + NSMutableAttributedString* subString = [self.textStorage attributedSubstringFromRange:runRange].mutableCopy; + [subString addAttribute:NSVerticalGlyphFormAttributeName value:@1 range:NSMakeRange(0, runRange.length)]; + line = CTLineCreateWithAttributedString((CFAttributedStringRef)subString); + if (NSInteger superscript = [attrs[NSSuperscriptAttributeName] integerValue] != 0) { + position.y -= runFont.descender + superscript * 0.5; + } + } else { + line = CTLineCreateWithAttributedString + ((CFAttributedStringRef)[self.textStorage attributedSubstringFromRange:runRange]); + } CFArrayRef runs = CTLineGetGlyphRuns((CTLineRef)CFAutorelease(line)); for (CFIndex i = 0; i < CFArrayGetCount(runs); ++i) { - CGPoint position = [self locationForGlyphAtIndex:glyphIndex]; CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, i); - CFIndex glyphCount = CTRunGetGlyphCount(run); CGAffineTransform matrix = CTRunGetTextMatrix(run); - CGPoint glyphOrigin = [textContainer.textView convertPointToBacking: - CGPointMake(origin.x + lineRect.origin.x + position.x, - -origin.y - lineRect.origin.y - position.y)]; - glyphOrigin = [textContainer.textView convertPointFromBacking: - CGPointMake(round(glyphOrigin.x), round(glyphOrigin.y))]; + CGPoint glyphOrigin = CGContextConvertPointToDeviceSpace(context, position); + glyphOrigin = CGContextConvertPointToUserSpace(context, CGPointMake(ceil(glyphOrigin.x), ceil(glyphOrigin.y))); matrix.tx = glyphOrigin.x; - matrix.ty = glyphOrigin.y; + matrix.ty = -glyphOrigin.y; CGContextSetTextMatrix(context, matrix); - CTRunDraw(run, context, CFRangeMake(0, glyphCount)); - glyphIndex += (NSUInteger)glyphCount; + CTRunDraw(run, context, CFRangeMake(0, 0)); + if (i < CFArrayGetCount(runs) - 1) { + position.x += CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + } } CGContextRestoreGState(context); } else { - NSPoint position = [self locationForGlyphAtIndex:runGlyphRange.location]; - position.x += origin.x; - position.y += origin.y; - NSFont* runFont = attrs[NSFontAttributeName]; - NSString* baselineClass = attrs[(id)kCTBaselineClassAttributeName]; - NSPoint offset = NSZeroPoint; - if (!verticalOrientation && - ([baselineClass isEqualToString:(id)kCTBaselineClassIdeographicCentered] || - [baselineClass isEqualToString:(id)kCTBaselineClassMath])) { + NSPoint glyphOrigin = origin; + if (!verticalOrientation) { NSFont* refFont = attrs[(id)kCTBaselineReferenceInfoAttributeName][(id)kCTBaselineReferenceFont]; - offset.y += (runFont.ascender + runFont.descender - refFont.ascender - refFont.descender) * 0.5; - } else if (verticalOrientation && runFont.pointSize < 24 && - [runFont.fontName isEqualToString:@"AppleColorEmoji"]) { - NSInteger superscript = [attrs[NSSuperscriptAttributeName] integerValue]; - offset.x += runFont.capHeight - runFont.pointSize; - offset.y += (runFont.capHeight - runFont.pointSize) * - (superscript == 0 ? 0.25 : (superscript == 1 ? 0.5 / 0.55 : 0.0)); + glyphOrigin.y += (runFont.ascender + runFont.descender - refFont.ascender - refFont.descender) * 0.5; } - NSPoint glyphOrigin = [textContainer.textView convertPointToBacking: - NSMakePoint(position.x + offset.x, position.y + offset.y)]; - glyphOrigin = [textContainer.textView convertPointFromBacking: - NSMakePoint(round(glyphOrigin.x), round(glyphOrigin.y))]; - [super drawGlyphsForGlyphRange:runGlyphRange - atPoint:NSMakePoint(glyphOrigin.x - position.x, - glyphOrigin.y - position.y)]; + glyphOrigin = CGContextConvertPointToDeviceSpace(context, glyphOrigin); + glyphOrigin = CGContextConvertPointToUserSpace(context, NSMakePoint(ceil(glyphOrigin.x), ceil(glyphOrigin.y))); + [super drawGlyphsForGlyphRange:runGlyphRange atPoint:glyphOrigin]; } }]; }]; - CGContextClipToRect(context, textContainer.textView.superview.bounds); } - (BOOL) layoutManager:(NSLayoutManager*)layoutManager @@ -1924,12 +1876,14 @@ - (BOOL) layoutManager:(NSLayoutManager*)layoutManager baselineOffset:(inout CGFloat*)baselineOffset inTextContainer:(NSTextContainer*)textContainer forGlyphRange:(NSRange)glyphRange { + NSParagraphStyle* rulerAttrs = textContainer.textView.defaultParagraphStyle; + if (rulerAttrs == nil) { + return NO; + } BOOL didModify = NO; BOOL verticalOrientation = textContainer.layoutOrientation == NSTextLayoutOrientationVertical; NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; - NSParagraphStyle* rulerAttrs = textContainer.textView.defaultParagraphStyle; - CGFloat lineSpacing = rulerAttrs.lineSpacing; CGFloat lineHeight = rulerAttrs.minimumLineHeight; CGFloat baseline = lineHeight * 0.5; if (!verticalOrientation) { @@ -1939,16 +1893,10 @@ - (BOOL) layoutManager:(NSLayoutManager*)layoutManager effectiveRange:NULL][(id)kCTBaselineReferenceFont]; baseline += (refFont.ascender + refFont.descender) * 0.5; } - CGFloat lineHeightDelta = lineFragmentUsedRect->size.height - lineHeight - lineSpacing; - if (fabs(lineHeightDelta) > 0.1) { - lineFragmentUsedRect->size.height = round(lineFragmentUsedRect->size.height - lineHeightDelta); - lineFragmentRect->size.height = round(lineFragmentRect->size.height - lineHeightDelta); - didModify |= YES; - } - CGFloat newBaselineOffset = floor(lineFragmentUsedRect->origin.y - lineFragmentRect->origin.y + baseline); - if (fabs(*baselineOffset - newBaselineOffset) > 0.1) { + CGFloat newBaselineOffset = round(lineFragmentUsedRect->origin.y - lineFragmentRect->origin.y + baseline); + if (isnormal(*baselineOffset - newBaselineOffset)) { *baselineOffset = newBaselineOffset; - didModify |= YES; + didModify = YES; } return didModify; } @@ -1960,8 +1908,9 @@ - (BOOL) layoutManager:(NSLayoutManager*)layoutManager } else { unichar charBeforeIndex = [layoutManager.textStorage.mutableString characterAtIndex:charIndex - 1]; - return self.contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D - : charBeforeIndex != '\t'; + SquirrelTextView* textView = (SquirrelTextView*)layoutManager.firstTextView; + return textView.contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D + : charBeforeIndex != '\t'; } } @@ -1985,24 +1934,13 @@ - (NSRect) layoutManager:(NSLayoutManager*)layoutManager proposedLineFragment:(NSRect)proposedRect glyphPosition:(NSPoint)glyphPosition characterIndex:(NSUInteger)charIndex { - CGFloat width = 0.0; - if (charIndex > 0 && [layoutManager.textStorage.mutableString - characterAtIndex:charIndex] == 0x8B) { - NSRange rubyRange; - id rubyAnnotation = - [layoutManager.textStorage attribute:(id)kCTRubyAnnotationAttributeName - atIndex:charIndex - 1 - effectiveRange:&rubyRange]; - if (rubyAnnotation != nil) { - NSAttributedString* rubyString = [layoutManager.textStorage - attributedSubstringFromRange:rubyRange]; - CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)rubyString); - CGRect rubyRect = CTLineGetBoundsWithOptions((CTLineRef)CFAutorelease(line), 0); - width = fdim(rubyRect.size.width, rubyString.size.width); + NSRect rect = {glyphPosition, NSZeroSize}; + if ([layoutManager.textStorage.mutableString characterAtIndex:charIndex] == 0x8B) { + if (NSValue* controlCharacterSize = [layoutManager.textStorage attribute:kControlCharacterSizeAttributeName atIndex:charIndex effectiveRange:NULL]) { + rect.size = controlCharacterSize.sizeValue; } } - return NSMakeRect(glyphPosition.x, glyphPosition.y, width, - NSMaxY(proposedRect) - glyphPosition.y); + return rect; } @end // SquirrelLayoutManager @@ -2011,11 +1949,36 @@ - (NSRect) layoutManager:(NSLayoutManager*)layoutManager #pragma mark - Typesetting extensions for TextKit 2 (MacOS 12 or higher) API_AVAILABLE(macos(12.0)) -@interface SquirrelTextLayoutFragment : NSTextLayoutFragment +@interface SquirrelTextLayoutFragment : NSTextLayoutFragment @end @implementation SquirrelTextLayoutFragment +- (NSTextLayoutOrientation)layoutOrientation { + return self.textLayoutManager.textContainer.layoutOrientation; +} + +- (CGRect)renderingSurfaceBounds { + CGRect bounds = super.renderingSurfaceBounds; + if (self.state == NSTextLayoutFragmentStateLayoutAvailable) { + SquirrelContentBlock contentBlock = ((SquirrelTextView*)self.textLayoutManager.textContainer.textView).contentBlock; + if (contentBlock == kLinearCandidatesBlock || contentBlock == kStackedCandidatesBlock) { + NSParagraphStyle* rulerStyle = self.textLayoutManager.textContainer.textView.defaultParagraphStyle; + if ([self.rangeInElement.location isEqual:self.textLayoutManager.documentRange.location]) { + CGFloat spacing = contentBlock == kStackedCandidatesBlock ? + rulerStyle.paragraphSpacingBefore : floor(rulerStyle.lineSpacing * 0.5); + bounds.origin.y -= spacing; + bounds.size.height += spacing; + } + if ([self.rangeInElement.endLocation isEqual:self.textLayoutManager.documentRange.endLocation]) { + bounds.size.height += contentBlock == kStackedCandidatesBlock ? + rulerStyle.paragraphSpacing : ceil(rulerStyle.lineSpacing * 0.5); + } + } + } + return bounds; +} + - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { if (@available(macOS 14.0, *)) { @@ -2023,11 +1986,10 @@ - (void)drawAtPoint:(CGPoint)point point.x -= self.layoutFragmentFrame.origin.x; point.y -= self.layoutFragmentFrame.origin.y; } - BOOL verticalOrientation = self.textLayoutManager.textContainer.layoutOrientation == NSTextLayoutOrientationVertical; for (NSTextLineFragment* lineFrag in self.textLineFragments) { CGRect lineRect = CGRectOffset(lineFrag.typographicBounds, point.x, point.y); CGFloat baseline = CGRectGetMidY(lineRect); - if (!verticalOrientation) { + if (self.layoutOrientation == NSTextLayoutOrientationHorizontal) { NSFont* refFont = [lineFrag.attributedString attribute:(id)kCTBaselineReferenceInfoAttributeName atIndex:lineFrag.characterRange.location @@ -2035,10 +1997,10 @@ - (void)drawAtPoint:(CGPoint)point baseline += (refFont.ascender + refFont.descender) * 0.5; } CGPoint renderOrigin = CGPointMake(NSMinX(lineRect) + lineFrag.glyphOrigin.x, - floor(baseline) - lineFrag.glyphOrigin.y); - CGPoint deviceOrigin = CGContextConvertPointToDeviceSpace(context, renderOrigin); - renderOrigin = CGContextConvertPointToUserSpace(context, - CGPointMake(round(deviceOrigin.x), round(deviceOrigin.y))); + round(baseline) - lineFrag.glyphOrigin.y); + renderOrigin = CGContextConvertPointToDeviceSpace(context, renderOrigin); + renderOrigin = CGContextConvertPointToUserSpace(context, CGPointMake(ceil(renderOrigin.x), + ceil(renderOrigin.y))); [lineFrag drawAtPoint:renderOrigin inContext:context]; } } @@ -2048,17 +2010,10 @@ - (void)drawAtPoint:(CGPoint)point __attribute__((objc_direct_members)) API_AVAILABLE(macos(12.0)) @interface SquirrelTextLayoutManager : NSTextLayoutManager - -@property(nonatomic, readonly) SquirrelContentBlock contentBlock; - @end @implementation SquirrelTextLayoutManager -- (SquirrelContentBlock)contentBlock { - return ((SquirrelTextView*)self.textContainer.textView).contentBlock; -} - - (BOOL) textLayoutManager:(NSTextLayoutManager*)textLayoutManager shouldBreakLineBeforeLocation:(id)location hyphenating:(BOOL)hyphenating { @@ -2071,18 +2026,20 @@ - (BOOL) textLayoutManager:(NSTextLayoutManager*)textLayoutManager } else { unichar charBeforeIndex = [contentStorage.textStorage.mutableString characterAtIndex:charIndex - 1]; - return self.contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D - : charBeforeIndex != '\t'; + SquirrelTextView* textView = (SquirrelTextView*)textLayoutManager.textContainer.textView; + return textView.contentBlock == kLinearCandidatesBlock ? charBeforeIndex == 0x1D + : charBeforeIndex != '\t'; } } - (NSTextLayoutFragment*)textLayoutManager:(NSTextLayoutManager*)textLayoutManager textLayoutFragmentForLocation:(id)location inTextElement:(NSTextElement*)textElement { - NSTextRange* textRange = [NSTextRange.alloc initWithLocation:location - endLocation:textElement.elementRange.endLocation]; - return [SquirrelTextLayoutFragment.alloc - initWithTextElement:textElement range:textRange]; + NSTextRange* textRange = [NSTextRange.alloc + initWithLocation:location + endLocation:textElement.elementRange.endLocation]; + return [SquirrelTextLayoutFragment.alloc initWithTextElement:textElement + range:textRange]; } @end // SquirrelTextLayoutManager @@ -2190,7 +2147,7 @@ - (NSRect)blockRectForRange:(NSRange)charRange { usingBlock:^BOOL(NSTextRange* _Nullable segRange, CGRect segFrame, CGFloat baseline, NSTextContainer* _Nonnull textContainer) { if (!CGRectIsEmpty(segFrame)) { - if (NSIsEmptyRect(firstLineRect) || CGRectGetMinY(segFrame) < NSMaxY(firstLineRect) - 0.1) { + if (NSIsEmptyRect(firstLineRect) || CGRectGetMinY(segFrame) < nexttoward(NSMaxY(firstLineRect), -INFINITY)) { firstLineRect = NSUnionRect(segFrame, firstLineRect); } else { finalLineRect = NSUnionRect(segFrame, finalLineRect); @@ -2199,13 +2156,12 @@ - (NSRect)blockRectForRange:(NSRange)charRange { return YES; }]; - if (_contentBlock == kLinearCandidatesBlock && self.defaultParagraphStyle.lineSpacing > 0.1) { + if (_contentBlock == kLinearCandidatesBlock && isnormal(self.defaultParagraphStyle.lineSpacing)) { firstLineRect.size.height += self.defaultParagraphStyle.lineSpacing; if (!NSIsEmptyRect(finalLineRect)) finalLineRect.size.height += self.defaultParagraphStyle.lineSpacing; } - if (NSIsEmptyRect(finalLineRect)) { return firstLineRect; } else { @@ -2217,29 +2173,37 @@ - (NSRect)blockRectForRange:(NSRange)charRange { NSRange glyphRange = [self.layoutManager glyphRangeForCharacterRange:charRange actualCharacterRange:NULL]; NSRange firstLineRange = NSMakeRange(NSNotFound, 0); - NSRect firstLineRect = [self.layoutManager - lineFragmentUsedRectForGlyphAtIndex:glyphRange.location - effectiveRange:&firstLineRange]; + NSRect firstLineRect = [self.layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&firstLineRange]; if (NSMaxRange(glyphRange) <= NSMaxRange(firstLineRange)) { CGFloat leading = [self.layoutManager locationForGlyphAtIndex:glyphRange.location].x; - CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) - ? [self.layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSMaxX(firstLineRect); - return NSMakeRect(NSMinX(firstLineRect) + leading, NSMinY(firstLineRect), - trailing - leading, NSHeight(firstLineRect)); + CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) ? + [self.layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x : NSMaxX(firstLineRect); + CGFloat height = NSHeight(firstLineRect); + if (self.contentBlock == kLinearCandidatesBlock && + NSMaxRange(firstLineRange) == self.layoutManager.numberOfGlyphs && + isnormal(self.defaultParagraphStyle.lineSpacing)) { + height += self.defaultParagraphStyle.lineSpacing; + } + return NSMakeRect(NSMinX(firstLineRect) + leading, NSMinY(firstLineRect), trailing - leading, height); } else { - NSRect finalLineRect = [self.layoutManager - lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 - effectiveRange:NULL]; + NSRange finalLineRange = NSMakeRange(NSNotFound, 0); + NSRect finalLineRect = [self.layoutManager lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:&finalLineRange]; CGFloat containerWidth = NSWidth([self.layoutManager usedRectForTextContainer:self.textContainer]); - return NSMakeRect(0.0, NSMinY(firstLineRect), containerWidth, - NSMaxY(finalLineRect) - NSMinY(firstLineRect)); + CGFloat height = NSMaxY(finalLineRect) - NSMinY(firstLineRect); + if (self.contentBlock == kLinearCandidatesBlock && + NSMaxRange(finalLineRange) == self.layoutManager.numberOfGlyphs && + isnormal(self.defaultParagraphStyle.lineSpacing)) { + height += self.defaultParagraphStyle.lineSpacing; + } + return NSMakeRect(0.0, NSMinY(firstLineRect), containerWidth, 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 */ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { SquirrelTextPolygon textPolygon = {.head = NSZeroRect, .body = NSZeroRect, .tail = NSZeroRect}; @@ -2259,7 +2223,7 @@ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { usingBlock:^BOOL(NSTextRange* _Nullable segRange, CGRect segFrame, CGFloat baseline, NSTextContainer* _Nonnull textContainer) { if (!CGRectIsEmpty(segFrame)) { - if (NSIsEmptyRect(headLineRect) || CGRectGetMinY(segFrame) < NSMaxY(headLineRect) - 0.1) { + if (NSIsEmptyRect(headLineRect) || CGRectGetMinY(segFrame) < nexttoward(NSMaxY(headLineRect), -INFINITY)) { headLineRect = NSUnionRect(segFrame, headLineRect); headLineRange = [headLineRange textRangeByFormingUnionWithTextRange:segRange]; } else { @@ -2269,7 +2233,7 @@ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { } return YES; }]; - if (_contentBlock == kLinearCandidatesBlock && self.defaultParagraphStyle.lineSpacing > 0.1) { + if (_contentBlock == kLinearCandidatesBlock && isnormal(self.defaultParagraphStyle.lineSpacing)) { headLineRect.size.height += self.defaultParagraphStyle.lineSpacing; if (!NSIsEmptyRect(tailLineRect)) tailLineRect.size.height += self.defaultParagraphStyle.lineSpacing; @@ -2305,25 +2269,31 @@ - (SquirrelTextPolygon)textPolygonForRange:(NSRange)charRange { NSRange glyphRange = [self.layoutManager glyphRangeForCharacterRange:charRange actualCharacterRange:NULL]; NSRange headLineRange = NSMakeRange(NSNotFound, 0); - NSRect headLineRect = [self.layoutManager - lineFragmentUsedRectForGlyphAtIndex:glyphRange.location - effectiveRange:&headLineRange]; + NSRect headLineRect = [self.layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&headLineRange]; CGFloat leading = [self.layoutManager locationForGlyphAtIndex:glyphRange.location].x; if (NSMaxRange(headLineRange) >= NSMaxRange(glyphRange)) { - CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(headLineRange) - ? [self.layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSMaxX(headLineRect); - textPolygon.body = NSMakeRect(leading, NSMinY(headLineRect), - trailing - leading, NSHeight(headLineRect)); + CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(headLineRange) ? + [self.layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x : NSMaxX(headLineRect); + CGFloat height = NSHeight(headLineRect); + if (self.contentBlock == kLinearCandidatesBlock && + NSMaxRange(headLineRange) == self.layoutManager.numberOfGlyphs && + isnormal(self.defaultParagraphStyle.lineSpacing)) { + height += self.defaultParagraphStyle.lineSpacing; + } + textPolygon.body = NSMakeRect(leading, NSMinY(headLineRect), trailing - leading, height); } else { CGFloat containerWidth = NSWidth([self.layoutManager usedRectForTextContainer:self.textContainer]); NSRange tailLineRange = NSMakeRange(NSNotFound, 0); - NSRect tailLineRect = [self.layoutManager - lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 - effectiveRange:&tailLineRange]; - CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(tailLineRange) - ? [self.layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x - : NSMaxX(tailLineRect); + NSRect tailLineRect = [self.layoutManager lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:&tailLineRange]; + if (self.contentBlock == kLinearCandidatesBlock && + NSMaxRange(tailLineRange) == self.layoutManager.numberOfGlyphs && + isnormal(self.defaultParagraphStyle.lineSpacing)) { + tailLineRect.size.height += self.defaultParagraphStyle.lineSpacing; + } + CGFloat trailing = NSMaxRange(glyphRange) < NSMaxRange(tailLineRange) ? + [self.layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x : NSMaxX(tailLineRect); if (NSMaxRange(tailLineRange) == NSMaxRange(glyphRange)) { if (glyphRange.location == headLineRange.location) { textPolygon.body = NSMakeRect(0.0, NSMinY(headLineRect), containerWidth, @@ -2475,8 +2445,9 @@ - (instancetype)init { _scrollView.hasVerticalScroller = YES; _scrollView.scrollerStyle = NSScrollerStyleOverlay; _scrollView.scrollerKnobStyle = NSScrollerKnobStyleDark; - _scrollView.contentView.wantsLayer = YES; - _scrollView.contentView.layer.geometryFlipped = YES; + _scrollView.wantsLayer = YES; + _scrollView.layer.geometryFlipped = YES; + _scrollView.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; _style = kDefaultStyle; _theme = _defaultTheme; @@ -2520,7 +2491,7 @@ - (instancetype)init { [_documentView.layer addSublayer:_gridLayer]; [_documentView.layer addSublayer:_nonHilitedCandidateLayer]; [_documentView.layer addSublayer:_hilitedCandidateLayer]; - _scrollView.contentView.layer.mask = _clipLayer; + _scrollView.layer.mask = _clipLayer; } return self; } @@ -2547,7 +2518,7 @@ - (void)updateColors { } if (_theme.hilitedCandidateBackColor != nil) { _hilitedCandidateLayer.fillColor = _theme.hilitedCandidateBackColor.CGColor; - if (_theme.shadowSize > 0.1) { + if (isnormal(_theme.shadowSize)) { _hilitedCandidateLayer.shadowOffset = CGSizeMake(_theme.shadowSize, _theme.shadowSize); _hilitedCandidateLayer.shadowOpacity = 1.0; } else { @@ -2574,8 +2545,7 @@ - (void)updateColors { static BOOL anyTruncated(SquirrelCandidateInfo* array, NSUInteger count) { for (NSUInteger i = 0; i < count; ++i) { - if (array[i].truncated) - return YES; + if (array[i].truncated) return YES; } return NO; } @@ -2608,12 +2578,7 @@ - (void)estimateBoundsOnScreen:(NSRect)screen } if (candidateCount > 0) { _documentRect = _candidateView.layoutText; - if (@available(macOS 12.0, *)) { - _documentRect.size.height += _theme.lineSpacing; - } else { - _documentRect.size.height += _theme.linear ? 0.0 : _theme.lineSpacing; - } - + _documentRect.size.height += _theme.lineSpacing; if (_theme.linear && !anyTruncated(candidateInfos, candidateCount)) { _documentRect.size.width -= _theme.fullWidth; } @@ -2647,7 +2612,8 @@ - (void)layoutContents { NSPoint origin = NSMakePoint(_theme.borderInsets.width, _theme.borderInsets.height); if (!_statusView.hidden) { // status - _contentRect.origin = NSMakePoint(origin.x + ceil(_theme.fullWidth * 0.5), origin.y); + _contentRect.origin = NSMakePoint(origin.x + ceil(_theme.fullWidth * 0.5), + origin.y); return; } if (!_preeditView.hidden) { @@ -2657,8 +2623,6 @@ - (void)layoutContents { _contentRect = _preeditRect; } if (!_scrollView.hidden) { - _clipRect.size.width = NSWidth(_documentRect); - _clipRect.size.height = NSHeight(_documentRect) - _clippedHeight; if (!_preeditView.hidden) { _clipRect.origin.x = origin.x; _clipRect.origin.y = NSMaxY(_preeditRect) + _theme.preeditSpacing; @@ -2693,8 +2657,8 @@ - (void)drawViewWithHilitedCandidate:(NSUInteger)hilitedCandidate _preeditView.needsDisplayInRect = _preeditView.bounds; // invalidate Rect beyond bound of textview to clear any out-of-bound drawing from last round if (!_scrollView.hidden) - _candidateView.needsDisplayInRect = [_candidateView convertRect:_documentView.bounds - fromView:_documentView]; + _candidateView.needsDisplayInRect = [_documentView convertRect:_documentView.bounds + toView:_candidateView]; if (!_pagingView.hidden) _pagingView.needsDisplayInRect = _pagingView.bounds; } @@ -2713,40 +2677,48 @@ - (void)highlightCandidate:(NSUInteger)hilitedCandidate { NSUInteger priorActivePage = _hilitedCandidate / _theme.pageSize; NSUInteger newActivePage = hilitedCandidate / _theme.pageSize; if (newActivePage != priorActivePage) { - self.needsDisplayInRect = [_documentView convertRect:_sectionRects[priorActivePage] toView:self]; - _candidateView.needsDisplayInRect = [_documentView convertRect:_sectionRects[priorActivePage] toView:_candidateView]; + self.needsDisplayInRect = [_documentView convertRect:_sectionRects[priorActivePage] + toView:self]; + _candidateView.needsDisplayInRect = [_documentView convertRect:_sectionRects[priorActivePage] + toView:_candidateView]; } - self.needsDisplayInRect = [_documentView convertRect:_sectionRects[newActivePage] toView:self]; - _candidateView.needsDisplayInRect = [_documentView convertRect:_sectionRects[newActivePage] toView:_candidateView]; + self.needsDisplayInRect = [_documentView convertRect:_sectionRects[newActivePage] + toView:self]; + _candidateView.needsDisplayInRect = [_documentView convertRect:_sectionRects[newActivePage] + toView:_candidateView]; } else { self.needsDisplayInRect = _clipRect; - _candidateView.needsDisplayInRect = [_documentView convertRect:_documentView.bounds toView:_candidateView]; + _candidateView.needsDisplayInRect = [_documentView convertRect:_documentView.bounds + toView:_candidateView]; } _hilitedCandidate = hilitedCandidate; [self unclipHighlightedCandidate]; } - (void)unclipHighlightedCandidate { + if (!isnormal(_clippedHeight)) { + return; + } if (_expanded) { NSUInteger activePage = _hilitedCandidate / _theme.pageSize; - if (NSMinY(_sectionRects[activePage]) < NSMinY(_scrollView.documentVisibleRect) - 0.1) { + if (NSMinY(_sectionRects[activePage]) < nexttoward(NSMinY(_scrollView.documentVisibleRect), -INFINITY)) { NSPoint origin = _scrollView.contentView.bounds.origin; origin.y -= NSMinY(_scrollView.documentVisibleRect) - NSMinY(_sectionRects[activePage]); [_scrollView.contentView scrollToPoint:origin]; _scrollView.verticalScroller.doubleValue = NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; - } else if (NSMaxY(_sectionRects[activePage]) > NSMaxY(_scrollView.documentVisibleRect) + 0.1) { + } else if (NSMaxY(_sectionRects[activePage]) > nexttoward(NSMaxY(_scrollView.documentVisibleRect), INFINITY)) { NSPoint origin = _scrollView.contentView.bounds.origin; origin.y += NSMaxY(_sectionRects[activePage]) - NSMaxY(_scrollView.documentVisibleRect); [_scrollView.contentView scrollToPoint:origin]; _scrollView.verticalScroller.doubleValue = NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; } } else { - if (NSMinY(_scrollView.documentVisibleRect) > _candidatePolygons[_hilitedCandidate].minY() + 0.1) { + if (NSMinY(_scrollView.documentVisibleRect) > nexttoward(_candidatePolygons[_hilitedCandidate].minY(), INFINITY)) { NSPoint origin = _scrollView.contentView.bounds.origin; origin.y -= NSMinY(_scrollView.documentVisibleRect) - _candidatePolygons[_hilitedCandidate].minY(); [_scrollView.contentView scrollToPoint:origin]; _scrollView.verticalScroller.doubleValue = NSMinY(_scrollView.documentVisibleRect) / _clippedHeight; - } else if (NSMaxY(_scrollView.documentVisibleRect) < _candidatePolygons[_hilitedCandidate].maxY() - 0.1) { + } else if (NSMaxY(_scrollView.documentVisibleRect) < nexttoward(_candidatePolygons[_hilitedCandidate].maxY(), -INFINITY)) { NSPoint origin = _scrollView.contentView.bounds.origin; origin.y += _candidatePolygons[_hilitedCandidate].maxY() - NSMaxY(_scrollView.documentVisibleRect); [_scrollView.contentView scrollToPoint:origin]; @@ -2832,6 +2804,12 @@ - (NSBezierPath*)updateFunctionButtonLayer { NSBezierPath* buttonPath = [NSBezierPath squirclePathForRect:buttonRect cornerRadius:cornerRadius]; _functionButtonLayer.path = buttonPath.quartzPath; _functionButtonLayer.fillColor = buttonColor.CGColor; + if (isnormal(_theme.shadowSize)) { + _functionButtonLayer.shadowOffset = CGSizeMake(_theme.shadowSize, _theme.shadowSize); + _functionButtonLayer.shadowOpacity = 1.0; + } else { + _functionButtonLayer.shadowOpacity = 0.0; + } _functionButtonLayer.hidden = NO; return buttonPath; } else { @@ -2849,10 +2827,11 @@ - (void)updateLayer { CGFloat hilitedCornerRadius = fmin(_theme.hilitedCornerRadius, _theme.candidateParagraphStyle.minimumLineHeight * 0.5); - /*** Preedit Rects **/ + /* Preedit */ _deleteBackRect = NSZeroRect; NSBezierPath* hilitedPreeditPath; if (!_preeditView.hidden) { + _preeditRect.origin = backgroundRect.origin; _preeditRect.size.width = NSWidth(backgroundRect); _preeditRect = [self backingAlignedRect:_preeditRect options:NSAlignAllEdgesNearest]; // Draw the highlighted part of preedit text @@ -2878,6 +2857,9 @@ - (void)updateLayer { NSMaxRange(_hilitedPreeditRange) + 2 == _preeditContents.length) { textPolygon.body.size.width += padding; } + if (NSMaxX(textPolygon.body) > NSMaxX(innerBox) - 2) { + textPolygon.body.size.width = NSMaxX(innerBox) - NSMinX(textPolygon.body); + } textPolygon.body = [self backingAlignedRect:NSIntersectionRect(textPolygon.body, innerBox) options:NSAlignAllEdgesNearest]; } @@ -2891,17 +2873,18 @@ - (void)updateLayer { textPolygon.tail = [self backingAlignedRect:NSIntersectionRect(textPolygon.tail, innerBox) options:NSAlignAllEdgesNearest]; } - hilitedPreeditPath = [NSBezierPath squirclePathForPolygon:textPolygon cornerRadius:hilitedCornerRadius]; + CGFloat cornerRadius = fmin(hilitedCornerRadius, fabs([_theme.preeditAttrs[NSFontAttributeName] descender])); + hilitedPreeditPath = [NSBezierPath squirclePathForPolygon:textPolygon cornerRadius:cornerRadius]; } _deleteBackRect = [_preeditView blockRectForRange:NSMakeRange(_preeditContents.length - 1, 1)]; _deleteBackRect.size.width += _theme.fullWidth; - _deleteBackRect.origin.x = NSMaxX(backgroundRect) - NSWidth(_deleteBackRect); - _deleteBackRect.origin.y += _theme.borderInsets.height; + _deleteBackRect.origin.x = NSMaxX(_preeditRect) - NSWidth(_deleteBackRect); + _deleteBackRect.origin.y = NSMaxY(_preeditRect) - NSHeight(_deleteBackRect); _deleteBackRect = [self backingAlignedRect:NSIntersectionRect(_deleteBackRect, _preeditRect) options:NSAlignAllEdgesNearest]; } - /*** Candidates Rects, all in documentView coordinates (except for `candidatesRect`) ***/ + /* Candidates (in documentView coordinates, except for `clipRect`) */ _candidatePolygons = NULL; _sectionRects = NULL; _tabularIndices = NULL; @@ -2911,7 +2894,7 @@ - (void)updateLayer { if (!_scrollView.hidden) { _clipRect.size.width = NSWidth(backgroundRect); _clipRect = [self backingAlignedRect:NSIntersectionRect(_clipRect, backgroundRect) - options:NSAlignAllEdgesNearest]; + options:NSAlignAllEdgesNearest]; _documentRect.size.width = NSWidth(backgroundRect); _documentRect = [_documentView backingAlignedRect:_documentRect options:NSAlignAllEdgesNearest]; @@ -2948,6 +2931,8 @@ - (void)updateLayer { candidatePolygon.body.size.width = NSWidth(_documentRect); } else if (!NSIsEmptyRect(candidatePolygon.tail)) { candidatePolygon.body.size.width += _theme.fullWidth; + } else if (NSMaxX(candidatePolygon.body) > NSMaxX(_documentRect) - 2) { + candidatePolygon.body.size.width = NSMaxX(_documentRect) - NSMinX(candidatePolygon.body); } candidatePolygon.body = [_documentView backingAlignedRect:NSIntersectionRect(candidatePolygon.body, _documentRect) options:NSAlignAllEdgesNearest]; @@ -2998,7 +2983,7 @@ - (void)updateLayer { } } - /*** Paging Rects ***/ + /* Paging */ _pageUpRect = NSZeroRect; _pageDownRect = NSZeroRect; _expanderRect = NSZeroRect; @@ -3011,33 +2996,30 @@ - (void)updateLayer { _pagingRect = [self backingAlignedRect:NSIntersectionRect(_pagingRect, backgroundRect) options:NSAlignAllEdgesNearest]; if (_theme.showPaging) { - _pageUpRect = [_pagingView blockRectForRange:NSMakeRange(0, 1)]; - _pageDownRect = [_pagingView blockRectForRange:NSMakeRange(_pagingContents.length - 1, 1)]; - _pageDownRect.origin.x += NSMinX(_pagingRect); + _pageUpRect = NSOffsetRect([_pagingView blockRectForRange:NSMakeRange(0, 1)], + NSMinX(_pagingRect), NSMinY(_pagingRect)); + _pageDownRect = NSOffsetRect([_pagingView blockRectForRange:NSMakeRange(_pagingContents.length - 1, 1)], + NSMinX(_pagingRect), NSMinY(_pagingRect)); _pageDownRect.size.width += _theme.fullWidth; - _pageDownRect.origin.y += NSMinY(_pagingRect); - _pageUpRect.origin.x += NSMinX(_pagingRect); // bypass the bug of getting wrong glyph position when tab is presented - _pageUpRect.size.width = NSWidth(_pageDownRect); - _pageUpRect.origin.y += NSMinY(_pagingRect); + _pageUpRect.size = _pageDownRect.size; _pageUpRect = [self backingAlignedRect:NSIntersectionRect(_pageUpRect, _pagingRect) options:NSAlignAllEdgesNearest]; _pageDownRect = [self backingAlignedRect:NSIntersectionRect(_pageDownRect, _pagingRect) options:NSAlignAllEdgesNearest]; } if (_theme.tabular) { - _expanderRect = [_pagingView blockRectForRange:NSMakeRange(_pagingContents.length / 2, 1)]; - _expanderRect.origin.x += NSMinX(_pagingRect); + _expanderRect = NSOffsetRect([_pagingView blockRectForRange:NSMakeRange(_pagingContents.length / 2, 1)], + NSMinX(_pagingRect), NSMinY(_pagingRect)); _expanderRect.size.width += _theme.fullWidth; - _expanderRect.origin.y += NSMinY(_pagingRect); _expanderRect = [self backingAlignedRect:NSIntersectionRect(_expanderRect, _pagingRect) options:NSAlignAllEdgesNearest]; } } - /*** Border Rects ***/ + /* Border */ CGFloat outerCornerRadius = fmin(_theme.cornerRadius, NSHeight(panelRect) * 0.5); - CGFloat innerCornerRadius = clamp(_theme.hilitedCornerRadius, + CGFloat innerCornerRadius = clamp(hilitedCornerRadius, outerCornerRadius - fmin(_theme.borderInsets.width, _theme.borderInsets.height), NSHeight(backgroundRect) * 0.5); NSBezierPath* panelPath; @@ -3050,10 +3032,12 @@ - (void)updateLayer { mainPanelRect.size.height -= NSHeight(_pagingRect); NSRect tailPanelRect = NSInsetRect(NSOffsetRect(_pagingRect, 0, _theme.borderInsets.height), -_theme.borderInsets.width, 0); - panelPath = [NSBezierPath squirclePathForPolygon:(SquirrelTextPolygon){mainPanelRect, tailPanelRect, NSZeroRect} cornerRadius:outerCornerRadius]; + panelPath = [NSBezierPath squirclePathForPolygon:(SquirrelTextPolygon){mainPanelRect, tailPanelRect, NSZeroRect} + cornerRadius:outerCornerRadius]; NSRect mainBackgroundRect = backgroundRect; mainBackgroundRect.size.height -= NSHeight(_pagingRect); - backgroundPath = [NSBezierPath squirclePathForPolygon:(SquirrelTextPolygon){mainBackgroundRect, _pagingRect, NSZeroRect} cornerRadius:innerCornerRadius]; + backgroundPath = [NSBezierPath squirclePathForPolygon:(SquirrelTextPolygon){mainBackgroundRect, _pagingRect, NSZeroRect} + cornerRadius:innerCornerRadius]; } NSBezierPath* borderPath = panelPath.copy; [borderPath appendBezierPath:backgroundPath]; @@ -3063,7 +3047,7 @@ - (void)updateLayer { [flip scaleXBy:1 yBy:-1]; NSBezierPath* shapePath = [flip transformBezierPath:panelPath]; - /*** Draw into layers ***/ + /* Draw into layers */ if (@available(macOS 10.14, *)) { _shape.path = shapePath.quartzPath; } @@ -3076,11 +3060,9 @@ - (void)updateLayer { } // highlighted candidate layer if (!_scrollView.hidden) { - NSAffineTransform* translate = NSAffineTransform.transform; - [translate translateXBy:-NSMinX(_clipRect) yBy:-NSMinY(_clipRect)]; - _clipLayer.path = [translate transformBezierPath:clipPath].quartzPath; + _clipLayer.path = [NSBezierPath squirclePathForRect:_scrollView.bounds cornerRadius:hilitedCornerRadius].quartzPath; NSBezierPath* activePagePath; - BOOL expanded = _candidateCount > _theme.pageSize; + BOOL expanded = _theme.tabular && _candidateCount > _theme.pageSize; if (expanded) { NSRect activePageRect = _sectionRects[_hilitedCandidate / _theme.pageSize]; activePagePath = [NSBezierPath squirclePathForRect:activePageRect cornerRadius:hilitedCornerRadius]; @@ -3088,7 +3070,7 @@ - (void)updateLayer { } if (_theme.candidateBackColor != nil) { NSBezierPath* nonHilitedCandidatePath = NSBezierPath.bezierPath; - BOOL stackColors = _theme.stackColors && _theme.candidateBackColor.alphaComponent < 0.999; + BOOL stackColors = _theme.stackColors && _theme.candidateBackColor.alphaComponent < nexttoward(1.0, 0.0); for (NSUInteger i = 0; i < _candidateCount; ++i) { if (i != _hilitedCandidate) { NSBezierPath* candidatePath = _theme.linear @@ -3109,7 +3091,7 @@ - (void)updateLayer { NSBezierPath* hilitedCandidatePath = _theme.linear ? [NSBezierPath squirclePathForPolygon:_candidatePolygons[_hilitedCandidate] cornerRadius:hilitedCornerRadius] : [NSBezierPath squirclePathForRect:_candidatePolygons[_hilitedCandidate].body cornerRadius:hilitedCornerRadius]; - if (_theme.stackColors && _theme.hilitedCandidateBackColor.alphaComponent < 0.999) + if (_theme.stackColors && _theme.hilitedCandidateBackColor.alphaComponent < nexttoward(1.0, 0.0)) [(expanded ? activePagePath : documentPath) appendBezierPath:hilitedCandidatePath]; _hilitedCandidateLayer.path = hilitedCandidatePath.quartzPath; _hilitedCandidateLayer.hidden = NO; @@ -3161,7 +3143,7 @@ - (void)updateLayer { NSBezierPath* nonCandidatePath = backgroundPath.copy; [nonCandidatePath appendBezierPath:clipPath]; if (_theme.stackColors && _theme.hilitedPreeditBackColor != nil && - _theme.hilitedPreeditBackColor.alphaComponent < 0.999) { + _theme.hilitedPreeditBackColor.alphaComponent < nexttoward(1.0, 0.0)) { if (hilitedPreeditPath != nil) [nonCandidatePath appendBezierPath:hilitedPreeditPath]; if (functionButtonPath != nil) @@ -3203,10 +3185,10 @@ - (SquirrelIndex)indexForMouseSpot:(NSPoint)spot { @end // SquirrelView -/* 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 */ +/** 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 */ @interface SquirrelToolTip : NSWindow typedef NS_CLOSED_ENUM(NSInteger, SquirrelDisplayType) { @@ -3215,8 +3197,8 @@ typedef NS_CLOSED_ENUM(NSInteger, SquirrelDisplayType) { @property(nonatomic, readonly, direct) BOOL empty; -- (void)showWithToolTip:(NSString* _Nullable)toolTip - display:(SquirrelDisplayType)display __attribute__((objc_direct)); +- (void)showToolTip:(NSString* _Nullable)toolTip + display:(SquirrelDisplayType)display __attribute__((objc_direct)); - (void)delayedShow:(NSTimer* _Nonnull)timer; - (void)delayedHide:(NSTimer* _Nonnull)timer; - (void)hide __attribute__((objc_direct)); @@ -3248,6 +3230,8 @@ - (instancetype)init { _textView.bezeled = YES; _textView.bezelStyle = NSTextFieldSquareBezel; _textView.selectable = NO; + _textView.usesSingleLineMode = NO; + _textView.lineBreakMode = NSLineBreakByWordWrapping; [contentView addSubview:_textView]; self.contentView = contentView; _empty = YES; @@ -3255,8 +3239,8 @@ - (instancetype)init { return self; } -- (void)showWithToolTip:(NSString*)toolTip - display:(SquirrelDisplayType)display { +- (void)showToolTip:(NSString*)toolTip + display:(SquirrelDisplayType)display { if (display == kDisplayNone || toolTip.length == 0) { [self clear]; return; @@ -3266,10 +3250,13 @@ - (void)showWithToolTip:(NSString*)toolTip _empty = NO; _textView.stringValue = toolTip; + _textView.preferredMaxLayoutWidth = NSWidth(panel.screen.visibleFrame) * 0.25; _textView.font = [NSFont toolTipsFontOfSize:0]; _textView.textColor = NSColor.windowFrameTextColor; [_textView sizeToFit]; NSSize contentSize = _textView.fittingSize; + contentSize.width += 3; + contentSize.height += 3; NSPoint spot = NSEvent.mouseLocation; NSCursor* cursor = NSCursor.currentSystemCursor; @@ -3279,10 +3266,10 @@ - (void)showWithToolTip:(NSString*)toolTip contentSize.width, contentSize.height); NSRect screenRect = panel.screen.visibleFrame; - if (NSMaxX(windowRect) > NSMaxX(screenRect) - 0.1) { + if (NSMaxX(windowRect) > nexttoward(NSMaxX(screenRect), -INFINITY)) { windowRect.origin.x = NSMaxX(screenRect) - NSWidth(windowRect); } - if (NSMinY(windowRect) < NSMinY(screenRect) + 0.1) { + if (NSMinY(windowRect) < nexttoward(NSMinY(screenRect), INFINITY)) { windowRect.origin.y = NSMinY(screenRect); } windowRect = [panel.screen backingAlignedRect:windowRect @@ -3439,8 +3426,10 @@ - (void)getLocked __attribute__((objc_direct)) { - (void)setIbeamRect:(NSRect)IbeamRect { if (!NSEqualRects(_IbeamRect, IbeamRect)) { _IbeamRect = IbeamRect; - _needsRedraw |= YES; - if (!NSIntersectsRect(IbeamRect, _screen.frame)) { + _needsRedraw = YES; + if (NSEqualRects(IbeamRect, NSZeroRect)) { + _initPosition = YES; + } else if (!NSIntersectsRect(_screen.frame, IbeamRect) && !NSContainsRect(_screen.frame, IbeamRect)) { [self willChangeValueForKey:@"screen"]; [self updateScreen]; [self didChangeValueForKey:@"screen"]; @@ -3450,8 +3439,10 @@ - (void)setIbeamRect:(NSRect)IbeamRect { } - (void)windowDidChangeBackingProperties:(NSNotification*)notification { - if ([notification.object isEqualTo:self]) - [self updateDisplayParameters]; + if ([notification.object isMemberOfClass:SquirrelPanel.class]) { + SquirrelPanel* panel = notification.object; + [panel updateDisplayParameters]; + } } - (void)observeValueForKeyPath:(NSString*)keyPath @@ -3493,6 +3484,8 @@ - (instancetype)init { self.backgroundColor = NSColor.clearColor; self.delegate = self; self.acceptsMouseMovedEvents = YES; + self.displaysWhenScreenProfileChanges = YES; + self.worksWhenModal = YES; NSFlippedView* contentView = NSFlippedView.alloc.init; contentView.autoresizesSubviews = NO; @@ -3557,12 +3550,11 @@ - (void)updateDisplayParameters __attribute__((objc_direct)) { [_view.theme.textAttrs[NSFontAttributeName] pointSize] / 144.0); _textWidthLimit = ceil((_view.theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * textWidthRatio - _view.theme.borderInsets.width * 2 - _view.theme.fullWidth); - if (_view.theme.lineLength > 0.1) { - _textWidthLimit = fmin(_view.theme.lineLength, _textWidthLimit); + if (isnormal(_view.theme.lineLength) && _view.theme.lineLength < _textWidthLimit) { + _textWidthLimit = _view.theme.lineLength; } if (_view.theme.tabular) { - _textWidthLimit = floor((_textWidthLimit + _view.theme.fullWidth) / (_view.theme.fullWidth * 2)) * - (_view.theme.fullWidth * 2) - _view.theme.fullWidth; + _textWidthLimit = floor(_textWidthLimit / (_view.theme.fullWidth * 2)) * (_view.theme.fullWidth * 2); } _view.candidateView.textContainer.size = NSMakeSize(_textWidthLimit, CGFLOAT_MAX); _view.preeditView.textContainer.size = NSMakeSize(_textWidthLimit, CGFLOAT_MAX); @@ -3589,7 +3581,7 @@ - (void)updateDisplayParameters __attribute__((objc_direct)) { : NSMakeSize(widthLimit, defaultBackImage.size.height / defaultBackImage.size.width * widthLimit); } if (@available(macOS 10.14, *)) { - _back.hidden = _view.theme.translucency < 0.001f; + _back.hidden = isfinite(_view.theme.translucency) && !isnormal(_view.theme.translucency); if (NSImage* darkBackImage = SquirrelView.darkTheme.backImage; darkBackImage.valid) { CGFloat widthLimit = _textWidthLimit + SquirrelView.darkTheme.fullWidth; darkBackImage.resizingMode = NSImageResizingModeStretch; @@ -3599,6 +3591,16 @@ - (void)updateDisplayParameters __attribute__((objc_direct)) { } } [_view updateColors]; + if (self.isVisible) { + [self showPreedit:[_view.preeditContents.string substringToIndex:fmax(0UL, _view.preeditContents.length - 2)] + selRange:_view.hilitedPreeditRange + caretPos:_caretPos + candidateIndices:_indexRange + hilitedCandidate:_hilitedCandidate + pageNum:_pageNum + finalPage:_finalPage + didCompose:YES]; + } } - (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { @@ -3735,11 +3737,12 @@ - (void)sendEvent:(NSEvent*)event { if (cursorIndex >= 0 && cursorIndex < _indexRange.length && _hilitedCandidate != cursorIndex) { [self highlightFunctionButton:kVoidSymbol displayToolTip:kDisplayNone]; if (_view.theme.linear && _view.candidateInfos[cursorIndex].truncated) { - [_toolTip showWithToolTip:[_view.candidateContents.mutableString substringWithRange: - _view.candidateInfos[cursorIndex].candidateRange()] - display:kDisplayNow]; + [_toolTip showToolTip:[_view.candidateContents.mutableString substringWithRange: + _view.candidateInfos[cursorIndex].candidateRange()] + display:kDisplayNow]; } else { - [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil) display:kDisplayOnRequest]; + [_toolTip showToolTip:[NSBundle.mainBundle localizedStringForKey:@"candidate" value:nil table:@"Tooltips"] + display:kDisplayOnRequest]; } self.sectionNum = cursorIndex / _view.theme.pageSize; [_inputController performAction:kHIGHLIGHT @@ -3766,7 +3769,7 @@ - (void)sendEvent:(NSEvent*)event { scrollLocus = NSZeroPoint; scrollByLine = NO; } else if ((event.phase == NSEventPhaseNone || event.momentumPhase == NSEventPhaseNone) && - !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { + isfinite(scrollLocus.x) && isfinite(scrollLocus.y)) { CGFloat scrollDistance = 0.0; // determine scrolling direction by confining to sectors within ±30º of any axis if (fabs(event.scrollingDeltaX) > fabs(event.scrollingDeltaY) * sqrt(3.0)) { @@ -3779,7 +3782,7 @@ - (void)sendEvent:(NSEvent*)event { // compare accumulated locus length against threshold and limit paging to max once if (scrollLocus.x > scrollThreshold) { if (_view.theme.vertical && - NSMaxY(_view.scrollView.documentVisibleRect) < NSMaxY(_view.documentRect) - 0.1) { + NSMaxY(_view.scrollView.documentVisibleRect) < nexttoward(NSMaxY(_view.documentRect), -INFINITY)) { scrollByLine = YES; NSPoint origin = _view.scrollView.contentView.bounds.origin; origin.y += fmin(scrollDistance, @@ -3792,7 +3795,7 @@ - (void)sendEvent:(NSEvent*)event { scrollLocus = NSMakePoint(INFINITY, INFINITY); } } else if (scrollLocus.y > scrollThreshold) { - if (NSMinY(_view.scrollView.documentVisibleRect) > NSMinY(_view.documentRect) + 0.1) { + if (NSMinY(_view.scrollView.documentVisibleRect) > nexttoward(NSMinY(_view.documentRect), INFINITY)) { scrollByLine = YES; NSPoint origin = _view.scrollView.contentView.bounds.origin; origin.y -= fmin(scrollDistance, @@ -3805,7 +3808,7 @@ - (void)sendEvent:(NSEvent*)event { } } else if (scrollLocus.x < -scrollThreshold) { if (_view.theme.vertical && - NSMinY(_view.scrollView.documentVisibleRect) > NSMinY(_view.documentRect) + 0.1) { + NSMinY(_view.scrollView.documentVisibleRect) > nexttoward(NSMinY(_view.documentRect), INFINITY)) { scrollByLine = YES; NSPoint origin = _view.scrollView.contentView.bounds.origin; origin.y += fmax(scrollDistance, @@ -3818,7 +3821,7 @@ - (void)sendEvent:(NSEvent*)event { scrollLocus = NSMakePoint(INFINITY, INFINITY); } } else if (scrollLocus.y < -scrollThreshold) { - if (NSMaxY(_view.scrollView.documentVisibleRect) < NSMaxY(_view.documentRect) - 0.1) { + if (NSMaxY(_view.scrollView.documentVisibleRect) < nexttoward(NSMaxY(_view.documentRect), -INFINITY)) { scrollByLine = YES; NSPoint origin = _view.scrollView.contentView.bounds.origin; origin.y -= fmax(scrollDistance, @@ -3860,15 +3863,15 @@ - (void)highlightCandidate:(NSUInteger)hilitedCandidate __attribute__((objc_dire NSColor* labelColor = priorCandidate == priorHilitedCandidate && _sectionNum == priorSectionNum ? _view.theme.labelForeColor : _view.theme.dimmedLabelForeColor; [_view.candidateContents addAttribute:NSForegroundColorAttributeName - value:labelColor - range:priorRange.labelRange()]; + value:labelColor + range:priorRange.labelRange()]; if (priorCandidate == priorHilitedCandidate) { [_view.candidateContents addAttribute:NSForegroundColorAttributeName - value:_view.theme.textForeColor - range:priorRange.textRange()]; + value:_view.theme.textForeColor + range:priorRange.textRange()]; [_view.candidateContents addAttribute:NSForegroundColorAttributeName - value:_view.theme.commentForeColor - range:priorRange.commentRange()]; + value:_view.theme.commentForeColor + range:priorRange.commentRange()]; } } NSUInteger newCandidate = i + _sectionNum * _view.theme.pageSize; @@ -3878,15 +3881,15 @@ - (void)highlightCandidate:(NSUInteger)hilitedCandidate __attribute__((objc_dire NSColor* labelColor = newCandidate == hilitedCandidate ? _view.theme.hilitedLabelForeColor : _view.theme.labelForeColor; [_view.candidateContents addAttribute:NSForegroundColorAttributeName - value:labelColor - range:newRange.labelRange()]; + value:labelColor + range:newRange.labelRange()]; if (newCandidate == hilitedCandidate) { [_view.candidateContents addAttribute:NSForegroundColorAttributeName - value:_view.theme.hilitedTextForeColor - range:newRange.textRange()]; + value:_view.theme.hilitedTextForeColor + range:newRange.textRange()]; [_view.candidateContents addAttribute:NSForegroundColorAttributeName - value:_view.theme.hilitedCommentForeColor - range:newRange.commentRange()]; + value:_view.theme.hilitedCommentForeColor + range:newRange.commentRange()]; } } } @@ -3926,30 +3929,36 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton value:_view.theme.hilitedPreeditForeColor range:NSMakeRange(0, 1)]; functionButton = _pageNum == 0 ? kHomeKey : kPageUpKey; - [_toolTip showWithToolTip:NSLocalizedString(_pageNum == 0 ? @"home" : @"page_up", nil) display:display]; + [_toolTip showToolTip:[NSBundle.mainBundle + localizedStringForKey:_pageNum == 0 ? @"home" : @"page_up" + value:nil table:@"Tooltips"] display:display]; break; case kPageDownKey: [_view.pagingContents addAttribute:NSForegroundColorAttributeName value:_view.theme.hilitedPreeditForeColor range:NSMakeRange(_view.pagingContents.length - 1, 1)]; functionButton = _finalPage ? kEndKey : kPageDownKey; - [_toolTip showWithToolTip:NSLocalizedString(_finalPage ? @"end" : @"page_down", nil) display:display]; + [_toolTip showToolTip:[NSBundle.mainBundle + localizedStringForKey:_finalPage ? @"end" : @"page_down" + value:nil table:@"Tooltips"] display:display]; break; case kExpandButton: [_view.pagingContents addAttribute:NSForegroundColorAttributeName value:_view.theme.hilitedPreeditForeColor range:NSMakeRange(_view.pagingContents.length / 2, 1)]; functionButton = _locked ? kLockButton : _view.expanded ? kCompressButton : kExpandButton; - [_toolTip showWithToolTip:NSLocalizedString(_locked ? @"unlock" : _view.expanded ? - @"compress" : @"expand", nil) display:display]; + [_toolTip showToolTip:[NSBundle.mainBundle + localizedStringForKey:_locked ? @"unlock" : _view.expanded ? @"compress" : @"expand" + value:nil table:@"Tooltips"] display:display]; break; case kBackSpaceKey: [_view.preeditContents addAttribute:NSForegroundColorAttributeName value:_view.theme.hilitedPreeditForeColor range:NSMakeRange(_view.preeditContents.length - 1, 1)]; functionButton = _caretPos == NSNotFound || _caretPos == 0 ? kEscapeKey : kBackSpaceKey; - [_toolTip showWithToolTip:NSLocalizedString(_caretPos == NSNotFound || _caretPos == 0 ? - @"escape" : @"delete", nil) display:display]; + [_toolTip showToolTip:[NSBundle.mainBundle + localizedStringForKey:_caretPos == NSNotFound || _caretPos == 0 ? @"escape" : @"delete" + value:nil table:@"Tooltips"] display:display]; break; } [_view highlightFunctionButton:functionButton]; @@ -3983,31 +3992,34 @@ - (void)show __attribute__((objc_direct)) { BOOL sweepVertical = NSWidth(_IbeamRect) > NSHeight(_IbeamRect); NSRect contentRect = _view.contentRect; // fixed line length (text width), but not applicable to status message - if (theme.lineLength > 0.1 && _statusMessage == nil) { + if (isnormal(theme.lineLength) && _statusMessage == nil) { 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) */ + // 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.hidden) { - if (theme.lineLength < 0.1 && theme.vertical + if (isfinite(theme.lineLength) && !isnormal(theme.lineLength)) { + BOOL attained = theme.vertical ? sweepVertical ? (NSMinY(_IbeamRect) - fmax(NSWidth(contentRect), _maxSizeAttained.width) - - border.width - floor(theme.fullWidth * 0.5) < NSMinY(screenRect) + 0.1) + - border.width - floor(theme.fullWidth * 0.5) < nexttoward(NSMinY(screenRect), INFINITY)) : (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(screenRect) * textWidthRatio - - border.width * 2 - theme.fullWidth < NSMinY(screenRect) + 0.1) + - border.width * 2 - theme.fullWidth < nexttoward(NSMinY(screenRect), INFINITY)) : sweepVertical ? (NSMinX(_IbeamRect) - kOffsetGap - NSWidth(screenRect) * textWidthRatio - - border.width * 2 - theme.fullWidth > NSMinX(screenRect) + 0.1) + - border.width * 2 - theme.fullWidth > nexttoward(NSMinX(screenRect), INFINITY)) : (NSMaxX(_IbeamRect) + fmax(NSWidth(contentRect), _maxSizeAttained.width) - + border.width + floor(theme.fullWidth * 0.5) > NSMaxX(screenRect) - 0.1)) { - if (NSWidth(contentRect) > _maxSizeAttained.width + 0.1) { - _maxSizeAttained.width = NSWidth(contentRect); - } else { - contentRect.size.width = _maxSizeAttained.width; + + border.width + floor(theme.fullWidth * 0.5) > nexttoward(NSMaxX(screenRect), -INFINITY)); + if (attained) { + if (NSWidth(contentRect) > nexttoward(_maxSizeAttained.width, INFINITY)) { + _maxSizeAttained.width = NSWidth(contentRect); + } else { + contentRect.size.width = _maxSizeAttained.width; + } } } CGFloat textHeight = fmax(NSHeight(contentRect), _maxSizeAttained.height) + border.height * 2; - if (theme.vertical ? (NSMinX(_IbeamRect) - textHeight - (sweepVertical ? kOffsetGap : 0) < NSMinX(screenRect) + 0.1) - : (NSMinY(_IbeamRect) - textHeight - (sweepVertical ? 0 : kOffsetGap) < NSMinY(screenRect) + 0.1)) { - if (NSHeight(contentRect) > _maxSizeAttained.height + 0.1) { + if (theme.vertical ? (NSMinX(_IbeamRect) - textHeight - (sweepVertical ? kOffsetGap : 0) < nexttoward(NSMinX(screenRect), INFINITY)) + : (NSMinY(_IbeamRect) - textHeight - (sweepVertical ? 0 : kOffsetGap) < nexttoward(NSMinY(screenRect), INFINITY))) { + if (NSHeight(contentRect) > nexttoward(_maxSizeAttained.height, INFINITY)) { _maxSizeAttained.height = NSHeight(contentRect); } else { contentRect.size.height = _maxSizeAttained.height; @@ -4019,22 +4031,15 @@ but only when the text would expand on the side of upstream (i.e. towards the be if (_statusMessage != nil) { // following system UI, middle-align status message with cursor _initPosition = YES; - if (theme.vertical) { - windowRect.size.width = NSHeight(contentRect) + border.height * 2; - windowRect.size.height = NSWidth(contentRect) + border.width * 2 + theme.fullWidth; - } else { - windowRect.size.width = NSWidth(contentRect) + border.width * 2 + theme.fullWidth; - windowRect.size.height = NSHeight(contentRect) + border.height * 2; - } - if (sweepVertical) { - // vertically centre-align (MidY) in screen coordinates - windowRect.origin.x = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); - windowRect.origin.y = NSMidY(_IbeamRect) - NSHeight(windowRect) * 0.5; - } else { - // horizontally centre-align (MidX) in screen coordinates - windowRect.origin.x = NSMidX(_IbeamRect) - NSWidth(windowRect) * 0.5; - windowRect.origin.y = NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); - } + windowRect.size = theme.vertical ? NSMakeSize(NSHeight(contentRect) + border.height * 2, + NSWidth(contentRect) + border.width * 2 + theme.fullWidth) + : NSMakeSize(NSWidth(contentRect) + border.width * 2 + theme.fullWidth, + NSHeight(contentRect) + border.height * 2); + // vertically/horizontally centre-align (midY/midX) in screen coordinates + windowRect.origin = sweepVertical ? NSMakePoint(NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect), + NSMidY(_IbeamRect) - NSHeight(windowRect) * 0.5) + : NSMakePoint(NSMidX(_IbeamRect) - NSWidth(windowRect) * 0.5, + NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect)); } else { if (theme.vertical) { // anchor is the top right corner in screen coordinates (MaxX, MaxY) @@ -4042,24 +4047,21 @@ but only when the text would expand on the side of upstream (i.e. towards the be NSMaxY(self.frame) - NSWidth(contentRect) - border.width * 2 - theme.fullWidth, NSHeight(contentRect) + border.height * 2, NSWidth(contentRect) + border.width * 2 + theme.fullWidth); - _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || !NSContainsRect(screenRect, windowRect); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || NSContainsRect(windowRect, _IbeamRect) || + (!NSContainsRect(screenRect, windowRect) && !NSIntersectsRect(screenRect, 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 (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(screenRect) * textWidthRatio - - border.width * 2 - theme.fullWidth < NSMinY(screenRect) + 0.1) { - windowRect.origin.y = NSMaxY(_IbeamRect) + kOffsetGap; - } else { - windowRect.origin.y = NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); - } + BOOL isOnLower = NSMinY(_IbeamRect) - kOffsetGap - NSHeight(screenRect) * textWidthRatio - + border.width * 2 - theme.fullWidth < nexttoward(NSMinY(screenRect), INFINITY); + windowRect.origin.y = isOnLower ? NSMaxY(_IbeamRect) + kOffsetGap + : NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); // Make the right edge of candidate block fixed at the left of cursor windowRect.origin.x = NSMinX(_IbeamRect) + border.height - NSWidth(windowRect); } else { - if (NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect) < NSMinX(screenRect) + 0.1) { - windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; - } else { - windowRect.origin.x = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); - } + BOOL isOnLefter = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect) < nexttoward(NSMinX(screenRect), INFINITY); + windowRect.origin.x = isOnLefter ? NSMaxX(_IbeamRect) + kOffsetGap + : NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); windowRect.origin.y = NSMinY(_IbeamRect) + border.width + ceil(theme.fullWidth * 0.5) - NSHeight(windowRect); } } @@ -4069,22 +4071,20 @@ but only when the text would expand on the side of upstream (i.e. towards the be NSMaxY(self.frame) - NSHeight(contentRect) - border.height * 2, NSWidth(contentRect) + border.width * 2 + theme.fullWidth, NSHeight(contentRect) + border.height * 2); - _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || !NSContainsRect(screenRect, windowRect); + _initPosition |= NSIntersectsRect(windowRect, _IbeamRect) || NSContainsRect(windowRect, _IbeamRect) || + (!NSContainsRect(screenRect, windowRect) && !NSIntersectsRect(screenRect, 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 (NSMinX(_IbeamRect) - kOffsetGap - NSWidth(screenRect) * textWidthRatio - border.width * 2 - theme.fullWidth > NSMinX(screenRect) + 0.1) { - windowRect.origin.x = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect); - } else { - windowRect.origin.x = NSMaxX(_IbeamRect) + kOffsetGap; - } + BOOL isOnLefter = NSMinX(_IbeamRect) - kOffsetGap - NSWidth(screenRect) * textWidthRatio - + border.width * 2 - theme.fullWidth > nexttoward(NSMinX(screenRect), INFINITY); + windowRect.origin.x = isOnLefter ? NSMinX(_IbeamRect) - kOffsetGap - NSWidth(windowRect) + : NSMaxX(_IbeamRect) + kOffsetGap; windowRect.origin.y = NSMinY(_IbeamRect) + border.height - NSHeight(windowRect); } else { - if (NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect) < NSMinY(screenRect) + 0.1) { - windowRect.origin.y = NSMaxY(_IbeamRect) + kOffsetGap; - } else { - windowRect.origin.y = NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); - } + BOOL isOnLower = NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect) < nexttoward(NSMinY(screenRect), INFINITY); + windowRect.origin.y = isOnLower ? NSMaxY(_IbeamRect) + kOffsetGap + : NSMinY(_IbeamRect) - kOffsetGap - NSHeight(windowRect); windowRect.origin.x = NSMaxX(_IbeamRect) - border.width - ceil(theme.fullWidth * 0.5); } } @@ -4106,19 +4106,19 @@ but only when the text would expand on the side of upstream (i.e. towards the be } } - if (NSMaxX(windowRect) > NSMaxX(screenRect) - 0.1) { + if (NSMaxX(windowRect) > nexttoward(NSMaxX(screenRect), -INFINITY)) { windowRect.origin.x = (_initPosition && sweepVertical ? fmin(NSMinX(_IbeamRect) - kOffsetGap, NSMaxX(screenRect)) : NSMaxX(screenRect)) - NSWidth(windowRect); } - if (NSMinX(windowRect) < NSMinX(screenRect) + 0.1) { + if (NSMinX(windowRect) < nexttoward(NSMinX(screenRect), INFINITY)) { windowRect.origin.x = _initPosition && sweepVertical ? fmax(NSMaxX(_IbeamRect) + kOffsetGap, NSMinX(screenRect)) : NSMinX(screenRect); } - if (NSMinY(windowRect) < NSMinY(screenRect) + 0.1) { + if (NSMinY(windowRect) < nexttoward(NSMinY(screenRect), INFINITY)) { windowRect.origin.y = _initPosition && !sweepVertical ? fmax(NSMaxY(_IbeamRect) + kOffsetGap, NSMinY(screenRect)) : NSMinY(screenRect); } - if (NSMaxY(windowRect) > NSMaxY(screenRect) - 0.1) { + if (NSMaxY(windowRect) > nexttoward(NSMaxY(screenRect), -INFINITY)) { windowRect.origin.y = (_initPosition && !sweepVertical ? fmin(NSMinY(_IbeamRect) - kOffsetGap, NSMaxY(screenRect)) : NSMaxY(screenRect)) - NSHeight(windowRect); } @@ -4138,27 +4138,23 @@ but only when the text would expand on the side of upstream (i.e. towards the be NSRect viewRect = NSIntegralRectWithOptions(self.contentView.bounds, NSAlignAllEdgesNearest); _view.frame = viewRect; if (!_view.statusView.hidden) { - _view.statusView.frame = NSMakeRect(NSMinX(viewRect) + border.width + ceil(theme.fullWidth * 0.5) - - _view.statusView.textContainerOrigin.x, + _view.statusView.frame = NSMakeRect(NSMinX(viewRect) + border.width + ceil(theme.fullWidth * 0.5) - _view.statusView.textContainerOrigin.x, NSMinY(viewRect) + border.height - _view.statusView.textContainerOrigin.y, NSWidth(viewRect) - border.width * 2 - theme.fullWidth, NSHeight(viewRect) - border.height * 2); } if (!_view.preeditView.hidden) { - _view.preeditView.frame = NSMakeRect(NSMinX(viewRect) + border.width + ceil(theme.fullWidth * 0.5) - - _view.preeditView.textContainerOrigin.x, + _view.preeditView.frame = NSMakeRect(NSMinX(viewRect) + border.width + ceil(theme.fullWidth * 0.5) - _view.preeditView.textContainerOrigin.x, NSMinY(viewRect) + border.height - _view.preeditView.textContainerOrigin.y, NSWidth(viewRect) - border.width * 2 - theme.fullWidth, NSHeight(_view.preeditRect)); } if (!_view.pagingView.hidden) { CGFloat leadOrigin = theme.linear ? NSMaxX(viewRect) - NSWidth(_view.pagingRect) - border.width + ceil(theme.fullWidth * 0.5) - : NSMinX(viewRect) + border.width + ceil(theme.fullWidth * 0.5); + : NSMinX(viewRect) + border.width + ceil(theme.fullWidth * 0.5); _view.pagingView.frame = NSMakeRect(leadOrigin - _view.pagingView.textContainerOrigin.x, - NSMaxY(viewRect) - border.height - NSHeight(_view.pagingRect) - - _view.pagingView.textContainerOrigin.y, - (theme.linear ? NSWidth(_view.pagingRect) - : NSWidth(viewRect) - border.width * 2) - theme.fullWidth, + NSMaxY(viewRect) - border.height - NSHeight(_view.pagingRect) - _view.pagingView.textContainerOrigin.y, + (theme.linear ? NSWidth(_view.pagingRect) : NSWidth(viewRect) - border.width * 2) - theme.fullWidth, NSHeight(_view.pagingRect)); } if (!_view.scrollView.hidden) { @@ -4168,9 +4164,9 @@ but only when the text would expand on the side of upstream (i.e. towards the be NSHeight(_view.clipRect)); _view.documentView.frame = NSMakeRect(0.0, 0.0, NSWidth(viewRect) - border.width * 2, NSHeight(_view.documentRect)); _view.candidateView.frame = NSMakeRect(ceil(theme.fullWidth * 0.5) - _view.candidateView.textContainerOrigin.x, - ceil(theme.lineSpacing * 0.5) - _view.candidateView.textContainerOrigin.y, - NSWidth(viewRect) - border.width * 2 - theme.fullWidth, - NSHeight(_view.documentRect) - theme.lineSpacing); + floor(theme.lineSpacing * 0.5) - _view.candidateView.textContainerOrigin.y, + NSWidth(viewRect) - border.width * 2 - theme.fullWidth, + NSHeight(_view.documentRect) - theme.lineSpacing); } if (!_back.hidden) { _back.frame = viewRect; @@ -4190,22 +4186,11 @@ - (void)hide { [_toolTip hide]; [self orderOut:nil]; _maxSizeAttained = NSZeroSize; - _initPosition = YES; + _IbeamRect = NSZeroRect; self.expanded = NO; self.sectionNum = 0; } -static CGFloat textWidth(NSAttributedString* string, BOOL vertical) { - if (vertical) { - NSMutableAttributedString* verticalString = string.mutableCopy; - [verticalString addAttribute:NSVerticalGlyphFormAttributeName - value:@YES range:NSMakeRange(0, verticalString.length)]; - return ceil(verticalString.size.width); - } else { - return ceil(string.size.width); - } -} - // Main function to add attributes to text output from librime - (void)showPreedit:(NSString*)preedit selRange:(NSRange)selRange @@ -4252,7 +4237,7 @@ - (void)showPreedit:(NSString*)preedit SquirrelCandidateInfo* candidateInfos; if (updateCandidates) { [_view.candidateContents deleteCharactersInRange:NSMakeRange(0, _view.candidateContents.length)]; - if (theme.lineLength > 0.1) { + if (isnormal(theme.lineLength)) { _maxSizeAttained.width = fmin(theme.lineLength, _textWidthLimit); } _indexRange = indexRange; @@ -4276,7 +4261,7 @@ - (void)showPreedit:(NSString*)preedit value:padding range:NSMakeRange(selRange.location - 1, 1)]; } - if (NSMaxRange(selRange) < _view.preeditContents.length) { + if (NSMaxRange(selRange) < _view.preeditContents.length - 1) { [_view.preeditContents addAttribute:NSKernAttributeName value:padding range:NSMakeRange(NSMaxRange(selRange) - 1, 1)]; @@ -4324,13 +4309,13 @@ - (void)showPreedit:(NSString*)preedit ? theme.candidateHilitedTemplate.mutableCopy : theme.candidateTemplate.mutableCopy; // plug in enumerator, candidate text and comment into the template NSRange enumRange = [candidate.mutableString rangeOfString:@"%c"]; - [candidate replaceCharactersInRange:enumRange withString:theme.labels[col]]; + [candidate replaceCharactersInRange:enumRange withString:theme.rawLabels[col]]; NSRange textRange = [candidate.mutableString rangeOfString:@"%@"]; NSString* text = _inputController.candidateTexts[idx + indexRange.location]; [candidate replaceCharactersInRange:textRange withString:text]; - NSRange commentRange = [candidate.mutableString rangeOfString:kTipSpecifier]; + NSRange commentRange = [candidate.mutableString rangeOfString:@"%s"]; NSString* comment = _inputController.candidateComments[idx + indexRange.location]; if (comment.length > 0) { [candidate replaceCharactersInRange:commentRange withString:[@"\u00A0" append:comment]]; @@ -4355,9 +4340,9 @@ - (void)showPreedit:(NSString*)preedit for (NSUInteger i = 1; i <= idx; ++i) { if (i == idx || candidateInfos[i].truncated != truncated) { [_view.candidateContents addAttribute:NSParagraphStyleAttributeName - value:truncated ? theme.truncatedParagraphStyle - : theme.candidateParagraphStyle - range:NSMakeRange(location, candidateInfos[i - 1].maxRange() - location)]; + value:truncated ? theme.truncatedParagraphStyle + : theme.candidateParagraphStyle + range:NSMakeRange(location, candidateInfos[i - 1].maxRange() - location)]; if (i < idx) { truncated = candidateInfos[i].truncated; location = candidateInfos[i].location; @@ -4366,8 +4351,8 @@ - (void)showPreedit:(NSString*)preedit } } else { [_view.candidateContents addAttribute:NSParagraphStyleAttributeName - value:theme.candidateParagraphStyle - range:NSMakeRange(0, _view.candidateContents.length)]; + value:theme.candidateParagraphStyle + range:NSMakeRange(0, _view.candidateContents.length)]; } } } @@ -4381,7 +4366,7 @@ - (void)showPreedit:(NSString*)preedit SquirrelCandidateInfo info = {.location = candidateStart, .text = textRange.location, .comment = NSMaxRange(textRange), .idx = idx, .col = col}; [_view.candidateContents appendAttributedString:candidate]; // for linear layout, middle-truncate candidates that are longer than one line - if (theme.linear && textWidth(candidate, theme.vertical) > + if (theme.linear && NSWidth([_view.candidateView blockRectForRange:NSMakeRange(candidateStart, candidate.length)]) > _textWidthLimit - theme.fullWidth * (theme.tabular ? 3 : 2)) { info.length = _view.candidateContents.length - candidateStart; info.truncated = YES; @@ -4390,8 +4375,8 @@ - (void)showPreedit:(NSString*)preedit [_view.candidateContents.mutableString appendString:@"\n"]; } [_view.candidateContents addAttribute:NSParagraphStyleAttributeName - value:theme.truncatedParagraphStyle - range:NSMakeRange(candidateStart, _view.candidateContents.length - candidateStart)]; + value:theme.truncatedParagraphStyle + range:NSMakeRange(candidateStart, _view.candidateContents.length - candidateStart)]; } else { if (theme.linear || idx < indexRange.length - 1) { // separator: linear = "\u3000\x1D"; tabular = "\u3000\t\x1D"; stacked = "\n" @@ -4436,14 +4421,16 @@ - (void)showPreedit:(NSString*)preedit paging:indexRange.length > 0 && (theme.tabular || theme.showPaging)]; CGFloat textWidth = clamp(NSWidth(_view.contentRect), _maxSizeAttained.width, _textWidthLimit); // right-align the backward delete symbol - if (preedit.length > 0 && rulerAttrsPreedit == nil) { - [_view.preeditContents replaceCharactersInRange:NSMakeRange(_view.preeditContents.length - 2, 1) - withString:@"\t"]; + if (preedit.length > 0 && + (rulerAttrsPreedit == nil || rulerAttrsPreedit.tabStops[0].location < nexttoward(textWidth, 0.0))) { + if (rulerAttrsPreedit == nil) { + [_view.preeditContents replaceCharactersInRange:NSMakeRange(_view.preeditContents.length - 2, 1) + withString:@"\t"]; + } NSMutableParagraphStyle* rulerAttrs = theme.preeditParagraphStyle.mutableCopy; - rulerAttrs.tabStops = @[[NSTextTab.alloc - initWithTextAlignment:NSTextAlignmentRight - location:textWidth - options:@{}]]; + rulerAttrs.tabStops = @[[NSTextTab.alloc initWithTextAlignment:NSTextAlignmentRight + location:textWidth + options:@{}]]; [_view.preeditContents addAttribute:NSParagraphStyleAttributeName value:rulerAttrs range:NSMakeRange(0, _view.preeditContents.length)]; @@ -4508,7 +4495,6 @@ - (void)showStatus:(NSString*)message __attribute__((objc_direct)) { paging:NO]; // disable remember_size and fixed line_length for status messages - _initPosition = YES; _maxSizeAttained = NSZeroSize; if (_statusTimer.valid) { [_statusTimer invalidate]; @@ -4565,11 +4551,10 @@ - (void)loadConfig:(SquirrelConfig*)config { } - (void)updateScriptVariant { - [SquirrelView.defaultTheme setScriptVariant:_optionSwitcher.currentScriptVariant]; + [SquirrelView.defaultTheme updateScriptVariant:_optionSwitcher.currentScriptVariant]; if (@available(macOS 10.14, *)) { - [SquirrelView.darkTheme setScriptVariant:_optionSwitcher.currentScriptVariant]; + [SquirrelView.darkTheme updateScriptVariant:_optionSwitcher.currentScriptVariant]; } } @end // SquirrelPanel - diff --git a/Localizable.xcstrings b/Tooltips.xcstrings similarity index 62% rename from Localizable.xcstrings rename to Tooltips.xcstrings index c3cf5fc66..c1b8e1010 100644 --- a/Localizable.xcstrings +++ b/Tooltips.xcstrings @@ -2,6 +2,7 @@ "sourceLanguage" : "en", "strings" : { "candidate" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -30,7 +31,7 @@ } }, "compress" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -59,7 +60,7 @@ } }, "delete" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -87,124 +88,8 @@ } } }, - "deploy_failure" : { - "extractionState" : "stale", - "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" : "stale", - "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" : "stale", - "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" : "stale", - "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" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -233,7 +118,7 @@ } }, "escape" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -262,7 +147,7 @@ } }, "expand" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -291,7 +176,7 @@ } }, "home" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -320,7 +205,7 @@ } }, "page_down" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -349,7 +234,7 @@ } }, "page_up" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -377,86 +262,8 @@ } } }, - "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" - } - } - } - }, - "Squirrel" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "鼠须管" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "鼠鬚管" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "鼠鬚筆" - } - } - } - }, "unlock" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { diff --git a/input_source.mm b/input_source.mm index 9cefff264..e4e71b383 100644 --- a/input_source.mm +++ b/input_source.mm @@ -14,7 +14,7 @@ typedef CF_OPTIONS(CFIndex, RimeInputMode) { CANT_INPUT_MODE = 1 << 2 }; -RimeInputMode GetEnabledInputModes(void); +RimeInputMode GetEnabledInputModes(Boolean includeAllInstalled); CFArrayRef GetPreferredLocale(void) { CFTypeRef locales[] = {CFSTR("zh-Hans"), CFSTR("zh-Hant"), CFSTR("zh-HK")}; @@ -27,76 +27,61 @@ CFArrayRef GetPreferredLocale(void) { CFArrayRef GetInputSourceList(Boolean includeAllInstalled) { CFTypeRef keys[] = {kTISPropertyBundleID}; CFTypeRef values[] = {CFBundleGetIdentifier(CFBundleGetMainBundle())}; - CFDictionaryRef property = CFDictionaryCreate(NULL, keys, values, 1, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionaryRef property = CFDictionaryCreate(NULL, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFArrayRef sourceList = TISCreateInputSourceList(property, includeAllInstalled); CFRelease(property); return sourceList; } void RegisterInputSource(void) { - if (GetEnabledInputModes() != 0) { // Already registered + if (GetEnabledInputModes(true) != 0) { // Already registered NSLog(@"Squirrel is already registered."); return; } CFStringRef installPath = CFSTR("/Library/Input Methods/Squirrel.app"); - if (CFURLRef installURL = CFURLCreateWithFileSystemPath - (NULL, installPath, kCFURLPOSIXPathStyle, false)) { - OSStatus registerError = TISRegisterInputSource((CFURLRef)CFAutorelease(installURL)); - if (registerError == noErr) { - NSLog(@"Squirrel has been successfully registered at %@", installPath); + if (CFURLRef installURL = CFURLCreateWithFileSystemPath(NULL, installPath, kCFURLPOSIXPathStyle, false)) { + if (OSStatus error = TISRegisterInputSource((CFURLRef)CFAutorelease(installURL)) != noErr) { + NSLog(@"Squirrel failed to register at %@ (error code: %d)", installPath, error); } else { - NSLog(@"Squirrel failed to register at %@ (%@)", installPath, - [NSError errorWithDomain:NSOSStatusErrorDomain - code:registerError userInfo:nil]); + NSLog(@"Squirrel has been successfully registered at %@", installPath); } } } -void EnableInputSource(void) { - if (GetEnabledInputModes() != 0) { +void EnableInputSource(RimeInputMode modesToEnable) { + if (GetEnabledInputModes(false) != 0) { // keep user's manually enabled input modes NSLog(@"Squirrel input method(s) is already enabled."); return; } - RimeInputMode input_modes_to_enable = 0; - CFArrayRef preferred = GetPreferredLocale(); - if (CFArrayGetCount(preferred) > 0) { - CFStringRef language = (CFStringRef)CFArrayGetValueAtIndex(preferred, 0); - if (CFStringCompare(language, CFSTR("zh-Hans"), - kCFCompareCaseInsensitive) == kCFCompareEqualTo) { - input_modes_to_enable |= HANS_INPUT_MODE; - } else if (CFStringCompare(language, CFSTR("zh-Hant"), - kCFCompareCaseInsensitive) == kCFCompareEqualTo) { - input_modes_to_enable |= HANT_INPUT_MODE; - } else if (CFStringCompare(language, CFSTR("zh-HK"), - kCFCompareCaseInsensitive) == kCFCompareEqualTo) { - input_modes_to_enable |= CANT_INPUT_MODE; + if (modesToEnable == 0) { + CFArrayRef preferred = GetPreferredLocale(); + if (CFArrayGetCount(preferred) > 0) { + CFStringRef language = (CFStringRef)CFArrayGetValueAtIndex(preferred, 0); + if (CFStringCompare(language, CFSTR("zh-Hans"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + modesToEnable |= HANS_INPUT_MODE; + } else if (CFStringCompare(language, CFSTR("zh-Hant"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + modesToEnable |= HANT_INPUT_MODE; + } else if (CFStringCompare(language, CFSTR("zh-HK"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + modesToEnable |= CANT_INPUT_MODE; + } + } else { + modesToEnable = HANS_INPUT_MODE; } - } else { - input_modes_to_enable = HANS_INPUT_MODE; + CFRelease(preferred); } - CFRelease(preferred); CFArrayRef sourceList = GetInputSourceList(true); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { - TISInputSourceRef inputSource = (TISInputSourceRef) - CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty - (inputSource, kTISPropertyInputSourceID); + TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && - (input_modes_to_enable & HANS_INPUT_MODE)) || - (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && - (input_modes_to_enable & HANT_INPUT_MODE)) || - (CFStringCompare(sourceID, kCantInputModeID, 0) == kCFCompareEqualTo && - (input_modes_to_enable & CANT_INPUT_MODE))) { - CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty - (inputSource, kTISPropertyInputSourceIsEnabled); + if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && (modesToEnable & HANS_INPUT_MODE)) || + (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && (modesToEnable & HANT_INPUT_MODE)) || + (CFStringCompare(sourceID, kCantInputModeID, 0) == kCFCompareEqualTo && (modesToEnable & CANT_INPUT_MODE))) { + CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceIsEnabled); if (!CFBooleanGetValue(isEnabled)) { - if (OSStatus enableError = TISEnableInputSource(inputSource) != noErr) { - NSLog(@"Failed to enable input source: %@ (%@)", sourceID, - [NSError errorWithDomain:NSOSStatusErrorDomain - code:enableError userInfo:nil]); + if (OSStatus error = TISEnableInputSource(inputSource) != noErr) { + NSLog(@"Failed to enable input source: %@ (error code: %d)", sourceID, error); } else { NSLog(@"Enabled input source: %@", sourceID); } @@ -106,54 +91,46 @@ void EnableInputSource(void) { CFRelease(sourceList); } -void SelectInputSource(void) { - RimeInputMode enabled_input_modes = GetEnabledInputModes(); - RimeInputMode input_mode_to_select = 0; - CFArrayRef preferred = GetPreferredLocale(); - for (CFIndex i = 0; i < CFArrayGetCount(preferred); ++i) { - CFStringRef language = (CFStringRef)CFArrayGetValueAtIndex(preferred, i); - if (CFStringCompare(language, CFSTR("zh-Hans"), kCFCompareCaseInsensitive) - == kCFCompareEqualTo && (enabled_input_modes & HANS_INPUT_MODE)) { - input_mode_to_select = HANS_INPUT_MODE; - break; - } else if (CFStringCompare(language, CFSTR("zh-Hant"), kCFCompareCaseInsensitive) - == kCFCompareEqualTo && (enabled_input_modes & HANT_INPUT_MODE)) { - input_mode_to_select = HANT_INPUT_MODE; - break; - } else if (CFStringCompare(language, CFSTR("zh-HK"), kCFCompareCaseInsensitive) - == kCFCompareEqualTo && (enabled_input_modes & CANT_INPUT_MODE)) { - input_mode_to_select = CANT_INPUT_MODE; - break; +void SelectInputSource(RimeInputMode modeToSelect) { + RimeInputMode enabledModes = GetEnabledInputModes(false); + if (modeToSelect == 0 || (enabledModes & modeToSelect) == 0) { + CFArrayRef preferred = GetPreferredLocale(); + for (CFIndex i = 0; i < CFArrayGetCount(preferred); ++i) { + CFStringRef language = (CFStringRef)CFArrayGetValueAtIndex(preferred, i); + if (CFStringCompare(language, CFSTR("zh-Hans"), kCFCompareCaseInsensitive) == kCFCompareEqualTo && + (enabledModes & HANS_INPUT_MODE)) { + modeToSelect = HANS_INPUT_MODE; + break; + } else if (CFStringCompare(language, CFSTR("zh-Hant"), kCFCompareCaseInsensitive) == kCFCompareEqualTo && + (enabledModes & HANT_INPUT_MODE)) { + modeToSelect = HANT_INPUT_MODE; + break; + } else if (CFStringCompare(language, CFSTR("zh-HK"), kCFCompareCaseInsensitive) == kCFCompareEqualTo && + (enabledModes & CANT_INPUT_MODE)) { + modeToSelect = CANT_INPUT_MODE; + break; + } } + CFRelease(preferred); } - CFRelease(preferred); - if (input_mode_to_select == 0) { + if (modeToSelect == 0) { NSLog(@"No enabled input sources."); return; } CFArrayRef sourceList = GetInputSourceList(false); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { - TISInputSourceRef inputSource = (TISInputSourceRef) - CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceID); + TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); - if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && - ((input_mode_to_select & HANS_INPUT_MODE) != 0)) || - (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && - ((input_mode_to_select & HANT_INPUT_MODE) != 0)) || - (CFStringCompare(sourceID, kCantInputModeID, 0) == kCFCompareEqualTo && - ((input_mode_to_select & CANT_INPUT_MODE) != 0))) { + if ((CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo && (modeToSelect & HANS_INPUT_MODE)) || + (CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo && (modeToSelect & HANT_INPUT_MODE)) || + (CFStringCompare(sourceID, kCantInputModeID, 0) == kCFCompareEqualTo && (modeToSelect & CANT_INPUT_MODE))) { // select the first enabled input mode in Squirrel. - CFBooleanRef isSelectable = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsSelectCapable); - CFBooleanRef isSelected = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsSelected); + CFBooleanRef isSelectable = (CFBooleanRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceIsSelectCapable); + CFBooleanRef isSelected = (CFBooleanRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceIsSelected); if (!CFBooleanGetValue(isSelected) && CFBooleanGetValue(isSelectable)) { - if (OSStatus selectError = TISSelectInputSource(inputSource) != 0) { - NSLog(@"Failed to select input source: %@ (%@)", sourceID, - [NSError errorWithDomain:NSOSStatusErrorDomain - code:selectError userInfo:nil]); + if (OSStatus error = TISSelectInputSource(inputSource) != noErr) { + NSLog(@"Failed to select input source: %@ (error code: %d)", sourceID, error); } else { NSLog(@"Selected input source: %@", sourceID); break; @@ -167,18 +144,14 @@ void SelectInputSource(void) { void DisableInputSource(void) { CFArrayRef sourceList = GetInputSourceList(false); for (CFIndex i = CFArrayGetCount(sourceList); i > 0; --i) { - TISInputSourceRef inputSource = (TISInputSourceRef) - CFArrayGetValueAtIndex(sourceList, i - 1); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty - (inputSource, kTISPropertyInputSourceID); + TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i - 1); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); if (CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo || CFStringCompare(sourceID, kHantInputModeID, 0) == kCFCompareEqualTo || CFStringCompare(sourceID, kCantInputModeID, 0) == kCFCompareEqualTo) { - if (OSStatus disableError = TISDisableInputSource(inputSource) != 0) { - NSLog(@"Failed to disable input source: %@ (%@)", sourceID, - [NSError errorWithDomain:NSOSStatusErrorDomain - code:disableError userInfo:nil]); + if (OSStatus error = TISDisableInputSource(inputSource) != noErr) { + NSLog(@"Failed to disable input source: %@ (error code: %d)", sourceID, error); } else { NSLog(@"Disabled input source: %@", sourceID); } @@ -187,14 +160,12 @@ void DisableInputSource(void) { CFRelease(sourceList); } -RimeInputMode GetEnabledInputModes(void) { +RimeInputMode GetEnabledInputModes(Boolean includeAllInstalled) { RimeInputMode input_modes = 0; - CFArrayRef sourceList = GetInputSourceList(false); + CFArrayRef sourceList = GetInputSourceList(includeAllInstalled); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { - TISInputSourceRef inputSource = (TISInputSourceRef) - CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty - (inputSource, kTISPropertyInputSourceID); + TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); // NSLog(@"Examining input source: %@", sourceID); if (CFStringCompare(sourceID, kHansInputModeID, 0) == kCFCompareEqualTo) { input_modes |= HANS_INPUT_MODE; diff --git a/main.mm b/main.mm index ee36e306d..ce34a4b1d 100644 --- a/main.mm +++ b/main.mm @@ -5,14 +5,17 @@ #import #import +typedef CF_OPTIONS(CFIndex, RimeInputMode) { + DEFAULT_INPUT_MODE = 1 << 0, + HANS_INPUT_MODE = 1 << 0, + HANT_INPUT_MODE = 1 << 1, + CANT_INPUT_MODE = 1 << 2 +}; + void RegisterInputSource(void); void DisableInputSource(void); -void EnableInputSource(void); -void SelectInputSource(void); - -// Each input method needs a unique connection name. -// Note that periods and spaces are not allowed in the connection name. -static NSString* const kConnectionName = @"Squirrel_1_Connection"; +void EnableInputSource(RimeInputMode modesToEnable); +void SelectInputSource(RimeInputMode modeToSelect); int main(int argc, char* argv[]) { if (argc > 1 && strcmp("--quit", argv[1]) == 0) { @@ -39,7 +42,19 @@ int main(int argc, char* argv[]) { } if (argc > 1 && strcmp("--enable-input-source", argv[1]) == 0) { - EnableInputSource(); + RimeInputMode modesToEnable = 0; + if (argc > 2) { + for (int i = 2; i < argc; ++i) { + if (strcmp("Hans", argv[i]) == 0 || strcmp("hans", argv[i]) == 0 || strcmp("HANS", argv[i])) { + modesToEnable |= HANS_INPUT_MODE; + } else if (strcmp("Hant", argv[i]) == 0 || strcmp("hant", argv[i]) == 0 || strcmp("HANT", argv[i])) { + modesToEnable |= HANT_INPUT_MODE; + } else if (strcmp("Cant", argv[i]) == 0 || strcmp("cant", argv[i]) == 0 || strcmp("CANT", argv[i])) { + modesToEnable |= CANT_INPUT_MODE; + } + } + } + EnableInputSource(modesToEnable); return 0; } @@ -49,7 +64,19 @@ int main(int argc, char* argv[]) { } if (argc > 1 && strcmp("--select-input-source", argv[1]) == 0) { - SelectInputSource(); + RimeInputMode modeToSelect = 0; + if (argc > 2) { + for (int i = 2; i < argc; ++i) { + if (strcmp("Hans", argv[i]) == 0 || strcmp("hans", argv[i]) == 0 || strcmp("HANS", argv[i])) { + modeToSelect |= HANS_INPUT_MODE; + } else if (strcmp("Hant", argv[i]) == 0 || strcmp("hant", argv[i]) == 0 || strcmp("HANT", argv[i])) { + modeToSelect |= HANT_INPUT_MODE; + } else if (strcmp("Cant", argv[i]) == 0 || strcmp("cant", argv[i]) == 0 || strcmp("CANT", argv[i])) { + modeToSelect |= CANT_INPUT_MODE; + } + } + } + SelectInputSource(modeToSelect); return 0; } @@ -89,8 +116,8 @@ int main(int argc, char* argv[]) { if (NSApp.squirrelAppDelegate.problematicLaunchDetected) { NSLog(@"Problematic launch detected!"); - NSArray* args = @[@"-v", NSLocalizedString(@"say_voice", nil), - NSLocalizedString(@"problematic_launch", nil)]; + NSArray* args = @[@"-v", [NSBundle.mainBundle localizedStringForKey:@"say_voice" value:nil table:@"Notifications"], + [NSBundle.mainBundle localizedStringForKey:@"problematic_launch" value:nil table:@"Notifications"]]; if (@available(macOS 10.13, *)) { NSURL* say = [NSURL fileURLWithPath:@"/usr/bin/say" isDirectory:NO]; [NSTask launchedTaskWithExecutableURL:say diff --git a/mul.lproj/MainMenu.xcstrings b/mul.lproj/MainMenu.xcstrings index a695a6408..19c83388a 100644 --- a/mul.lproj/MainMenu.xcstrings +++ b/mul.lproj/MainMenu.xcstrings @@ -2,8 +2,7 @@ "sourceLanguage" : "en", "strings" : { "774.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Deploy\"; ObjectID = \"774\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -32,8 +31,7 @@ } }, "776.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Check for updates…\"; ObjectID = \"776\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -62,8 +60,7 @@ } }, "780.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"ㄓ⃣Squirrel Switcher\"; ObjectID = \"780\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -92,8 +89,7 @@ } }, "797.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Rime Wiki…\"; ObjectID = \"797\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -122,8 +118,7 @@ } }, "802.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Settings…\"; ObjectID = \"802\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -152,8 +147,7 @@ } }, "804.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Sync user data\"; ObjectID = \"804\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -182,8 +176,7 @@ } }, "809.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Error and warning logs\"; ObjectID = \"809\";", - "extractionState" : "extracted_with_value", + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { @@ -213,4 +206,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/package/sign_update b/package/sign_update new file mode 100755 index 000000000..06779e730 Binary files /dev/null and b/package/sign_update differ