diff --git a/Assets.xcassets/RimeIcon.appiconset/Contents.json b/Assets.xcassets/RimeIcon.appiconset/Contents.json index acfdfc399..7c8a1bdd1 100644 --- a/Assets.xcassets/RimeIcon.appiconset/Contents.json +++ b/Assets.xcassets/RimeIcon.appiconset/Contents.json @@ -1,13 +1,13 @@ { "images" : [ { - "filename" : "rime-16 1.png", + "filename" : "rime-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { - "filename" : "rime-32 1.png", + "filename" : "rime-32.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-1024.png b/Assets.xcassets/RimeIcon.appiconset/rime-1024.png index 7d21cc7c9..8a872cf81 100644 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-1024.png and b/Assets.xcassets/RimeIcon.appiconset/rime-1024.png differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-128.png b/Assets.xcassets/RimeIcon.appiconset/rime-128.png index c24666c32..9c44d83e4 100644 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-128.png and b/Assets.xcassets/RimeIcon.appiconset/rime-128.png differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png b/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png deleted file mode 100644 index 119930f70..000000000 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-16 1.png and /dev/null differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-16.png b/Assets.xcassets/RimeIcon.appiconset/rime-16.png new file mode 100644 index 000000000..ae789c138 Binary files /dev/null and b/Assets.xcassets/RimeIcon.appiconset/rime-16.png differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-256.png b/Assets.xcassets/RimeIcon.appiconset/rime-256.png index e12ead07c..14d4b5c93 100644 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-256.png and b/Assets.xcassets/RimeIcon.appiconset/rime-256.png differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-32 1.png b/Assets.xcassets/RimeIcon.appiconset/rime-32 1.png deleted file mode 100644 index 0c358b5ba..000000000 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-32 1.png and /dev/null differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-32.png b/Assets.xcassets/RimeIcon.appiconset/rime-32.png index 0c358b5ba..0a1894f8d 100644 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-32.png and b/Assets.xcassets/RimeIcon.appiconset/rime-32.png differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-512.png b/Assets.xcassets/RimeIcon.appiconset/rime-512.png index a0c224363..b591f683e 100644 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-512.png and b/Assets.xcassets/RimeIcon.appiconset/rime-512.png differ diff --git a/Assets.xcassets/RimeIcon.appiconset/rime-64.png b/Assets.xcassets/RimeIcon.appiconset/rime-64.png index 565b945da..de6d2aa3d 100644 Binary files a/Assets.xcassets/RimeIcon.appiconset/rime-64.png and b/Assets.xcassets/RimeIcon.appiconset/rime-64.png differ diff --git a/INSTALL.md b/INSTALL.md index 48b60c427..04dfb0dff 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -61,9 +61,11 @@ Choose one of the following options. ``` sh export BUILD_UNIVERSAL=1 -make -C librime xcode/deps/boost +export BOOST_ROOT="$(pwd)/librime/deps/boost_1_83_0" -export BOOST_ROOT="$(pwd)/librime/deps/boost_1_78_0" +export CMAKE_GENERATOR=Ninja + +bash librime/install-boost.sh ``` Let's set `BUILD_UNIVERSAL` to tell `make` that we are building Boost as @@ -96,8 +98,14 @@ port install boost -no_static * Make sure you have updated all the dependencies. If you cloned squirrel with the command in this guide, you've already done it. But if not, this command will update submodules. -``` +``` sh git submodule update --init --recursive + +export BUILD_UNIVERSAL=1 +export CMAKE_GENERATOR=Ninja + +make -C librime +make deps ``` * With all dependencies ready, build `Squirrel.app`: @@ -109,11 +117,14 @@ make To build only for the native architecture, and/or specify the lowest supported macOS version, pass variable `ARCHS`/`MACOSX_DEPLOYMENT_TARGET` to `make`: ``` sh -# for Universal macOS App, targetting Ventura -make ARCHS='arm64 x86_64' MACOSX_DEPLOYMENT_TARGET='13.0' +# for Universal macOS App +make ARCHS='arm64 x86_64' MACOSX_DEPLOYMENT_TARGET='10.15' + +# for Mac computers with Apple Silicon +make ARCHS='arm64' MACOSX_DEPLOYMENT_TARGET='10.15' -# for ARM macOS App, targetting Ventura -make ARCHS='arm64' MACOSX_DEPLOYMENT_TARGET='13.0' +# for Intel-based Mac +make ARCHS='x86_64' MACOSX_DEPLOYMENT_TARGET='10.15' ``` ## Install it on your Mac @@ -123,7 +134,7 @@ make ARCHS='arm64' MACOSX_DEPLOYMENT_TARGET='13.0' Just add `package` after `make` ``` -make package ARCHS='arm64' MACOSX_DEPLOYMENT_TARGET='13.0' +make package ARCHS='arm64' MACOSX_DEPLOYMENT_TARGET='10.14' ``` Define or echo `DEV_ID` to automatically handle code signing and [notarization](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution) (Apple Developer ID needed) diff --git a/Makefile b/Makefile index 98783ab2a..b5b418568 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ all: release install: install-release -# Change to `xcode/dist-with-icu` if boost is linked to icu libraries. -RIME_DIST_TARGET = xcode/dist +# Change to `dist-with-icu` if boost is linked to icu libraries. +RIME_DIST_TARGET = install RIME_BIN_DIR = librime/dist/bin RIME_LIB_DIR = librime/dist/lib @@ -39,7 +39,7 @@ $(RIME_LIBRARY): $(MAKE) librime $(RIME_DEPS): - $(MAKE) -C librime xcode/deps + $(MAKE) -C librime deps librime: $(RIME_DEPS) $(MAKE) -C librime $(RIME_DIST_TARGET) @@ -67,7 +67,7 @@ plum-data: $(MAKE) copy-plum-data opencc-data: - $(MAKE) -C librime xcode/deps/opencc + $(MAKE) -C librime deps/opencc $(MAKE) copy-opencc-data copy-plum-data: @@ -88,9 +88,9 @@ _=$() $() export CMAKE_OSX_ARCHITECTURES = $(subst $(_),;,$(ARCHS)) endif -ifdef MACOSX_DEPLOYMENT_TARGET +# https://cmake.org/cmake/help/latest/envvar/MACOSX_DEPLOYMENT_TARGET.html +MACOSX_DEPLOYMENT_TARGET ?= 10.15 BUILD_SETTINGS += MACOSX_DEPLOYMENT_TARGET="$(MACOSX_DEPLOYMENT_TARGET)" -endif release: $(DEPS_CHECK) bash package/add_data_files @@ -170,5 +170,5 @@ clean: clean-deps: $(MAKE) -C plum clean - $(MAKE) -C librime xcode/clean + $(MAKE) -C librime clean $(MAKE) clean-sparkle diff --git a/README.md b/README.md index 6b5c8ec03..08b2214e9 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ 安裝輸入法 --- -本品適用於 macOS 12.0+ +本品適用於 macOS 10.15+ 初次安裝,如果在部份應用程序中打不出字,請註銷並重新登錄。 diff --git a/Sparkle b/Sparkle index 8fb9c83ad..9684a433e 160000 --- a/Sparkle +++ b/Sparkle @@ -1 +1 @@ -Subproject commit 8fb9c83adf6f74364ee57bf314ceee2fa77d0ac2 +Subproject commit 9684a433ee0b55da607791c56f501975f5e11ae5 diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 6898ec0ae..ac6c0b5d0 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -303,8 +303,8 @@ 44AC95171430CF6000C888FB /* SquirrelApplicationDelegate.m */, 44AC95181430CF6000C888FB /* SquirrelInputController.h */, 44AC95191430CF6000C888FB /* SquirrelInputController.m */, - A47C48DE105E8CE8006D528B /* macos_keycode.m */, A44571AB0DBF42C200F793F9 /* macos_keycode.h */, + A47C48DE105E8CE8006D528B /* macos_keycode.m */, 32CA4F630368D1EE00C91783 /* Squirrel_Prefix.pch */, 4443A8391828CC5100731305 /* input_source.m */, 29B97316FDCFA39411CA2CEA /* main.m */, @@ -505,7 +505,8 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1220; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Squirrel" */; compatibilityVersion = "Xcode 10.0"; @@ -600,12 +601,13 @@ C01FCF4B08A954540054247B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0.17.2; + CURRENT_PROJECT_VERSION = 0.16.2u; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -631,7 +633,7 @@ "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CPLUSPLUSFLAGS = ( "-DLEOPARD", @@ -650,11 +652,12 @@ C01FCF4C08A954540054247B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.17.2; + CURRENT_PROJECT_VERSION = 0.16.2u; DEAD_CODE_STRIPPING = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -679,7 +682,7 @@ "$(inherited)", "$(LIBRARY_SEARCH_PATHS_QUOTED_FOR_TARGET_1)", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; OTHER_CODE_SIGN_FLAGS = "--deep"; OTHER_CPLUSPLUSFLAGS = ( "-DLEOPARD", @@ -722,6 +725,7 @@ DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -743,7 +747,7 @@ /usr/local/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; @@ -777,6 +781,7 @@ CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/Release"; DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; @@ -795,8 +800,8 @@ /usr/lib, ); LIBRARY_SEARCH_PATHS = "$(SRCROOT)/lib"; - MACOSX_DEPLOYMENT_TARGET = 12.0; - ONLY_ACTIVE_ARCH = YES; + MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; SYSTEM_HEADER_SEARCH_PATHS = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Tk.framework/Headers; }; diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h index 4f3e421d6..a4bbc061e 100644 --- a/SquirrelApplicationDelegate.h +++ b/SquirrelApplicationDelegate.h @@ -2,6 +2,7 @@ @class SquirrelConfig; @class SquirrelPanel; +@class SquirrelOptionSwitcher; // Note: the SquirrelApplicationDelegate is instantiated automatically as an outlet of NSApp's instance @interface SquirrelApplicationDelegate : NSObject @@ -13,17 +14,18 @@ @property(nonatomic, readonly, strong) SquirrelConfig *config; @property(nonatomic, readonly) BOOL enableNotifications; --(IBAction)deploy:(id)sender; --(IBAction)syncUserData:(id)sender; --(IBAction)configure:(id)sender; --(IBAction)openWiki:(id)sender; +- (IBAction)deploy:(id)sender; +- (IBAction)syncUserData:(id)sender; +- (IBAction)configure:(id)sender; +- (IBAction)openWiki:(id)sender; --(void)setupRime; --(void)startRimeWithFullCheck:(BOOL)fullCheck; --(void)loadSettings; --(void)loadSchemaSpecificSettings:(NSString *)schemaId; +- (void)setupRime; +- (void)startRimeWithFullCheck:(BOOL)fullCheck; +- (void)loadSettings; +- (void)loadSchemaSpecificSettings:(NSString *)schemaId; +- (void)loadSchemaSpecificLabels:(NSString *)schemaId; -@property (nonatomic, readonly) BOOL problematicLaunchDetected; +@property(nonatomic, readonly) BOOL problematicLaunchDetected; @end @@ -34,4 +36,4 @@ @end // also used in main.m -extern void show_message(const char* msg_text, const char* msg_id); +extern void show_message(const char *msg_text, const char *msg_id); diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m index db1479ab6..0ac1b683a 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.m @@ -8,7 +8,7 @@ @implementation SquirrelApplicationDelegate --(IBAction)deploy:(id)sender +- (IBAction)deploy:(id)sender { NSLog(@"Start maintenance..."); [self shutdownRime]; @@ -16,23 +16,23 @@ -(IBAction)deploy:(id)sender [self loadSettings]; } --(IBAction)syncUserData:(id)sender +- (IBAction)syncUserData:(id)sender { NSLog(@"Sync user data"); rime_get_api()->sync_user_data(); } --(IBAction)configure:(id)sender +- (IBAction)configure:(id)sender { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[@"file://" stringByAppendingString:(@"~/Library/Rime").stringByStandardizingPath]]]; + [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:[@"~/Library/Rime/" stringByExpandingTildeInPath]]]; } --(IBAction)openWiki:(id)sender +- (IBAction)openWiki:(id)sender { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kRimeWikiURL]]; } -void show_message(const char* msg_text, const char* msg_id) { +void show_message(const char *msg_text, const char *msg_id) { @autoreleasepool { id notification = [[NSClassFromString(@"NSUserNotification") alloc] init]; [notification performSelector:@selector(setTitle:) @@ -47,37 +47,32 @@ void show_message(const char* msg_text, const char* msg_id) { } } -static void show_status_message(const char *msg_text_long, const char *msg_text_short, const char* msg_id) { - SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; +static void show_status_message(const char *msg_text_long, const char *msg_text_short, const char *msg_id) { + SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; if (panel) { NSString *msgLong = msg_text_long ? @(msg_text_long) : nil; NSString *msgShort = msg_text_short ? @(msg_text_short) : nil; - [panel updateStatusLong: msgLong statusShort: msgShort]; + [panel updateStatusLong:msgLong statusShort:msgShort]; } } -void notification_handler(void* context_object, RimeSessionId session_id, - const char* message_type, const char* message_value) { +void notification_handler(void *context_object, RimeSessionId session_id, + const char *message_type, const char *message_value) { if (!strcmp(message_type, "deploy")) { if (!strcmp(message_value, "start")) { show_message("deploy_start", message_type); - } - else if (!strcmp(message_value, "success")) { + } else if (!strcmp(message_value, "success")) { show_message("deploy_success", message_type); - } - else if (!strcmp(message_value, "failure")) { + } else if (!strcmp(message_value, "failure")) { show_message("deploy_failure", message_type); } return; } - // off? id app_delegate = (__bridge id)context_object; - if (app_delegate && ![app_delegate enableNotifications]) { - return; - } // schema change - if (!strcmp(message_type, "schema")) { - const char* schema_name = strchr(message_value, '/'); + if (!strcmp(message_type, "schema") && + app_delegate && [app_delegate enableNotifications]) { + const char *schema_name = strchr(message_value, '/'); if (schema_name) { ++schema_name; show_status_message(schema_name, schema_name, message_type); @@ -85,23 +80,29 @@ void notification_handler(void* context_object, RimeSessionId session_id, return; } // option change - if (!strcmp(message_type, "option")) { + if (!strcmp(message_type, "option") && app_delegate) { Bool state = message_value[0] != '!'; - const char* option_name = message_value + !state; - struct rime_string_slice_t state_label_long = rime_get_api()->get_state_label_abbreviated(session_id, option_name, state, NO); - struct rime_string_slice_t state_label_short = rime_get_api()->get_state_label_abbreviated(session_id, option_name, state, YES); - - if (state_label_long.str || state_label_short.str) { - const char *short_message = state_label_short.length < strlen(state_label_short.str) ? NULL : state_label_short.str; - show_status_message(state_label_long.str, short_message, message_type); + const char *option_name = message_value + !state; + if ([[app_delegate panel].optionSwitcher containsOption:@(option_name)]) { + if ([[app_delegate panel].optionSwitcher updateGroupState:@(message_value) ofOption:@(option_name)]) { + [app_delegate loadSchemaSpecificSettings:[app_delegate panel].optionSwitcher.schemaId]; + } + } + if ([app_delegate enableNotifications]) { + RimeStringSlice state_label_long = rime_get_api()->get_state_label_abbreviated(session_id, option_name, state, NO); + RimeStringSlice state_label_short = rime_get_api()->get_state_label_abbreviated(session_id, option_name, state, YES); + if (state_label_long.str || state_label_short.str) { + const char *short_message = state_label_short.length < strlen(state_label_short.str) ? NULL : state_label_short.str; + show_status_message(state_label_long.str, short_message, message_type); + } } } } --(void)setupRime +- (void)setupRime { - NSString* userDataDir = (@"~/Library/Rime").stringByStandardizingPath; - NSFileManager* fileManager = [NSFileManager defaultManager]; + NSString *userDataDir = (@"~/Library/Rime").stringByStandardizingPath; + NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:userDataDir]) { if (![fileManager createDirectoryAtPath:userDataDir withIntermediateDirectories:YES @@ -117,12 +118,12 @@ -(void)setupRime squirrel_traits.distribution_code_name = "Squirrel"; squirrel_traits.distribution_name = "鼠鬚管"; squirrel_traits.distribution_version = - [[NSBundle mainBundle].infoDictionary[@"CFBundleVersion"] UTF8String]; + [[NSBundle mainBundle].infoDictionary[@"CFBundleVersion"] UTF8String]; squirrel_traits.app_name = "rime.squirrel"; rime_get_api()->setup(&squirrel_traits); } --(void)startRimeWithFullCheck:(BOOL)fullCheck +- (void)startRimeWithFullCheck:(BOOL)fullCheck { NSLog(@"Initializing la rime..."); rime_get_api()->initialize(NULL); @@ -138,21 +139,21 @@ - (void)shutdownRime { rime_get_api()->finalize(); } --(void)loadSettings { +- (void)loadSettings { _config = [[SquirrelConfig alloc] init]; if (![_config openBaseConfig]) { return; } _enableNotifications = - ![[_config getString:@"show_notifications_when"] isEqualToString:@"never"]; + ![[_config getString:@"show_notifications_when"] isEqualToString:@"never"]; [self.panel loadConfig:_config forDarkMode:NO]; if (@available(macOS 10.14, *)) { [self.panel loadConfig:_config forDarkMode:YES]; } } --(void)loadSchemaSpecificSettings:(NSString *)schemaId { +- (void)loadSchemaSpecificSettings:(NSString *)schemaId { if (schemaId.length == 0 || [schemaId characterAtIndex:0] == '.') { return; } @@ -164,7 +165,7 @@ -(void)loadSchemaSpecificSettings:(NSString *)schemaId { [self.panel loadConfig:self.config forDarkMode:NO]; } if (@available(macOS 10.14, *)) { - if ([schema openWithSchemaId:schemaId baseConfig:self.config] && + if ([schema openWithSchemaId:schemaId baseConfig:self.config] && [schema hasSection:@"style"]) { [self.panel loadConfig:schema forDarkMode:YES]; } else { @@ -174,47 +175,63 @@ -(void)loadSchemaSpecificSettings:(NSString *)schemaId { [schema close]; } +- (void)loadSchemaSpecificLabels:(NSString *)schemaId { + if (schemaId.length == 0 || [schemaId characterAtIndex:0] == '.') { + return; + } + SquirrelConfig *defaultConfig = [[SquirrelConfig alloc] init]; + [defaultConfig openWithConfigId:@"default"]; + SquirrelConfig *schema = [[SquirrelConfig alloc] init]; + if ([schema openWithSchemaId:schemaId baseConfig:defaultConfig] && + [schema hasSection:@"menu"]) { + [self.panel loadLabelConfig:schema]; + } else { + [self.panel loadLabelConfig:defaultConfig]; + } + [schema close]; + [defaultConfig close]; +} + // prevent freezing the system --(BOOL)problematicLaunchDetected +- (BOOL)problematicLaunchDetected { BOOL detected = NO; - NSString* logfile = - [NSTemporaryDirectory() stringByAppendingPathComponent:@"squirrel_launch.dat"]; + NSString *logfile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"squirrel_launch.dat"]; //NSLog(@"[DEBUG] archive: %@", logfile); - NSData* archive = [NSData dataWithContentsOfFile:logfile + NSData *archive = [NSData dataWithContentsOfFile:logfile options:NSDataReadingUncached error:nil]; if (archive) { - NSDate* previousLaunch = [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class fromData:archive error:NULL]; + NSDate *previousLaunch = [NSKeyedUnarchiver unarchivedObjectOfClass:NSDate.class fromData:archive error:NULL]; if (previousLaunch && previousLaunch.timeIntervalSinceNow >= -2) { detected = YES; } } - NSDate* now = [NSDate date]; - NSData* record = [NSKeyedArchiver archivedDataWithRootObject:now requiringSecureCoding:NO error:NULL]; + NSDate *now = [NSDate date]; + NSData *record = [NSKeyedArchiver archivedDataWithRootObject:now requiringSecureCoding:NO error:NULL]; [record writeToFile:logfile atomically:NO]; return detected; } --(void)workspaceWillPowerOff:(NSNotification *)aNotification +- (void)workspaceWillPowerOff:(NSNotification *)aNotification { NSLog(@"Finalizing before logging out."); [self shutdownRime]; } --(void)rimeNeedsReload:(NSNotification *)aNotification +- (void)rimeNeedsReload:(NSNotification *)aNotification { NSLog(@"Reloading rime on demand."); [self deploy:nil]; } --(void)rimeNeedsSync:(NSNotification *)aNotification +- (void)rimeNeedsSync:(NSNotification *)aNotification { NSLog(@"Sync rime on demand."); [self syncUserData:nil]; } --(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { NSLog(@"Squirrel is quitting."); rime_get_api()->cleanup_all_sessions(); @@ -224,15 +241,15 @@ -(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender //add an awakeFromNib item so that we can set the action method. Note that //any menuItems without an action will be disabled when displayed in the Text //Input Menu. --(void)awakeFromNib +- (void)awakeFromNib { - NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter; + NSNotificationCenter *center = [NSWorkspace sharedWorkspace].notificationCenter; [center addObserver:self selector:@selector(workspaceWillPowerOff:) name:NSWorkspaceWillPowerOffNotification object:nil]; - NSDistributedNotificationCenter* notifCenter = [NSDistributedNotificationCenter defaultCenter]; + NSDistributedNotificationCenter *notifCenter = [NSDistributedNotificationCenter defaultCenter]; [notifCenter addObserver:self selector:@selector(rimeNeedsReload:) name:@"SquirrelReloadNotification" @@ -242,10 +259,9 @@ -(void)awakeFromNib selector:@selector(rimeNeedsSync:) name:@"SquirrelSyncNotification" object:nil]; - } --(void)dealloc +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; @@ -258,9 +274,8 @@ -(void)dealloc @implementation NSApplication (SquirrelApp) --(SquirrelApplicationDelegate *)squirrelAppDelegate { +- (SquirrelApplicationDelegate *)squirrelAppDelegate { return (SquirrelApplicationDelegate *)self.delegate; } @end - diff --git a/SquirrelConfig.h b/SquirrelConfig.h index c4bd50145..69b77d98b 100644 --- a/SquirrelConfig.h +++ b/SquirrelConfig.h @@ -3,6 +3,29 @@ typedef NSDictionary SquirrelAppOptions; typedef NSMutableDictionary SquirrelMutableAppOptions; +@interface SquirrelOptionSwitcher : NSObject + +@property(nonatomic, strong, readonly) NSString *schemaId; +@property(nonatomic, strong, readonly) NSArray *optionNames; +@property(nonatomic, strong, readonly) NSArray *optionStates; +@property(nonatomic, strong, readonly) NSDictionary *> *optionGroups; +@property(nonatomic, strong, readonly) NSDictionary *switcher; + +- (instancetype)initWithSchemaId:(NSString *)schemaId + switcher:(NSDictionary *)switcher + optionGroups:(NSDictionary *> *)optionGroups; + +// return whether switcher options has been successfully updated +- (BOOL)updateSwitcher:(NSDictionary *)switcher; + +- (BOOL)updateGroupState:(NSString *)optionState ofOption:(NSString *)optionName; + +- (BOOL)containsOption:(NSString *)optionName; + +- (NSMutableDictionary *)mutableSwitcher; + +@end + @interface SquirrelConfig : NSObject @property(nonatomic, readonly) BOOL isOpen; @@ -12,12 +35,14 @@ typedef NSMutableDictionary SquirrelMutableAppOptions; - (BOOL)openBaseConfig; - (BOOL)openWithSchemaId:(NSString *)schemaId baseConfig:(SquirrelConfig *)config; +- (BOOL)openUserConfig:(NSString *)configId; +- (BOOL)openWithConfigId:(NSString *)configId; - (void)close; - (BOOL)hasSection:(NSString *)section; - (BOOL)getBool:(NSString *)option; -- (NSInteger)getInt:(NSString *)option; +- (int)getInt:(NSString *)option; - (double)getDouble:(NSString *)option; - (NSNumber *)getOptionalBool:(NSString *)option; - (NSNumber *)getOptionalInt:(NSString *)option; @@ -26,7 +51,12 @@ typedef NSMutableDictionary SquirrelMutableAppOptions; - (NSString *)getString:(NSString *)option; // 0xaabbggrr or 0xbbggrr - (NSColor *)getColor:(NSString *)option; +// file path (absolute or relative to ~/Library/Rime) +- (NSColor *)getPattern:(NSString *)option; + +- (NSArray *)getList:(NSString *)option; +- (SquirrelOptionSwitcher *)getOptionSwitcher; - (SquirrelAppOptions *)getAppOptions:(NSString *)appName; @end diff --git a/SquirrelConfig.m b/SquirrelConfig.m index d816f1b40..3c40058e6 100644 --- a/SquirrelConfig.m +++ b/SquirrelConfig.m @@ -2,6 +2,81 @@ #import +@implementation SquirrelOptionSwitcher { + NSString *_schemaId; + NSDictionary *_switcher; + NSDictionary *> *_optionGroups; + NSArray *_optionNames; +} + +- (instancetype)initWithSchemaId:(NSString *)schemaId + switcher:(NSDictionary *)switcher + optionGroups:(NSDictionary *> *)optionGroups{ + self = [super init]; + if (self) { + _schemaId = schemaId; + _switcher = switcher; + _optionGroups = optionGroups; + _optionNames = [switcher allKeys]; + } + return self; +} + +- (NSString *)schemaId { + return _schemaId; +} + +- (NSArray *)optionNames { + return _optionNames; +} + +- (NSArray *)optionStates { + return [_switcher allValues]; +} + +- (NSDictionary *)switcher { + return _switcher; +} + +- (BOOL)updateSwitcher:(NSDictionary *)switcher { + if (switcher.count != _switcher.count) { + return NO; + } + NSMutableDictionary *updatedSwitcher = + [[NSMutableDictionary alloc] initWithCapacity:switcher.count]; + for (NSString *option in _optionNames) { + if (switcher[option] == nil) { + return NO; + } + updatedSwitcher[option] = switcher[option]; + } + _switcher = [updatedSwitcher copy]; + return YES; +} + +- (BOOL)updateGroupState:(NSString *)optionState ofOption:(NSString *)optionName { + NSArray *optionGroup = _optionGroups[optionName]; + if (!optionGroup || ![optionGroup containsObject:optionState]) { + return NO; + } + NSMutableDictionary *updatedSwitcher = [_switcher mutableCopy]; + for (NSString *option in optionGroup) { + updatedSwitcher[option] = optionState; + } + _switcher = [updatedSwitcher copy]; + return YES; +} + +- (BOOL)containsOption:(NSString *)optionName { + return [_optionNames containsObject:optionName]; +} + +- (NSMutableDictionary *)mutableSwitcher { + return [_switcher mutableCopy]; +} + +@end + @implementation SquirrelConfig { NSMutableDictionary *_cache; RimeConfig _config; @@ -44,6 +119,18 @@ - (BOOL)openWithSchemaId:(NSString *)schemaId return _isOpen; } +- (BOOL)openUserConfig:(NSString *)configId { + [self close]; + _isOpen = !!rime_get_api()->user_config_open(configId.UTF8String, &_config); + return _isOpen; +} + +- (BOOL)openWithConfigId:(NSString *)configId { + [self close]; + _isOpen = !!rime_get_api()->config_open(configId.UTF8String, &_config); + return _isOpen; +} + - (void)close { if (_isOpen) { rime_get_api()->config_close(&_config); @@ -67,8 +154,8 @@ - (BOOL)getBool:(NSString *)option { return [self getOptionalBool:option].boolValue; } -- (NSInteger)getInt:(NSString *)option { - return [self getOptionalInt:option].integerValue; +- (int)getInt:(NSString *)option { + return [self getOptionalInt:option].intValue; } - (double)getDouble:(NSString *)option { @@ -76,7 +163,7 @@ - (double)getDouble:(NSString *)option { } - (NSNumber *)getOptionalBool:(NSString *)option { - NSNumber* cachedValue = [self cachedValueOfClass:[NSNumber class] forKey:option]; + NSNumber *cachedValue = [self cachedValueOfClass:[NSNumber class] forKey:option]; if (cachedValue) { return cachedValue; } @@ -97,7 +184,6 @@ - (NSNumber *)getOptionalInt:(NSString *)option { return _cache[option] = @(value); } return [_baseConfig getOptionalInt:option]; - } - (NSNumber *)getOptionalDouble:(NSString *)option { @@ -118,7 +204,7 @@ - (NSString *)getString:(NSString *)option { return cachedValue; } const char *value = - _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String) : NULL; + _isOpen ? rime_get_api()->config_get_cstring(&_config, option.UTF8String) : NULL; if (value) { return _cache[option] = @(value); } @@ -138,9 +224,72 @@ - (NSColor *)getColor:(NSString *)option { return [_baseConfig getColor:option]; } +- (NSColor *)getPattern:(NSString *)option { + NSColor *cachedValue = [self cachedValueOfClass:[NSColor class] forKey:option]; + if (cachedValue) { + return cachedValue; + } + NSColor *pattern = [self patternFromFile:[self getString:option]]; + if (pattern) { + _cache[option] = pattern; + return pattern; + } + return [_baseConfig getPattern:option]; +} + +- (NSArray *)getList:(NSString *)option { + NSMutableArray *strList = [[NSMutableArray alloc] init]; + RimeConfigIterator iterator; + rime_get_api()->config_begin_list(&iterator, &_config, option.UTF8String); + while (rime_get_api()->config_next(&iterator)) { + [strList addObject:[self getString:@(iterator.path)]]; + } + rime_get_api()->config_end(&iterator); + return strList; +} + +- (SquirrelOptionSwitcher *)getOptionSwitcher { + NSMutableDictionary *switcher = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *> *optionGroups = [[NSMutableDictionary alloc] init]; + RimeConfigIterator switchIter; + rime_get_api()->config_begin_list(&switchIter, &_config, "switches"); + while (rime_get_api()->config_next(&switchIter)) { + int reset = [self getInt:[@(switchIter.path) stringByAppendingString:@"/reset"]]; + NSString *name = [self getString:[@(switchIter.path) stringByAppendingString:@"/name"]]; + if (name) { + if ([self hasSection:[@"style/!" stringByAppendingString:name]] || + [self hasSection:[@"style/" stringByAppendingString:name]]) { + switcher[name] = reset ? name : [@"!" stringByAppendingString:name]; + optionGroups[name] = @[name]; + } + } else { + NSMutableArray *optionGroup = [[NSMutableArray alloc] init]; + BOOL hasStyleSection = NO; + RimeConfigIterator optionIter; + rime_get_api()->config_begin_list(&optionIter, &_config, [@(switchIter.path) stringByAppendingString:@"/options"].UTF8String); + while (rime_get_api()->config_next(&optionIter)) { + NSString *option = [self getString:@(optionIter.path)]; + [optionGroup addObject:option]; + hasStyleSection |= [self hasSection:[@"style/" stringByAppendingString:option]]; + } + rime_get_api()->config_end(&optionIter); + if (hasStyleSection) { + for (NSUInteger i = 0; i < optionGroup.count; ++i) { + switcher[optionGroup[i]] = optionGroup[reset]; + optionGroups[optionGroup[i]] = optionGroup; + } + } + } + } + rime_get_api()->config_end(&switchIter); + return [[SquirrelOptionSwitcher alloc] initWithSchemaId:_schemaId + switcher:switcher + optionGroups:optionGroups]; +} + - (SquirrelAppOptions *)getAppOptions:(NSString *)appName { - NSString * rootKey = [@"app_options/" stringByAppendingString:appName]; - SquirrelMutableAppOptions* appOptions = [[SquirrelMutableAppOptions alloc] init]; + NSString *rootKey = [@"app_options/" stringByAppendingString:appName]; + SquirrelMutableAppOptions *appOptions = [[SquirrelMutableAppOptions alloc] init]; RimeConfigIterator iterator; rime_get_api()->config_begin_map(&iterator, &_config, rootKey.UTF8String); while (rime_get_api()->config_next(&iterator)) { @@ -188,4 +337,18 @@ - (NSColor *)colorFromString:(NSString *)string { } } +- (NSColor *)patternFromFile:(NSString *)filePath { + if (filePath == nil) { + return nil; + } + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager changeCurrentDirectoryPath:[@"~/Library/Rime" stringByStandardizingPath]]; + NSString *patternFile = [filePath stringByStandardizingPath]; + if ([fileManager fileExistsAtPath:patternFile]) { + NSColor *pattern = [NSColor colorWithPatternImage:[[NSImage alloc] initByReferencingFile:patternFile]]; + return pattern; + } + return nil; +} + @end diff --git a/SquirrelInputController.h b/SquirrelInputController.h index 5e718b325..92d8c0197 100644 --- a/SquirrelInputController.h +++ b/SquirrelInputController.h @@ -2,6 +2,11 @@ #import @interface SquirrelInputController : IMKInputController -- (BOOL)selectCandidate:(NSInteger)index; -- (BOOL)pageUp:(BOOL)up; + +- (BOOL)perform:(NSUInteger)action onIndex:(NSUInteger)index; + @end + +#define kSELECT 0x1 +#define kDELETE 0x2 +#define kCHOOSE 0x3 diff --git a/SquirrelInputController.m b/SquirrelInputController.m index 02f0ef8a4..73d34ac4f 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.m @@ -7,12 +7,13 @@ #import #import -@interface SquirrelInputController(Private) --(void)createSession; --(void)destroySession; --(void)rimeConsumeCommittedText; --(void)rimeUpdate; --(void)updateAppOptions; +@interface SquirrelInputController (Private) +- (void)createSession; +- (void)destroySession; +- (void)rimeConsumeCommittedText; +- (void)updateStyleOptions; +- (void)rimeUpdate; +- (void)updateAppOptions; @end const int N_KEY_ROLL_OVER = 50; @@ -22,7 +23,7 @@ @implementation SquirrelInputController { NSString *_preeditString; NSRange _selRange; NSUInteger _caretPos; - NSArray *_candidates; + NSArray *_candidates; NSUInteger _lastModifier; NSEventType _lastEventType; RimeSessionId _session; @@ -39,11 +40,11 @@ @implementation SquirrelInputController { } /*! - @method - @abstract Receive incoming event - @discussion This method receives key events from the client application. + @method + @abstract Receive incoming event + @discussion This method receives key events from the client application. */ -- (BOOL)handleEvent:(NSEvent*)event client:(id)sender +- (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 @@ -64,7 +65,7 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender } } - NSString* app = [_currentClient bundleIdentifier]; + NSString *app = [_currentClient bundleIdentifier]; if (![_currentApp isEqualToString:app]) { _currentApp = [app copy]; @@ -133,11 +134,12 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender } break; case NSEventTypeKeyDown: { // ignore Command+X hotkeys. - if (modifiers & OSX_COMMAND_MASK) + if (modifiers & OSX_COMMAND_MASK) { break; + } int keyCode = event.keyCode; - NSString* keyChars = event.charactersIgnoringModifiers; + NSString *keyChars = event.charactersIgnoringModifiers; if (!isalpha(keyChars.UTF8String[0])) { keyChars = event.characters; } @@ -145,9 +147,9 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender // sender, modifiers, keyCode, keyChars); // translate osx keyevents to rime keyevents - int rime_keycode = osx_keycode_to_rime_keycode( - keyCode, keyChars.UTF8String[0], modifiers & OSX_SHIFT_MASK, - modifiers & OSX_CAPITAL_MASK); + int rime_keycode = osx_keycode_to_rime_keycode(keyCode, keyChars.UTF8String[0], + modifiers & OSX_SHIFT_MASK, + modifiers & OSX_CAPITAL_MASK); if (rime_keycode) { int rime_modifiers = osx_modifiers_to_rime_modifiers(modifiers); handled = [self processKey:rime_keycode modifiers:rime_modifiers]; @@ -165,10 +167,8 @@ - (BOOL)handleEvent:(NSEvent*)event client:(id)sender return handled; } --(BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers +- (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { - // TODO add special key event preprocessing here - // with linear candidate list, arrow keys may behave differently. Bool is_linear = NSApp.squirrelAppDelegate.panel.linear; if (is_linear != rime_get_api()->get_option(_session, "_linear")) { @@ -187,9 +187,9 @@ -(BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers if (!handled) { BOOL isVimBackInCommandMode = rime_keycode == XK_Escape || - ((rime_modifiers & kControlMask) && (rime_keycode == XK_c || - rime_keycode == XK_C || - rime_keycode == XK_bracketleft)); + ((rime_modifiers & kControlMask) && (rime_keycode == XK_c || + rime_keycode == XK_C || + rime_keycode == XK_bracketleft)); if (isVimBackInCommandMode && rime_get_api()->get_option(_session, "vim_mode") && !rime_get_api()->get_option(_session, "ascii_mode")) { @@ -200,16 +200,15 @@ -(BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers // Simulate key-ups for every interesting key-down for chord-typing. if (handled) { - bool is_chording_key = - (rime_keycode >= XK_space && rime_keycode <= XK_asciitilde) || - rime_keycode == XK_Control_L || rime_keycode == XK_Control_R || - rime_keycode == XK_Alt_L || rime_keycode == XK_Alt_R || - rime_keycode == XK_Shift_L || rime_keycode == XK_Shift_R; + BOOL is_chording_key = + (rime_keycode >= XK_space && rime_keycode <= XK_asciitilde) || + rime_keycode == XK_Control_L || rime_keycode == XK_Control_R || + rime_keycode == XK_Alt_L || rime_keycode == XK_Alt_R || + rime_keycode == XK_Shift_L || rime_keycode == XK_Shift_R; if (is_chording_key && rime_get_api()->get_option(_session, "_chord_typing")) { [self updateChord:rime_keycode modifiers:rime_modifiers]; - } - else if ((rime_modifiers & kReleaseMask) == 0) { + } else if ((rime_modifiers & kReleaseMask) == 0) { // non-chording key pressed [self clearChord]; } @@ -218,38 +217,38 @@ -(BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers return handled; } -- (BOOL)selectCandidate:(NSInteger)index { - BOOL success = rime_get_api()->select_candidate_on_current_page(_session, (int) index); - if (success) { - [self rimeUpdate]; - } - return success; -} - -- (BOOL)pageUp:(BOOL)up { +- (BOOL)perform:(NSUInteger)action onIndex:(NSUInteger)index { BOOL handled = NO; - if (up) { - handled = rime_get_api()->change_page(_session, True); - } else { - handled = rime_get_api()->change_page(_session, False); + if (index == NSPageUpFunctionKey && action == kSELECT) { + handled = rime_get_api()->process_key(_session, XK_Page_Up, 0); + } else if (index == NSPageDownFunctionKey && action == kSELECT) { + handled = rime_get_api()->process_key(_session, XK_Page_Down, 0); + } else if (index >= 0 && index < 10) { + if (action == kSELECT) { + handled = rime_get_api()->select_candidate_on_current_page(_session, (int)index); + } else if (action == kCHOOSE) { + handled = rime_get_api()->choose_candidate_on_current_page(_session, (int)index); + } else if (action == kDELETE) { + handled = rime_get_api()->delete_candidate_on_current_page(_session, (int)index); + } } - if (handled) { + if (handled && action != kCHOOSE) { [self rimeUpdate]; } return handled; } --(void)onChordTimer:(NSTimer *)timer +- (void)onChordTimer:(NSTimer *)timer { // chord release triggered by timer int processed_keys = 0; if (_chordKeyCount && _session) { // simulate key-ups for (int i = 0; i < _chordKeyCount; ++i) { - if (rime_get_api()->process_key(_session, - _chordKeyCodes[i], - (_chordModifiers[i] | kReleaseMask))) + if (rime_get_api()->process_key(_session, _chordKeyCodes[i], + (_chordModifiers[i] | kReleaseMask))) { ++processed_keys; + } } } [self clearChord]; @@ -258,12 +257,13 @@ -(void)onChordTimer:(NSTimer *)timer } } --(void)updateChord:(int)keycode modifiers:(int)modifiers +- (void)updateChord:(int)keycode modifiers:(int)modifiers { //NSLog(@"update chord: {%s} << %x", _chord, keycode); for (int i = 0; i < _chordKeyCount; ++i) { - if (_chordKeyCodes[i] == keycode) + if (_chordKeyCodes[i] == keycode) { return; + } } if (_chordKeyCount >= N_KEY_ROLL_OVER) { // you are cheating. only one human typist (fingers <= 10) is supported. @@ -288,7 +288,7 @@ -(void)updateChord:(int)keycode modifiers:(int)modifiers repeats:NO]; } --(void)clearChord +- (void)clearChord { _chordKeyCount = 0; if (_chordTimer) { @@ -299,13 +299,13 @@ -(void)clearChord } } --(NSUInteger)recognizedEvents:(id)sender +- (NSUInteger)recognizedEvents:(id)sender { //NSLog(@"recognizedEvents:"); return NSEventMaskKeyDown | NSEventMaskFlagsChanged; } --(void)activateServer:(id)sender +- (void)activateServer:(id)sender { //NSLog(@"activateServer:"); NSString *keyboardLayout = [NSApp.squirrelAppDelegate.config getString:@"keyboard_layout"]; @@ -322,7 +322,7 @@ -(void)activateServer:(id)sender _preeditString = @""; } --(instancetype)initWithServer:(IMKServer*)server delegate:(id)delegate client:(id)inputClient +- (instancetype)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { //NSLog(@"initWithServer:delegate:client:"); if (self = [super initWithServer:server delegate:delegate client:inputClient]) { @@ -332,7 +332,7 @@ -(instancetype)initWithServer:(IMKServer*)server delegate:(id)delegate client:(i return self; } --(void)deactivateServer:(id)sender +- (void)deactivateServer:(id)sender { //NSLog(@"deactivateServer:"); [NSApp.squirrelAppDelegate.panel hide]; @@ -340,24 +340,24 @@ -(void)deactivateServer:(id)sender } /*! - @method - @abstract Called when a user action was taken that ends an input session. - Typically triggered by the user selecting a new input method - or keyboard layout. - @discussion When this method is called your controller should send the - current input buffer to the client via a call to - insertText:replacementRange:. Additionally, this is the time - to clean up if that is necessary. + @method + @abstract Called when a user action was taken that ends an input session. + Typically triggered by the user selecting a new input method + or keyboard layout. + @discussion When this method is called your controller should send the + current input buffer to the client via a call to + insertText:replacementRange:. Additionally, this is the time + to clean up if that is necessary. */ --(void)commitComposition:(id)sender +- (void)commitComposition:(id)sender { //NSLog(@"commitComposition:"); // commit raw input if (_session) { - const char* raw_input = rime_get_api()->get_input(_session); + const char *raw_input = rime_get_api()->get_input(_session); if (raw_input) { - [self commitString: @(raw_input)]; + [self commitString:@(raw_input)]; rime_get_api()->clear_composition(_session); } } @@ -367,47 +367,47 @@ -(void)commitComposition:(id)sender // > though we specified the showPrefPanel: in SunPinyinApplicationDelegate as the // > action receiver, the IMKInputController will actually receive the event. // so here we deliver messages to our responsible SquirrelApplicationDelegate --(void)deploy:(id)sender +- (void)deploy:(id)sender { [NSApp.squirrelAppDelegate deploy:sender]; } --(void)syncUserData:(id)sender +- (void)syncUserData:(id)sender { [NSApp.squirrelAppDelegate syncUserData:sender]; } --(void)configure:(id)sender +- (void)configure:(id)sender { [NSApp.squirrelAppDelegate configure:sender]; } --(void)checkForUpdates:(id)sender +- (void)checkForUpdates:(id)sender { [NSApp.squirrelAppDelegate.updater performSelector:@selector(checkForUpdates:) withObject:sender]; } --(void)openWiki:(id)sender +- (void)openWiki:(id)sender { [NSApp.squirrelAppDelegate openWiki:sender]; } --(NSMenu*)menu +- (NSMenu *)menu { return NSApp.squirrelAppDelegate.menu; } --(NSArray*)candidates:(id)sender +- (NSArray *)candidates:(id)sender { return _candidates; } --(void)dealloc +- (void)dealloc { [self destroySession]; } --(void)commitString:(NSString*)string +- (void)commitString:(NSString *)string { //NSLog(@"commitString:"); [_currentClient insertText:string @@ -418,15 +418,16 @@ -(void)commitString:(NSString*)string [NSApp.squirrelAppDelegate.panel hide]; } --(void)showPreeditString:(NSString*)preedit - selRange:(NSRange)range - caretPos:(NSUInteger)pos +- (void)showPreeditString:(NSString *)preedit + selRange:(NSRange)range + caretPos:(NSUInteger)pos { //NSLog(@"showPreeditString: '%@'", preedit); if ([_preeditString isEqualToString:preedit] && - _caretPos == pos && _selRange.location == range.location && _selRange.length == range.length) + NSEqualRanges(_selRange, range) && _caretPos == pos) { return; + } _preeditString = preedit; _selRange = range; @@ -434,8 +435,8 @@ -(void)showPreeditString:(NSString*)preedit //NSLog(@"selRange.location = %ld, selRange.length = %ld; caretPos = %ld", // range.location, range.length, pos); - NSDictionary* attrs; - NSMutableAttributedString* attrString = [[NSMutableAttributedString alloc] initWithString:preedit]; + NSDictionary *attrs; + NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:preedit]; if (range.location > 0) { NSRange convertedRange = NSMakeRange(0, range.location); attrs = [self markForStyle:kTSMHiliteConvertedText atRange:convertedRange]; @@ -449,22 +450,33 @@ -(void)showPreeditString:(NSString*)preedit [_currentClient setMarkedText:attrString selectionRange:NSMakeRange(pos, 0) replacementRange:NSMakeRange(NSNotFound, 0)]; - } --(void)showPanelWithPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index +- (void)showPanelWithPreedit:(NSString *)preedit + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidates:(NSArray *)candidates + comments:(NSArray *)comments + highlighted:(NSUInteger)index + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage { //NSLog(@"showPanelWithPreedit:...:"); _candidates = candidates; NSRect inputPos; [_currentClient attributesForCharacterIndex:0 lineHeightRectangle:&inputPos]; - SquirrelPanel* panel = NSApp.squirrelAppDelegate.panel; + SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; + if (@available(macOS 14.0, *)) { // avoid overlapping with cursor effects view + if (_lastModifier & OSX_CAPITAL_MASK) { + if (NSHeight(inputPos) > NSWidth(inputPos)) { + inputPos.size.height += 26; + inputPos.origin.y -= 26; + } else { + inputPos.size.width += 33; + inputPos.origin.x -= 33; + } + } + } panel.position = inputPos; panel.inputController = self; [panel showPreedit:preedit @@ -472,8 +484,10 @@ -(void)showPanelWithPreedit:(NSString*)preedit caretPos:caretPos candidates:candidates comments:comments - labels:labels highlighted:index + pageNum:pageNum + lastPage:lastPage + turnPage:NSNotFound update:YES]; } @@ -481,11 +495,11 @@ -(void)showPanelWithPreedit:(NSString*)preedit // implementation of private interface -@implementation SquirrelInputController(Private) +@implementation SquirrelInputController (Private) --(void)createSession +- (void)createSession { - NSString* app = [_currentClient bundleIdentifier]; + NSString *app = [_currentClient bundleIdentifier]; NSLog(@"createSession: %@", app); _currentApp = [app copy]; _session = rime_get_api()->create_session(); @@ -497,13 +511,14 @@ -(void)createSession } } --(void)updateAppOptions +- (void)updateAppOptions { - if (!_currentApp) + if (!_currentApp) { return; - SquirrelAppOptions* appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp]; + } + SquirrelAppOptions *appOptions = [NSApp.squirrelAppDelegate.config getAppOptions:_currentApp]; if (appOptions) { - for (NSString* key in appOptions) { + for (NSString *key in appOptions) { BOOL value = appOptions[key].boolValue; NSLog(@"set app option: %@ = %d", key, value); rime_get_api()->set_option(_session, key.UTF8String, value); @@ -511,7 +526,7 @@ -(void)updateAppOptions } } --(void)destroySession +- (void)destroySession { //NSLog(@"destroySession:"); if (_session) { @@ -521,38 +536,68 @@ -(void)destroySession [self clearChord]; } --(void)rimeConsumeCommittedText +- (void)rimeConsumeCommittedText { RIME_STRUCT(RimeCommit, commit); if (rime_get_api()->get_commit(_session, &commit)) { NSString *commitText = @(commit.text); - [self commitString: commitText]; + [self commitString:commitText]; rime_get_api()->free_commit(&commit); } } -NSString *substr(const char *str, int length) { - char substring[length+1]; - strncpy(substring, str, length); - substring[length] = '\0'; - return [NSString stringWithCString:substring encoding:NSUTF8StringEncoding]; +- (void)updateStyleOptions +{ + // update the list of switchers that change styles and color-themes + SquirrelOptionSwitcher *optionSwitcher; + SquirrelConfig *schema = [[SquirrelConfig alloc] init]; + if ([schema openWithSchemaId:_schemaId baseConfig:NSApp.squirrelAppDelegate.config] && + [schema hasSection:@"style"]) { + optionSwitcher = [schema getOptionSwitcher]; + } else { + optionSwitcher = [[SquirrelOptionSwitcher alloc] initWithSchemaId:_schemaId switcher:@{} optionGroups:@{}]; + } + [schema close]; + NSMutableDictionary *switcher = [optionSwitcher mutableSwitcher]; + NSSet *prevStates = [NSSet setWithArray:optionSwitcher.optionStates]; + for (NSString *state in prevStates) { + NSString *updatedState; + NSArray *optionGroup = [optionSwitcher.switcher allKeysForObject:state]; + for (NSString *option in optionGroup) { + if (rime_get_api()->get_option(_session, option.UTF8String)) { + updatedState = option; + break; + } + } + updatedState = updatedState ? : [@"!" stringByAppendingString:optionGroup[0]]; + if (![updatedState isEqualToString:state]) { + for (NSString *option in optionGroup) { + switcher[option] = updatedState; + } + } + } + [optionSwitcher updateSwitcher:switcher]; + [NSApp.squirrelAppDelegate.panel setOptionSwitcher:optionSwitcher]; } --(void)rimeUpdate +- (void)rimeUpdate { //NSLog(@"rimeUpdate"); [self rimeConsumeCommittedText]; + BOOL switcherMenu = rime_get_api()->get_option(_session, "dumb"); RIME_STRUCT(RimeStatus, status); if (rime_get_api()->get_status(_session, &status)) { // enable schema specific ui style - if (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id) != 0) { + if (!switcherMenu && (!_schemaId || strcmp(_schemaId.UTF8String, status.schema_id))) { _schemaId = @(status.schema_id); + [self updateStyleOptions]; + [NSApp.squirrelAppDelegate loadSchemaSpecificLabels:_schemaId]; [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId]; // inline preedit _inlinePreedit = (NSApp.squirrelAppDelegate.panel.inlinePreedit && !rime_get_api()->get_option(_session, "no_inline")) || - rime_get_api()->get_option(_session, "inline"); + rime_get_api()->get_option(_session, "inline"); _inlineCandidate = (NSApp.squirrelAppDelegate.panel.inlineCandidate && !rime_get_api()->get_option(_session, "no_inline")); // if not inline, embed soft cursor in preedit string @@ -567,70 +612,54 @@ -(void)rimeUpdate const char *preedit = ctx.composition.preedit; NSString *preeditText = preedit ? @(preedit) : @""; - NSUInteger start = substr(preedit, ctx.composition.sel_start).length; - NSUInteger end = substr(preedit, ctx.composition.sel_end).length; - NSUInteger caretPos = substr(preedit, ctx.composition.cursor_pos).length; - NSRange selRange = NSMakeRange(start, end - start); - if (_inlineCandidate) { + NSUInteger start = [[NSString alloc] initWithBytes:preedit length:ctx.composition.sel_start encoding:NSUTF8StringEncoding].length; + NSUInteger end = [[NSString alloc] initWithBytes:preedit length:ctx.composition.sel_end encoding:NSUTF8StringEncoding].length; + NSUInteger caretPos = [[NSString alloc] initWithBytes:preedit length:ctx.composition.cursor_pos encoding:NSUTF8StringEncoding].length; + if (_inlineCandidate && !switcherMenu) { const char *candidatePreview = ctx.commit_text_preview; NSString *candidatePreviewText = candidatePreview ? @(candidatePreview) : @""; if (_inlinePreedit) { - if ((caretPos >= NSMaxRange(selRange)) && (caretPos < preeditText.length)) { - candidatePreviewText = [candidatePreviewText stringByAppendingString:[preeditText substringWithRange:NSMakeRange(caretPos, preeditText.length-caretPos)]]; + if ((caretPos >= end) && (caretPos < preeditText.length)) { + candidatePreviewText = [candidatePreviewText stringByAppendingString:[preeditText substringWithRange:NSMakeRange(caretPos, preeditText.length - caretPos)]]; } - [self showPreeditString:candidatePreviewText selRange:NSMakeRange(selRange.location, candidatePreviewText.length-selRange.location) caretPos:candidatePreviewText.length-(preeditText.length-caretPos)]; + [self showPreeditString:candidatePreviewText + selRange:NSMakeRange(start, candidatePreviewText.length - (preeditText.length - end) - start) + caretPos:caretPos <= start ? caretPos : candidatePreviewText.length - (preeditText.length - caretPos)]; } else { - if ((NSMaxRange(selRange) < caretPos) && (caretPos > selRange.location)) { - candidatePreviewText = [candidatePreviewText substringWithRange:NSMakeRange(0, candidatePreviewText.length-(caretPos-NSMaxRange(selRange)))]; - } else if ((NSMaxRange(selRange) < preeditText.length) && (caretPos <= selRange.location)) { - candidatePreviewText = [candidatePreviewText substringWithRange:NSMakeRange(0, candidatePreviewText.length-(preeditText.length-NSMaxRange(selRange)))]; + if ((end < caretPos) && (caretPos > start)) { + candidatePreviewText = [candidatePreviewText substringWithRange:NSMakeRange(0, candidatePreviewText.length - (caretPos - end))]; + } else if ((end < preeditText.length) && (caretPos < end)) { + candidatePreviewText = [candidatePreviewText substringWithRange:NSMakeRange(0, candidatePreviewText.length - (preeditText.length - end))]; } - [self showPreeditString:candidatePreviewText selRange:NSMakeRange(selRange.location, candidatePreviewText.length-selRange.location) caretPos:candidatePreviewText.length]; + [self showPreeditString:candidatePreviewText + selRange:NSMakeRange(start - (caretPos < end), candidatePreviewText.length - start + (caretPos < end)) + caretPos:caretPos < end ? caretPos - 1 : candidatePreviewText.length]; } } else { - if (_inlinePreedit) { - [self showPreeditString:preeditText selRange:selRange caretPos:caretPos]; + if (_inlinePreedit && !switcherMenu) { + [self showPreeditString:preeditText selRange:NSMakeRange(start, end - start) caretPos:caretPos]; } else { - NSRange empty = {0, 0}; // TRICKY: display a non-empty string to prevent iTerm2 from echoing each character in preedit. - // note this is a full-shape space U+3000; using half shape characters like "..." will result in + // note this is a full-width EM space U+2003; using narrow characters like "..." will result in // an unstable baseline when composing Chinese characters. - [self showPreeditString:(preedit ? @" " : @"") selRange:empty caretPos:0]; + [self showPreeditString:(preedit ? @" " : @"") selRange:NSMakeRange(0, 0) caretPos:0]; } } // update candidates - NSMutableArray *candidates = [NSMutableArray array]; - NSMutableArray *comments = [NSMutableArray array]; - NSUInteger i; - for (i = 0; i < ctx.menu.num_candidates; ++i) { + NSMutableArray *candidates = [[NSMutableArray alloc] initWithCapacity:ctx.menu.num_candidates]; + NSMutableArray *comments = [[NSMutableArray alloc] initWithCapacity:ctx.menu.num_candidates]; + for (int i = 0; i < ctx.menu.num_candidates; ++i) { [candidates addObject:@(ctx.menu.candidates[i].text)]; - if (ctx.menu.candidates[i].comment) { - [comments addObject:@(ctx.menu.candidates[i].comment)]; - } - else { - [comments addObject:@""]; - } - } - NSArray* labels; - if (ctx.menu.select_keys) { - labels = @[@(ctx.menu.select_keys)]; - } else if (ctx.select_labels) { - NSMutableArray *selectLabels = [NSMutableArray array]; - for (i = 0; i < ctx.menu.page_size; ++i) { - char* label_str = ctx.select_labels[i]; - [selectLabels addObject:@(label_str)]; - } - labels = selectLabels; - } else { - labels = @[]; + [comments addObject:@(ctx.menu.candidates[i].comment ? : "")]; } - [self showPanelWithPreedit:(_inlinePreedit ? nil : preeditText) - selRange:selRange - caretPos:caretPos + [self showPanelWithPreedit:(_inlinePreedit && !switcherMenu ? nil : preeditText) + selRange:NSMakeRange(start, end - start) + caretPos:(switcherMenu ? NSNotFound : caretPos) candidates:candidates comments:comments - labels:labels - highlighted:ctx.menu.highlighted_candidate_index]; + highlighted:ctx.menu.highlighted_candidate_index + pageNum:ctx.menu.page_no + lastPage:ctx.menu.is_last_page]; rime_get_api()->free_context(&ctx); } else { [NSApp.squirrelAppDelegate.panel hide]; diff --git a/SquirrelPanel.h b/SquirrelPanel.h index 159b2a8ad..589974e30 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.h @@ -1,38 +1,46 @@ #import #import "SquirrelInputController.h" - @class SquirrelConfig; +@class SquirrelOptionSwitcher; @interface SquirrelPanel : NSPanel // Linear candidate list, as opposed to stacked candidate list. @property(nonatomic, readonly) BOOL linear; +// Tabled candidate list, a subtype of linear candidate list with tabled layout. +@property(nonatomic, readonly) BOOL tabled; // Vertical text, as opposed to horizontal text. @property(nonatomic, readonly) BOOL vertical; // Show preedit text inline. @property(nonatomic, readonly) BOOL inlinePreedit; // Show first candidate inline @property(nonatomic, readonly) BOOL inlineCandidate; - +// Store switch options that change style (color theme) settings +@property(nonatomic, strong) SquirrelOptionSwitcher *optionSwitcher; // position of input caret on screen. @property(nonatomic, assign) NSRect position; -// position of input caret on screen. + @property(nonatomic, assign) SquirrelInputController *inputController; --(void)showPreedit:(NSString*)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray*)candidates - comments:(NSArray*)comments - labels:(NSArray*)labels - highlighted:(NSUInteger)index - update:(BOOL)update; +- (void)showPreedit:(NSString *)preedit + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidates:(NSArray *)candidates + comments:(NSArray *)comments + highlighted:(NSUInteger)index + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage + turnPage:(NSUInteger)turnPage + update:(BOOL)update; + +- (void)hide; --(void)hide; +- (void)updateStatusLong:(NSString *)messageLong + statusShort:(NSString *)messageShort; --(void)updateStatusLong:(NSString*)messageLong statusShort:(NSString*)messageShort; +- (void)loadConfig:(SquirrelConfig *)config + forDarkMode:(BOOL)isDark; --(void)loadConfig:(SquirrelConfig*)config - forDarkMode:(BOOL)isDark; +- (void)loadLabelConfig:(SquirrelConfig *)config; @end diff --git a/SquirrelPanel.m b/SquirrelPanel.m index 892471dd4..3215d53d6 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.m @@ -3,35 +3,172 @@ #import "SquirrelConfig.h" #import +@implementation NSBezierPath (BezierPathQuartzUtilities) +- (CGPathRef)quartzPath { +// if (@available(macOS 14.0, *)) { +// return self.CGPath; +// } + // Need to begin a path here. + CGPathRef immutablePath = NULL; + // Then draw the path elements. + NSUInteger numElements = self.elementCount; + if (numElements > 0) { + CGMutablePathRef path = CGPathCreateMutable(); + NSPoint points[3]; + BOOL didClosePath = YES; + for (NSUInteger i = 0; i < numElements; i++) { + switch ([self elementAtIndex:i associatedPoints:points]) { + case NSMoveToBezierPathElement: + CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); + break; + case NSLineToBezierPathElement: + CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); + didClosePath = NO; + break; + case NSCurveToBezierPathElement: + CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, + points[1].x, points[1].y, + points[2].x, points[2].y); + didClosePath = NO; + break; +// case NSBezierPathElementQuadraticCurveTo: +// CGPathAddQuadCurveToPoint(path, NULL, points[0].x, points[0].y, +// points[1].x, points[1].y); +// didClosePath = NO; +// break; + case NSClosePathBezierPathElement: + CGPathCloseSubpath(path); + didClosePath = YES; + break; + } + } + // Be sure the path is closed or Quartz may not do valid hit detection. + if (!didClosePath) { + CGPathCloseSubpath(path); + } + immutablePath = CFAutorelease(CGPathCreateCopy(path)); + CGPathRelease(path); + } + return immutablePath; +} + +@end + +@implementation NSMutableAttributedString (NSMutableAttributedStringMarkDownFormatting) + +- (void)formatMarkDown { + NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:@"((\\*{1,2}|\\^|~{1,2})|((?<=\\b)_{1,2})|<(b|strong|i|em|u|sup|sub|s)>)(.+?)(\\2|\\3(?=\\b)|<\\/\\4>)" options:NSRegularExpressionUseUnicodeWordBoundaries error:nil]; + NSInteger __block offset = 0; + [regex enumerateMatchesInString:self.string options:0 range:NSMakeRange(0, self.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + result = [result resultByAdjustingRangesWithOffset:offset]; + NSString *tag = [self.string substringWithRange:[result rangeAtIndex:1]]; + if ([tag isEqualToString:@"**"] || [tag isEqualToString:@"__"] || + [tag isEqualToString:@""] || [tag isEqualToString:@""]) { + [self applyFontTraits:NSBoldFontMask range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"*"] || [tag isEqualToString:@"_"] || + [tag isEqualToString:@""] || [tag isEqualToString:@""]) { + [self applyFontTraits:NSItalicFontMask range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@""]) { + [self addAttribute:NSUnderlineStyleAttributeName + value:@(NSUnderlineStyleSingle) range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"~~"] || [tag isEqualToString:@""]) { + [self addAttribute:NSStrikethroughStyleAttributeName + value:@(NSUnderlineStyleSingle) range:[result rangeAtIndex:5]]; + } else if ([tag isEqualToString:@"^"] || [tag isEqualToString:@""]) { + [self superscriptRange:[result rangeAtIndex:5]]; + [self enumerateAttribute:NSFontAttributeName inRange:[result rangeAtIndex:5] options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + NSFont *font = [[NSFontManager sharedFontManager] convertFont:value toSize:[value pointSize] * 7 / 12]; + [self addAttribute:NSFontAttributeName value:font range:range]; + }]; + } else if ([tag isEqualToString:@"~"] || [tag isEqualToString:@""]) { + [self subscriptRange:[result rangeAtIndex:5]]; + [self enumerateAttribute:NSFontAttributeName inRange:[result rangeAtIndex:5] options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + NSFont *font = [[NSFontManager sharedFontManager] convertFont:value toSize:[value pointSize] * 7 / 12]; + [self addAttribute:NSFontAttributeName value:font range:range]; + }]; + } + [self deleteCharactersInRange:[result rangeAtIndex:6]]; + [self deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:6].length + [result rangeAtIndex:1].length; + }]; + if (offset != 0) { // repeat until no more nested markdown + [self formatMarkDown]; + } +} + +- (void)annotateRuby { + NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:@"(\\uFFF9)\\s*(\\w+?)\\s*((\\uFFFA)(.+?)(\\uFFFB))" + options:0 error:nil]; + NSInteger __block offset = 0; + [regex enumerateMatchesInString:self.string options:0 range:NSMakeRange(0, self.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + result = [result resultByAdjustingRangesWithOffset:offset]; + if (@available(macOS 12.0, *)) { + NSString *rubyString = [self.string substringWithRange:[result rangeAtIndex:5]]; + NSFont *rubyFont = [self attribute:NSFontAttributeName atIndex:[result rangeAtIndex:5].location effectiveRange:NULL]; + rubyFont = [[NSFontManager sharedFontManager] convertFont:rubyFont toSize:[rubyFont pointSize] / 2]; + NSColor *rubyColor = [self attribute:NSForegroundColorAttributeName atIndex:[result rangeAtIndex:5].location effectiveRange:NULL]; + NSDictionary *rubyAttrs = @{NSFontAttributeName: rubyFont, NSForegroundColorAttributeName: rubyColor}; + NSArray *rubySubStrings = [rubyString componentsSeparatedByString:@" "]; + NSRange baseRange = [result rangeAtIndex:2]; + if (rubySubStrings.count > baseRange.length) { + rubySubStrings = @[rubyString]; + } + for (NSUInteger i = 0; i < rubySubStrings.count; ++i) { + CTRubyAnnotationRef rubyAnnotation = CTRubyAnnotationCreateWithAttributes(kCTRubyAlignmentDistributeSpace, kCTRubyOverhangNone, kCTRubyPositionBefore, (__bridge CFStringRef)rubySubStrings[i], (__bridge CFDictionaryRef)rubyAttrs); + [self addAttribute:CFBridgingRelease(kCTRubyAnnotationAttributeName) value:CFBridgingRelease(rubyAnnotation) range:NSMakeRange(baseRange.location + i, 1)]; + } + [self deleteCharactersInRange:[result rangeAtIndex:3]]; + [self deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:3].length + [result rangeAtIndex:1].length; + } else { + NSFont *rubyFont = [self attribute:NSFontAttributeName atIndex:[result rangeAtIndex:5].location effectiveRange:NULL]; + rubyFont = [[NSFontManager sharedFontManager] convertFont:rubyFont toSize:[rubyFont pointSize] / 2]; + [self superscriptRange:[result rangeAtIndex:5]]; + [self addAttribute:NSFontAttributeName value:rubyFont range:[result rangeAtIndex:5]]; + [self addAttribute:NSBaselineOffsetAttributeName value:@(rubyFont.ascender) range:[result rangeAtIndex:5]]; + [self deleteCharactersInRange:[result rangeAtIndex:6]]; + [self deleteCharactersInRange:[result rangeAtIndex:4]]; + [self deleteCharactersInRange:[result rangeAtIndex:1]]; + offset -= [result rangeAtIndex:6].length + [result rangeAtIndex:4].length + [result rangeAtIndex:1].length; + } + }]; +} + +@end + static const CGFloat kOffsetHeight = 5; static const CGFloat kDefaultFontSize = 24; static const CGFloat kBlendedBackgroundColorFraction = 1.0 / 5; static const NSTimeInterval kShowStatusDuration = 1.2; -static NSString *const kDefaultCandidateFormat = @"%c.\u00A0%@"; +static NSString *const kDefaultCandidateFormat = @"%c. %@"; +static NSString *const kTipSpecifier = @"%s"; +static NSString *const kFullWidthSpace = @" "; @interface SquirrelTheme : NSObject -@property(nonatomic, assign) BOOL native; -@property(nonatomic, assign) BOOL memorizeSize; - @property(nonatomic, strong, readonly) NSColor *backgroundColor; -@property(nonatomic, strong, readonly) NSColor *highlightedBackColor; -@property(nonatomic, strong, readonly) NSColor *candidateBackColor; +@property(nonatomic, strong, readonly) NSColor *backgroundImage; +@property(nonatomic, strong, readonly) NSColor *highlightedStripColor; @property(nonatomic, strong, readonly) NSColor *highlightedPreeditColor; @property(nonatomic, strong, readonly) NSColor *preeditBackgroundColor; @property(nonatomic, strong, readonly) NSColor *borderColor; @property(nonatomic, readonly) CGFloat cornerRadius; -@property(nonatomic, readonly) CGFloat hilitedCornerRadius; -@property(nonatomic, readonly) CGFloat surroundingExtraExpansion; -@property(nonatomic, readonly) CGFloat shadowSize; +@property(nonatomic, readonly) CGFloat highlightedCornerRadius; +@property(nonatomic, readonly) CGFloat separatorWidth; @property(nonatomic, readonly) NSSize edgeInset; -@property(nonatomic, readonly) CGFloat borderWidth; @property(nonatomic, readonly) CGFloat linespace; @property(nonatomic, readonly) CGFloat preeditLinespace; @property(nonatomic, readonly) CGFloat alpha; -@property(nonatomic, readonly) BOOL translucency; -@property(nonatomic, readonly) BOOL mutualExclusive; +@property(nonatomic, readonly) CGFloat translucency; +@property(nonatomic, readonly) CGFloat lineLength; +@property(nonatomic, readonly) BOOL showPaging; +@property(nonatomic, readonly) BOOL rememberSize; +@property(nonatomic, readonly) BOOL tabled; @property(nonatomic, readonly) BOOL linear; @property(nonatomic, readonly) BOOL vertical; @property(nonatomic, readonly) BOOL inlinePreedit; @@ -45,142 +182,219 @@ @interface SquirrelTheme : NSObject @property(nonatomic, strong, readonly) NSDictionary *commentHighlightedAttrs; @property(nonatomic, strong, readonly) NSDictionary *preeditAttrs; @property(nonatomic, strong, readonly) NSDictionary *preeditHighlightedAttrs; +@property(nonatomic, strong, readonly) NSDictionary *pagingAttrs; +@property(nonatomic, strong, readonly) NSDictionary *pagingHighlightedAttrs; +@property(nonatomic, strong, readonly) NSDictionary *statusAttrs; @property(nonatomic, strong, readonly) NSParagraphStyle *paragraphStyle; @property(nonatomic, strong, readonly) NSParagraphStyle *preeditParagraphStyle; +@property(nonatomic, strong, readonly) NSParagraphStyle *pagingParagraphStyle; +@property(nonatomic, strong, readonly) NSParagraphStyle *statusParagraphStyle; -@property(nonatomic, strong, readonly) NSString *prefixLabelFormat, *suffixLabelFormat; -@property(nonatomic, strong, readonly) NSString *statusMessageType; +@property(nonatomic, strong, readonly) NSAttributedString *symbolBackFill; +@property(nonatomic, strong, readonly) NSAttributedString *symbolBackStroke; +@property(nonatomic, strong, readonly) NSAttributedString *symbolForwardFill; +@property(nonatomic, strong, readonly) NSAttributedString *symbolForwardStroke; -- (void)setCandidateFormat:(NSString *)candidateFormat; -- (void)setStatusMessageType:(NSString *)statusMessageType; +@property(nonatomic, strong, readonly) NSArray *labels; +@property(nonatomic, strong, readonly) NSArray *candidateFormats; +@property(nonatomic, strong, readonly) NSArray *candidateHighlightedFormats; +@property(nonatomic, strong, readonly) NSString *statusMessageType; - (void)setBackgroundColor:(NSColor *)backgroundColor - highlightedBackColor:(NSColor *)highlightedBackColor - candidateBackColor:(NSColor *)candidateBackColor + backgroundImage:(NSColor *)backgroundImage + highlightedStripColor:(NSColor *)highlightedStripColor highlightedPreeditColor:(NSColor *)highlightedPreeditColor preeditBackgroundColor:(NSColor *)preeditBackgroundColor borderColor:(NSColor *)borderColor; -- (void)setCornerRadius:(CGFloat)cornerRadius - hilitedCornerRadius:(CGFloat)hilitedCornerRadius - srdExtraExpansion:(CGFloat)surroundingExtraExpansion - shadowSize:(CGFloat)shadowSize - edgeInset:(NSSize)edgeInset - borderWidth:(CGFloat)borderWidth - linespace:(CGFloat)linespace - preeditLinespace:(CGFloat)preeditLinespace - alpha:(CGFloat)alpha - translucency:(BOOL)translucency - mutualExclusive:(BOOL)mutualExclusive - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate; - -- (void) setAttrs:(NSMutableDictionary *)attrs - highlightedAttrs:(NSMutableDictionary *)highlightedAttrs - labelAttrs:(NSMutableDictionary *)labelAttrs - labelHighlightedAttrs:(NSMutableDictionary *)labelHighlightedAttrs - commentAttrs:(NSMutableDictionary *)commentAttrs - commentHighlightedAttrs:(NSMutableDictionary *)commentHighlightedAttrs - preeditAttrs:(NSMutableDictionary *)preeditAttrs - preeditHighlightedAttrs:(NSMutableDictionary *)preeditHighlightedAttrs; - -- (void) setParagraphStyle:(NSParagraphStyle *)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle *)preeditParagraphStyle; +- (void) setCornerRadius:(CGFloat)cornerRadius + highlightedCornerRadius:(CGFloat)highlightedCornerRadius + separatorWidth:(CGFloat)separatorWidth + edgeInset:(NSSize)edgeInset + linespace:(CGFloat)linespace + preeditLinespace:(CGFloat)preeditLinespace + alpha:(CGFloat)alpha + translucency:(CGFloat)translucency + lineLength:(CGFloat)lineLength + showPaging:(BOOL)showPaging + rememberSize:(BOOL)rememberSize + tabled:(BOOL)tabled + linear:(BOOL)linear + vertical:(BOOL)vertical + inlinePreedit:(BOOL)inlinePreedit + inlineCandidate:(BOOL)inlineCandidate; + +- (void) setAttrs:(NSDictionary *)attrs + highlightedAttrs:(NSDictionary *)highlightedAttrs + labelAttrs:(NSDictionary *)labelAttrs + labelHighlightedAttrs:(NSDictionary *)labelHighlightedAttrs + commentAttrs:(NSDictionary *)commentAttrs + commentHighlightedAttrs:(NSDictionary *)commentHighlightedAttrs + preeditAttrs:(NSDictionary *)preeditAttrs + preeditHighlightedAttrs:(NSDictionary *)preeditHighlightedAttrs + pagingAttrs:(NSDictionary *)pagingAttrs + pagingHighlightedAttrs:(NSDictionary *)pagingHighlightedAttrs + statusAttrs:(NSDictionary *)statusAttrs; + +- (void)setParagraphStyle:(NSParagraphStyle *)paragraphStyle + preeditParagraphStyle:(NSParagraphStyle *)preeditParagraphStyle + pagingParagraphStyle:(NSParagraphStyle *)pagingParagraphStyle + statusParagraphStyle:(NSParagraphStyle *)statusParagraphStyle; + +- (void)setLabels:(NSArray *)labels; + +- (void)setCandidateFormat:(NSString *)candidateFormat; + +- (void)setStatusMessageType:(NSString *)statusMessageType; @end @implementation SquirrelTheme -- (void)setCandidateFormat:(NSString *)candidateFormat { - // in the candiate format, everything other than '%@' is considered part of the label - NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; - if (candidateRange.location == NSNotFound) { - _prefixLabelFormat = candidateFormat; - _suffixLabelFormat = nil; - return; - } - if (candidateRange.location > 0) { - // everything before '%@' is prefix label - NSRange prefixLabelRange = NSMakeRange(0, candidateRange.location); - _prefixLabelFormat = [candidateFormat substringWithRange:prefixLabelRange]; - } else { - _prefixLabelFormat = nil; - } - if (NSMaxRange(candidateRange) < candidateFormat.length) { - // everything after '%@' is suffix label - NSRange suffixLabelRange = NSMakeRange(NSMaxRange(candidateRange), - candidateFormat.length - NSMaxRange(candidateRange)); - _suffixLabelFormat = [candidateFormat substringWithRange:suffixLabelRange]; - } else { - // '%@' is at the end, so suffix label does not exist - _suffixLabelFormat = nil; +static NSArray * formatLabels(NSAttributedString *format, NSArray *labels) { + NSRange enumRange = NSMakeRange(0, 0); + NSMutableArray *formatted = [[NSMutableArray alloc] initWithCapacity:labels.count]; + NSCharacterSet *labelCharacters = [NSCharacterSet characterSetWithCharactersInString:[labels componentsJoinedByString:@""]]; + if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xff10, 10)] + isSupersetOfSet:labelCharacters]) { // 01..9 + if ([format.string containsString:@"%c\u20dd"]) { // ①..⑨⓪ + enumRange = [format.string rangeOfString:@"%c\u20dd"]; + for (NSString *label in labels) { + unichar chars[] = {[label characterAtIndex:0] == 0xff10 ? 0x24ea : [label characterAtIndex:0] - 0xff11 + 0x2460, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } else if ([format.string containsString:@"(%c)"]) { // ⑴..⑼⑽ + enumRange = [format.string rangeOfString:@"(%c)"]; + for (NSString *label in labels) { + unichar chars[] = {[label characterAtIndex:0] == 0xff10 ? 0x247d : [label characterAtIndex:0] - 0xff11 + 0x2474, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } else if ([format.string containsString:@"%c."]) { // ⒈..⒐🄀 + enumRange = [format.string rangeOfString:@"%c."]; + for (NSString *label in labels) { + if ([label characterAtIndex:0] == 0xff10) { + unichar chars[] = {0xd83c, 0xdd00, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:3]]; + [formatted addObject:[newFormat copy]]; + } else { + unichar chars[] = {[label characterAtIndex:0] - 0xff11 + 0x2488, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } + } else if ([format.string containsString:@"%c,"]) { //🄂..🄊🄁 + enumRange = [format.string rangeOfString:@"%c,"]; + for (NSString *label in labels) { + unichar chars[] = {0xd83c, [label characterAtIndex:0] - 0xff10 + 0xdd01, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } + } else if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xff21, 26)] + isSupersetOfSet:labelCharacters]) { // A..Z + if ([format.string containsString:@"%c\u20dd"]) { // Ⓐ..Ⓩ + enumRange = [format.string rangeOfString:@"%c\u20dd"]; + for (NSString *label in labels) { + unichar chars[] = {[label characterAtIndex:0] - 0xff21 + 0x24b6, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } else if ([format.string containsString:@"(%c)"]) { // 🄐..🄩 + enumRange = [format.string rangeOfString:@"(%c)"]; + for (NSString *label in labels) { + unichar chars[] = {0xd83c, [label characterAtIndex:0] - 0xff21 + 0xdd10, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } else if ([format.string containsString:@"%c\u20de"]) { // 🄰..🅉 + enumRange = [format.string rangeOfString:@"%c\u20de"]; + for (NSString *label in labels) { + unichar chars[] = {0xd83c, [label characterAtIndex:0] - 0xff21 + 0xdd30, 0x0}; + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithCharacters:chars length:2]]; + [formatted addObject:[newFormat copy]]; + } + } } -} - -- (void)setStatusMessageType:(NSString *)type { - if ([type isEqualToString: @"long"] || [type isEqualToString: @"short"] || [type isEqualToString: @"mix"]) { - _statusMessageType = type; - } else { - _statusMessageType = @"mix"; + if (enumRange.length == 0) { + enumRange = [format.string rangeOfString:@"%c"]; + for (NSString *label in labels) { + NSMutableAttributedString *newFormat = [format mutableCopy]; + [newFormat replaceCharactersInRange:enumRange withString:label]; + [formatted addObject:[newFormat copy]]; + } } + return [formatted copy]; } - (void)setBackgroundColor:(NSColor *)backgroundColor - highlightedBackColor:(NSColor *)highlightedBackColor - candidateBackColor:(NSColor *)candidateBackColor + backgroundImage:(NSColor *)backgroundImage + highlightedStripColor:(NSColor *)highlightedStripColor highlightedPreeditColor:(NSColor *)highlightedPreeditColor preeditBackgroundColor:(NSColor *)preeditBackgroundColor borderColor:(NSColor *)borderColor { _backgroundColor = backgroundColor; - _highlightedBackColor = highlightedBackColor; - _candidateBackColor = candidateBackColor; + _backgroundImage = backgroundImage; + _highlightedStripColor = highlightedStripColor; _highlightedPreeditColor = highlightedPreeditColor; _preeditBackgroundColor = preeditBackgroundColor; _borderColor = borderColor; } -- (void)setCornerRadius:(double)cornerRadius - hilitedCornerRadius:(double)hilitedCornerRadius - srdExtraExpansion:(double)surroundingExtraExpansion - shadowSize:(double)shadowSize - edgeInset:(NSSize)edgeInset - borderWidth:(double)borderWidth - linespace:(double)linespace - preeditLinespace:(double)preeditLinespace - alpha:(double)alpha - translucency:(BOOL)translucency - mutualExclusive:(BOOL)mutualExclusive - linear:(BOOL)linear - vertical:(BOOL)vertical - inlinePreedit:(BOOL)inlinePreedit - inlineCandidate:(BOOL)inlineCandidate { +- (void) setCornerRadius:(CGFloat)cornerRadius + highlightedCornerRadius:(CGFloat)highlightedCornerRadius + separatorWidth:(CGFloat)separatorWidth + edgeInset:(NSSize)edgeInset + linespace:(CGFloat)linespace + preeditLinespace:(CGFloat)preeditLinespace + alpha:(CGFloat)alpha + translucency:(CGFloat)translucency + lineLength:(CGFloat)lineLength + showPaging:(BOOL)showPaging + rememberSize:(BOOL)rememberSize + tabled:(BOOL)tabled + linear:(BOOL)linear + vertical:(BOOL)vertical + inlinePreedit:(BOOL)inlinePreedit + inlineCandidate:(BOOL)inlineCandidate { _cornerRadius = cornerRadius; - _hilitedCornerRadius = hilitedCornerRadius; - _surroundingExtraExpansion = surroundingExtraExpansion; - _shadowSize = shadowSize; + _highlightedCornerRadius = highlightedCornerRadius; + _separatorWidth = separatorWidth; _edgeInset = edgeInset; - _borderWidth = borderWidth; _linespace = linespace; + _preeditLinespace = preeditLinespace; _alpha = alpha; _translucency = translucency; - _mutualExclusive = mutualExclusive; - _preeditLinespace = preeditLinespace; + _lineLength = lineLength; + _showPaging = showPaging; + _rememberSize = rememberSize; + _tabled = tabled; _linear = linear; _vertical = vertical; _inlinePreedit = inlinePreedit; _inlineCandidate = inlineCandidate; } -- (void) setAttrs:(NSMutableDictionary *)attrs - highlightedAttrs:(NSMutableDictionary *)highlightedAttrs - labelAttrs:(NSMutableDictionary *)labelAttrs - labelHighlightedAttrs:(NSMutableDictionary *)labelHighlightedAttrs - commentAttrs:(NSMutableDictionary *)commentAttrs - commentHighlightedAttrs:(NSMutableDictionary *)commentHighlightedAttrs - preeditAttrs:(NSMutableDictionary *)preeditAttrs - preeditHighlightedAttrs:(NSMutableDictionary *)preeditHighlightedAttrs { +- (void) setAttrs:(NSDictionary *)attrs + highlightedAttrs:(NSDictionary *)highlightedAttrs + labelAttrs:(NSDictionary *)labelAttrs + labelHighlightedAttrs:(NSDictionary *)labelHighlightedAttrs + commentAttrs:(NSDictionary *)commentAttrs + commentHighlightedAttrs:(NSDictionary *)commentHighlightedAttrs + preeditAttrs:(NSDictionary *)preeditAttrs + preeditHighlightedAttrs:(NSDictionary *)preeditHighlightedAttrs + pagingAttrs:(NSDictionary *)pagingAttrs + pagingHighlightedAttrs:(NSDictionary *)pagingHighlightedAttrs + statusAttrs:(NSDictionary *)statusAttrs { _attrs = attrs; _highlightedAttrs = highlightedAttrs; _labelAttrs = labelAttrs; @@ -189,12 +403,128 @@ - (void) setAttrs:(NSMutableDictionary *)attrs _commentHighlightedAttrs = commentHighlightedAttrs; _preeditAttrs = preeditAttrs; _preeditHighlightedAttrs = preeditHighlightedAttrs; + _pagingAttrs = pagingAttrs; + _pagingHighlightedAttrs = pagingHighlightedAttrs; + _statusAttrs = statusAttrs; + NSMutableDictionary *symbolAttrs = [pagingAttrs mutableCopy]; + if (@available(macOS 12.0, *)) { + NSTextAttachment *attmBackFill = [[NSTextAttachment alloc] init]; + attmBackFill.image = [NSImage imageWithSystemSymbolName:@"arrowtriangle.backward.circle.fill" accessibilityDescription:nil]; + NSMutableDictionary *attrsBackFill = [symbolAttrs mutableCopy]; + attrsBackFill[NSAttachmentAttributeName] = attmBackFill; + _symbolBackFill = [[NSAttributedString alloc] initWithString:@"\uFFFC" attributes:attrsBackFill]; + + NSTextAttachment *attmBackStroke = [[NSTextAttachment alloc] init]; + attmBackStroke.image = [NSImage imageWithSystemSymbolName:@"arrowtriangle.backward.circle" accessibilityDescription:nil]; + NSMutableDictionary *attrsBackStroke = [symbolAttrs mutableCopy]; + attrsBackStroke[NSAttachmentAttributeName] = attmBackStroke; + _symbolBackStroke = [[NSAttributedString alloc] initWithString:@"\uFFFC" attributes:attrsBackStroke]; + + NSTextAttachment *attmForwardFill = [[NSTextAttachment alloc] init]; + attmForwardFill.image = [NSImage imageWithSystemSymbolName:@"arrowtriangle.forward.circle.fill" accessibilityDescription:nil]; + NSMutableDictionary *attrsForwardFill = [symbolAttrs mutableCopy]; + attrsForwardFill[NSAttachmentAttributeName] = attmForwardFill; + _symbolForwardFill = [[NSAttributedString alloc] initWithString:@"\uFFFC" attributes:attrsForwardFill]; + + NSTextAttachment *attmForwardStroke = [[NSTextAttachment alloc] init]; + attmForwardStroke.image = [NSImage imageWithSystemSymbolName:@"arrowtriangle.forward.circle" accessibilityDescription:nil]; + NSMutableDictionary *attrsForwardStroke = [symbolAttrs mutableCopy]; + attrsForwardStroke[NSAttachmentAttributeName] = attmForwardStroke; + _symbolForwardStroke = [[NSAttributedString alloc] initWithString:@"\uFFFC" attributes:attrsForwardStroke]; + } else { + NSFont *symbolFont = [NSFont fontWithDescriptor:[[NSFontDescriptor fontDescriptorWithName:@"AppleSymbols" size:0.0] + fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitUIOptimized] + size:[labelAttrs[NSFontAttributeName] pointSize]]; + symbolAttrs[NSFontAttributeName] = symbolFont; + if (_vertical || !_linear) { + symbolAttrs[NSBaselineOffsetAttributeName] = @([pagingAttrs[NSBaselineOffsetAttributeName] doubleValue] - symbolFont.leading); + } + + NSMutableDictionary *symbolAttrsBackFill = [symbolAttrs mutableCopy]; + NSMutableDictionary *symbolAttrsBackStroke = [symbolAttrs mutableCopy]; + NSMutableDictionary *symbolAttrsForwardFill = [symbolAttrs mutableCopy]; + NSMutableDictionary *symbolAttrsForwardStroke = [symbolAttrs mutableCopy]; + if (@available(macOS 10.13, *)) { + symbolAttrsBackFill[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithCGGlyph:0xE92 forFont:symbolFont baseString:@"◀"]; + symbolAttrsBackStroke[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithCGGlyph:0xE95 forFont:symbolFont baseString:@"◁"]; + symbolAttrsForwardFill[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithCGGlyph:0xE93 forFont:symbolFont baseString:@"▶"]; + symbolAttrsForwardStroke[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithCGGlyph:0xE94 forFont:symbolFont baseString:@"▷"]; + } else { + symbolAttrsBackFill[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithGlyphName:@"gid4966" forFont:symbolFont baseString:@"◀"]; + symbolAttrsBackStroke[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithGlyphName:@"gid4969" forFont:symbolFont baseString:@"◁"]; + symbolAttrsForwardFill[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithGlyphName:@"gid4967" forFont:symbolFont baseString:@"▶"]; + symbolAttrsForwardStroke[NSGlyphInfoAttributeName] = [NSGlyphInfo glyphInfoWithGlyphName:@"gid4968" forFont:symbolFont baseString:@"▷"]; + } + _symbolBackFill = [[NSAttributedString alloc] initWithString:@"◀" attributes:symbolAttrsBackFill]; + _symbolBackStroke = [[NSAttributedString alloc] initWithString:@"◁" attributes:symbolAttrsBackStroke]; + _symbolForwardFill = [[NSAttributedString alloc] initWithString:@"▶" attributes:symbolAttrsForwardFill]; + _symbolForwardStroke = [[NSAttributedString alloc] initWithString:@"▷" attributes:symbolAttrsForwardStroke]; + } } -- (void) setParagraphStyle:(NSParagraphStyle *)paragraphStyle - preeditParagraphStyle:(NSParagraphStyle *)preeditParagraphStyle { +- (void)setParagraphStyle:(NSParagraphStyle *)paragraphStyle + preeditParagraphStyle:(NSParagraphStyle *)preeditParagraphStyle + pagingParagraphStyle:(NSParagraphStyle *)pagingParagraphStyle + statusParagraphStyle:(NSParagraphStyle *)statusParagraphStyle { _paragraphStyle = paragraphStyle; _preeditParagraphStyle = preeditParagraphStyle; + _pagingParagraphStyle = pagingParagraphStyle; + _statusParagraphStyle = statusParagraphStyle; +} + +- (void)setLabels:(NSArray *)labels { + _labels = labels; +} + +- (void)setCandidateFormat:(NSString *)candidateFormat { + // validate candidate format: must have enumerator '%c' before candidate '%@' + if (![candidateFormat containsString:@"%@"]) { + candidateFormat = [candidateFormat stringByAppendingString:@"%@"]; + } + if (![candidateFormat containsString:@"%c"]) { + candidateFormat = [@"%c" stringByAppendingString:candidateFormat]; + } + NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; + NSRange labelRange = [candidateFormat rangeOfString:@"%c"]; + if (labelRange.location > candidateRange.location) { + candidateFormat = kDefaultCandidateFormat; + candidateRange = [candidateFormat rangeOfString:@"%@"]; + } + labelRange = NSMakeRange(0, candidateRange.location); + NSRange commentRange = NSMakeRange(NSMaxRange(candidateRange), candidateFormat.length - NSMaxRange(candidateRange)); + // parse markdown formats + NSMutableAttributedString *format = [[NSMutableAttributedString alloc] initWithString:candidateFormat]; + NSMutableAttributedString *highlightedFormat = [format mutableCopy]; + [format addAttributes:_labelAttrs range:labelRange]; + [highlightedFormat addAttributes:_labelHighlightedAttrs range:labelRange]; + [format addAttributes:_attrs range:candidateRange]; + [highlightedFormat addAttributes:_highlightedAttrs range:candidateRange]; + if (commentRange.length > 0) { + [format addAttributes:_commentAttrs range:commentRange]; + [highlightedFormat addAttributes:_commentHighlightedAttrs range:commentRange]; + } + [format formatMarkDown]; + [highlightedFormat formatMarkDown]; + // add placeholder for comment '%s' + candidateRange = [format.string rangeOfString:@"%@"]; + commentRange = NSMakeRange(NSMaxRange(candidateRange), format.length - NSMaxRange(candidateRange)); + if (commentRange.length > 0) { + [format replaceCharactersInRange:commentRange withString:[kTipSpecifier stringByAppendingString:[format.string substringWithRange:commentRange]]]; + [highlightedFormat replaceCharactersInRange:commentRange withString:[kTipSpecifier stringByAppendingString:[highlightedFormat.string substringWithRange:commentRange]]]; + } else { + [format appendAttributedString:[[NSAttributedString alloc] initWithString:kTipSpecifier attributes:_commentAttrs]]; + [highlightedFormat appendAttributedString:[[NSAttributedString alloc] initWithString:kTipSpecifier attributes:_commentHighlightedAttrs]]; + } + _candidateFormats = formatLabels(format, _labels); + _candidateHighlightedFormats = formatLabels(highlightedFormat, _labels); +} + +- (void)setStatusMessageType:(NSString *)type { + if ([type isEqualToString:@"long"] || [type isEqualToString:@"short"] || [type isEqualToString:@"mix"]) { + _statusMessageType = type; + } else { + _statusMessageType = @"mix"; + } } @end @@ -202,22 +532,31 @@ - (void) setParagraphStyle:(NSParagraphStyle *)paragraphStyle @interface SquirrelView : NSView @property(nonatomic, readonly) NSTextView *textView; +@property(nonatomic, readonly) NSTextStorage *textStorage; +@property(nonatomic, readonly) NSEdgeInsets insets; @property(nonatomic, readonly) NSArray *candidateRanges; -@property(nonatomic, readonly) NSInteger hilightedIndex; +@property(nonatomic, readonly) NSUInteger highlightedIndex; @property(nonatomic, readonly) NSRange preeditRange; @property(nonatomic, readonly) NSRange highlightedPreeditRange; +@property(nonatomic, readonly) NSRange pagingRange; @property(nonatomic, readonly) NSRect contentRect; -@property(nonatomic, readonly) BOOL isDark; -@property(nonatomic, strong, readonly) SquirrelTheme *currentTheme; -@property(nonatomic, readonly) NSTextLayoutManager *layoutManager; -@property(nonatomic, assign) CGFloat seperatorWidth; +@property(nonatomic, readonly) NSMutableArray *candidatePaths; +@property(nonatomic, readonly) NSMutableArray *pagingPaths; +@property(nonatomic, readonly) NSUInteger pagingButton; @property(nonatomic, readonly) CAShapeLayer *shape; +@property(nonatomic, readonly, strong) SquirrelTheme *currentTheme; +@property(nonatomic, readonly) BOOL isDark; + +- (void)drawViewWithInsets:(NSEdgeInsets)insets + candidateRanges:(NSArray *)candidateRanges + highlightedIndex:(NSUInteger)highlightedIndex + preeditRange:(NSRange)preeditRange + highlightedPreeditRange:(NSRange)highlightedPreeditRange + pagingRange:(NSRange)pagingRange + pagingButton:(NSUInteger)pagingButton; + +- (NSRect)contentRectForRange:(NSRange)range; -- (void) drawViewWith:(NSArray *)candidateRanges - hilightedIndex:(NSInteger)hilightedIndex - preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange; -- (NSRect)contentRectForRange:(NSTextRange *)range; @end @implementation SquirrelView @@ -230,13 +569,23 @@ - (BOOL)isFlipped { return YES; } +- (BOOL)wantsUpdateLayer { + return YES; +} + - (BOOL)isDark { - if ([NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]] == NSAppearanceNameDarkAqua) { - return YES; + if (@available(macOS 10.14, *)) { + if ([NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]] == NSAppearanceNameDarkAqua) { + return YES; + } } return NO; } +- (BOOL)allowsVibrancy { + return YES; +} + - (SquirrelTheme *)selectTheme:(BOOL)isDark { return isDark ? _darkTheme : _defaultTheme; } @@ -245,235 +594,292 @@ - (SquirrelTheme *)currentTheme { return [self selectTheme:self.isDark]; } -- (NSTextLayoutManager *)layoutManager { - return _textView.textLayoutManager; -} - - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { self.wantsLayer = YES; + self.layer.geometryFlipped = YES; self.layer.masksToBounds = YES; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; + } + + if (@available(macOS 12.0, *)) { + NSTextLayoutManager *textLayoutManager = [[NSTextLayoutManager alloc] init]; + textLayoutManager.usesFontLeading = NO; + NSTextContainer *textContainer = [[NSTextContainer alloc] + initWithSize:NSMakeSize(NSViewWidthSizable, CGFLOAT_MAX)]; + textContainer.lineFragmentPadding = 0; + textLayoutManager.textContainer = textContainer; + NSTextContentStorage *contentStorage = [[NSTextContentStorage alloc] init]; + [contentStorage addTextLayoutManager:textLayoutManager]; + _textView = [[NSTextView alloc] initWithFrame:frameRect + textContainer:textLayoutManager.textContainer]; + _textStorage = _textView.textContentStorage.textStorage; + } else { + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + layoutManager.backgroundLayoutEnabled = YES; + layoutManager.usesFontLeading = NO; + layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; + NSTextContainer *textContainer = [[NSTextContainer alloc] + initWithContainerSize:NSMakeSize(NSViewWidthSizable, CGFLOAT_MAX)]; + textContainer.lineFragmentPadding = 0; + [layoutManager addTextContainer:textContainer]; + _textStorage = [[NSTextStorage alloc] init]; + [_textStorage addLayoutManager:layoutManager]; + _textView = [[NSTextView alloc] initWithFrame:frameRect + textContainer:textContainer]; } - _textView = [[NSTextView alloc] initWithFrame:frameRect]; _textView.drawsBackground = NO; _textView.editable = NO; _textView.selectable = NO; - self.layoutManager.textContainer.lineFragmentPadding = 0.0; - _defaultTheme = [[SquirrelTheme alloc] init]; - _darkTheme = [[SquirrelTheme alloc] init]; + _textView.wantsLayer = NO; + _shape = [[CAShapeLayer alloc] init]; + _defaultTheme = [[SquirrelTheme alloc] init]; + if (@available(macOS 10.14, *)) { + _darkTheme = [[SquirrelTheme alloc] init]; + } return self; } -- (NSTextRange *)convertRange:(NSRange)range { +- (NSTextRange *)getTextRangeFromRange:(NSRange)range API_AVAILABLE(macos(12.0)) { if (range.location == NSNotFound) { return nil; } else { - id startLocation = [self.layoutManager locationFromLocation:[self.layoutManager documentRange].location withOffset:range.location]; - id endLocation = [self.layoutManager locationFromLocation:startLocation withOffset:range.length]; + NSTextContentStorage *contentStorage = _textView.textContentStorage; + id startLocation = [contentStorage locationFromLocation:contentStorage.documentRange.location withOffset:range.location]; + id endLocation = [contentStorage locationFromLocation:startLocation withOffset:range.length]; return [[NSTextRange alloc] initWithLocation:startLocation endLocation:endLocation]; } } // Get the rectangle containing entire contents, expensive to calculate - (NSRect)contentRect { - NSMutableArray *ranges = [_candidateRanges mutableCopy]; - if (_preeditRange.length > 0) { - [ranges addObject:[NSValue valueWithRange:_preeditRange]]; - } - CGFloat x0 = CGFLOAT_MAX; - CGFloat x1 = CGFLOAT_MIN; - CGFloat y0 = CGFLOAT_MAX; - CGFloat y1 = CGFLOAT_MIN; - for (NSUInteger i = 0; i < ranges.count; i += 1) { - NSRange range = [ranges[i] rangeValue]; - NSRect rect = [self contentRectForRange:[self convertRange: range]]; - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - } - return NSMakeRect(x0, y0, x1-x0, y1-y0); -} - -// Get the rectangle containing the range of text, will first convert to glyph range, expensive to calculate -- (NSRect)contentRectForRange:(NSTextRange *)range { - __block CGFloat x0 = CGFLOAT_MAX; - __block CGFloat x1 = CGFLOAT_MIN; - __block CGFloat y0 = CGFLOAT_MAX; - __block CGFloat y1 = CGFLOAT_MIN; - [self.layoutManager enumerateTextSegmentsInRange:range type:NSTextLayoutManagerSegmentTypeStandard options:NSTextLayoutManagerSegmentOptionsRangeNotRequired usingBlock:^(NSTextRange *_, CGRect rect, CGFloat baseline, NSTextContainer *tectContainer) { - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - return YES; - }]; - return NSMakeRect(x0, y0, x1-x0, y1-y0); + if (@available(macOS 12.0, *)) { + [_textView.textLayoutManager ensureLayoutForRange:_textView.textContentStorage.documentRange]; + return [_textView.textLayoutManager usageBoundsForTextContainer]; + } else { + [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer]; + return [_textView.layoutManager usedRectForTextContainer:_textView.textContainer]; + } } -// Will triger - (void)drawRect:(NSRect)dirtyRect -- (void) drawViewWith:(NSArray *)candidateRanges - hilightedIndex:(NSInteger)hilightedIndex - preeditRange:(NSRange)preeditRange - highlightedPreeditRange:(NSRange)highlightedPreeditRange { +// Get the rectangle containing the range of text, will first convert to glyph or text range, expensive to calculate +- (NSRect)contentRectForRange:(NSRange)range { + if (@available(macOS 12.0, *)) { + NSTextRange *textRange = [self getTextRangeFromRange:range]; + __block NSRect contentRect = NSZeroRect; + [_textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options:NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^(NSTextRange *segRange, CGRect segFrame, CGFloat baseline, NSTextContainer *textContainer) { + contentRect = NSUnionRect(contentRect, segFrame); + return YES; + }]; + CGFloat lineSpacing = [[_textStorage attribute:NSParagraphStyleAttributeName + atIndex:NSMaxRange(range) - 1 effectiveRange:NULL] lineSpacing]; + contentRect.size.height += lineSpacing; + return contentRect; + } else { + NSTextContainer *textContainer = _textView.textContainer; + NSLayoutManager *layoutManager = _textView.layoutManager; + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:range + actualCharacterRange:NULL]; + NSRange firstLineRange = NSMakeRange(NSNotFound, 0); + NSRect firstLineRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&firstLineRange]; + if (NSMaxRange(glyphRange) <= NSMaxRange(firstLineRange)) { + CGFloat startX = [layoutManager locationForGlyphAtIndex:glyphRange.location].x; + CGFloat endX = NSMaxRange(glyphRange) < NSMaxRange(firstLineRange) + ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x : NSWidth(firstLineRect); + return NSMakeRect(NSMinX(firstLineRect) + startX, NSMinY(firstLineRect), + endX - startX, NSHeight(firstLineRect)); + } else { + NSRect finalLineRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:NULL]; + return NSMakeRect(NSMinX(firstLineRect), NSMinY(firstLineRect), + textContainer.size.width, NSMaxY(finalLineRect) - NSMinY(firstLineRect)); + } + } +} + +// Will triger - (void)updateLayer +- (void)drawViewWithInsets:(NSEdgeInsets)insets + candidateRanges:(NSArray *)candidateRanges + highlightedIndex:(NSUInteger)highlightedIndex + preeditRange:(NSRange)preeditRange + highlightedPreeditRange:(NSRange)highlightedPreeditRange + pagingRange:(NSRange)pagingRange + pagingButton:(NSUInteger)pagingButton { + _insets = insets; _candidateRanges = candidateRanges; - _hilightedIndex = hilightedIndex; + _highlightedIndex = highlightedIndex; _preeditRange = preeditRange; _highlightedPreeditRange = highlightedPreeditRange; + _pagingRange = pagingRange; + _pagingButton = pagingButton; + _candidatePaths = [[NSMutableArray alloc] initWithCapacity:candidateRanges.count]; + _pagingPaths = [[NSMutableArray alloc] initWithCapacity:pagingRange.length > 0 ? 2 : 0]; self.needsDisplay = YES; } -// A tweaked sign function, to winddown corner radius when the size is small -double sign(double number) { - if (number >= 2) { - return 1; - } else if (number <= -2) { - return -1; - } else { - return number / 2; - } -} - // Bezier cubic curve, which has continuous roundness -CGMutablePathRef drawSmoothLines(NSArray *vertex, NSSet * __nullable straightCorner, CGFloat alpha, CGFloat beta) { - beta = MAX(0.00001, beta); - CGMutablePathRef path = CGPathCreateMutable(); - if (vertex.count < 1) +static NSBezierPath * drawRoundedPolygon(NSArray *vertex, CGFloat radius) { + NSBezierPath *path = [NSBezierPath bezierPath]; + if (vertex.count < 1) { return path; - NSPoint previousPoint = [vertex[vertex.count-1] pointValue]; - NSPoint point = [vertex[0] pointValue]; + } + NSPoint previousPoint = vertex.lastObject.pointValue; + NSPoint point = vertex.firstObject.pointValue; NSPoint nextPoint; - NSPoint control1; - NSPoint control2; - NSPoint target = previousPoint; - NSPoint diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y); - if (!straightCorner || ![straightCorner containsObject:[NSNumber numberWithUnsignedInteger:vertex.count - 1]]) { - target.x += sign(diff.x/beta)*beta; - target.y += sign(diff.y/beta)*beta; - } - CGPathMoveToPoint(path, NULL, target.x, target.y); - for (NSUInteger i = 0; i < vertex.count; i += 1) { - previousPoint = [vertex[(vertex.count+i-1)%vertex.count] pointValue]; - point = [vertex[i] pointValue]; - nextPoint = [vertex[(i+1)%vertex.count] pointValue]; - target = point; - if (straightCorner && [straightCorner containsObject:[NSNumber numberWithUnsignedInteger:i]]) { - CGPathAddLineToPoint(path, NULL, target.x, target.y); + NSPoint startPoint; + NSPoint endPoint = previousPoint; + CGFloat arcRadius; + CGVector diff = CGVectorMake(point.x - previousPoint.x, point.y - previousPoint.y); + if (ABS(diff.dx) >= ABS(diff.dy)) { + endPoint.x += diff.dx / 2; + endPoint.y = point.y; + } else { + endPoint.y += diff.dy / 2; + endPoint.x = point.x; + } + [path moveToPoint:endPoint]; + for (NSUInteger i = 0; i < vertex.count; ++i) { + startPoint = endPoint; + point = vertex[i].pointValue; + nextPoint = vertex[(i + 1) % vertex.count].pointValue; + arcRadius = MIN(radius, MAX(ABS(point.x - startPoint.x), ABS(point.y - startPoint.y))); + endPoint = point; + diff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); + if (ABS(diff.dx) > ABS(diff.dy)) { + endPoint.x += diff.dx / 2; + arcRadius = MIN(arcRadius, ABS(diff.dx) / 2); + endPoint.y = nextPoint.y; + point.y = nextPoint.y; } else { - control1 = point; - diff = NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y); - target.x -= sign(diff.x/beta)*beta; - control1.x -= sign(diff.x/beta)*alpha; - target.y -= sign(diff.y/beta)*beta; - control1.y -= sign(diff.y/beta)*alpha; - - CGPathAddLineToPoint(path, NULL, target.x, target.y); - target = point; - control2 = point; - diff = NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y); - control2.x += sign(diff.x/beta)*alpha; - target.x += sign(diff.x/beta)*beta; - control2.y += sign(diff.y/beta)*alpha; - target.y += sign(diff.y/beta)*beta; - - CGPathAddCurveToPoint(path, NULL, control1.x, control1.y, control2.x, control2.y, target.x, target.y); - } - } - CGPathCloseSubpath(path); + endPoint.y += diff.dy / 2; + arcRadius = MIN(arcRadius, ABS(diff.dy) / 2); + endPoint.x = nextPoint.x; + point.x = nextPoint.x; + } + [path appendBezierPathWithArcFromPoint:point toPoint:endPoint radius:arcRadius]; + } + [path closePath]; return path; } -NSArray *rectVertex(NSRect rect) { - return @[ - @(rect.origin), - @(NSMakePoint(rect.origin.x, rect.origin.y+rect.size.height)), - @(NSMakePoint(rect.origin.x+rect.size.width, rect.origin.y+rect.size.height)), - @(NSMakePoint(rect.origin.x+rect.size.width, rect.origin.y)) - ]; +static NSArray * rectVertex(NSRect rect) { + return @[@(rect.origin), + @(NSMakePoint(rect.origin.x, rect.origin.y + rect.size.height)), + @(NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height)), + @(NSMakePoint(rect.origin.x + rect.size.width, rect.origin.y))]; } -BOOL nearEmptyRect(NSRect rect) { - return rect.size.height * rect.size.width < 1; +static inline BOOL nearEmptyRect(NSRect rect) { + return NSHeight(rect) * NSWidth(rect) < 1; } // Calculate 3 boxes containing the text in range. leadingRect and trailingRect are incomplete line rectangle -// bodyRect is complete lines in the middle -- (void)multilineRectForRange:(NSTextRange *)range leadingRect:(NSRect *)leadingRect bodyRect:(NSRect *)bodyRect trailingRect:(NSRect *)trailingRect extraSurounding:(CGFloat)extraSurounding bounds:(NSRect)bounds { - NSSize edgeInset = self.currentTheme.edgeInset; - NSMutableArray *lineRects = [[NSMutableArray alloc] init]; - [self.layoutManager enumerateTextSegmentsInRange:range type:NSTextLayoutManagerSegmentTypeStandard options:NSTextLayoutManagerSegmentOptionsRangeNotRequired usingBlock:^(NSTextRange *_, CGRect rect, CGFloat baseline, NSTextContainer *tectContainer) { - if (!nearEmptyRect(rect)) { - NSRect newRect = rect; - newRect.origin.x += edgeInset.width; - newRect.origin.y += edgeInset.height; - newRect.size.height += self.currentTheme.linespace; - newRect.origin.y -= self.currentTheme.linespace / 2; - [lineRects addObject:[NSValue valueWithRect:newRect]]; - } - return YES; - }]; - - *leadingRect = NSZeroRect; - *bodyRect = NSZeroRect; - *trailingRect = NSZeroRect; - - if (lineRects.count == 1) { - *bodyRect = [lineRects[0] rectValue]; - } else if (lineRects.count == 2) { - *leadingRect = [lineRects[0] rectValue]; - *trailingRect = [lineRects[1] rectValue]; - } else if (lineRects.count > 2) { - *leadingRect = [lineRects[0] rectValue]; - *trailingRect = [lineRects[lineRects.count-1] rectValue]; - CGFloat x0 = CGFLOAT_MAX; - CGFloat x1 = CGFLOAT_MIN; - CGFloat y0 = CGFLOAT_MAX; - CGFloat y1 = CGFLOAT_MIN; - for (NSUInteger i = 1; i < lineRects.count-1; i += 1) { - NSRect rect = [lineRects[i] rectValue]; - x0 = MIN(NSMinX(rect), x0); - x1 = MAX(NSMaxX(rect), x1); - y0 = MIN(NSMinY(rect), y0); - y1 = MAX(NSMaxY(rect), y1); - } - y0 = MIN(NSMaxY(*leadingRect), y0); - y1 = MAX(NSMinY(*trailingRect), y1); - *bodyRect = NSMakeRect(x0, y0, x1-x0, y1-y0); - } - - if (extraSurounding > 0) { - if (nearEmptyRect(*leadingRect) && nearEmptyRect(*trailingRect)) { - expandHighlightWidth(bodyRect, extraSurounding); - } else { - if (!(nearEmptyRect(*leadingRect))) { - expandHighlightWidth(leadingRect, extraSurounding); +// bodyRect is the complete line fragment in the middle if the range spans no less than one full line +- (void)multilineRectForRange:(NSRange)charRange leadingRect:(NSRectPointer)leadingRect bodyRect:(NSRectPointer)bodyRect trailingRect:(NSRectPointer)trailingRect { + if (@available(macOS 12.0, *)) { + NSTextRange *textRange = [self getTextRangeFromRange:charRange]; + CGFloat lineSpacing = [[_textStorage attribute:NSParagraphStyleAttributeName atIndex:charRange.location effectiveRange:NULL] lineSpacing]; + NSMutableArray *lineRects = [[NSMutableArray alloc] initWithCapacity:2]; + NSMutableArray *lineRanges = [[NSMutableArray alloc] initWithCapacity:2]; + [_textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options:NSTextLayoutManagerSegmentOptionsMiddleFragmentsExcluded + usingBlock:^(NSTextRange *segRange, CGRect segFrame, CGFloat baseline, NSTextContainer *textContainer) { + if (!nearEmptyRect(segFrame)) { + segFrame.size.height += lineSpacing; + [lineRects addObject:[NSValue valueWithRect:segFrame]]; + [lineRanges addObject:segRange]; } - if (!(nearEmptyRect(*trailingRect))) { - expandHighlightWidth(trailingRect, extraSurounding); + return YES; + }]; + if (lineRects.count == 1) { + *bodyRect = lineRects[0].rectValue; + } else { + CGFloat containerWidth = _textView.textContainer.size.width; + NSRect leadingLineRect = lineRects.firstObject.rectValue; + leadingLineRect.size.width = containerWidth - NSMinX(leadingLineRect); + NSRect trailingLineRect = lineRects.lastObject.rectValue; + if (NSMaxX(trailingLineRect) == NSMaxX(leadingLineRect)) { + if (NSMinX(leadingLineRect) == NSMinX(trailingLineRect)) { + *bodyRect = NSUnionRect(leadingLineRect, trailingLineRect); + } else { + *leadingRect = leadingLineRect; + *bodyRect = NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } else { + *trailingRect = trailingLineRect; + if (NSMinX(leadingLineRect) == NSMinX(trailingLineRect)) { + *bodyRect = NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + *leadingRect = leadingLineRect; + if (lineRanges.lastObject.location > lineRanges.firstObject.endLocation) { + *bodyRect = NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } } } - } - - if (!nearEmptyRect(*leadingRect) && !nearEmptyRect(*trailingRect)) { - leadingRect->size.width = NSMaxX(bounds) - leadingRect->origin.x; - trailingRect->size.width = NSMaxX(*trailingRect) - NSMinX(bounds); - trailingRect->origin.x = NSMinX(bounds); - if (!nearEmptyRect(*bodyRect)) { - bodyRect->size.width = bounds.size.width; - bodyRect->origin.x = bounds.origin.x; + } else { + NSLayoutManager *layoutManager = _textView.layoutManager; + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange + actualCharacterRange:NULL]; + NSRange leadingLineRange = NSMakeRange(NSNotFound, 0); + NSRect leadingLineRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphRange.location + effectiveRange:&leadingLineRange]; + CGFloat startX = [layoutManager locationForGlyphAtIndex:glyphRange.location].x; + if (NSMaxRange(leadingLineRange) >= NSMaxRange(glyphRange)) { + CGFloat endX = NSMaxRange(glyphRange) < NSMaxRange(leadingLineRange) + ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x : NSWidth(leadingLineRect); + *bodyRect = NSMakeRect(startX, NSMinY(leadingLineRect), + endX - startX, NSHeight(leadingLineRect)); } else { - CGFloat diff = NSMinY(*trailingRect) - NSMaxY(*leadingRect); - leadingRect->size.height += diff / 2; - trailingRect->size.height += diff / 2; - trailingRect->origin.y -= diff / 2; + CGFloat containerWidth = _textView.textContainer.size.width; + NSRange trailingLineRange = NSMakeRange(NSNotFound, 0); + NSRect trailingLineRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:NSMaxRange(glyphRange) - 1 + effectiveRange:&trailingLineRange]; + CGFloat endX = NSMaxRange(glyphRange) < NSMaxRange(trailingLineRange) + ? [layoutManager locationForGlyphAtIndex:NSMaxRange(glyphRange)].x : NSWidth(trailingLineRect); + if (NSMaxRange(trailingLineRange) == NSMaxRange(glyphRange)) { + if (glyphRange.location == leadingLineRange.location) { + *bodyRect = NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + *leadingRect = NSMakeRect(startX, NSMinY(leadingLineRect), + containerWidth - startX, NSHeight(leadingLineRect)); + *bodyRect = NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMaxY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } else { + *trailingRect = NSMakeRect(0.0, NSMinY(trailingLineRect), endX, + NSHeight(trailingLineRect)); + if (glyphRange.location == leadingLineRange.location) { + *bodyRect = NSMakeRect(0.0, NSMinY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMinY(leadingLineRect)); + } else { + *leadingRect = NSMakeRect(startX, NSMinY(leadingLineRect), + containerWidth - startX, NSHeight(leadingLineRect)); + if (trailingLineRange.location > NSMaxRange(leadingLineRange)) { + *bodyRect = NSMakeRect(0.0, NSMaxY(leadingLineRect), containerWidth, + NSMinY(trailingLineRect) - NSMaxY(leadingLineRect)); + } + } + } } } } // Based on the 3 boxes from multilineRectForRange, calculate the vertex of the polygon containing the text in range -NSArray * multilineRectVertex(NSRect leadingRect, NSRect bodyRect, NSRect trailingRect) { +static NSArray * multilineRectVertex(NSRect leadingRect, NSRect bodyRect, NSRect trailingRect) { if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) && nearEmptyRect(trailingRect)) { return rectVertex(leadingRect); } else if (nearEmptyRect(bodyRect) && nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect)) { @@ -481,418 +887,434 @@ - (void)multilineRectForRange:(NSTextRange *)range leadingRect:(NSRect *)leading } else if (nearEmptyRect(leadingRect) && nearEmptyRect(trailingRect) && !nearEmptyRect(bodyRect)) { return rectVertex(bodyRect); } else if (nearEmptyRect(trailingRect) && !nearEmptyRect(bodyRect)) { - NSArray * leadingVertex = rectVertex(leadingRect); - NSArray * bodyVertex = rectVertex(bodyRect); - return @[bodyVertex[0], bodyVertex[1], bodyVertex[2], leadingVertex[3], leadingVertex[0], leadingVertex[1]]; + NSArray *leadingVertex = rectVertex(leadingRect); + NSArray *bodyVertex = rectVertex(bodyRect); + return @[leadingVertex[0], leadingVertex[1], bodyVertex[0], bodyVertex[1], bodyVertex[2], leadingVertex[3]]; } else if (nearEmptyRect(leadingRect) && !nearEmptyRect(bodyRect)) { - NSArray * trailingVertex = rectVertex(trailingRect); - NSArray * bodyVertex = rectVertex(bodyRect); - return @[trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], bodyVertex[3], bodyVertex[0]]; - } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && nearEmptyRect(bodyRect) && NSMaxX(leadingRect)>NSMinX(trailingRect)) { - NSArray * leadingVertex = rectVertex(leadingRect); - NSArray * trailingVertex = rectVertex(trailingRect); - return @[trailingVertex[0], trailingVertex[1], trailingVertex[2], trailingVertex[3], leadingVertex[2], leadingVertex[3], leadingVertex[0], leadingVertex[1]]; + NSArray *trailingVertex = rectVertex(trailingRect); + NSArray *bodyVertex = rectVertex(bodyRect); + return @[bodyVertex[0], trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], bodyVertex[3]]; + } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && + nearEmptyRect(bodyRect) && NSMaxX(leadingRect) > NSMinX(trailingRect)) { + NSArray *leadingVertex = rectVertex(leadingRect); + NSArray *trailingVertex = rectVertex(trailingRect); + return @[leadingVertex[0], leadingVertex[1], trailingVertex[0], trailingVertex[1], + trailingVertex[2], trailingVertex[3], leadingVertex[2], leadingVertex[3]]; } else if (!nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && !nearEmptyRect(bodyRect)) { - NSArray * leadingVertex = rectVertex(leadingRect); - NSArray * bodyVertex = rectVertex(bodyRect); - NSArray * trailingVertex = rectVertex(trailingRect); - return @[trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], leadingVertex[3], leadingVertex[0], leadingVertex[1], bodyVertex[0]]; + NSArray *leadingVertex = rectVertex(leadingRect); + NSArray *bodyVertex = rectVertex(bodyRect); + NSArray *trailingVertex = rectVertex(trailingRect); + return @[leadingVertex[0], leadingVertex[1], bodyVertex[0], trailingVertex[1], + trailingVertex[2], trailingVertex[3], bodyVertex[2], leadingVertex[3]]; } else { return @[]; } } -// If the point is outside the innerBox, will extend to reach the outerBox -void expand(NSMutableArray *vertex, NSRect innerBorder, NSRect outerBorder) { - for (NSUInteger i = 0; i < vertex.count; i += 1){ - NSPoint point = [vertex[i] pointValue]; - if (point.x < innerBorder.origin.x) { - point.x = outerBorder.origin.x; - } else if (point.x > innerBorder.origin.x+innerBorder.size.width) { - point.x = outerBorder.origin.x+outerBorder.size.width; - } - if (point.y < innerBorder.origin.y) { - point.y = outerBorder.origin.y; - } else if (point.y > innerBorder.origin.y+innerBorder.size.height) { - point.y = outerBorder.origin.y+outerBorder.size.height; - } - [vertex replaceObjectAtIndex:i withObject:@(point)]; +static inline NSColor * hooverColor(NSColor *color, BOOL darkTheme) { + if (@available(macOS 10.14, *)) { + return [color colorWithSystemEffect:NSColorSystemEffectRollover]; } -} - -CGPoint direction(CGPoint diff) { - if (diff.y == 0 && diff.x > 0) { - return NSMakePoint(0, 1); - } else if (diff.y == 0 && diff.x < 0) { - return NSMakePoint(0, -1); - } else if (diff.x == 0 && diff.y > 0) { - return NSMakePoint(-1, 0); - } else if (diff.x == 0 && diff.y < 0) { - return NSMakePoint(1, 0); + if (darkTheme) { + return [color highlightWithLevel:0.3]; } else { - return NSMakePoint(0, 0); - } -} - -CAShapeLayer *shapeFromPath(CGPathRef path) { - CAShapeLayer *layer = [CAShapeLayer layer]; - layer.path = path; - layer.fillRule = kCAFillRuleEvenOdd; - return layer; -} - -// Assumes clockwise iteration -void enlarge(NSMutableArray *vertex, CGFloat by) { - if (by != 0) { - NSPoint previousPoint; - NSPoint point; - NSPoint nextPoint; - NSArray *original = [[NSArray alloc] initWithArray:vertex]; - NSPoint newPoint; - NSPoint displacement; - for (NSUInteger i = 0; i < original.count; i += 1){ - previousPoint = [original[(original.count+i-1)%original.count] pointValue]; - point = [original[i] pointValue]; - nextPoint = [original[(i+1)%original.count] pointValue]; - newPoint = point; - displacement = direction(NSMakePoint(point.x - previousPoint.x, point.y - previousPoint.y)); - newPoint.x += by * displacement.x; - newPoint.y += by * displacement.y; - displacement = direction(NSMakePoint(nextPoint.x - point.x, nextPoint.y - point.y)); - newPoint.x += by * displacement.x; - newPoint.y += by * displacement.y; - [vertex replaceObjectAtIndex:i withObject:@(newPoint)]; - } - } -} - -// Add gap between horizontal candidates -void expandHighlightWidth(NSRect *rect, CGFloat extraSurrounding) { - if (!nearEmptyRect(*rect)) { - rect->size.width += extraSurrounding; - rect->origin.x -= extraSurrounding / 2; - } -} - -void removeCorner(NSMutableArray *highlightedPoints, NSMutableSet *rightCorners, NSRect containingRect) { - if (highlightedPoints && rightCorners) { - NSSet *originalRightCorners = [[NSSet alloc] initWithSet:rightCorners]; - for (NSNumber *cornerIndex in originalRightCorners) { - NSUInteger index = cornerIndex.unsignedIntegerValue; - NSPoint corner = [highlightedPoints[index] pointValue]; - CGFloat dist = MIN(NSMaxY(containingRect) - corner.y, corner.y - NSMinY(containingRect)); - if (dist < 1e-2) { - [rightCorners removeObject:cornerIndex]; - } - } + return [color shadowWithLevel:0.3]; } } -- (void) linearMultilineForRect:(NSRect)bodyRect leadingRect:(NSRect)leadingRect trailingRect:(NSRect)trailingRect points1:(NSMutableArray **)highlightedPoints points2:(NSMutableArray **)highlightedPoints2 rightCorners:(NSMutableSet **)rightCorners rightCorners2:(NSMutableSet **)rightCorners2 { - // Handles the special case where containing boxes are separated - if (nearEmptyRect(bodyRect) && !nearEmptyRect(leadingRect) && !nearEmptyRect(trailingRect) && NSMaxX(trailingRect) < NSMinX(leadingRect)) { - *highlightedPoints = [rectVertex(leadingRect) mutableCopy]; - *highlightedPoints2 = [rectVertex(trailingRect) mutableCopy]; - *rightCorners = [[NSMutableSet alloc] initWithObjects:@(2), @(3), nil]; - *rightCorners2 = [[NSMutableSet alloc] initWithObjects:@(0), @(1), nil]; - } else { - *highlightedPoints = [multilineRectVertex(leadingRect, bodyRect, trailingRect) mutableCopy]; - } -} - -- (CGPathRef)drawHighlightedWith:(SquirrelTheme *)theme highlightedRange:(NSRange)highlightedRange backgroundRect:(NSRect)backgroundRect preeditRect:(NSRect)preeditRect containingRect:(NSRect)containingRect extraExpansion:(CGFloat)extraExpansion { - NSRect currentContainingRect = containingRect; - currentContainingRect.size.width += extraExpansion * 2; - currentContainingRect.size.height += extraExpansion * 2; - currentContainingRect.origin.x -= extraExpansion; - currentContainingRect.origin.y -= extraExpansion; - - CGFloat halfLinespace = theme.linespace / 2; - NSRect innerBox = backgroundRect; - innerBox.size.width -= (theme.edgeInset.width + 1) * 2 - 2 * extraExpansion; - innerBox.origin.x += theme.edgeInset.width + 1 - extraExpansion; - innerBox.size.height += 2 * extraExpansion; - innerBox.origin.y -= extraExpansion; - if (_preeditRange.length == 0) { - innerBox.origin.y += theme.edgeInset.height + 1; - innerBox.size.height -= (theme.edgeInset.height + 1) * 2; - } else { - innerBox.origin.y += preeditRect.size.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + 1; - innerBox.size.height -= theme.edgeInset.height + preeditRect.size.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + 2; - } - innerBox.size.height -= theme.linespace; - innerBox.origin.y += halfLinespace; - NSRect outerBox = backgroundRect; - outerBox.size.height -= preeditRect.size.height + MAX(0, theme.hilitedCornerRadius + theme.borderWidth) - 2 * extraExpansion; - outerBox.size.width -= MAX(0, theme.hilitedCornerRadius + theme.borderWidth) - 2 * extraExpansion; - outerBox.origin.x += MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 - extraExpansion; - outerBox.origin.y += preeditRect.size.height + MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2 - extraExpansion; - - double effectiveRadius = MAX(0, theme.hilitedCornerRadius + 2 * extraExpansion / theme.hilitedCornerRadius * MAX(0, theme.cornerRadius - theme.hilitedCornerRadius)); - CGMutablePathRef path = CGPathCreateMutable(); - - if (theme.linear){ - NSRect leadingRect; - NSRect bodyRect; - NSRect trailingRect; - [self multilineRectForRange:[self convertRange:highlightedRange] leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect extraSurounding:_seperatorWidth bounds:outerBox]; - - NSMutableArray *highlightedPoints; - NSMutableArray *highlightedPoints2; - NSMutableSet *rightCorners; - NSMutableSet *rightCorners2; - [self linearMultilineForRect:bodyRect leadingRect:leadingRect trailingRect:trailingRect points1:&highlightedPoints points2:&highlightedPoints2 rightCorners:&rightCorners rightCorners2:&rightCorners2]; - - // Expand the boxes to reach proper border - enlarge(highlightedPoints, extraExpansion); - expand(highlightedPoints, innerBox, outerBox); - removeCorner(highlightedPoints, rightCorners, currentContainingRect); - - path = drawSmoothLines(highlightedPoints, rightCorners, 0.3*effectiveRadius, 1.4*effectiveRadius); - if (highlightedPoints2.count > 0) { - enlarge(highlightedPoints2, extraExpansion); - expand(highlightedPoints2, innerBox, outerBox); - removeCorner(highlightedPoints2, rightCorners2, currentContainingRect); - CGPathRef path2 = drawSmoothLines(highlightedPoints2, rightCorners2, 0.3*effectiveRadius, 1.4*effectiveRadius); - CGPathAddPath(path, NULL, path2); - } +static inline NSColor * disabledColor(NSColor *color, BOOL darkTheme) { + if (@available(macOS 10.14, *)) { + return [color colorWithSystemEffect:NSColorSystemEffectDisabled]; + } + if (darkTheme) { + return [color shadowWithLevel:0.3]; } else { - NSRect highlightedRect = [self contentRectForRange:[self convertRange:highlightedRange]]; - if (!nearEmptyRect(highlightedRect)) { - highlightedRect.size.width = backgroundRect.size.width; - highlightedRect.size.height += theme.linespace; - highlightedRect.origin = NSMakePoint(backgroundRect.origin.x, highlightedRect.origin.y + theme.edgeInset.height - halfLinespace); - if (NSMaxRange(highlightedRange) == _textView.string.length) { - highlightedRect.size.height += theme.edgeInset.height - halfLinespace; - } - if (highlightedRange.location - ((_preeditRange.location == NSNotFound ? 0 : _preeditRange.location)+_preeditRange.length) <= 1) { - if (_preeditRange.length == 0) { - highlightedRect.size.height += theme.edgeInset.height - halfLinespace; - highlightedRect.origin.y -= theme.edgeInset.height - halfLinespace; - } else { - highlightedRect.size.height += theme.hilitedCornerRadius / 2; - highlightedRect.origin.y -= theme.hilitedCornerRadius / 2; - } - } - NSMutableArray *highlightedPoints = [rectVertex(highlightedRect) mutableCopy]; - enlarge(highlightedPoints, extraExpansion); - expand(highlightedPoints, innerBox, outerBox); - path = drawSmoothLines(highlightedPoints, nil, 0.3*effectiveRadius, 1.4*effectiveRadius); - } + return [color highlightWithLevel:0.3]; } - return path; -} - -- (NSRect)carveInset:(NSRect)rect theme:(SquirrelTheme *)theme { - NSRect newRect = rect; - newRect.size.height -= (theme.hilitedCornerRadius + theme.borderWidth) * 2; - newRect.size.width -= (theme.hilitedCornerRadius + theme.borderWidth) * 2; - newRect.origin.x += theme.hilitedCornerRadius + theme.borderWidth; - newRect.origin.y += theme.hilitedCornerRadius + theme.borderWidth; - return newRect; } // All draws happen here -- (void)drawRect:(NSRect)dirtyRect { - CGPathRef backgroundPath = CGPathCreateMutable(); - CGPathRef highlightedPath = CGPathCreateMutable(); - CGMutablePathRef candidatePaths = CGPathCreateMutable(); - CGMutablePathRef highlightedPreeditPath = CGPathCreateMutable(); - CGPathRef preeditPath = CGPathCreateMutable(); - SquirrelTheme * theme = self.currentTheme; - - NSPoint textFieldOrigin = dirtyRect.origin; - textFieldOrigin.y += theme.edgeInset.height; - textFieldOrigin.x += theme.edgeInset.width; +- (void)updateLayer { + NSBezierPath *backgroundPath; + NSBezierPath *borderPath; + NSBezierPath *textContainerPath; + NSBezierPath *highlightedPath; + NSBezierPath *highlightedPreeditPath; + NSBezierPath *candidateBlockPath; + NSBezierPath *candidateHorzGridPath; + NSBezierPath *candidateVertGridPath; + NSBezierPath *pageUpPath; + NSBezierPath *pageDownPath; + + SquirrelTheme *theme = self.currentTheme; + NSRect backgroundRect = self.bounds; + NSRect textContainerRect = NSInsetRect(backgroundRect, theme.edgeInset.width, theme.edgeInset.height); + + NSRange visibleRange; + if (@available(macOS 12.0, *)) { + visibleRange = NSMakeRange(0, _textStorage.length); + } else { + NSRange containerGlyphRange = {NSNotFound, 0}; + [_textView.layoutManager textContainerForGlyphAtIndex:0 effectiveRange:&containerGlyphRange]; + visibleRange = [_textView.layoutManager characterRangeForGlyphRange:containerGlyphRange actualGlyphRange:NULL]; + } + NSRange preeditRange = NSIntersectionRange(_preeditRange, visibleRange); + NSRange candidateBlockRange = NSIntersectionRange(NSUnionRange(_candidateRanges.firstObject.rangeValue, theme.linear && _pagingRange.length > 0 ? _pagingRange : _candidateRanges.lastObject.rangeValue), visibleRange); + NSRange pagingRange = NSIntersectionRange(_pagingRange, visibleRange); - // Draw preedit Rect - NSRect backgroundRect = dirtyRect; - NSRect containingRect = dirtyRect; + NSRect preeditRect = NSZeroRect; + NSRect candidateBlockRect = NSZeroRect; + NSRect pagingLineRect = NSZeroRect; + if (preeditRange.length > 0) { + preeditRect = [self contentRectForRange:preeditRange]; + if (candidateBlockRange.length > 0) { + preeditRect.size.height += theme.preeditLinespace; + } + } + if (candidateBlockRange.length > 0) { + candidateBlockRect = NSInsetRect([self contentRectForRange:candidateBlockRange], 0.0, -theme.linespace / 2); + if (preeditRange.length == 0) { + candidateBlockRect.origin.y += theme.linespace / 2; + } + } + if (!theme.linear && pagingRange.length > 0) { + pagingLineRect = [self contentRectForRange:pagingRange]; + pagingLineRect.origin.y -= theme.pagingParagraphStyle.paragraphSpacingBefore; + pagingLineRect.size.height += theme.pagingParagraphStyle.paragraphSpacingBefore; + } + [NSBezierPath setDefaultLineWidth:0]; // Draw preedit Rect - NSRect preeditRect = NSZeroRect; - if (_preeditRange.length > 0) { - preeditRect = [self contentRectForRange:[self convertRange:_preeditRange]]; - if (!nearEmptyRect(preeditRect)) { - preeditRect.size.width = backgroundRect.size.width; - preeditRect.size.height += theme.edgeInset.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2; - preeditRect.origin = backgroundRect.origin; - if (_candidateRanges.count == 0) { - preeditRect.size.height += theme.edgeInset.height - theme.preeditLinespace / 2 - theme.hilitedCornerRadius / 2; + if (preeditRange.length > 0) { + preeditRect.size.width = textContainerRect.size.width; + preeditRect.origin = textContainerRect.origin; + // Draw highlighted part of preedit text + NSRange highlightedPreeditRange = NSIntersectionRange(_highlightedPreeditRange, visibleRange); + if (highlightedPreeditRange.length > 0 && theme.highlightedPreeditColor != nil) { + NSRect innerBox = NSInsetRect(preeditRect, theme.separatorWidth / 2, 0); + if (candidateBlockRange.length > 0) { + innerBox.size.height -= theme.preeditLinespace; + } + NSRect leadingRect = NSZeroRect; + NSRect bodyRect = NSZeroRect; + NSRect trailingRect = NSZeroRect; + [self multilineRectForRange:highlightedPreeditRange leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect]; + leadingRect = nearEmptyRect(leadingRect) ? NSZeroRect + : NSIntersectionRect(NSOffsetRect(leadingRect, _insets.left, theme.edgeInset.height), innerBox); + bodyRect = nearEmptyRect(bodyRect) ? NSZeroRect + : NSIntersectionRect(NSOffsetRect(bodyRect, _insets.left, theme.edgeInset.height), innerBox); + trailingRect = nearEmptyRect(trailingRect) ? NSZeroRect + : NSIntersectionRect(NSOffsetRect(trailingRect, _insets.left, theme.edgeInset.height), innerBox); + NSArray *highlightedPreeditPoints; + NSArray *highlightedPreeditPoints2; + // Handles the special case where containing boxes are separated + if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && !NSIsEmptyRect(trailingRect) + && NSMaxX(trailingRect) < NSMinX(leadingRect)) { + highlightedPreeditPoints = rectVertex(leadingRect); + highlightedPreeditPoints2 = rectVertex(trailingRect); + } else { + highlightedPreeditPoints = multilineRectVertex(leadingRect, bodyRect, trailingRect); } - containingRect.size.height -= preeditRect.size.height; - containingRect.origin.y += preeditRect.size.height; - if (theme.preeditBackgroundColor != nil) { - preeditPath = drawSmoothLines(rectVertex(preeditRect), nil, 0, 0); + highlightedPreeditPath = drawRoundedPolygon(highlightedPreeditPoints, MIN(theme.highlightedCornerRadius, theme.preeditParagraphStyle.maximumLineHeight / 3)); + if (highlightedPreeditPoints2.count > 0) { + [highlightedPreeditPath appendBezierPath:drawRoundedPolygon(highlightedPreeditPoints2, MIN(theme.highlightedCornerRadius, theme.preeditParagraphStyle.maximumLineHeight / 3))]; } } } - containingRect = [self carveInset:containingRect theme:theme]; - // Draw highlighted Rect - for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) { - NSRange candidateRange = [_candidateRanges[i] rangeValue]; - if (i == _hilightedIndex) { - // Draw highlighted Rect - if (candidateRange.length > 0 && theme.highlightedBackColor != nil) { - highlightedPath = [self drawHighlightedWith:theme highlightedRange:candidateRange backgroundRect:backgroundRect preeditRect:preeditRect containingRect:containingRect extraExpansion:0]; + // Draw candidate Rect + if (candidateBlockRange.length > 0) { + candidateBlockRect.size.width = textContainerRect.size.width; + candidateBlockRect.origin.x = textContainerRect.origin.x; + candidateBlockRect.origin.y += textContainerRect.origin.y; + candidateBlockRect = NSIntersectionRect(candidateBlockRect, textContainerRect); + candidateBlockPath = drawRoundedPolygon(rectVertex(candidateBlockRect), theme.highlightedCornerRadius); + + // Draw candidate highlight rect + if (theme.linear) { + CGFloat gridOriginY = NSMinY(candidateBlockRect); + CGFloat tabInterval = theme.separatorWidth * 2; + if (theme.tabled) { + candidateHorzGridPath = [NSBezierPath bezierPath]; + candidateVertGridPath = [NSBezierPath bezierPath]; } - } else { - // Draw other highlighted Rect - if (candidateRange.length > 0 && theme.candidateBackColor != nil) { - CGPathRef candidatePath = [self drawHighlightedWith:theme highlightedRange:candidateRange backgroundRect:backgroundRect preeditRect:preeditRect containingRect:containingRect extraExpansion:theme.surroundingExtraExpansion]; - CGPathAddPath(candidatePaths, NULL, candidatePath); + for (NSUInteger i = 0; i < _candidateRanges.count; ++i) { + NSRange candidateRange = NSIntersectionRange([_candidateRanges[i] rangeValue], visibleRange); + if (candidateRange.length == 0) { + break; + } + NSRect leadingRect = NSZeroRect; + NSRect bodyRect = NSZeroRect; + NSRect trailingRect = NSZeroRect; + [self multilineRectForRange:candidateRange leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect]; + leadingRect = nearEmptyRect(leadingRect) ? NSZeroRect + : NSInsetRect(NSOffsetRect(leadingRect, _insets.left, theme.edgeInset.height), -theme.separatorWidth / 2, 0); + bodyRect = nearEmptyRect(bodyRect) ? NSZeroRect + : NSInsetRect(NSOffsetRect(bodyRect, _insets.left, theme.edgeInset.height), -theme.separatorWidth / 2, 0); + trailingRect = nearEmptyRect(trailingRect) ? NSZeroRect + : NSInsetRect(NSOffsetRect(trailingRect, _insets.left, theme.edgeInset.height), -theme.separatorWidth / 2, 0); + if (preeditRange.length == 0) { + leadingRect.origin.y += theme.linespace / 2; + bodyRect.origin.y += theme.linespace / 2; + trailingRect.origin.y += theme.linespace / 2; + } + if (!NSIsEmptyRect(leadingRect)) { + leadingRect.origin.y -= theme.linespace / 2; + leadingRect.size.height += theme.linespace / 2; + leadingRect = NSIntersectionRect(leadingRect, candidateBlockRect); + } + if (!NSIsEmptyRect(trailingRect)) { + trailingRect.size.height += theme.linespace / 2; + trailingRect = NSIntersectionRect(trailingRect, candidateBlockRect); + } + if (!NSIsEmptyRect(bodyRect)) { + if (NSIsEmptyRect(leadingRect)) { + bodyRect.origin.y -= theme.linespace / 2; + bodyRect.size.height += theme.linespace / 2; + } + if (NSIsEmptyRect(trailingRect)) { + bodyRect.size.height += theme.linespace / 2; + } + bodyRect = NSIntersectionRect(bodyRect, candidateBlockRect); + } + if (theme.tabled) { + CGFloat bottomEdge = NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); + if (ABS(bottomEdge - gridOriginY) > 2 && ABS(bottomEdge - NSMaxY(candidateBlockRect)) > 2) { // horizontal border + [candidateHorzGridPath moveToPoint:NSMakePoint(NSMinX(candidateBlockRect) + theme.separatorWidth / 2, bottomEdge)]; + [candidateHorzGridPath lineToPoint:NSMakePoint(NSMaxX(candidateBlockRect) - theme.separatorWidth / 2, bottomEdge)]; + [candidateHorzGridPath closePath]; + gridOriginY = bottomEdge; + } + CGPoint leadOrigin = (NSIsEmptyRect(leadingRect) ? bodyRect : leadingRect).origin; + if (leadOrigin.x > NSMinX(candidateBlockRect) + theme.separatorWidth / 2) { // vertical bar + [candidateVertGridPath moveToPoint:NSMakePoint(leadOrigin.x, leadOrigin.y + theme.linespace / 2 + theme.paragraphStyle.maximumLineHeight - theme.highlightedCornerRadius)]; + [candidateVertGridPath lineToPoint:NSMakePoint(leadOrigin.x, leadOrigin.y + theme.linespace / 2 + theme.highlightedCornerRadius)]; + [candidateVertGridPath closePath]; + } + CGFloat tailEdge = NSMaxX(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); + CGFloat tabPosition = pow(2, ceil(log2((tailEdge - leadOrigin.x) / tabInterval))) * tabInterval + leadOrigin.x; + if (NSIsEmptyRect(trailingRect)) { + bodyRect.size.width += tabPosition - tailEdge; + } else if (NSIsEmptyRect(bodyRect)) { + trailingRect.size.width += tabPosition - tailEdge; + } else { + bodyRect = NSMakeRect(NSMinX(candidateBlockRect), NSMinY(bodyRect), NSWidth(candidateBlockRect), NSHeight(bodyRect) + NSHeight(trailingRect)); + trailingRect = NSZeroRect; + } + } + NSArray *candidatePoints; + NSArray *candidatePoints2; + // Handles the special case where containing boxes are separated + if (NSIsEmptyRect(bodyRect) && !NSIsEmptyRect(leadingRect) && + !NSIsEmptyRect(trailingRect) && NSMaxX(trailingRect) < NSMinX(leadingRect)) { + candidatePoints = rectVertex(leadingRect); + candidatePoints2 = rectVertex(trailingRect); + } else { + candidatePoints = multilineRectVertex(leadingRect, bodyRect, trailingRect); + } + NSBezierPath *candidatePath = drawRoundedPolygon(candidatePoints, theme.highlightedCornerRadius); + if (candidatePoints2.count > 0) { + [candidatePath appendBezierPath:drawRoundedPolygon(candidatePoints2, theme.highlightedCornerRadius)]; + } + _candidatePaths[i] = candidatePath; + } + } else { // stacked layout + for (NSUInteger i = 0; i < _candidateRanges.count; ++i) { + NSRange candidateRange = NSIntersectionRange([_candidateRanges[i] rangeValue], visibleRange); + if (candidateRange.length == 0) { + break; + } + NSRect candidateRect = NSInsetRect([self contentRectForRange:candidateRange], 0.0, -theme.linespace / 2); + candidateRect.size.width = textContainerRect.size.width; + candidateRect.origin.x = textContainerRect.origin.x; + candidateRect.origin.y += textContainerRect.origin.y; + if (preeditRange.length == 0) { + candidateRect.origin.y += theme.linespace / 2; + } + candidateRect = NSIntersectionRect(candidateRect, candidateBlockRect); + NSArray *candidatePoints = rectVertex(candidateRect); + NSBezierPath *candidatePath = drawRoundedPolygon(candidatePoints, theme.highlightedCornerRadius); + _candidatePaths[i] = candidatePath; } } } - // Draw highlighted part of preedit text - if (_highlightedPreeditRange.length > 0 && theme.highlightedPreeditColor != nil) { - NSRect innerBox = preeditRect; - innerBox.size.width -= (theme.edgeInset.width + 1) * 2; - innerBox.origin.x += theme.edgeInset.width + 1; - innerBox.origin.y += theme.edgeInset.height + 1; - if (_candidateRanges.count == 0) { - innerBox.size.height -= (theme.edgeInset.height + 1) * 2; + // Draw paging Rect + if (pagingRange.length > 0) { + NSRect pageDownRect = NSOffsetRect([self contentRectForRange:NSMakeRange(NSMaxRange(pagingRange) - 1, 1)], + _insets.left, theme.edgeInset.height); + pageDownRect.size.width += theme.separatorWidth / 2; + NSRect pageUpRect = NSOffsetRect([self contentRectForRange:NSMakeRange(pagingRange.location, 1)], + _insets.left, theme.edgeInset.height); + pageUpRect.origin.x -= theme.separatorWidth / 2; + pageUpRect.size.width = NSWidth(pageDownRect); // bypass the bug of getting wrong glyph position when tab is presented + if (theme.linear) { + pageDownRect = NSInsetRect(pageDownRect, 0.0, -theme.linespace / 2); + pageUpRect = NSInsetRect(pageUpRect, 0.0, -theme.linespace / 2); + } + if (preeditRange.length == 0) { + pageDownRect = NSOffsetRect(pageDownRect, 0.0, theme.linespace / 2); + pageUpRect = NSOffsetRect(pageUpRect, 0.0, theme.linespace / 2); + } + pageDownRect = NSIntersectionRect(pageDownRect, textContainerRect); + pageUpRect = NSIntersectionRect(pageUpRect, textContainerRect); + pageDownPath = drawRoundedPolygon(rectVertex(pageDownRect), + MIN(theme.highlightedCornerRadius, MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) / 3)); + pageUpPath = drawRoundedPolygon(rectVertex(pageUpRect), + MIN(theme.highlightedCornerRadius, MIN(NSWidth(pageUpRect), NSHeight(pageUpRect)) / 3)); + _pagingPaths[0] = pageUpPath; + _pagingPaths[1] = pageDownPath; + } + + // Draw borders + backgroundPath = drawRoundedPolygon(rectVertex(backgroundRect), + MIN(theme.cornerRadius, NSHeight(backgroundRect) / 3)); + textContainerPath = drawRoundedPolygon(rectVertex(textContainerRect), + MIN(theme.highlightedCornerRadius, NSHeight(textContainerRect) / 3)); + if (theme.edgeInset.width > 0 || theme.edgeInset.height > 0) { + borderPath = [backgroundPath copy]; + [borderPath appendBezierPath:textContainerPath]; + borderPath.windingRule = NSEvenOddWindingRule; + } + + // set layers + _shape.path = [backgroundPath quartzPath]; + _shape.fillColor = [[NSColor whiteColor] CGColor]; + _shape.cornerRadius = MIN(theme.cornerRadius, NSHeight(backgroundRect) / 3); + CAShapeLayer *textContainerLayer = [[CAShapeLayer alloc] init]; + textContainerLayer.path = [textContainerPath quartzPath]; + textContainerLayer.fillColor = [[NSColor whiteColor] CGColor]; + textContainerLayer.cornerRadius = MIN(theme.highlightedCornerRadius, NSHeight(textContainerRect) / 3); + [self.layer setSublayers:nil]; + self.layer.cornerRadius = MIN(theme.cornerRadius, NSHeight(backgroundRect) / 3); + CALayer *panelLayer = [[CALayer alloc] init]; + [self.layer addSublayer:panelLayer]; + if (theme.backgroundImage) { + CAShapeLayer *backgroundImageLayer = [[CAShapeLayer alloc] init]; + if (theme.vertical) { + const CGAffineTransform rotate = CGAffineTransformMakeRotation(-M_PI / 2); + backgroundImageLayer.path = CFAutorelease(CGPathCreateCopyByTransformingPath([textContainerPath quartzPath], &rotate)); + backgroundImageLayer.fillColor = [theme.backgroundImage CGColor]; + [backgroundImageLayer setAffineTransform:CGAffineTransformMakeRotation(M_PI / 2)]; } else { - innerBox.size.height -= theme.edgeInset.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 + 2; - } - NSRect outerBox = preeditRect; - outerBox.size.height -= MAX(0, theme.hilitedCornerRadius + theme.borderWidth); - outerBox.size.width -= MAX(0, theme.hilitedCornerRadius + theme.borderWidth); - outerBox.origin.x += MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2; - outerBox.origin.y += MAX(0, theme.hilitedCornerRadius + theme.borderWidth) / 2; - - NSRect leadingRect; - NSRect bodyRect; - NSRect trailingRect; - [self multilineRectForRange:[self convertRange:_highlightedPreeditRange] leadingRect:&leadingRect bodyRect:&bodyRect trailingRect:&trailingRect extraSurounding:0 bounds:outerBox]; - - NSMutableArray *highlightedPreeditPoints; - NSMutableArray *highlightedPreeditPoints2; - NSMutableSet *rightCorners; - NSMutableSet *rightCorners2; - [self linearMultilineForRect:bodyRect leadingRect:leadingRect trailingRect:trailingRect points1:&highlightedPreeditPoints points2:&highlightedPreeditPoints2 rightCorners:&rightCorners rightCorners2:&rightCorners2]; - - containingRect = [self carveInset:preeditRect theme:theme]; - expand(highlightedPreeditPoints, innerBox, outerBox); - removeCorner(highlightedPreeditPoints, rightCorners, containingRect); - highlightedPreeditPath = drawSmoothLines(highlightedPreeditPoints, rightCorners, 0.3*theme.hilitedCornerRadius, 1.4*theme.hilitedCornerRadius); - if (highlightedPreeditPoints2.count > 0) { - expand(highlightedPreeditPoints2, innerBox, outerBox); - removeCorner(highlightedPreeditPoints2, rightCorners2, containingRect); - CGPathRef highlightedPreeditPath2 = drawSmoothLines(highlightedPreeditPoints2, rightCorners2, 0.3*theme.hilitedCornerRadius, 1.4*theme.hilitedCornerRadius); - CGPathAddPath(highlightedPreeditPath, NULL, highlightedPreeditPath2); - } - } + backgroundImageLayer.path = [textContainerPath quartzPath]; + backgroundImageLayer.fillColor = [theme.backgroundImage CGColor]; + } + backgroundImageLayer.cornerRadius = MIN(theme.cornerRadius, NSHeight(backgroundRect) / 3); + [panelLayer addSublayer:backgroundImageLayer]; + } + CAShapeLayer *backgroundLayer = [[CAShapeLayer alloc] init]; + backgroundLayer.path = [textContainerPath quartzPath]; + backgroundLayer.fillColor = [theme.backgroundColor CGColor]; + backgroundLayer.cornerRadius = MIN(theme.cornerRadius, NSHeight(backgroundRect) / 3); + [panelLayer addSublayer:backgroundLayer]; + if (theme.preeditBackgroundColor && + (preeditRange.length > 0 || !NSIsEmptyRect(pagingLineRect))) { + backgroundLayer.fillColor = [theme.preeditBackgroundColor CGColor]; + if (!candidateBlockPath.empty) { + [textContainerPath appendBezierPath:candidateBlockPath]; + textContainerPath.windingRule = NSEvenOddWindingRule; + backgroundLayer.path = [textContainerPath quartzPath]; + backgroundLayer.fillRule = kCAFillRuleEvenOdd; + CAShapeLayer *candidateLayer = [[CAShapeLayer alloc] init]; + candidateLayer.path = [candidateBlockPath quartzPath]; + candidateLayer.fillColor = [theme.backgroundColor CGColor]; + [panelLayer addSublayer:candidateLayer]; + } + } + if (@available(macOS 10.14, *)) { + if (theme.translucency > 0) { + panelLayer.opacity = 1.0 - theme.translucency; + } + } + if (_highlightedIndex < _candidatePaths.count && theme.highlightedStripColor) { + CAShapeLayer *highlightedLayer = [[CAShapeLayer alloc] init]; + highlightedPath = _candidatePaths[_highlightedIndex]; + highlightedLayer.path = [highlightedPath quartzPath]; + highlightedLayer.fillColor = [theme.highlightedStripColor CGColor]; + CAShapeLayer *candidateMaskLayer = [[CAShapeLayer alloc] init]; + candidateMaskLayer.path = [candidateBlockPath quartzPath]; + candidateMaskLayer.fillColor = [[NSColor whiteColor] CGColor]; + highlightedLayer.mask = candidateMaskLayer; + [self.layer addSublayer:highlightedLayer]; + } + NSColor *buttonColor = theme.linear ? theme.highlightedStripColor : theme.highlightedPreeditColor; + if (pagingRange.length > 0 && buttonColor) { + CAShapeLayer *pagingLayer = [[CAShapeLayer alloc] init]; + switch (_pagingButton) { + case NSPageUpFunctionKey: { + pagingLayer.path = [pageUpPath quartzPath]; + pagingLayer.fillColor = [hooverColor(buttonColor, self.isDark) CGColor]; + } break; + case NSBeginFunctionKey: { + pagingLayer.path = [pageUpPath quartzPath]; + pagingLayer.fillColor = [disabledColor(buttonColor, self.isDark) CGColor]; + } break; + case NSPageDownFunctionKey: { + pagingLayer.path = [pageDownPath quartzPath]; + pagingLayer.fillColor = [hooverColor(buttonColor, self.isDark) CGColor]; + } break; + case NSEndFunctionKey: { + pagingLayer.path = [pageDownPath quartzPath]; + pagingLayer.fillColor = [disabledColor(buttonColor, self.isDark) CGColor]; + } break; + } + pagingLayer.mask = textContainerLayer; + [self.layer addSublayer:pagingLayer]; + } + if (theme.highlightedPreeditColor) { + if (!highlightedPreeditPath.empty) { + CAShapeLayer *highlightedPreeditLayer = [[CAShapeLayer alloc] init]; + highlightedPreeditLayer.path = [highlightedPreeditPath quartzPath]; + highlightedPreeditLayer.fillColor = [theme.highlightedPreeditColor CGColor]; + highlightedPreeditLayer.mask = textContainerLayer; + [self.layer addSublayer:highlightedPreeditLayer]; + } + } + if (theme.tabled) { + CAShapeLayer *horzGridLayer = [[CAShapeLayer alloc] init]; + horzGridLayer.path = [candidateHorzGridPath quartzPath]; + horzGridLayer.strokeColor = [[theme.backgroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction ofColor:(self.isDark ? [NSColor lightGrayColor] : [NSColor blackColor])] CGColor]; + horzGridLayer.lineWidth = theme.edgeInset.height / 2; + horzGridLayer.lineCap = kCALineCapRound; + [panelLayer addSublayer:horzGridLayer]; + CAShapeLayer *vertGridLayer = [[CAShapeLayer alloc] init]; + vertGridLayer.path = [candidateVertGridPath quartzPath]; + vertGridLayer.strokeColor = [[theme.backgroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction ofColor:(self.isDark ? [NSColor lightGrayColor] : [NSColor blackColor])] CGColor]; + vertGridLayer.lineWidth = theme.edgeInset.width / 2; + vertGridLayer.lineCap = kCALineCapRound; + [panelLayer addSublayer:vertGridLayer]; + } + if (theme.borderColor && !borderPath.empty) { + CAShapeLayer *borderLayer = [[CAShapeLayer alloc] init]; + borderLayer.path = [borderPath quartzPath]; + borderLayer.fillColor = [theme.borderColor CGColor]; + borderLayer.fillRule = kCAFillRuleEvenOdd; + [panelLayer addSublayer:borderLayer]; + } + [_textView display]; +} - [NSBezierPath setDefaultLineWidth:0]; - backgroundPath = drawSmoothLines(rectVertex(backgroundRect), nil, theme.cornerRadius*0.3, theme.cornerRadius*1.4); - _shape.path = CGPathCreateMutableCopy(backgroundPath); - - [self.layer setSublayers: NULL]; - CGMutablePathRef backPath = CGPathCreateMutableCopy(backgroundPath); - if (!CGPathIsEmpty(preeditPath)) { - CGPathAddPath(backPath, NULL, preeditPath); - } - if (theme.mutualExclusive) { - if (!CGPathIsEmpty(highlightedPath)) { - CGPathAddPath(backPath, NULL, highlightedPath); - } - if (!CGPathIsEmpty(candidatePaths)) { - CGPathAddPath(backPath, NULL, candidatePaths); - } - } - CAShapeLayer *panelLayer = shapeFromPath(backPath); - panelLayer.fillColor = theme.backgroundColor.CGColor; - CAShapeLayer *panelLayerMask = shapeFromPath(backgroundPath); - panelLayer.mask = panelLayerMask; - [self.layer addSublayer: panelLayer]; - - if (theme.preeditBackgroundColor && !CGPathIsEmpty(preeditPath)) { - CAShapeLayer *layer = shapeFromPath(preeditPath); - layer.fillColor = theme.preeditBackgroundColor.CGColor; - CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath); - if (theme.mutualExclusive && !CGPathIsEmpty(highlightedPreeditPath)) { - CGPathAddPath(maskPath, NULL, highlightedPreeditPath); - } - CAShapeLayer *mask = shapeFromPath(maskPath); - layer.mask = mask; - [panelLayer addSublayer: layer]; - } - if (theme.borderWidth > 0 && theme.borderColor) { - CAShapeLayer *borderLayer = shapeFromPath(backgroundPath); - borderLayer.lineWidth = theme.borderWidth * 2; - borderLayer.strokeColor = theme.borderColor.CGColor; - borderLayer.fillColor = NULL; - [panelLayer addSublayer: borderLayer]; - } - if (theme.highlightedPreeditColor && !CGPathIsEmpty(highlightedPreeditPath)) { - CAShapeLayer *layer = shapeFromPath(highlightedPreeditPath); - layer.fillColor = theme.highlightedPreeditColor.CGColor; - [panelLayer addSublayer: layer]; - } - if (theme.candidateBackColor && !CGPathIsEmpty(candidatePaths)) { - CAShapeLayer *layer = shapeFromPath(candidatePaths); - layer.fillColor = theme.candidateBackColor.CGColor; - [panelLayer addSublayer: layer]; - } - if (theme.highlightedBackColor && !CGPathIsEmpty(highlightedPath)) { - CAShapeLayer *layer = shapeFromPath(highlightedPath); - layer.fillColor = theme.highlightedBackColor.CGColor; - if (theme.shadowSize > 0) { - CAShapeLayer *shadowLayer = [CAShapeLayer layer]; - shadowLayer.shadowColor = NSColor.blackColor.CGColor; - shadowLayer.shadowOffset = NSMakeSize(theme.shadowSize/2, (theme.vertical ? -1 : 1) * theme.shadowSize/2); - shadowLayer.shadowPath = highlightedPath; - shadowLayer.shadowRadius = theme.shadowSize; - shadowLayer.shadowOpacity = 0.2; - CGMutablePathRef maskPath = CGPathCreateMutableCopy(backgroundPath); - CGPathAddPath(maskPath, NULL, highlightedPath); - if (!CGPathIsEmpty(preeditPath)) { - CGPathAddPath(maskPath, NULL, preeditPath); +- (BOOL)convertClickSpot:(NSPoint)spot toIndex:(NSUInteger *)index { + if (NSPointInRect(spot, self.bounds)) { + if (_pagingPaths.count > 0) { + if ([_pagingPaths[0] containsPoint:spot]) { + *index = NSPageUpFunctionKey; // borrow function-key unicode for readability + return YES; } - CAShapeLayer *shadowLayerMask = shapeFromPath(maskPath); - shadowLayer.mask = shadowLayerMask; - layer.strokeColor = [NSColor.blackColor colorWithAlphaComponent:0.15].CGColor; - layer.lineWidth = 0.5; - [layer addSublayer: shadowLayer]; - } - [panelLayer addSublayer: layer]; - } - [_textView setTextContainerInset:NSMakeSize(textFieldOrigin.x, textFieldOrigin.y)]; -} - -- (BOOL)clickAtPoint:(NSPoint)_point index:(NSInteger *)_index { - if (CGPathContainsPoint(_shape.path, nil, _point, NO)) { - NSPoint point = NSMakePoint(_point.x - self.textView.textContainerInset.width, - _point.y - self.textView.textContainerInset.height); - NSTextLayoutFragment *fragment = [self.layoutManager textLayoutFragmentForPosition:point]; - if (fragment) { - point = NSMakePoint(point.x - NSMinX(fragment.layoutFragmentFrame), - point.y - NSMinY(fragment.layoutFragmentFrame)); - NSInteger index = [self.layoutManager offsetFromLocation: self.layoutManager.documentRange.location toLocation: fragment.rangeInElement.location]; - for (NSUInteger i = 0; i < fragment.textLineFragments.count; i += 1) { - NSTextLineFragment *lineFragment = fragment.textLineFragments[i]; - if (CGRectContainsPoint(lineFragment.typographicBounds, point)) { - point = NSMakePoint(point.x - NSMinX(lineFragment.typographicBounds), - point.y - NSMinY(lineFragment.typographicBounds)); - index += [lineFragment characterIndexForPoint:point]; - for (NSUInteger i = 0; i < _candidateRanges.count; i += 1) { - NSRange range = [_candidateRanges[i] rangeValue]; - if (index >= range.location && index < NSMaxRange(range)) { - *_index = i; - break; - } - } - break; - } + if ([_pagingPaths[1] containsPoint:spot]) { + *index = NSPageDownFunctionKey; // borrow function-key unicode for readability + return YES; + } + } + for (NSUInteger i = 0; i < _candidatePaths.count; ++i) { + if ([_candidatePaths[i] containsPoint:spot]) { + *index = i; + return YES; } } - return YES; - } else { - return NO; } + return NO; } @end @@ -901,28 +1323,40 @@ @implementation SquirrelPanel { SquirrelView *_view; NSVisualEffectView *_back; - NSRect _screenRect; - CGFloat _maxHeight; + NSScreen *_screen; + NSSize _maxSize; + CGFloat _textWidthLimit; - NSString *_statusMessage; - NSTimer *_statusTimer; - NSString *_preedit; NSRange _selRange; NSUInteger _caretPos; - NSArray *_candidates; - NSArray *_comments; - NSArray *_labels; + NSArray *_candidates; + NSArray *_comments; NSUInteger _index; - NSUInteger _cursorIndex; - NSPoint _scrollDirection; - NSDate *_scrollTime; + NSUInteger _pageNum; + NSUInteger _turnPage; + BOOL _lastPage; + + BOOL _mouseDown; + NSPoint _scrollLocus; + BOOL _initPosition; + + NSString *_statusMessage; + NSTimer *_statusTimer; +} + +- (BOOL)isFloatingPanel { + return YES; } - (BOOL)linear { return _view.currentTheme.linear; } +- (BOOL)tabled { + return _view.currentTheme.tabled; +} + - (BOOL)vertical { return _view.currentTheme.vertical; } @@ -935,193 +1369,258 @@ - (BOOL)inlineCandidate { return _view.currentTheme.inlineCandidate; } -NSAttributedString *insert(NSString *separator, NSAttributedString *betweenText) { - NSRange range = [betweenText.string rangeOfComposedCharacterSequenceAtIndex:0]; - NSAttributedString *attributedSeperator = [[NSAttributedString alloc] initWithString:separator attributes:[betweenText attributesAtIndex:0 effectiveRange:nil]]; - NSUInteger i = NSMaxRange(range); - NSMutableAttributedString *workingString = [[betweenText attributedSubstringFromRange:range] mutableCopy]; - while (i < betweenText.length) { - range = [betweenText.string rangeOfComposedCharacterSequenceAtIndex:i]; - [workingString appendAttributedString:attributedSeperator]; - [workingString appendAttributedString:[betweenText attributedSubstringFromRange:range]]; - i = NSMaxRange(range); ++ (NSColor *)secondaryTextColor { + if (@available(macOS 10.10, *)) { + return [NSColor secondaryLabelColor]; + } else { + return [NSColor disabledControlTextColor]; } - return workingString; } -+ (NSColor *)secondaryTextColor { - return [NSColor secondaryLabelColor]; ++ (NSColor *)accentColor { + if (@available(macOS 10.14, *)) { + return [NSColor controlAccentColor]; + } else { + return [NSColor colorForControlTint:[NSColor currentControlTint]]; + } } - (void)initializeUIStyleForDarkMode:(BOOL)isDark { SquirrelTheme *theme = [_view selectTheme:isDark]; - theme.native = YES; - theme.memorizeSize = YES; - theme.candidateFormat = kDefaultCandidateFormat; - - NSColor *secondaryTextColor = [[self class] secondaryTextColor]; - NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init]; + NSColor *secondaryTextColor = [SquirrelPanel secondaryTextColor]; + NSColor *accentColor = [SquirrelPanel accentColor]; + NSFont *userFont = [NSFont fontWithDescriptor:getFontDescriptor([NSFont userFontOfSize:0.0].fontName) + size:kDefaultFontSize]; + NSFont *userMonoFont = [NSFont fontWithDescriptor:getFontDescriptor([NSFont userFixedPitchFontOfSize:0.0].fontName) + size:kDefaultFontSize]; + NSMutableDictionary *defaultAttrs = [[NSMutableDictionary alloc] init]; + // prevent mac terminal from hijacking non-alphabetic keys on non-inline mode + defaultAttrs[IMKCandidatesSendServerKeyEventFirst] = @(YES); + + NSMutableDictionary *attrs = [defaultAttrs mutableCopy]; attrs[NSForegroundColorAttributeName] = [NSColor controlTextColor]; - attrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; + attrs[NSFontAttributeName] = userFont; - NSMutableDictionary *highlightedAttrs = [[NSMutableDictionary alloc] init]; - highlightedAttrs[NSForegroundColorAttributeName] = [NSColor selectedControlTextColor]; - highlightedAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; + NSMutableDictionary *highlightedAttrs = [defaultAttrs mutableCopy]; + highlightedAttrs[NSForegroundColorAttributeName] = [NSColor selectedMenuItemTextColor]; + highlightedAttrs[NSFontAttributeName] = userFont; + // Use left-to-right embedding to prevent right-to-left text from changing the layout of the candidate. + attrs[NSWritingDirectionAttributeName] = @[@(0)]; + highlightedAttrs[NSWritingDirectionAttributeName] = @[@(0)]; NSMutableDictionary *labelAttrs = [attrs mutableCopy]; + labelAttrs[NSForegroundColorAttributeName] = accentColor; + labelAttrs[NSFontAttributeName] = userMonoFont; + NSMutableDictionary *labelHighlightedAttrs = [highlightedAttrs mutableCopy]; + labelHighlightedAttrs[NSForegroundColorAttributeName] = [NSColor alternateSelectedControlTextColor]; + labelHighlightedAttrs[NSFontAttributeName] = userMonoFont; - NSMutableDictionary *commentAttrs = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *commentAttrs = [defaultAttrs mutableCopy]; commentAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - commentAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; + commentAttrs[NSFontAttributeName] = userFont; + + NSMutableDictionary *commentHighlightedAttrs = [defaultAttrs mutableCopy]; + commentHighlightedAttrs[NSForegroundColorAttributeName] = [NSColor alternateSelectedControlTextColor]; + commentHighlightedAttrs[NSFontAttributeName] = userFont; + + NSMutableDictionary *preeditAttrs = [defaultAttrs mutableCopy]; + preeditAttrs[NSForegroundColorAttributeName] = [NSColor textColor]; + preeditAttrs[NSFontAttributeName] = userFont; + preeditAttrs[NSLigatureAttributeName] = @(0); + + NSMutableDictionary *preeditHighlightedAttrs = [defaultAttrs mutableCopy]; + preeditHighlightedAttrs[NSForegroundColorAttributeName] = [NSColor selectedTextColor]; + preeditHighlightedAttrs[NSFontAttributeName] = userFont; + preeditHighlightedAttrs[NSLigatureAttributeName] = @(0); + + NSMutableDictionary *pagingAttrs = [defaultAttrs mutableCopy]; + pagingAttrs[NSForegroundColorAttributeName] = theme.linear ? accentColor : [NSColor controlTextColor]; + + NSMutableDictionary *pagingHighlightedAttrs = [defaultAttrs mutableCopy]; + pagingHighlightedAttrs[NSForegroundColorAttributeName] = theme.linear + ? [NSColor alternateSelectedControlTextColor] : [NSColor selectedMenuItemTextColor]; + + NSMutableParagraphStyle *preeditParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + NSMutableParagraphStyle *pagingParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + NSMutableParagraphStyle *statusParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + + preeditParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; + preeditParagraphStyle.alignment = NSTextAlignmentLeft; + paragraphStyle.alignment = NSTextAlignmentLeft; + pagingParagraphStyle.alignment = NSTextAlignmentLeft; + statusParagraphStyle.alignment = NSTextAlignmentLeft; + + // 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 + preeditParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; + paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; + pagingParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; + statusParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; + + NSMutableDictionary *statusAttrs = [commentAttrs mutableCopy]; + statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; + + preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; + preeditHighlightedAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; + + [theme setAttrs:attrs + highlightedAttrs:highlightedAttrs + labelAttrs:labelAttrs + labelHighlightedAttrs:labelHighlightedAttrs + commentAttrs:commentAttrs + commentHighlightedAttrs:commentHighlightedAttrs + preeditAttrs:preeditAttrs + preeditHighlightedAttrs:preeditHighlightedAttrs + pagingAttrs:pagingAttrs + pagingHighlightedAttrs:pagingHighlightedAttrs + statusAttrs:statusAttrs]; - NSMutableDictionary *commentHighlightedAttrs = [commentAttrs mutableCopy]; - - NSMutableDictionary *preeditAttrs = [[NSMutableDictionary alloc] init]; - preeditAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - preeditAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSMutableDictionary *preeditHighlightedAttrs = [[NSMutableDictionary alloc] init]; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = [NSColor controlTextColor]; - preeditHighlightedAttrs[NSFontAttributeName] = [NSFont userFontOfSize:kDefaultFontSize]; - - NSParagraphStyle *paragraphStyle = [NSParagraphStyle defaultParagraphStyle]; - NSParagraphStyle *preeditParagraphStyle = [NSParagraphStyle defaultParagraphStyle]; - - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs]; [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle]; + preeditParagraphStyle:preeditParagraphStyle + pagingParagraphStyle:pagingParagraphStyle + statusParagraphStyle:statusParagraphStyle]; + + [theme setLabels:@[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"0"]]; + [theme setCandidateFormat:kDefaultCandidateFormat]; } - (instancetype)init { - self = [super initWithContentRect:_position styleMask:NSWindowStyleMaskNonactivatingPanel backing:NSBackingStoreBuffered defer:YES]; + self = [super initWithContentRect:_position + styleMask:NSWindowStyleMaskNonactivatingPanel | NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES]; + if (self) { self.alphaValue = 1.0; - // _window.level = NSScreenSaverWindowLevel + 1; - // ^ May fix visibility issue in fullscreen games. - self.level = CGShieldingWindowLevel(); - self.hasShadow = YES; + self.level = kCGCursorWindowLevel - 10; + self.hasShadow = NO; self.opaque = NO; + self.displaysWhenScreenProfileChanges = YES; self.backgroundColor = [NSColor clearColor]; NSView *contentView = [[NSView alloc] init]; - _view = [[SquirrelView alloc] initWithFrame:self.contentView.frame]; - _back = [[NSVisualEffectView alloc] init]; - _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; - _back.material = NSVisualEffectMaterialHUDWindow; - _back.state = NSVisualEffectStateActive; - _back.wantsLayer = YES; - _back.layer.mask = _view.shape; - [contentView addSubview:_back]; + _view = [[SquirrelView alloc] initWithFrame:self.contentView.bounds]; + if (@available(macOS 10.14, *)) { + _back = [[NSVisualEffectView alloc] init]; + _back.blendingMode = NSVisualEffectBlendingModeBehindWindow; + _back.material = NSVisualEffectMaterialHUDWindow; + _back.state = NSVisualEffectStateActive; + _back.wantsLayer = YES; + _back.layer.mask = _view.shape; + [contentView addSubview:_back]; + } [contentView addSubview:_view]; [contentView addSubview:_view.textView]; - + self.contentView = contentView; [self initializeUIStyleForDarkMode:NO]; - [self initializeUIStyleForDarkMode:YES]; - _maxHeight = 0; + if (@available(macOS 10.14, *)) { + [self initializeUIStyleForDarkMode:YES]; + } + _maxSize = NSZeroSize; + _initPosition = YES; } return self; } -- (NSPoint)mousePosition { - NSPoint point = NSEvent.mouseLocation; - point = [self convertPointFromScreen:point]; - return [_view convertPoint:point fromView:nil]; -} - - (void)sendEvent:(NSEvent *)event { switch (event.type) { - case NSEventTypeLeftMouseDown: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint: point index:&index]) { - if (index >= 0 && index < _candidates.count) { - _index = index; + case NSEventTypeLeftMouseDown: + case NSEventTypeRightMouseDown: { + NSPoint spot = [_view convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger cursorIndex = NSNotFound; + if ([_view convertClickSpot:spot toIndex:&cursorIndex]) { + if ((cursorIndex >= 0 && cursorIndex < _candidates.count) || + cursorIndex == NSPageUpFunctionKey || cursorIndex == NSPageDownFunctionKey) { + _index = cursorIndex; + _mouseDown = YES; } } } break; case NSEventTypeLeftMouseUp: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint: point index:&index]) { - if (index >= 0 && index < _candidates.count && index == _index) { - [_inputController selectCandidate:index]; + NSPoint spot = [_view convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger cursorIndex = NSNotFound; + if (_mouseDown && [_view convertClickSpot:spot toIndex:&cursorIndex]) { + if (cursorIndex == _index || cursorIndex == _turnPage) { + [_inputController perform:kSELECT onIndex:cursorIndex]; + } + } + _mouseDown = NO; + } break; + case NSEventTypeRightMouseUp: { + NSPoint spot = [_view convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger cursorIndex = NSNotFound; + if (_mouseDown && [_view convertClickSpot:spot toIndex:&cursorIndex]) { + if (cursorIndex == _index && (cursorIndex >= 0 && cursorIndex < _candidates.count)) { + [_inputController perform:kDELETE onIndex:cursorIndex]; } } + _mouseDown = NO; } break; case NSEventTypeMouseEntered: { self.acceptsMouseMovedEvents = YES; } break; case NSEventTypeMouseExited: { self.acceptsMouseMovedEvents = NO; - if (_cursorIndex != _index) { - [self showPreedit:_preedit selRange:_selRange caretPos:_caretPos candidates:_candidates comments:_comments labels:_labels - highlighted:_index update:NO]; - } } break; case NSEventTypeMouseMoved: { - NSPoint point = [self mousePosition]; - NSInteger index = -1; - if ([_view clickAtPoint: point index:&index]) { - if (index >= 0 && index < _candidates.count && _cursorIndex != index) { - [self showPreedit:_preedit selRange:_selRange caretPos:_caretPos candidates:_candidates comments:_comments labels:_labels - highlighted:index update:NO]; + NSPoint spot = [_view convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger cursorIndex = NSNotFound; + if ([_view convertClickSpot:spot toIndex:&cursorIndex]) { + if (cursorIndex >= 0 && cursorIndex < _candidates.count && _index != cursorIndex) { + [_inputController perform:kCHOOSE onIndex:cursorIndex]; + _index = cursorIndex; + [self showPreedit:_preedit selRange:_selRange caretPos:_caretPos + candidates:_candidates comments:_comments highlighted:cursorIndex + pageNum:_pageNum lastPage:_lastPage turnPage:NSNotFound update:NO]; + } else if ((cursorIndex == NSPageUpFunctionKey || cursorIndex == NSPageDownFunctionKey) && _turnPage != cursorIndex) { + _turnPage = cursorIndex; + [self showPreedit:_preedit selRange:_selRange caretPos:_caretPos + candidates:_candidates comments:_comments highlighted:_index + pageNum:_pageNum lastPage:_lastPage turnPage:cursorIndex update:NO]; } } } break; + case NSEventTypeLeftMouseDragged: { + _mouseDown = NO; + _maxSize = NSZeroSize; // reset the remember_size references after moving the panel + [self performWindowDragWithEvent:event]; + } break; case NSEventTypeScrollWheel: { + SquirrelTheme *theme = _view.currentTheme; + CGFloat scrollThreshold = [theme.attrs[NSParagraphStyleAttributeName] maximumLineHeight] + [theme.attrs[NSParagraphStyleAttributeName] lineSpacing]; if (event.phase == NSEventPhaseBegan) { - _scrollDirection = NSMakePoint(0, 0); - } else if (event.phase == NSEventPhaseEnded || (event.phase == NSEventPhaseNone && event.momentumPhase != NSEventPhaseNone)) { - if (_scrollDirection.x > 10 && ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) { - if (_view.currentTheme.vertical) { - [self.inputController pageUp:NO]; - } else { - [self.inputController pageUp:YES]; - } - } else if (_scrollDirection.x < -10 && ABS(_scrollDirection.x) > ABS(_scrollDirection.y)) { - if (_view.currentTheme.vertical) { - [self.inputController pageUp:YES]; - } else { - [self.inputController pageUp:NO]; - } - } else if (_scrollDirection.y > 10 && ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) { - [self.inputController pageUp:YES]; - } else if (_scrollDirection.y < -10 && ABS(_scrollDirection.x) < ABS(_scrollDirection.y)) { - [self.inputController pageUp:NO]; + _scrollLocus = NSZeroPoint; + } else if ((event.phase == NSEventPhaseNone || event.momentumPhase == NSEventPhaseNone) && + _scrollLocus.x != NSNotFound && _scrollLocus.y != NSNotFound) { + // determine scrolling direction by confining to sectors within ±30º of any axis + if (ABS(event.scrollingDeltaX) > ABS(event.scrollingDeltaY) * sqrt(3)) { + _scrollLocus.x += event.scrollingDeltaX * (event.hasPreciseScrollingDeltas ? 1 : 10); + } else if (ABS(event.scrollingDeltaY) > ABS(event.scrollingDeltaX) * sqrt(3)) { + _scrollLocus.y += event.scrollingDeltaY * (event.hasPreciseScrollingDeltas ? 1 : 10); } - _scrollDirection = NSMakePoint(0, 0); - } else if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) { - if (_scrollTime && [_scrollTime timeIntervalSinceNow] > 1.0) { - _scrollDirection = NSMakePoint(0, 0); + // compare accumulated locus length against threshold and limit paging to max once + if (_scrollLocus.x > scrollThreshold) { + [_inputController perform:kSELECT onIndex:(theme.vertical ? NSPageDownFunctionKey : NSPageUpFunctionKey)]; + _scrollLocus = NSMakePoint(NSNotFound, NSNotFound); + } else if (_scrollLocus.y > scrollThreshold) { + [_inputController perform:kSELECT onIndex:NSPageUpFunctionKey]; + _scrollLocus = NSMakePoint(NSNotFound, NSNotFound); + } else if (_scrollLocus.x < -scrollThreshold) { + [_inputController perform:kSELECT onIndex:(theme.vertical ? NSPageUpFunctionKey : NSPageDownFunctionKey)]; + _scrollLocus = NSMakePoint(NSNotFound, NSNotFound); + } else if (_scrollLocus.y < -scrollThreshold) { + [_inputController perform:kSELECT onIndex:NSPageDownFunctionKey]; + _scrollLocus = NSMakePoint(NSNotFound, NSNotFound); } - _scrollTime = [NSDate now]; - if ((_scrollDirection.y >= 0 && event.scrollingDeltaY > 0) || (_scrollDirection.y <= 0 && event.scrollingDeltaY < 0)) { - _scrollDirection.y += event.scrollingDeltaY; - } else { - _scrollDirection = NSMakePoint(0, 0); - } - if (ABS(_scrollDirection.y) > 10) { - if (_scrollDirection.y > 10) { - [self.inputController pageUp:YES]; - } else if (_scrollDirection.y < -10) { - [self.inputController pageUp:NO]; - } - _scrollDirection = NSMakePoint(0, 0); - } - } else { - _scrollDirection.x += event.scrollingDeltaX; - _scrollDirection.y += event.scrollingDeltaY; } - } + } break; default: break; } @@ -1129,124 +1628,177 @@ - (void)sendEvent:(NSEvent *)event { } - (void)getCurrentScreen { - // get current screen - _screenRect = [NSScreen mainScreen].frame; - NSArray *screens = [NSScreen screens]; - - NSUInteger i; - for (i = 0; i < screens.count; ++i) { - NSRect rect = [screens[i] frame]; - if (NSPointInRect(_position.origin, rect)) { - _screenRect = rect; - break; + _screen = [NSScreen mainScreen]; + NSArray *screens = [NSScreen screens]; + for (NSUInteger i = 0; i < screens.count; ++i) { + if (NSPointInRect(_position.origin, [screens[i] frame])) { + _screen = screens[i]; + return; } } } -- (CGFloat)getMaxTextWidth:(SquirrelTheme *)theme { - NSFont *currentFont = theme.attrs[NSFontAttributeName]; - CGFloat fontScale = currentFont.pointSize / 12; - CGFloat textWidthRatio = MIN(1.0, 1.0 / (theme.vertical ? 4 : 3) + fontScale / 12); - return theme.vertical - ? NSHeight(_screenRect) * textWidthRatio - theme.edgeInset.height * 2 - : NSWidth(_screenRect) * textWidthRatio - theme.edgeInset.width * 2; -} - -// Get the window size, the windows will be the dirtyRect in SquirrelView.drawRect -- (void)show { +- (void)getTextWidthLimit { [self getCurrentScreen]; + NSRect screenRect = _screen.visibleFrame; SquirrelTheme *theme = _view.currentTheme; + CGFloat textWidthRatio = MIN(1.0, 1.0 / (theme.vertical ? 4 : 3) + [theme.attrs[NSFontAttributeName] pointSize] / 144.0); + _textWidthLimit = (theme.vertical ? NSHeight(screenRect) : NSWidth(screenRect)) * textWidthRatio - theme.separatorWidth - theme.edgeInset.width * 2; + if (theme.lineLength > 0) { + _textWidthLimit = MIN(theme.lineLength, _textWidthLimit); + } + if (theme.tabled) { + CGFloat tabInterval = theme.separatorWidth * 2; + _textWidthLimit = floor((_textWidthLimit + theme.separatorWidth) / tabInterval) * tabInterval - theme.separatorWidth; + } + _view.textView.textContainer.size = NSMakeSize(_textWidthLimit, CGFLOAT_MAX); +} - NSAppearance *requestedAppearance = theme.native ? nil : [NSAppearance appearanceNamed:NSAppearanceNameAqua]; - if (self.appearance != requestedAppearance) { - self.appearance = requestedAppearance; +// Get the window size, it will be the dirtyRect in SquirrelView.drawRect +- (void)show { + if (@available(macOS 10.14, *)) { + NSAppearance *requestedAppearance = [NSAppearance appearanceNamed: + (_view.isDark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua)]; + if (self.appearance != requestedAppearance) { + self.appearance = requestedAppearance; + } } //Break line if the text is too long, based on screen size. - CGFloat textWidth = [self getMaxTextWidth:theme]; - CGFloat maxTextHeight = theme.vertical ? _screenRect.size.width - theme.edgeInset.width * 2 : _screenRect.size.height - theme.edgeInset.height * 2; - _view.textView.textContainer.containerSize = NSMakeSize(textWidth, maxTextHeight); - - NSRect windowRect; - // in vertical mode, the width and height are interchanged + SquirrelTheme *theme = _view.currentTheme; + NSTextContainer *textContainer = _view.textView.textContainer; + NSEdgeInsets insets = _view.insets; + CGFloat textWidthRatio = MIN(1.0, 1.0 / (theme.vertical ? 4 : 3) + [theme.attrs[NSFontAttributeName] pointSize] / 144.0); + NSRect screenRect = _screen.visibleFrame; + CGFloat textHeightLimit = (theme.vertical ? NSWidth(screenRect) : NSHeight(screenRect)) * textWidthRatio - insets.top - insets.bottom; + + // the sweep direction of the client app changes the behavior of adjusting squirrel panel position + BOOL sweepVertical = NSWidth(_position) > NSHeight(_position); NSRect contentRect = _view.contentRect; - if (theme.memorizeSize && ((theme.vertical && NSMidY(_position) / NSHeight(_screenRect) < 0.5) || - (!theme.vertical && NSMinX(_position)+MAX(contentRect.size.width, _maxHeight)+theme.edgeInset.width*2 > NSMaxX(_screenRect)))) { - if (contentRect.size.width >= _maxHeight) { - _maxHeight = contentRect.size.width; - } else { - contentRect.size.width = _maxHeight; - _view.textView.textContainer.containerSize = NSMakeSize(_maxHeight, maxTextHeight); + NSRect maxContentRect = contentRect; + if (theme.lineLength > 0) { // fixed line length (text width) + if (_statusMessage == nil) { // not applicable to status message + maxContentRect.size.width = _textWidthLimit; + } + } + if (theme.rememberSize) { // remember panel size (fix the top leading anchor of the panel in screen coordiantes) + if ((theme.vertical ? (NSMinY(_position) - NSMinY(screenRect) <= NSHeight(screenRect) * textWidthRatio + kOffsetHeight) + : (sweepVertical ? (NSMinX(_position) - NSMinX(screenRect) > NSWidth(screenRect) * textWidthRatio + kOffsetHeight) + : (NSMinX(_position) + MAX(NSWidth(maxContentRect), _maxSize.width) + insets.right > NSMaxX(screenRect)))) && + theme.lineLength == 0) { + if (NSWidth(maxContentRect) >= _maxSize.width) { + _maxSize.width = NSWidth(maxContentRect); + } else { + maxContentRect.size.width = _maxSize.width; + [textContainer setSize:NSMakeSize(_maxSize.width, textHeightLimit)]; + } + } + if (theme.vertical ? (NSMinX(_position) - NSMinX(screenRect) < MAX(NSHeight(maxContentRect), _maxSize.height) + insets.top + insets.bottom + (sweepVertical ? kOffsetHeight : 0)) + : (NSMinY(_position) - NSMinY(screenRect) < MAX(NSHeight(maxContentRect), _maxSize.height) + insets.top + insets.bottom + (sweepVertical ? 0 : kOffsetHeight))) { + if (NSHeight(maxContentRect) >= _maxSize.height) { + _maxSize.height = NSHeight(maxContentRect); + } else { + maxContentRect.size.height = _maxSize.height; + } } } + _initPosition |= NSIntersectsRect(self.frame, _position); + NSRect windowRect; if (theme.vertical) { - windowRect.size = NSMakeSize(contentRect.size.height + theme.edgeInset.height * 2, - contentRect.size.width + theme.edgeInset.width * 2); - // To avoid jumping up and down while typing, use the lower screen when typing on upper, and vice versa - if (NSMidY(_position) / NSHeight(_screenRect) >= 0.5) { - windowRect.origin.y = NSMinY(_position) - kOffsetHeight - NSHeight(windowRect); + windowRect.size = NSMakeSize(NSHeight(maxContentRect) + insets.top + insets.bottom, + NSWidth(maxContentRect) + insets.left + insets.right); + if (_initPosition ) { + // To avoid jumping up and down while typing, use the lower screen when typing on upper, and vice versa + if (NSMinY(_position) - NSMinY(screenRect) > NSHeight(screenRect) * textWidthRatio + kOffsetHeight) { + windowRect.origin.y = NSMinY(_position) + (sweepVertical ? insets.left : -kOffsetHeight) - NSHeight(windowRect); + } else { + windowRect.origin.y = NSMaxY(_position) + (sweepVertical ? 0 : kOffsetHeight); + } + // Make the right edge of candidate block fixed at the left of cursor + windowRect.origin.x = NSMinX(_position) - (sweepVertical ? kOffsetHeight : 0) - NSWidth(windowRect); + if (!sweepVertical && _view.preeditRange.length > 0) { + NSRect preeditRect = [_view contentRectForRange:_view.preeditRange]; + windowRect.origin.x += round(NSHeight(preeditRect) + [theme.preeditAttrs[NSFontAttributeName] descender] + insets.top); + } } else { - windowRect.origin.y = NSMaxY(_position) + kOffsetHeight; - } - // Make the first candidate fixed at the left of cursor - windowRect.origin.x = NSMinX(_position) - windowRect.size.width - kOffsetHeight; - if (_view.preeditRange.length > 0) { - NSSize preeditSize = [_view contentRectForRange:[_view convertRange:_view.preeditRange]].size; - windowRect.origin.x += preeditSize.height + theme.edgeInset.width; + windowRect.origin.x = NSMaxX(self.frame) - NSWidth(windowRect); + windowRect.origin.y = NSMaxY(self.frame) - NSHeight(windowRect); } } else { - windowRect.size = NSMakeSize(contentRect.size.width + theme.edgeInset.width * 2, - contentRect.size.height + theme.edgeInset.height * 2); - windowRect.origin = NSMakePoint(NSMinX(_position), - NSMinY(_position) - kOffsetHeight - NSHeight(windowRect)); + windowRect.size = NSMakeSize(NSWidth(maxContentRect) + insets.left + insets.right, + NSHeight(maxContentRect) + insets.top + insets.bottom); + 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(_position) - NSMinX(screenRect) > NSWidth(screenRect) * textWidthRatio + kOffsetHeight) { + windowRect.origin.x = NSMinX(_position) - kOffsetHeight - NSWidth(windowRect); + } else { + windowRect.origin.x = NSMaxX(_position) + kOffsetHeight; + } + windowRect.origin.y = NSMinY(_position) - NSHeight(windowRect); + } else { + windowRect.origin = NSMakePoint(NSMinX(_position) - insets.left, + NSMinY(_position) - kOffsetHeight - NSHeight(windowRect)); + } + } else { + windowRect.origin = NSMakePoint(NSMinX(self.frame), NSMaxY(self.frame) - NSHeight(windowRect)); + } } - if (NSMaxX(windowRect) > NSMaxX(_screenRect)) { - windowRect.origin.x = NSMaxX(_screenRect) - NSWidth(windowRect); + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = (_initPosition && sweepVertical ? NSMinX(_position) - kOffsetHeight : NSMaxX(screenRect)) - NSWidth(windowRect); } - if (NSMinX(windowRect) < NSMinX(_screenRect)) { - windowRect.origin.x = NSMinX(_screenRect); + if (NSMinX(windowRect) < NSMinX(screenRect)) { + windowRect.origin.x = _initPosition && sweepVertical ? NSMaxX(_position) + kOffsetHeight : NSMinX(screenRect); } - if (NSMinY(windowRect) < NSMinY(_screenRect)) { - if (theme.vertical) { - windowRect.origin.y = NSMinY(_screenRect); - } else { - windowRect.origin.y = NSMaxY(_position) + kOffsetHeight; - } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = _initPosition && !sweepVertical ? NSMaxY(_position) + kOffsetHeight : NSMinY(screenRect); } - if (NSMaxY(windowRect) > NSMaxY(_screenRect)) { - windowRect.origin.y = NSMaxY(_screenRect) - NSHeight(windowRect); + if (NSMaxY(windowRect) > NSMaxY(screenRect)) { + windowRect.origin.y = (_initPosition && !sweepVertical ? NSMinY(_position) - kOffsetHeight : NSMaxY(screenRect)) - NSHeight(windowRect); } - if (NSMinY(windowRect) < NSMinY(_screenRect)) { - windowRect.origin.y = NSMinY(_screenRect); + + if (theme.vertical) { + windowRect.origin.x += round(NSHeight(maxContentRect) - NSHeight(contentRect)); + windowRect.size.width -= round(NSHeight(maxContentRect) - NSHeight(contentRect)); + } else { + windowRect.origin.y += round(NSHeight(maxContentRect) - NSHeight(contentRect)); + windowRect.size.height -= round(NSHeight(maxContentRect) - NSHeight(contentRect)); } - [self setFrame:windowRect display:YES]; + // rotate the view, the core in vertical mode! if (theme.vertical) { - self.contentView.boundsRotation = -90; - _view.textView.boundsRotation = 0; - [self.contentView setBoundsOrigin:NSMakePoint(0, windowRect.size.width)]; - [_view.textView setBoundsOrigin:NSMakePoint(0, 0)]; - } else { - self.contentView.boundsRotation = 0; - _view.textView.boundsRotation = 0; - [self.contentView setBoundsOrigin:NSMakePoint(0, 0)]; - [_view.textView setBoundsOrigin:NSMakePoint(0, 0)]; - } - BOOL translucency = theme.translucency; - [_view setFrame:self.contentView.bounds]; - [_view.textView setFrame:self.contentView.bounds]; - if (translucency) { - [_back setFrame:self.contentView.bounds]; - _back.appearance = NSApp.effectiveAppearance; - [_back setHidden:NO]; + [self setFrame:[_screen backingAlignedRect:windowRect options:NSAlignMaxXOutward | NSAlignMaxYInward | NSAlignWidthNearest | NSAlignHeightNearest] display:YES]; + [self.contentView setBoundsRotation:-90.0]; + [self.contentView setBoundsOrigin:NSMakePoint(0.0, NSWidth(windowRect))]; } else { - [_back setHidden:YES]; + [self setFrame:[_screen backingAlignedRect:windowRect options:NSAlignMinXInward | NSAlignMaxYInward | NSAlignWidthNearest | NSAlignHeightNearest] display:YES]; + [self.contentView setBoundsRotation:0.0]; + [self.contentView setBoundsOrigin:NSZeroPoint]; + } + NSRect frameRect = self.contentView.bounds; + NSRect textFrameRect = NSMakeRect(NSMinX(frameRect) + insets.left, NSMinY(frameRect) + insets.bottom, + NSWidth(frameRect) - insets.left - insets.right, + NSHeight(frameRect) - insets.top - insets.bottom); + [_view.textView setBoundsRotation:0.0]; + [_view setBoundsOrigin:NSZeroPoint]; + [_view.textView setBoundsOrigin:NSZeroPoint]; + [_view setFrame:frameRect]; + [_view.textView setFrame:textFrameRect]; + + if (@available(macOS 10.14, *)) { + if (theme.translucency > 0) { + [_back setFrame:frameRect]; + [_back setAppearance:NSApp.effectiveAppearance]; + [_back setHidden:NO]; + } else { + [_back setHidden:YES]; + } } - self.alphaValue = theme.alpha; - [self invalidateShadow]; + [self setAlphaValue:theme.alpha]; [self orderFront:nil]; + _initPosition = NO; // voila ! } @@ -1256,32 +1808,172 @@ - (void)hide { _statusTimer = nil; } [self orderOut:nil]; - _maxHeight = 0; + _maxSize = NSZeroSize; + _initPosition = YES; +} + +- (void)setLayoutForRange:(NSRange)charRange + withReferenceFont:(NSFont *)refFont + paragraphStyle:(NSParagraphStyle *)style { + BOOL verticalLayout = _view.currentTheme.vertical; + CGFloat refFontHeight = refFont.ascender - refFont.descender; + CGFloat lineHeight = MAX(style.lineHeightMultiple > 0 ? refFontHeight * style.lineHeightMultiple : refFontHeight, + style.minimumLineHeight); + lineHeight = style.maximumLineHeight > 0 ? MIN(lineHeight, style.maximumLineHeight) : lineHeight; + if (@available(macOS 12.0, *)) { + NSUInteger i = charRange.location; + NSRange runRange = NSMakeRange(i, 0); + while (i < NSMaxRange(charRange)) { + NSDictionary *attrs = [_view.textStorage attributesAtIndex:i + longestEffectiveRange:&runRange inRange:charRange]; + NSNumber *baselineOffset = attrs[NSBaselineOffsetAttributeName]; + CGFloat offset = (baselineOffset ? baselineOffset.doubleValue : 0.0) + lineHeight / 2 - refFontHeight / 2; + NSNumber *superscript = attrs[NSSuperscriptAttributeName]; + if (verticalLayout) { + CTRubyAnnotationRef rubyAnnotation = (__bridge CTRubyAnnotationRef)(attrs[CFBridgingRelease(kCTRubyAnnotationAttributeName)]); + offset = rubyAnnotation ? 0.0 : offset; + } + if (superscript) { + NSFont *runFont = verticalLayout ? [attrs[NSFontAttributeName] verticalFont] : attrs[NSFontAttributeName]; + offset += superscript.integerValue == 1 ? runFont.descender / 3 : runFont.ascender / 3; + } + [_view.textStorage addAttribute:NSBaselineOffsetAttributeName + value:@(offset) range:runRange]; + i = NSMaxRange(runRange); + } + } else { + NSLayoutManager *layoutManager = _view.textView.layoutManager; + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:charRange actualCharacterRange:NULL]; + NSUInteger i = glyphRange.location; + NSRange lineRange = NSMakeRange(i, 0); + while (i < NSMaxRange(glyphRange)) { + NSRect rect = [layoutManager lineFragmentRectForGlyphAtIndex:i effectiveRange:&lineRange]; + NSRect usedRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:i effectiveRange:NULL]; + CGFloat alignment = usedRect.origin.y - rect.origin.y + (verticalLayout ? lineHeight / 2 : lineHeight / 2 + refFont.xHeight / 2); + // typesetting glyphs + NSUInteger j = lineRange.location; + while (j < NSMaxRange(lineRange)) { + NSPoint runGlyphPosition = [layoutManager locationForGlyphAtIndex:j]; + NSUInteger runCharLocation = [layoutManager characterIndexForGlyphAtIndex:j]; + NSRange runRange = [layoutManager rangeOfNominallySpacedGlyphsContainingIndex:j]; + NSDictionary *attrs = [_view.textStorage attributesAtIndex:runCharLocation effectiveRange:NULL]; + NSFont *runFont = attrs[NSFontAttributeName]; + NSFont *systemFont = [NSFont systemFontOfSize:runFont.pointSize]; + NSString *baselineClass = attrs[CFBridgingRelease(kCTBaselineClassAttributeName)]; + NSNumber *baselineOffset = attrs[NSBaselineOffsetAttributeName]; + CGFloat offset = baselineOffset ? baselineOffset.doubleValue : 0.0; + NSNumber *superscript = attrs[NSSuperscriptAttributeName]; + if (verticalLayout) { + NSNumber *verticalGlyph = attrs[NSVerticalGlyphFormAttributeName]; + if (verticalGlyph ? verticalGlyph.boolValue : YES) { + runFont = runFont.verticalFont; + systemFont = systemFont.verticalFont; + } + } + CGFloat runFontHeight = runFont.ascender - runFont.descender; + CGFloat systemFontHeight = systemFont.ascender - systemFont.descender; + if (superscript) { + offset += superscript.integerValue == 1 ? refFont.ascender / 3 : refFont.descender / 3; + } + if (verticalLayout) { + if ([baselineClass isEqualToString:CFBridgingRelease(kCTBaselineClassRoman)] || !runFont.vertical) { + runGlyphPosition.y = alignment - offset + refFont.xHeight / 2; + } else { + runGlyphPosition.y = alignment - offset + ([runFont.fontName isEqualToString:@"AppleColorEmoji"] ? (runFontHeight - systemFontHeight) / 3 : 0.0); + runGlyphPosition.x += [runFont.fontName isEqualToString:@"AppleColorEmoji"] ? (runFontHeight - systemFontHeight) * 2 / 3 : 0.0; + } + } else { + runGlyphPosition.y = alignment - offset + ([baselineClass isEqualToString:CFBridgingRelease(kCTBaselineClassIdeographicCentered)] ? runFont.xHeight / 2 - refFont.xHeight / 2 : 0.0); + } + [layoutManager setLocation:runGlyphPosition forStartOfGlyphRange:runRange]; + j = NSMaxRange(runRange); + } + i = NSMaxRange(lineRange); + } + } +} + +- (BOOL)shouldBreakLineWithRange:(NSRange)range { + [_view.textStorage fixFontAttributeInRange:range]; + if (@available(macOS 12.0, *)) { + NSTextRange *textRange = [_view getTextRangeFromRange:range]; + NSUInteger __block lineCount = 0; + [_view.textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options:NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^(NSTextRange *segRange, CGRect segFrame, CGFloat baseline, NSTextContainer *textContainer) { + ++lineCount; + return YES; + }]; + return lineCount > 1; + } else { + NSRange glyphRange = [_view.textView.layoutManager glyphRangeForCharacterRange:range + actualCharacterRange:NULL]; + NSUInteger loc = glyphRange.location; + NSRange lineRange = NSMakeRange(loc, 0); + NSUInteger lineCount = 0; + while (loc < NSMaxRange(glyphRange)) { + [_view.textView.layoutManager lineFragmentUsedRectForGlyphAtIndex:loc + effectiveRange:&lineRange]; + ++lineCount; + loc = NSMaxRange(lineRange); + } + return lineCount > 1; + } +} + +- (BOOL)shouldUseTabsInRange:(NSRange)range maxLineLength:(CGFloat *)maxLineLength { + [_view.textStorage fixFontAttributeInRange:range]; + if (@available(macOS 12.0, *)) { + NSTextRange *textRange = [_view getTextRangeFromRange:range]; + CGFloat __block rangeEdge; + [_view.textView.textLayoutManager + enumerateTextSegmentsInRange:textRange + type:NSTextLayoutManagerSegmentTypeStandard + options:NSTextLayoutManagerSegmentOptionsRangeNotRequired + usingBlock:^(NSTextRange *segRange, CGRect segFrame, CGFloat baseline, NSTextContainer *textContainer) { + rangeEdge = NSMaxX(segFrame); + return YES; + }]; + [_view.textView.textLayoutManager ensureLayoutForRange:_view.textView.textContentStorage.documentRange]; + NSRect container = [_view.textView.textLayoutManager usageBoundsForTextContainer]; + *maxLineLength = MAX(MIN(_textWidthLimit, NSWidth(container)), _maxSize.width); + return NSMinX(container) + *maxLineLength > rangeEdge; + } else { + NSUInteger glyphIndex = [_view.textView.layoutManager glyphIndexForCharacterAtIndex:range.location]; + CGFloat rangeEdge = NSMaxX([_view.textView.layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphIndex effectiveRange:NULL]); + NSRect container = [_view.textView.layoutManager usedRectForTextContainer:_view.textView.textContainer]; + *maxLineLength = MAX(MIN(_textWidthLimit, NSWidth(container)), _maxSize.width); + return NSMinX(container) + *maxLineLength > rangeEdge; + } } // Main function to add attributes to text output from librime - (void)showPreedit:(NSString *)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos - candidates:(NSArray *)candidates - comments:(NSArray *)comments - labels:(NSArray *)labels + candidates:(NSArray *)candidates + comments:(NSArray *)comments highlighted:(NSUInteger)index + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage + turnPage:(NSUInteger)turnPage update:(BOOL)update { - if (update) { _preedit = preedit; _selRange = selRange; _caretPos = caretPos; _candidates = candidates; _comments = comments; - _labels = labels; _index = index; + _pageNum = pageNum; + _lastPage = lastPage; } - _cursorIndex = index; - + + [self getTextWidthLimit]; NSUInteger numCandidates = candidates.count; - if (numCandidates || (preedit && preedit.length)) { + if (numCandidates > 0 || (preedit && preedit.length)) { _statusMessage = nil; if (_statusTimer) { [_statusTimer invalidate]; @@ -1297,202 +1989,229 @@ - (void)showPreedit:(NSString *)preedit return; } - SquirrelTheme *theme = _view.currentTheme; - [self getCurrentScreen]; - CGFloat maxTextWidth = [self getMaxTextWidth:theme]; + if (numCandidates == 0) { + _index = index = NSNotFound; + } + _turnPage = turnPage; + if (_turnPage == NSPageUpFunctionKey) { + turnPage = pageNum ? NSPageUpFunctionKey : NSBeginFunctionKey; + } else if (_turnPage == NSPageDownFunctionKey) { + turnPage = lastPage ? NSEndFunctionKey : NSPageDownFunctionKey; + } - NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init]; - NSUInteger candidateStartPos = 0; + SquirrelTheme *theme = _view.currentTheme; + _view.textView.layoutOrientation = theme.vertical + ? NSTextLayoutOrientationVertical : NSTextLayoutOrientationHorizontal; + if (theme.lineLength > 0) { + _maxSize.width = MIN(theme.lineLength, _textWidthLimit); + } + NSEdgeInsets insets = NSEdgeInsetsMake(theme.edgeInset.height + theme.linespace / 2, + theme.edgeInset.width + theme.separatorWidth / 2, + theme.edgeInset.height + theme.linespace / 2, + theme.edgeInset.width + theme.separatorWidth / 2); + + NSTextStorage *text = _view.textStorage; + [text setAttributedString:[[NSMutableAttributedString alloc] init]]; NSRange preeditRange = NSMakeRange(NSNotFound, 0); NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); + NSMutableArray *candidateRanges = [[NSMutableArray alloc] initWithCapacity:numCandidates]; + NSRange pagingRange = NSMakeRange(NSNotFound, 0); + NSMutableParagraphStyle *paragraphStyleCandidate; + // preedit if (preedit) { - NSMutableAttributedString *line = [[NSMutableAttributedString alloc] init]; + NSMutableAttributedString *preeditLine = [[NSMutableAttributedString alloc] init]; if (selRange.location > 0) { - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringToIndex:selRange.location] - attributes:theme.preeditAttrs]]; + [preeditLine appendAttributedString:[[NSAttributedString alloc] + initWithString:[preedit substringToIndex:selRange.location] + attributes:theme.preeditAttrs]]; } if (selRange.length > 0) { - NSUInteger highlightedPreeditStart = line.length; - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringWithRange:selRange] - attributes:theme.preeditHighlightedAttrs]]; - highlightedPreeditRange = NSMakeRange(highlightedPreeditStart, line.length - highlightedPreeditStart); + NSUInteger highlightedPreeditStart = preeditLine.length; + [preeditLine appendAttributedString:[[NSAttributedString alloc] + initWithString:[preedit substringWithRange:selRange] + attributes:theme.preeditHighlightedAttrs]]; + highlightedPreeditRange = NSMakeRange(highlightedPreeditStart, + preeditLine.length - highlightedPreeditStart); } if (NSMaxRange(selRange) < preedit.length) { - [line - appendAttributedString: - [[NSAttributedString alloc] - initWithString:[preedit substringFromIndex:NSMaxRange(selRange)] - attributes:theme.preeditAttrs]]; - } - [text appendAttributedString:line]; - - [text addAttribute:NSParagraphStyleAttributeName - value:theme.preeditParagraphStyle - range:NSMakeRange(0, text.length)]; - - preeditRange = NSMakeRange(0, text.length); - if (numCandidates) { - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\n" - attributes:theme.preeditAttrs]]; - } - candidateStartPos = text.length; - } - - NSMutableArray *candidateRanges = [[NSMutableArray alloc] init]; - // candidates - NSUInteger i; - for (i = 0; i < candidates.count; ++i) { - NSMutableAttributedString *line = [[NSMutableAttributedString alloc] init]; - - NSDictionary *attrs; - NSDictionary *labelAttrs; - NSDictionary *commentAttrs; - if (i == index) { - attrs = theme.highlightedAttrs; - labelAttrs = theme.labelHighlightedAttrs; - commentAttrs = theme.commentHighlightedAttrs; + [preeditLine appendAttributedString:[[NSAttributedString alloc] + initWithString:[preedit substringFromIndex:NSMaxRange(selRange)] + attributes:theme.preeditAttrs]]; + } + // force caret to be rendered horizontally in vertical layout + if (caretPos != NSNotFound) { + [preeditLine addAttributes:@{NSVerticalGlyphFormAttributeName: @NO} + range:NSMakeRange(caretPos - (caretPos < NSMaxRange(selRange)), 1)]; + } + preeditRange = NSMakeRange(0, preeditLine.length); + [text appendAttributedString:preeditLine]; + + insets.top = theme.edgeInset.height; + if (numCandidates > 0) { + [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.preeditAttrs]]; } else { - attrs = theme.attrs; - labelAttrs = theme.labelAttrs; - commentAttrs = theme.commentAttrs; - } - - CGFloat labelWidth = 0.0; - - if (theme.prefixLabelFormat != nil) { - NSString *labelString; - if (labels.count > 1 && i < labels.count) { - NSString *labelFormat = [theme.prefixLabelFormat stringByReplacingOccurrencesOfString:@"%c" withString:@"%@"]; - labelString = [NSString stringWithFormat:labelFormat, labels[i]]; - } else if (labels.count == 1 && i < [labels[0] length]) { - // custom: A. B. C... - char labelCharacter = [labels[0] characterAtIndex:i]; - labelString = [NSString stringWithFormat:theme.prefixLabelFormat, labelCharacter]; - } else { - // default: 1. 2. 3... - NSString *labelFormat = [theme.prefixLabelFormat stringByReplacingOccurrencesOfString:@"%c" withString:@"%lu"]; - labelString = [NSString stringWithFormat:labelFormat, i+1]; - } + insets.bottom = theme.edgeInset.height; + goto typesetter; + } + } - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:labelString - attributes:labelAttrs]]; - // get the label size for indent - if (!theme.linear) { - NSMutableAttributedString *str = [line mutableCopy]; - if (theme.vertical) { - [str addAttribute:NSVerticalGlyphFormAttributeName value:@(1) range:NSMakeRange(0, str.length)]; - } - labelWidth = [str boundingRectWithSize:NSZeroSize options:NSStringDrawingUsesLineFragmentOrigin].size.width; - } + // candidate items + NSUInteger candidateBlockStart = text.length; + NSUInteger lineStart = text.length; + if (theme.linear) { + paragraphStyleCandidate = [theme.paragraphStyle copy]; + } + CGFloat tabInterval = theme.separatorWidth * 2; + for (NSUInteger idx = 0; idx < candidates.count; ++idx) { + // attributed labels are already included in candidateFormats + NSMutableAttributedString *item = (idx == index) ? [theme.candidateHighlightedFormats[idx] mutableCopy] : [theme.candidateFormats[idx] mutableCopy]; + NSRange candidateRange = [item.string rangeOfString:@"%@"]; + // get the label size for indent + CGFloat labelWidth = theme.linear ? 0.0 : ceil([item attributedSubstringFromRange:NSMakeRange(0, candidateRange.location)].size.width); + + [item replaceCharactersInRange:candidateRange withString:candidates[idx]]; + + NSRange commentRange = [item.string rangeOfString:kTipSpecifier]; + if (idx < comments.count && [comments[idx] length] != 0) { + [item replaceCharactersInRange:commentRange withString:[@" " stringByAppendingString:comments[idx]]]; + } else { + [item deleteCharactersInRange:commentRange]; } - NSUInteger candidateStart = line.length; - NSString *candidate = candidates[i]; - NSAttributedString *candidateAttributedString = [[NSAttributedString alloc] - initWithString:candidate - attributes:attrs]; - CGFloat candidateWidth = [candidateAttributedString boundingRectWithSize:NSZeroSize options:NSStringDrawingUsesLineFragmentOrigin].size.width; - if (candidateWidth <= maxTextWidth * 0.2) { - // Unicode Word Joiner - candidateAttributedString = insert(@"\u2060", candidateAttributedString); - } - - [line appendAttributedString:candidateAttributedString]; - - // Use left-to-right marks to prevent right-to-left text from changing the - // layout of non-candidate text. - [line addAttribute:NSWritingDirectionAttributeName value:@[@0] range:NSMakeRange(candidateStart, line.length-candidateStart)]; - - if (theme.suffixLabelFormat != nil) { - NSString *labelString; - if (labels.count > 1 && i < labels.count) { - NSString *labelFormat = [theme.suffixLabelFormat stringByReplacingOccurrencesOfString:@"%c" withString:@"%@"]; - labelString = [NSString stringWithFormat:labelFormat, labels[i]]; - } else if (labels.count == 1 && i < [labels[0] length]) { - // custom: A. B. C... - char labelCharacter = [labels[0] characterAtIndex:i]; - labelString = [NSString stringWithFormat:theme.suffixLabelFormat, labelCharacter]; - } else { - // default: 1. 2. 3... - NSString *labelFormat = [theme.suffixLabelFormat stringByReplacingOccurrencesOfString:@"%c" withString:@"%lu"]; - labelString = [NSString stringWithFormat:labelFormat, i+1]; + [item formatMarkDown]; + [item annotateRuby]; + if (!theme.linear) { + paragraphStyleCandidate = [theme.paragraphStyle mutableCopy]; + paragraphStyleCandidate.headIndent = labelWidth; + } + [item addAttribute:NSParagraphStyleAttributeName + value:paragraphStyleCandidate + range:NSMakeRange(0, item.length)]; + + // determine if the line is too wide and line break is needed, based on screen size. + if (lineStart != text.length) { + NSUInteger separatorStart = text.length; + NSMutableAttributedString *separator = [[NSMutableAttributedString alloc] initWithString:theme.linear ? (theme.tabled ? [kFullWidthSpace stringByAppendingString:@"\t"] : kFullWidthSpace) : @"\n" attributes:theme.commentAttrs]; + if (theme.tabled) { + CGFloat widthInTabs = (ceil([text attributedSubstringFromRange:candidateRanges.lastObject.rangeValue].size.width) + theme.separatorWidth) / tabInterval; + NSUInteger numPaddingTabs = pow(2, ceil(log2(widthInTabs))) - ceil(widthInTabs); + [separator replaceCharactersInRange:NSMakeRange(2, 0) withString:[@"\t" stringByPaddingToLength:numPaddingTabs withString:@"\t" startingAtIndex:0]]; } - [line appendAttributedString: - [[NSAttributedString alloc] - initWithString:labelString - attributes:labelAttrs]]; - } - - if (i < comments.count && [comments[i] length] != 0) { - CGFloat candidateAndLabelWidth = [line boundingRectWithSize:NSZeroSize options:NSStringDrawingUsesLineFragmentOrigin].size.width; - NSString *comment = comments[i]; - NSAttributedString *commentAttributedString = [[NSAttributedString alloc] - initWithString:comment - attributes:commentAttrs]; - CGFloat commentWidth = [commentAttributedString boundingRectWithSize:NSZeroSize options:NSStringDrawingUsesLineFragmentOrigin].size.width; - if (commentWidth <= maxTextWidth * 0.2) { - // Unicode Word Joiner - commentAttributedString = insert(@"\u2060", commentAttributedString); + [separator addAttribute:NSVerticalGlyphFormAttributeName value:@(NO) + range:NSMakeRange(0, separator.length)]; + NSRange separatorRange = NSMakeRange(separatorStart, separator.length); + [text appendAttributedString:separator]; + [text appendAttributedString:item]; + if (theme.linear && (ceil(item.size.width) > _textWidthLimit || [self shouldBreakLineWithRange:NSMakeRange(lineStart, text.length - lineStart)])) { + [text replaceCharactersInRange:separatorRange withString:theme.tabled ? [kFullWidthSpace stringByAppendingString:@"\n"] : @"\n"]; + lineStart = separatorStart + (theme.tabled ? 2 : 1); } - - NSString *commentSeparator; - if (candidateAndLabelWidth + commentWidth <= maxTextWidth * 0.3) { - // Non-Breaking White Space - commentSeparator = @"\u00A0"; - } else { - commentSeparator = @" "; + } else { // at the start of a new line, no need to determine line break + [text appendAttributedString:item]; + } + // for linear layout, middle-truncate candidates that are longer than one line + if (theme.linear && ceil(item.size.width) > _textWidthLimit) { + if (idx < numCandidates - 1 || theme.showPaging) { + [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.commentAttrs]]; } - [line appendAttributedString:[[NSAttributedString alloc] - initWithString:commentSeparator - attributes:commentAttrs]]; - [line appendAttributedString:commentAttributedString]; + NSMutableParagraphStyle *paragraphStyleTruncating = [paragraphStyleCandidate mutableCopy]; + paragraphStyleTruncating.lineBreakMode = NSLineBreakByTruncatingMiddle; + [text addAttribute:NSParagraphStyleAttributeName value:paragraphStyleTruncating range:NSMakeRange(lineStart, item.length)]; + [candidateRanges addObject:[NSValue valueWithRange:NSMakeRange(lineStart, text.length - lineStart - 1)]]; + lineStart = text.length; + } else { + [candidateRanges addObject:[NSValue valueWithRange:NSMakeRange(text.length - item.length, item.length)]]; } + } - NSAttributedString *separator = [[NSMutableAttributedString alloc] - initWithString:(theme.linear ? @" " : @"\n") - attributes:attrs]; - - NSMutableAttributedString *str = [separator mutableCopy]; - if (theme.vertical) { - [str addAttribute:NSVerticalGlyphFormAttributeName value:@(1) range:NSMakeRange(0, str.length)]; + // paging indication + if (theme.showPaging) { + NSMutableAttributedString *paging = [[NSMutableAttributedString alloc] + initWithAttributedString:(pageNum > 0 ? theme.symbolBackFill : theme.symbolBackStroke)]; + [paging appendAttributedString:[[NSAttributedString alloc] + initWithString:[NSString stringWithFormat:@" %lu ", pageNum + 1] attributes:theme.pagingAttrs]]; + [paging appendAttributedString:[[NSAttributedString alloc] + initWithAttributedString:(lastPage ? theme.symbolForwardStroke : theme.symbolForwardFill)]]; + if (_turnPage == NSPageUpFunctionKey) { + [paging addAttributes:theme.pagingHighlightedAttrs range:NSMakeRange(0, 1)]; + } else if (_turnPage == NSPageDownFunctionKey) { + [paging addAttributes:theme.pagingHighlightedAttrs range:NSMakeRange(paging.length - 1, 1)]; } - _view.seperatorWidth = [str boundingRectWithSize:NSZeroSize options:0].size.width; - NSMutableParagraphStyle *paragraphStyleCandidate = [theme.paragraphStyle mutableCopy]; - if (i == 0) { - paragraphStyleCandidate.paragraphSpacingBefore = theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2; - } else { - [text appendAttributedString:separator]; - } + [text appendAttributedString:[[NSAttributedString alloc] initWithString:theme.linear ? (theme.tabled ? [kFullWidthSpace stringByAppendingString:@"\t"] : kFullWidthSpace) : @"\n" attributes:theme.commentAttrs]]; + NSUInteger pagingStart = text.length; + CGFloat maxLineLength; + [text appendAttributedString:paging]; if (theme.linear) { - paragraphStyleCandidate.lineSpacing = theme.linespace; + if ([self shouldBreakLineWithRange:NSMakeRange(lineStart, text.length - lineStart)]) { + [text replaceCharactersInRange:NSMakeRange(pagingStart - 1, 1) withString:theme.tabled ? @"\n\t" : [@"\n" stringByAppendingString:kFullWidthSpace]]; + lineStart = pagingStart; + pagingStart += 1; + } + if ([self shouldUseTabsInRange:NSMakeRange(pagingStart, paging.length) maxLineLength:&maxLineLength]) { + paragraphStyleCandidate = [theme.paragraphStyle mutableCopy]; + if (theme.tabled) { + maxLineLength = ceil(maxLineLength / tabInterval) * tabInterval - theme.separatorWidth; + } else { + [text replaceCharactersInRange:NSMakeRange(pagingStart - 1, 1) withString:@"\t"]; + } + CGFloat candidateEndPosition = ceil([text attributedSubstringFromRange:NSMakeRange(lineStart, pagingStart - 1 - lineStart)].size.width); + NSMutableArray *tabStops = [[NSMutableArray alloc] init]; + for (NSUInteger i = 1; tabInterval * i < candidateEndPosition; ++i) { + [tabStops addObject:[[NSTextTab alloc] initWithType:NSLeftTabStopType location:tabInterval * i]]; + } + [tabStops addObject:[[NSTextTab alloc] initWithType:NSRightTabStopType location:maxLineLength]]; + paragraphStyleCandidate.tabStops = tabStops; + } + [text addAttribute:NSParagraphStyleAttributeName + value:paragraphStyleCandidate + range:NSMakeRange(lineStart, text.length - lineStart)]; + } else { + NSMutableParagraphStyle *paragraphStylePaging = [theme.pagingParagraphStyle mutableCopy]; + if ([self shouldUseTabsInRange:NSMakeRange(pagingStart, paging.length) maxLineLength:&maxLineLength]) { + [text replaceCharactersInRange:NSMakeRange(pagingStart + 1, 1) withString:@"\t"]; + [text replaceCharactersInRange:NSMakeRange(pagingStart + paging.length - 2, 1) withString:@"\t"]; + paragraphStylePaging.tabStops = @[[[NSTextTab alloc] initWithType:NSCenterTabStopType location:maxLineLength / 2], + [[NSTextTab alloc] initWithType:NSRightTabStopType location:maxLineLength]]; + } + [text addAttribute:NSParagraphStyleAttributeName + value:paragraphStylePaging + range:NSMakeRange(pagingStart, paging.length)]; + insets.bottom = theme.edgeInset.height; } - paragraphStyleCandidate.headIndent = labelWidth; - [line addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate - range:NSMakeRange(0, line.length)]; + pagingRange = NSMakeRange(text.length - paging.length, paging.length); + } - NSRange candidateRange = NSMakeRange(text.length, line.length); - [candidateRanges addObject: [NSValue valueWithRange:candidateRange]]; - [text appendAttributedString:line]; +typesetter: + [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; + if (preedit) { + [self setLayoutForRange:preeditRange + withReferenceFont:(theme.vertical ? [theme.preeditAttrs[NSFontAttributeName] verticalFont] : theme.preeditAttrs[NSFontAttributeName]) + paragraphStyle:theme.preeditParagraphStyle]; + } + if (numCandidates > 0) { + NSRange candidateBlockRange = NSMakeRange(candidateBlockStart, (!theme.linear && pagingRange.length > 0 ? pagingRange.location : text.length) - candidateBlockStart); + [self setLayoutForRange:candidateBlockRange + withReferenceFont:(theme.vertical ? [theme.attrs[NSFontAttributeName] verticalFont] : theme.attrs[NSFontAttributeName]) + paragraphStyle:theme.paragraphStyle]; + if (!theme.linear && pagingRange.length > 0) { + [self setLayoutForRange:pagingRange + withReferenceFont:theme.pagingAttrs[NSFontAttributeName] + paragraphStyle:theme.pagingParagraphStyle]; + } } // text done! - [_view.textView.textContentStorage setAttributedString:text]; - if (theme.vertical) { - _view.textView.layoutOrientation = NSTextLayoutOrientationVertical; - } else { - _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal; - } - [_view drawViewWith:candidateRanges hilightedIndex:index preeditRange:preeditRange highlightedPreeditRange:highlightedPreeditRange]; + [self setAnimationBehavior:caretPos == NSNotFound + ? NSWindowAnimationBehaviorUtilityWindow : NSWindowAnimationBehaviorDefault]; + [_view drawViewWithInsets:insets + candidateRanges:candidateRanges + highlightedIndex:index + preeditRange:preeditRange + highlightedPreeditRange:highlightedPreeditRange + pagingRange:pagingRange + pagingButton:turnPage]; [self show]; } @@ -1517,25 +2236,34 @@ - (void)updateStatusLong:(NSString *)messageLong statusShort:(NSString *)message - (void)showStatus:(NSString *)message { SquirrelTheme *theme = _view.currentTheme; - NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:message attributes:theme.attrs]; - [text addAttribute:NSParagraphStyleAttributeName - value:theme.paragraphStyle - range:NSMakeRange(0, text.length)]; - - [_view.textView.textContentStorage setAttributedString:text]; - if (theme.vertical) { - _view.textView.layoutOrientation = NSTextLayoutOrientationVertical; - } else { - _view.textView.layoutOrientation = NSTextLayoutOrientationHorizontal; - } - NSRange emptyRange = NSMakeRange(NSNotFound, 0); - NSArray *candidateRanges = @[[NSValue valueWithRange: NSMakeRange(0, text.length)]]; - [_view drawViewWith:candidateRanges hilightedIndex:-1 preeditRange:emptyRange highlightedPreeditRange:emptyRange]; - [self show]; - + NSEdgeInsets insets = NSEdgeInsetsMake(theme.edgeInset.height, theme.edgeInset.width + theme.separatorWidth / 2, + theme.edgeInset.height, theme.edgeInset.width + theme.separatorWidth / 2); + _view.textView.layoutOrientation = theme.vertical ? + NSTextLayoutOrientationVertical : NSTextLayoutOrientationHorizontal; + + NSTextStorage *text = _view.textStorage; + [text setAttributedString:[[NSMutableAttributedString alloc] initWithString:message attributes:theme.statusAttrs]]; + + [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; + [self setLayoutForRange:NSMakeRange(0, text.length) + withReferenceFont:(theme.vertical ? [theme.statusAttrs[NSFontAttributeName] verticalFont] : theme.statusAttrs[NSFontAttributeName]) + paragraphStyle:theme.statusParagraphStyle]; + + // disable remember_size and fixed line_length for status messages + _initPosition = YES; + _maxSize = NSZeroSize; if (_statusTimer) { [_statusTimer invalidate]; } + [self setAnimationBehavior:NSWindowAnimationBehaviorUtilityWindow]; + [_view drawViewWithInsets:insets + candidateRanges:@[] + highlightedIndex:NSNotFound + preeditRange:NSMakeRange(NSNotFound, 0) + highlightedPreeditRange:NSMakeRange(NSNotFound, 0) + pagingRange:NSMakeRange(NSNotFound, 0) + pagingButton:NSNotFound]; + [self show]; _statusTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration target:self selector:@selector(hideStatus:) @@ -1547,67 +2275,101 @@ - (void)hideStatus:(NSTimer *)timer { [self hide]; } -static inline NSColor *blendColors(NSColor *foregroundColor, - NSColor *backgroundColor) { - if (!backgroundColor) { - // return foregroundColor; +static inline NSColor * blendColors(NSColor *foregroundColor, + NSColor *backgroundColor) { + if (!backgroundColor) { // return foregroundColor; backgroundColor = [NSColor lightGrayColor]; } - return [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction ofColor:backgroundColor] + return [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction + ofColor:backgroundColor] colorWithAlphaComponent:foregroundColor.alphaComponent]; } -static NSFontDescriptor *getFontDescriptor(NSString *fullname) { - if (fullname == nil) { +static inline NSColor * inverseColor(NSColor *color) { + if (color == nil) { return nil; + } else { + return [NSColor colorWithColorSpace:color.colorSpace + hue:color.hueComponent + saturation:color.saturationComponent + brightness:1 - color.brightnessComponent + alpha:color.alphaComponent]; } +} - NSArray *fontNames = [fullname componentsSeparatedByString:@","]; - NSMutableArray *validFontDescriptors = [NSMutableArray arrayWithCapacity:fontNames.count]; +static NSFontDescriptor * getFontDescriptor(NSString *fullname) { + if (fullname == nil) { + return nil; + } + NSArray *fontNames = [fullname componentsSeparatedByString:@","]; + NSMutableArray *validFontDescriptors = [[NSMutableArray alloc] initWithCapacity:fontNames.count]; for (__strong NSString *fontName in fontNames) { fontName = [fontName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([NSFont fontWithName:fontName size:0.0] != nil) { // 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. - [validFontDescriptors addObject:[NSFontDescriptor fontDescriptorWithName:fontName - size:0.0]]; + NSFontDescriptor *fontDescriptor = [NSFontDescriptor fontDescriptorWithName:fontName size:0.0]; + NSFontDescriptor *UIFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitUIOptimized]; + [validFontDescriptors addObject:([NSFont fontWithDescriptor:UIFontDescriptor size:0.0] != nil ? UIFontDescriptor : fontDescriptor)]; } } if (validFontDescriptors.count == 0) { return nil; - } else if (validFontDescriptors.count == 1) { - return validFontDescriptors[0]; } - NSFontDescriptor *initialFontDescriptor = validFontDescriptors[0]; - NSArray *fallbackDescriptors = [validFontDescriptors - subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)]; - NSDictionary *attributes = @{NSFontCascadeListAttribute : fallbackDescriptors}; + NSFontDescriptor *emojiFontDescriptor = [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0]; + NSArray *fallbackDescriptors = + [[validFontDescriptors subarrayWithRange:NSMakeRange(1, validFontDescriptors.count - 1)] + arrayByAddingObject:emojiFontDescriptor]; + NSDictionary *attributes = @{NSFontCascadeListAttribute: fallbackDescriptors}; return [initialFontDescriptor fontDescriptorByAddingAttributes:attributes]; } -static void updateCandidateListLayout(BOOL *isLinearCandidateList, SquirrelConfig *config, NSString *prefix) { - NSString* candidateListLayout = [config getString:[prefix stringByAppendingString:@"/candidate_list_layout"]]; +static CGFloat getLineHeight(NSFont *font, BOOL vertical) { + if (vertical) { + font = font.verticalFont; + } + CGFloat lineHeight = 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 = MAX(lineHeight, fallbackFont.ascender - fallbackFont.descender); + } + return lineHeight; +} + +static void updateCandidateListLayout(BOOL *isLinearCandidateList, BOOL *isTabledCandidateList, SquirrelConfig *config, NSString *prefix) { + NSString *candidateListLayout = [config getString:[prefix stringByAppendingString:@"/candidate_list_layout"]]; if ([candidateListLayout isEqualToString:@"stacked"]) { - *isLinearCandidateList = false; + *isLinearCandidateList = NO; + *isTabledCandidateList = NO; } else if ([candidateListLayout isEqualToString:@"linear"]) { - *isLinearCandidateList = true; + *isLinearCandidateList = YES; + *isTabledCandidateList = NO; + } else if ([candidateListLayout isEqualToString:@"tabled"]) { + // `tabled` is a derived layout of `linear`; tabled implies linear + *isLinearCandidateList = YES; + *isTabledCandidateList = YES; } else { // Deprecated. Not to be confused with text_orientation: horizontal NSNumber *horizontal = [config getOptionalBool:[prefix stringByAppendingString:@"/horizontal"]]; if (horizontal) { *isLinearCandidateList = horizontal.boolValue; + *isTabledCandidateList = NO; } } } static void updateTextOrientation(BOOL *isVerticalText, SquirrelConfig *config, NSString *prefix) { - NSString* textOrientation = [config getString:[prefix stringByAppendingString:@"/text_orientation"]]; + NSString *textOrientation = [config getString:[prefix stringByAppendingString:@"/text_orientation"]]; if ([textOrientation isEqualToString:@"horizontal"]) { - *isVerticalText = false; + *isVerticalText = NO; } else if ([textOrientation isEqualToString:@"vertical"]) { - *isVerticalText = true; + *isVerticalText = YES; } else { NSNumber *vertical = [config getOptionalBool:[prefix stringByAppendingString:@"/vertical"]]; if (vertical) { @@ -1616,46 +2378,80 @@ static void updateTextOrientation(BOOL *isVerticalText, SquirrelConfig *config, } } --(void)loadConfig:(SquirrelConfig *)config forDarkMode:(BOOL)isDark { +- (void)loadLabelConfig:(SquirrelConfig *)config { + SquirrelTheme *theme = [_view selectTheme:NO]; + [SquirrelPanel updateTheme:theme withLabelConfig:config]; + if (@available(macOS 10.14, *)) { + SquirrelTheme *darkTheme = [_view selectTheme:YES]; + [SquirrelPanel updateTheme:darkTheme withLabelConfig:config]; + } +} + ++ (void)updateTheme:(SquirrelTheme *)theme withLabelConfig:(SquirrelConfig *)config { + int menuSize = [config getInt:@"menu/page_size"] ? : 5; + NSMutableArray *labels = [[NSMutableArray alloc] initWithCapacity:menuSize]; + NSString *selectKeys = [config getString:@"menu/alternative_select_keys"]; + if (selectKeys) { + NSString *keyCaps = [[selectKeys uppercaseString] + stringByApplyingTransform:NSStringTransformFullwidthToHalfwidth reverse:YES]; + for (int i = 0; i < menuSize; ++i) { + labels[i] = [keyCaps substringWithRange:NSMakeRange(i, 1)]; + } + } else { + NSArray *selectLabels = [config getList:@"menu/alternative_select_labels"]; + if (selectLabels) { + for (int i = 0; i < menuSize; ++i) { + labels[i] = selectLabels[i]; + } + } else { + NSString *numerals = @"1234567890"; + for (int i = 0; i < menuSize; ++i) { + labels[i] = [numerals substringWithRange:NSMakeRange(i, 1)]; + } + } + } + [theme setLabels:labels]; +} + +- (void)loadConfig:(SquirrelConfig *)config forDarkMode:(BOOL)isDark { SquirrelTheme *theme = [_view selectTheme:isDark]; - [[self class] updateTheme:theme withConfig:config forDarkMode:isDark]; + NSSet *styleOptions = [NSSet setWithArray:self.optionSwitcher.optionStates]; + [SquirrelPanel updateTheme:theme withConfig:config styleOptions:styleOptions forDarkMode:isDark]; } -+(void)updateTheme:(SquirrelTheme *)theme withConfig:(SquirrelConfig *)config forDarkMode:(BOOL)isDark { ++ (void)updateTheme:(SquirrelTheme *)theme withConfig:(SquirrelConfig *)config styleOptions:(NSSet *)styleOptions forDarkMode:(BOOL)isDark { + // INTERFACE BOOL linear = NO; + BOOL tabled = NO; BOOL vertical = NO; - updateCandidateListLayout(&linear, config, @"style"); + updateCandidateListLayout(&linear, &tabled, config, @"style"); updateTextOrientation(&vertical, config, @"style"); - BOOL inlinePreedit = [config getBool:@"style/inline_preedit"]; - BOOL inlineCandidate = [config getBool:@"style/inline_candidate"]; - BOOL translucency = [config getBool:@"style/translucency"]; - BOOL mutualExclusive = [config getBool:@"style/mutual_exclusive"]; - NSNumber *memorizeSizeConfig = [config getOptionalBool:@"style/memorize_size"]; - if (memorizeSizeConfig) { - theme.memorizeSize = memorizeSizeConfig.boolValue; - } - + NSNumber *inlinePreedit = [config getOptionalBool:@"style/inline_preedit"]; + NSNumber *inlineCandidate = [config getOptionalBool:@"style/inline_candidate"]; + NSNumber *showPaging = [config getOptionalBool:@"style/show_paging"]; + NSNumber *rememberSize = [config getOptionalBool:@"style/remember_size"]; NSString *statusMessageType = [config getString:@"style/status_message_type"]; NSString *candidateFormat = [config getString:@"style/candidate_format"]; + // TYPOGRAPHY NSString *fontName = [config getString:@"style/font_face"]; - CGFloat fontSize = [config getDouble:@"style/font_point"]; + NSNumber *fontSize = [config getOptionalDouble:@"style/font_point"]; NSString *labelFontName = [config getString:@"style/label_font_face"]; - CGFloat labelFontSize = [config getDouble:@"style/label_font_point"]; + NSNumber *labelFontSize = [config getOptionalDouble:@"style/label_font_point"]; NSString *commentFontName = [config getString:@"style/comment_font_face"]; - CGFloat commentFontSize = [config getDouble:@"style/comment_font_point"]; - NSNumber *alphaValue = [config getOptionalDouble:@"style/alpha"]; - CGFloat alpha = alphaValue ? fmin(fmax(alphaValue.doubleValue, 0.0), 1.0) : 1.0; - CGFloat cornerRadius = [config getDouble:@"style/corner_radius"]; - CGFloat hilitedCornerRadius = [config getDouble:@"style/hilited_corner_radius"]; - CGFloat surroundingExtraExpansion = [config getDouble:@"style/surrounding_extra_expansion"]; - CGFloat borderHeight = [config getDouble:@"style/border_height"]; - CGFloat borderWidth = [config getDouble:@"style/border_width"]; - CGFloat lineSpacing = [config getDouble:@"style/line_spacing"]; - CGFloat spacing = [config getDouble:@"style/spacing"]; - CGFloat baseOffset = [config getDouble:@"style/base_offset"]; - CGFloat shadowSize = fmax(0,[config getDouble:@"style/shadow_size"]); - + NSNumber *commentFontSize = [config getOptionalDouble:@"style/comment_font_point"]; + NSNumber *alpha = [config getOptionalDouble:@"style/alpha"]; + NSNumber *translucency = [config getOptionalDouble:@"style/translucency"]; + NSNumber *cornerRadius = [config getOptionalDouble:@"style/corner_radius"]; + NSNumber *highlightedCornerRadius = [config getOptionalDouble:@"style/hilited_corner_radius"]; + NSNumber *borderHeight = [config getOptionalDouble:@"style/border_height"]; + NSNumber *borderWidth = [config getOptionalDouble:@"style/border_width"]; + NSNumber *lineSpacing = [config getOptionalDouble:@"style/line_spacing"]; + NSNumber *spacing = [config getOptionalDouble:@"style/spacing"]; + NSNumber *baseOffset = [config getOptionalDouble:@"style/base_offset"]; + NSNumber *lineLength = [config getOptionalDouble:@"style/line_length"]; + // CHROMATICS NSColor *backgroundColor; + NSColor *backgroundImage; NSColor *borderColor; NSColor *preeditBackgroundColor; NSColor *candidateLabelColor; @@ -1666,246 +2462,139 @@ +(void)updateTheme:(SquirrelTheme *)theme withConfig:(SquirrelConfig *)config fo NSColor *candidateTextColor; NSColor *highlightedCandidateTextColor; NSColor *highlightedCandidateBackColor; - NSColor *candidateBackColor; NSColor *commentTextColor; NSColor *highlightedCommentTextColor; NSString *colorScheme; if (isDark) { - colorScheme = [config getString:@"style/color_scheme_dark"]; + for (NSString *option in styleOptions) { + if ((colorScheme = [config getString:[NSString stringWithFormat:@"style/%@/color_scheme_dark", option]])) break; + } + colorScheme = colorScheme ? : [config getString:@"style/color_scheme_dark"]; } if (!colorScheme) { - colorScheme = [config getString:@"style/color_scheme"]; + for (NSString *option in styleOptions) { + if ((colorScheme = [config getString:[NSString stringWithFormat:@"style/%@/color_scheme", option]])) break; + } + colorScheme = colorScheme ? : [config getString:@"style/color_scheme"]; } BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"]; - if (!isNative) { - NSString *prefix = [@"preset_color_schemes/" stringByAppendingString:colorScheme]; - config.colorSpace = [config getString:[prefix stringByAppendingString:@"/color_space"]]; - backgroundColor = [config getColor:[prefix stringByAppendingString:@"/back_color"]]; - borderColor = [config getColor:[prefix stringByAppendingString:@"/border_color"]]; - preeditBackgroundColor = [config getColor:[prefix stringByAppendingString:@"/preedit_back_color"]]; - textColor = [config getColor:[prefix stringByAppendingString:@"/text_color"]]; - highlightedTextColor = - [config getColor:[prefix stringByAppendingString:@"/hilited_text_color"]]; - if (highlightedTextColor == nil) { - highlightedTextColor = textColor; - } - highlightedBackColor = - [config getColor:[prefix stringByAppendingString:@"/hilited_back_color"]]; - candidateTextColor = - [config getColor:[prefix stringByAppendingString:@"/candidate_text_color"]]; - if (candidateTextColor == nil) { - // in non-inline mode, 'text_color' is for rendering preedit text. - // if not otherwise specified, candidate text is also rendered in this color. - candidateTextColor = textColor; - } - candidateLabelColor = - [config getColor:[prefix stringByAppendingString:@"/label_color"]]; - highlightedCandidateLabelColor = - [config getColor:[prefix stringByAppendingString:@"/label_hilited_color"]]; - if (!highlightedCandidateLabelColor) { - // for backward compatibility, 'label_hilited_color' and 'hilited_candidate_label_color' - // are both valid - highlightedCandidateLabelColor = - [config getColor:[prefix stringByAppendingString:@"/hilited_candidate_label_color"]]; - } - highlightedCandidateTextColor = - [config getColor:[prefix stringByAppendingString:@"/hilited_candidate_text_color"]]; - if (highlightedCandidateTextColor == nil) { - highlightedCandidateTextColor = highlightedTextColor; - } - highlightedCandidateBackColor = - [config getColor:[prefix stringByAppendingString:@"/hilited_candidate_back_color"]]; - if (highlightedCandidateBackColor == nil) { - highlightedCandidateBackColor = highlightedBackColor; - } - candidateBackColor = - [config getColor:[prefix stringByAppendingString:@"/candidate_back_color"]]; - commentTextColor = - [config getColor:[prefix stringByAppendingString:@"/comment_text_color"]]; - highlightedCommentTextColor = - [config getColor:[prefix stringByAppendingString:@"/hilited_comment_text_color"]]; + NSArray *configPrefixes = isNative ? [@"style/" stringsByAppendingPaths:styleOptions.allObjects] : + [[NSArray arrayWithObject:[@"preset_color_schemes/" stringByAppendingString:colorScheme]] + arrayByAddingObjectsFromArray:[@"style/" stringsByAppendingPaths:styleOptions.allObjects]]; + + // get color scheme and then check possible overrides from styleSwitcher + for (NSString *prefix in configPrefixes) { + // CHROMATICS override + if (@available(macOS 10.12, *)) { + config.colorSpace = [config getString:[prefix stringByAppendingString:@"/color_space"]] ? : config.colorSpace; + } + backgroundColor = [config getColor:[prefix stringByAppendingString:@"/back_color"]] ? : backgroundColor; + backgroundImage = [config getPattern:[prefix stringByAppendingString:@"/back_image"]] ? : backgroundImage; + borderColor = [config getColor:[prefix stringByAppendingString:@"/border_color"]] ? : borderColor; + preeditBackgroundColor = [config getColor:[prefix stringByAppendingString:@"/preedit_back_color"]] ? : preeditBackgroundColor; + textColor = [config getColor:[prefix stringByAppendingString:@"/text_color"]] ? : textColor; + highlightedTextColor = [config getColor:[prefix stringByAppendingString:@"/hilited_text_color"]] ? : highlightedTextColor; + highlightedBackColor = [config getColor:[prefix stringByAppendingString:@"/hilited_back_color"]] ? : highlightedBackColor; + candidateTextColor = [config getColor:[prefix stringByAppendingString:@"/candidate_text_color"]] ? : candidateTextColor; + highlightedCandidateTextColor = [config getColor:[prefix stringByAppendingString:@"/hilited_candidate_text_color"]] ? : highlightedCandidateTextColor; + highlightedCandidateBackColor = [config getColor:[prefix stringByAppendingString:@"/hilited_candidate_back_color"]] ? : highlightedCandidateBackColor; + commentTextColor = [config getColor:[prefix stringByAppendingString:@"/comment_text_color"]] ? : commentTextColor; + highlightedCommentTextColor = [config getColor:[prefix stringByAppendingString:@"/hilited_comment_text_color"]] ? : highlightedCommentTextColor; + candidateLabelColor = [config getColor:[prefix stringByAppendingString:@"/label_color"]] ? : candidateLabelColor; + // for backward compatibility, 'label_hilited_color' and 'hilited_candidate_label_color' are both valid + highlightedCandidateLabelColor = [config getColor:[prefix stringByAppendingString:@"/label_hilited_color"]] ? : + [config getColor:[prefix stringByAppendingString:@"/hilited_candidate_label_color"]] ? : highlightedCandidateLabelColor; // the following per-color-scheme configurations, if exist, will // override configurations with the same name under the global 'style' section - - updateCandidateListLayout(&linear, config, prefix); + // INTERFACE override + updateCandidateListLayout(&linear, &tabled, config, prefix); updateTextOrientation(&vertical, config, prefix); - - NSNumber *inlinePreeditOverridden = - [config getOptionalBool:[prefix stringByAppendingString:@"/inline_preedit"]]; - if (inlinePreeditOverridden) { - inlinePreedit = inlinePreeditOverridden.boolValue; - } - NSNumber *inlineCandidateOverridden = - [config getOptionalBool:[prefix stringByAppendingString:@"/inline_candidate"]]; - if (inlineCandidateOverridden) { - inlineCandidate = inlineCandidateOverridden.boolValue; - } - NSNumber *translucencyOverridden = - [config getOptionalBool:[prefix stringByAppendingString:@"/translucency"]]; - if (translucencyOverridden) { - translucency = translucencyOverridden.boolValue; - } - NSNumber *mutualExclusiveOverridden = - [config getOptionalBool:[prefix stringByAppendingString:@"/mutual_exclusive"]]; - if (mutualExclusiveOverridden) { - mutualExclusive = mutualExclusiveOverridden.boolValue; - } - NSString *candidateFormatOverridden = - [config getString:[prefix stringByAppendingString:@"/candidate_format"]]; - if (candidateFormatOverridden) { - candidateFormat = candidateFormatOverridden; - } - - NSString *fontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/font_face"]]; - if (fontNameOverridden) { - fontName = fontNameOverridden; - } - NSNumber *fontSizeOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/font_point"]]; - if (fontSizeOverridden) { - fontSize = fontSizeOverridden.integerValue; - } - NSString *labelFontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/label_font_face"]]; - if (labelFontNameOverridden) { - labelFontName = labelFontNameOverridden; - } - NSNumber *labelFontSizeOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/label_font_point"]]; - if (labelFontSizeOverridden) { - labelFontSize = labelFontSizeOverridden.integerValue; - } - NSString *commentFontNameOverridden = - [config getString:[prefix stringByAppendingString:@"/comment_font_face"]]; - if (commentFontNameOverridden) { - commentFontName = commentFontNameOverridden; - } - NSNumber *commentFontSizeOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/comment_font_point"]]; - if (commentFontSizeOverridden) { - commentFontSize = commentFontSizeOverridden.integerValue; - } - NSNumber *alphaOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/alpha"]]; - if (alphaOverridden) { - alpha = fmin(fmax(alphaOverridden.doubleValue, 0.0), 1.0); - } - NSNumber *cornerRadiusOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/corner_radius"]]; - if (cornerRadiusOverridden) { - cornerRadius = cornerRadiusOverridden.doubleValue; - } - NSNumber *hilitedCornerRadiusOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/hilited_corner_radius"]]; - if (hilitedCornerRadiusOverridden) { - hilitedCornerRadius = hilitedCornerRadiusOverridden.doubleValue; - } - NSNumber *surroundingExtraExpansionOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/surrounding_extra_expansion"]]; - if (surroundingExtraExpansionOverridden) { - surroundingExtraExpansion = surroundingExtraExpansionOverridden.doubleValue; - } - NSNumber *borderHeightOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/border_height"]]; - if (borderHeightOverridden) { - borderHeight = borderHeightOverridden.doubleValue; - } - NSNumber *borderWidthOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/border_width"]]; - if (borderWidthOverridden) { - borderWidth = borderWidthOverridden.doubleValue; - } - NSNumber *lineSpacingOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/line_spacing"]]; - if (lineSpacingOverridden) { - lineSpacing = lineSpacingOverridden.doubleValue; - } - NSNumber *spacingOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/spacing"]]; - if (spacingOverridden) { - spacing = spacingOverridden.doubleValue; - } - NSNumber *baseOffsetOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/base_offset"]]; - if (baseOffsetOverridden) { - baseOffset = baseOffsetOverridden.doubleValue; - } - NSNumber *shadowSizeOverridden = - [config getOptionalDouble:[prefix stringByAppendingString:@"/shadow_size"]]; - if (shadowSizeOverridden) { - shadowSize = shadowSizeOverridden.doubleValue; - } - } - - if (fontSize == 0) { // default size - fontSize = kDefaultFontSize; - } - if (labelFontSize == 0) { - labelFontSize = fontSize; - } - if (commentFontSize == 0) { - commentFontSize = fontSize; - } - NSFontDescriptor *fontDescriptor = nil; - NSFont *font = nil; - if (fontName != nil) { - fontDescriptor = getFontDescriptor(fontName); - if (fontDescriptor != nil) { - font = [NSFont fontWithDescriptor:fontDescriptor size:fontSize]; - } - } - if (font == nil) { - // use default font - font = [NSFont userFontOfSize:fontSize]; - } - NSFontDescriptor *labelFontDescriptor = nil; - NSFont *labelFont = nil; - if (labelFontName != nil) { - labelFontDescriptor = getFontDescriptor(labelFontName); - if (labelFontDescriptor == nil) { - labelFontDescriptor = fontDescriptor; - } - if (labelFontDescriptor != nil) { - labelFont = [NSFont fontWithDescriptor:labelFontDescriptor size:labelFontSize]; - } - } - if (labelFont == nil) { - if (fontDescriptor != nil) { - labelFont = [NSFont fontWithDescriptor:fontDescriptor size:labelFontSize]; - } else { - labelFont = [NSFont fontWithName:font.fontName size:labelFontSize]; - } - } - NSFontDescriptor *commentFontDescriptor = nil; - NSFont *commentFont = nil; - if (commentFontName != nil) { - commentFontDescriptor = getFontDescriptor(commentFontName); - if (commentFontDescriptor == nil) { - commentFontDescriptor = fontDescriptor; - } - if (commentFontDescriptor != nil) { - commentFont = [NSFont fontWithDescriptor:commentFontDescriptor size:commentFontSize]; - } - } - if (commentFont == nil) { - if (fontDescriptor != nil) { - commentFont = [NSFont fontWithDescriptor:fontDescriptor size:commentFontSize]; - } else { - commentFont = [NSFont fontWithName:font.fontName size:commentFontSize]; - } - } - - NSMutableParagraphStyle *paragraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - paragraphStyle.paragraphSpacing = lineSpacing / 2; - paragraphStyle.paragraphSpacingBefore = lineSpacing / 2; - - NSMutableParagraphStyle *preeditParagraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - preeditParagraphStyle.paragraphSpacing = spacing / 2 + hilitedCornerRadius / 2; + inlinePreedit = [config getOptionalBool:[prefix stringByAppendingString:@"/inline_preedit"]] ? : inlinePreedit; + inlineCandidate = [config getOptionalBool:[prefix stringByAppendingString:@"/inline_candidate"]] ? : inlineCandidate; + showPaging = [config getOptionalBool:[prefix stringByAppendingString:@"/show_paging"]] ? : showPaging; + rememberSize = [config getOptionalBool:[prefix stringByAppendingString:@"/remember_size"]] ? : rememberSize; + statusMessageType = [config getString:[prefix stringByAppendingString:@"style/status_message_type"]] ? : statusMessageType; + candidateFormat = [config getString:[prefix stringByAppendingString:@"/candidate_format"]] ? : candidateFormat; + // TYPOGRAPHY override + fontName = [config getString:[prefix stringByAppendingString:@"/font_face"]] ? : fontName; + fontSize = [config getOptionalDouble:[prefix stringByAppendingString:@"/font_point"]] ? : fontSize; + labelFontName = [config getString:[prefix stringByAppendingString:@"/label_font_face"]] ? : labelFontName; + labelFontSize = [config getOptionalDouble:[prefix stringByAppendingString:@"/label_font_point"]] ? : labelFontSize; + commentFontName = [config getString:[prefix stringByAppendingString:@"/comment_font_face"]] ? : commentFontName; + commentFontSize = [config getOptionalDouble:[prefix stringByAppendingString:@"/comment_font_point"]] ? : commentFontSize; + alpha = [config getOptionalDouble:[prefix stringByAppendingString:@"/alpha"]] ? : alpha; + translucency = [config getOptionalDouble:[prefix stringByAppendingString:@"/translucency"]] ? : translucency; + cornerRadius = [config getOptionalDouble:[prefix stringByAppendingString:@"/corner_radius"]] ? : cornerRadius; + highlightedCornerRadius = [config getOptionalDouble:[prefix stringByAppendingString:@"/hilited_corner_radius"]] ? : highlightedCornerRadius; + borderHeight = [config getOptionalDouble:[prefix stringByAppendingString:@"/border_height"]] ? : borderHeight; + borderWidth = [config getOptionalDouble:[prefix stringByAppendingString:@"/border_width"]] ? : borderWidth; + lineSpacing = [config getOptionalDouble:[prefix stringByAppendingString:@"/line_spacing"]] ? : lineSpacing; + spacing = [config getOptionalDouble:[prefix stringByAppendingString:@"/spacing"]] ? : spacing; + baseOffset = [config getOptionalDouble:[prefix stringByAppendingString:@"/base_offset"]] ? : baseOffset; + lineLength = [config getOptionalDouble:[prefix stringByAppendingString:@"/line_length"]] ? : lineLength; + } + + // TYPOGRAPHY refinement + fontSize = fontSize ? : @(kDefaultFontSize); + labelFontSize = labelFontSize ? : fontSize; + commentFontSize = commentFontSize ? : fontSize; + NSDictionary *monoDigitAttrs = @{NSFontFeatureSettingsAttribute: + @[@{NSFontFeatureTypeIdentifierKey: @(kNumberSpacingType), + NSFontFeatureSelectorIdentifierKey: @(kMonospacedNumbersSelector)}, + @{NSFontFeatureTypeIdentifierKey: @(kTextSpacingType), + NSFontFeatureSelectorIdentifierKey: @(kHalfWidthTextSelector)}] }; + + NSFontDescriptor *fontDescriptor = getFontDescriptor(fontName); + NSFont *font = [NSFont fontWithDescriptor:(fontDescriptor ? : getFontDescriptor([NSFont userFontOfSize:0].fontName)) + size:MAX(fontSize.doubleValue, 0)]; + + NSFontDescriptor *labelFontDescriptor = [(getFontDescriptor(labelFontName) ? : fontDescriptor) + fontDescriptorByAddingAttributes:monoDigitAttrs]; + NSFont *labelFont = labelFontDescriptor ? [NSFont fontWithDescriptor:labelFontDescriptor size:MAX(labelFontSize.doubleValue, 0)] + : [NSFont monospacedDigitSystemFontOfSize:MAX(labelFontSize.doubleValue, 0) weight:NSFontWeightRegular]; + + NSFontDescriptor *commentFontDescriptor = getFontDescriptor(commentFontName); + NSFont *commentFont = [NSFont fontWithDescriptor:(commentFontDescriptor ? : fontDescriptor) + size:MAX(commentFontSize.doubleValue, 0)]; + + NSFont *pagingFont; + if (@available(macOS 12.0, *)) { + pagingFont = [NSFont monospacedDigitSystemFontOfSize:MAX(labelFontSize.doubleValue, 0) weight:NSFontWeightRegular]; + } else { + pagingFont = [NSFont fontWithDescriptor:[[NSFontDescriptor fontDescriptorWithName:@"AppleSymbols" size:0] + fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitUIOptimized] + size:MAX(labelFontSize.doubleValue, 0)]; + } + + CGFloat fontHeight = getLineHeight(font, vertical); + CGFloat labelFontHeight = getLineHeight(labelFont, vertical); + CGFloat commentFontHeight = getLineHeight(commentFont, vertical); + CGFloat lineHeight = MAX(fontHeight, MAX(labelFontHeight, commentFontHeight)); + CGFloat separatorWidth = ceil([[NSAttributedString alloc] initWithString:kFullWidthSpace + attributes:@{NSFontAttributeName: commentFont}].size.width); + + NSMutableParagraphStyle *preeditParagraphStyle = [theme.preeditParagraphStyle mutableCopy]; + preeditParagraphStyle.minimumLineHeight = fontHeight; + preeditParagraphStyle.maximumLineHeight = fontHeight; + preeditParagraphStyle.paragraphSpacing = MAX(spacing.doubleValue, 0); + + NSMutableParagraphStyle *paragraphStyle = [theme.paragraphStyle mutableCopy]; + paragraphStyle.minimumLineHeight = lineHeight; + paragraphStyle.maximumLineHeight = lineHeight; + paragraphStyle.paragraphSpacing = MAX(lineSpacing.doubleValue / 2, 0); + paragraphStyle.paragraphSpacingBefore = MAX(lineSpacing.doubleValue / 2, 0); + paragraphStyle.tabStops = @[]; + paragraphStyle.defaultTabInterval = separatorWidth * 2; + + NSMutableParagraphStyle *pagingParagraphStyle = [theme.pagingParagraphStyle mutableCopy]; + pagingParagraphStyle.minimumLineHeight = pagingFont.ascender - pagingFont.descender; + pagingParagraphStyle.maximumLineHeight = pagingFont.ascender - pagingFont.descender; + + NSMutableParagraphStyle *statusParagraphStyle = [theme.statusParagraphStyle mutableCopy]; + statusParagraphStyle.minimumLineHeight = commentFontHeight; + statusParagraphStyle.maximumLineHeight = commentFontHeight; NSMutableDictionary *attrs = [theme.attrs mutableCopy]; NSMutableDictionary *highlightedAttrs = [theme.highlightedAttrs mutableCopy]; @@ -1915,6 +2604,9 @@ +(void)updateTheme:(SquirrelTheme *)theme withConfig:(SquirrelConfig *)config fo NSMutableDictionary *commentHighlightedAttrs = [theme.commentHighlightedAttrs mutableCopy]; NSMutableDictionary *preeditAttrs = [theme.preeditAttrs mutableCopy]; NSMutableDictionary *preeditHighlightedAttrs = [theme.preeditHighlightedAttrs mutableCopy]; + NSMutableDictionary *pagingAttrs = [theme.pagingAttrs mutableCopy]; + NSMutableDictionary *pagingHighlightedAttrs = [theme.pagingHighlightedAttrs mutableCopy]; + NSMutableDictionary *statusAttrs = [theme.statusAttrs mutableCopy]; attrs[NSFontAttributeName] = font; highlightedAttrs[NSFontAttributeName] = font; @@ -1924,29 +2616,71 @@ +(void)updateTheme:(SquirrelTheme *)theme withConfig:(SquirrelConfig *)config fo commentHighlightedAttrs[NSFontAttributeName] = commentFont; preeditAttrs[NSFontAttributeName] = font; preeditHighlightedAttrs[NSFontAttributeName] = font; - attrs[NSBaselineOffsetAttributeName] = @(baseOffset); - highlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - labelAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - labelHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - commentAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - commentHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - preeditAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - preeditHighlightedAttrs[NSBaselineOffsetAttributeName] = @(baseOffset); - - NSColor *secondaryTextColor = [[self class] secondaryTextColor]; - - backgroundColor = backgroundColor ? backgroundColor : [NSColor windowBackgroundColor]; - candidateTextColor = candidateTextColor ? candidateTextColor : [NSColor controlTextColor]; - candidateLabelColor = candidateLabelColor ? candidateLabelColor : - isNative ? secondaryTextColor : blendColors(candidateTextColor, backgroundColor); - highlightedCandidateTextColor = highlightedCandidateTextColor ? highlightedCandidateTextColor : [NSColor selectedControlTextColor]; - highlightedCandidateBackColor = highlightedCandidateBackColor ? highlightedCandidateBackColor : [NSColor selectedTextBackgroundColor]; - highlightedCandidateLabelColor = highlightedCandidateLabelColor ? highlightedCandidateLabelColor : - isNative ? secondaryTextColor : blendColors(highlightedCandidateTextColor, highlightedCandidateBackColor); - commentTextColor = commentTextColor ? commentTextColor : secondaryTextColor; - highlightedCommentTextColor = highlightedCommentTextColor ? highlightedCommentTextColor : commentTextColor; - textColor = textColor ? textColor : secondaryTextColor; - highlightedTextColor = highlightedTextColor ? highlightedTextColor : [NSColor controlTextColor]; + pagingAttrs[NSFontAttributeName] = linear ? labelFont : pagingFont; + statusAttrs[NSFontAttributeName] = commentFont; + + NSFont *refFont = CFBridgingRelease(CTFontCreateForString((CTFontRef)font, (CFStringRef)kFullWidthSpace, CFRangeMake(0, 1))); + labelAttrs[CFBridgingRelease(kCTBaselineClassAttributeName)] = CFBridgingRelease(kCTBaselineClassIdeographicCentered); + labelHighlightedAttrs[CFBridgingRelease(kCTBaselineClassAttributeName)] = CFBridgingRelease(kCTBaselineClassIdeographicCentered); + labelAttrs[CFBridgingRelease(kCTBaselineReferenceInfoAttributeName)] = @{CFBridgingRelease(kCTBaselineReferenceFont): refFont}; + labelHighlightedAttrs[CFBridgingRelease(kCTBaselineReferenceInfoAttributeName)] = @{CFBridgingRelease(kCTBaselineReferenceFont): refFont}; + + attrs[NSBaselineOffsetAttributeName] = baseOffset; + highlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; + labelAttrs[NSBaselineOffsetAttributeName] = baseOffset; + labelHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; + commentAttrs[NSBaselineOffsetAttributeName] = baseOffset; + commentHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; + preeditAttrs[NSBaselineOffsetAttributeName] = baseOffset; + preeditHighlightedAttrs[NSBaselineOffsetAttributeName] = baseOffset; + pagingAttrs[NSBaselineOffsetAttributeName] = baseOffset; + statusAttrs[NSBaselineOffsetAttributeName] = baseOffset; + + preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; + preeditHighlightedAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; + statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; + + labelAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); + labelHighlightedAttrs[NSVerticalGlyphFormAttributeName] = @(vertical); + pagingAttrs[NSVerticalGlyphFormAttributeName] = @(NO); + + // CHROMATICS refinement + NSColor *secondaryTextColor = [SquirrelPanel secondaryTextColor]; + NSColor *accentColor = [SquirrelPanel accentColor]; + + if (@available(macOS 10.14, *)) { + if (theme.translucency > 0 && + ((backgroundColor.brightnessComponent >= 0.5 && isDark) || + (backgroundColor.brightnessComponent < 0.5 && !isDark))) { + backgroundColor = inverseColor(backgroundColor); + borderColor = inverseColor(borderColor); + preeditBackgroundColor = inverseColor(preeditBackgroundColor); + candidateTextColor = inverseColor(candidateTextColor); + highlightedCandidateTextColor = [inverseColor(highlightedCandidateTextColor) highlightWithLevel:highlightedCandidateTextColor.brightnessComponent]; + highlightedCandidateBackColor = [inverseColor(highlightedCandidateBackColor) shadowWithLevel:1 - highlightedCandidateBackColor.brightnessComponent]; + candidateLabelColor = inverseColor(candidateLabelColor); + highlightedCandidateLabelColor = [inverseColor(highlightedCandidateLabelColor) highlightWithLevel:highlightedCandidateLabelColor.brightnessComponent]; + commentTextColor = inverseColor(commentTextColor); + highlightedCommentTextColor = [inverseColor(highlightedCommentTextColor) highlightWithLevel:highlightedCommentTextColor.brightnessComponent]; + textColor = inverseColor(textColor); + highlightedTextColor = [inverseColor(highlightedTextColor) highlightWithLevel:highlightedTextColor.brightnessComponent]; + highlightedBackColor = [inverseColor(highlightedBackColor) shadowWithLevel:1 - highlightedBackColor.brightnessComponent]; + } + } + + backgroundColor = backgroundColor ? : [NSColor controlBackgroundColor]; + borderColor = borderColor ? : isNative ? [NSColor gridColor] : nil; + preeditBackgroundColor = preeditBackgroundColor ? : isNative ? [NSColor windowBackgroundColor] : nil; + candidateTextColor = candidateTextColor ? : [NSColor controlTextColor]; + highlightedCandidateTextColor = highlightedCandidateTextColor ? : [NSColor selectedMenuItemTextColor]; + highlightedCandidateBackColor = highlightedCandidateBackColor ? : isNative ? [NSColor alternateSelectedControlColor] : nil; + candidateLabelColor = candidateLabelColor ? : isNative ? accentColor : blendColors(highlightedCandidateBackColor, highlightedCandidateTextColor); + highlightedCandidateLabelColor = highlightedCandidateLabelColor ? : isNative ? [NSColor alternateSelectedControlTextColor] : blendColors(highlightedCandidateTextColor, highlightedCandidateBackColor); + commentTextColor = commentTextColor ? : secondaryTextColor; + highlightedCommentTextColor = highlightedCommentTextColor ? : [NSColor alternateSelectedControlTextColor]; + textColor = textColor ? textColor : [NSColor textColor]; + highlightedTextColor = highlightedTextColor ? : [NSColor selectedTextColor]; + highlightedBackColor = highlightedBackColor ? : isNative ? [NSColor selectedTextBackgroundColor] : nil; attrs[NSForegroundColorAttributeName] = candidateTextColor; highlightedAttrs[NSForegroundColorAttributeName] = highlightedCandidateTextColor; @@ -1956,52 +2690,56 @@ +(void)updateTheme:(SquirrelTheme *)theme withConfig:(SquirrelConfig *)config fo commentHighlightedAttrs[NSForegroundColorAttributeName] = highlightedCommentTextColor; preeditAttrs[NSForegroundColorAttributeName] = textColor; preeditHighlightedAttrs[NSForegroundColorAttributeName] = highlightedTextColor; - - [theme setStatusMessageType:statusMessageType]; - - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs]; + pagingAttrs[NSForegroundColorAttributeName] = linear ? candidateLabelColor : textColor; + pagingHighlightedAttrs[NSForegroundColorAttributeName] = linear ? highlightedCandidateLabelColor : highlightedTextColor; + statusAttrs[NSForegroundColorAttributeName] = commentTextColor; + + NSSize edgeInset = vertical ? NSMakeSize(MAX(borderHeight.doubleValue, 0), MAX(borderWidth.doubleValue, 0)) : + NSMakeSize(MAX(borderWidth.doubleValue, 0), MAX(borderHeight.doubleValue, 0)); + + [theme setCornerRadius:MIN(cornerRadius.doubleValue, lineHeight / 2) + highlightedCornerRadius:MIN(highlightedCornerRadius.doubleValue, lineHeight / 3) + separatorWidth:separatorWidth + edgeInset:edgeInset + linespace:MAX(lineSpacing.doubleValue, 0) + preeditLinespace:MAX(spacing.doubleValue, 0) + alpha:(alpha ? MIN(MAX(alpha.doubleValue, 0.0), 1.0) : 1.0) + translucency:(translucency ? MIN(MAX(translucency.doubleValue, 0.0), 1.0) : 0.0) + lineLength:lineLength.doubleValue ? MAX(lineLength.doubleValue, separatorWidth * 5) : 0.0 + showPaging:showPaging.boolValue + rememberSize:rememberSize.boolValue + tabled:tabled + linear:linear + vertical:vertical + inlinePreedit:inlinePreedit.boolValue + inlineCandidate:inlineCandidate.boolValue]; + + [theme setAttrs:attrs + highlightedAttrs:highlightedAttrs + labelAttrs:labelAttrs + labelHighlightedAttrs:labelHighlightedAttrs + commentAttrs:commentAttrs + commentHighlightedAttrs:commentHighlightedAttrs + preeditAttrs:preeditAttrs + preeditHighlightedAttrs:preeditHighlightedAttrs + pagingAttrs:pagingAttrs + pagingHighlightedAttrs:pagingHighlightedAttrs + statusAttrs:statusAttrs]; [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle]; + preeditParagraphStyle:preeditParagraphStyle + pagingParagraphStyle:pagingParagraphStyle + statusParagraphStyle:statusParagraphStyle]; [theme setBackgroundColor:backgroundColor - highlightedBackColor:highlightedCandidateBackColor - candidateBackColor:candidateBackColor + backgroundImage:backgroundImage + highlightedStripColor:highlightedCandidateBackColor highlightedPreeditColor:highlightedBackColor preeditBackgroundColor:preeditBackgroundColor borderColor:borderColor]; - NSSize edgeInset; - if (vertical) { - edgeInset = NSMakeSize(borderHeight + cornerRadius, borderWidth + cornerRadius); - } else { - edgeInset = NSMakeSize(borderWidth + cornerRadius, borderHeight + cornerRadius); - } - - [theme setCornerRadius:cornerRadius - hilitedCornerRadius:hilitedCornerRadius - srdExtraExpansion:surroundingExtraExpansion - shadowSize:shadowSize - edgeInset:edgeInset - borderWidth:MIN(borderHeight, borderWidth) - linespace:lineSpacing - preeditLinespace:spacing - alpha:alpha - translucency:translucency - mutualExclusive:mutualExclusive - linear:linear - vertical:vertical - inlinePreedit:inlinePreedit - inlineCandidate:inlineCandidate]; - - theme.native = isNative; - theme.candidateFormat = (candidateFormat ? candidateFormat : kDefaultCandidateFormat); + [theme setCandidateFormat:candidateFormat ? : kDefaultCandidateFormat]; + [theme setStatusMessageType:statusMessageType]; } + @end diff --git a/action-install.sh b/action-install.sh index 8f17d2e4e..e06071daa 100755 --- a/action-install.sh +++ b/action-install.sh @@ -2,14 +2,14 @@ set -e -rime_version=1.8.5 -rime_git_hash=08dd95f +rime_version=1.9.0m +rime_git_hash=434e898 rime_archive="rime-${rime_git_hash}-macOS.tar.bz2" -rime_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_archive}" +rime_download_url="https://github.com/groverlynn/librime/releases/download/${rime_version}/${rime_archive}" rime_deps_archive="rime-deps-${rime_git_hash}-macOS.tar.bz2" -rime_deps_download_url="https://github.com/rime/librime/releases/download/${rime_version}/${rime_deps_archive}" +rime_deps_download_url="https://github.com/groverlynn/librime/releases/download/${rime_version}/${rime_deps_archive}" mkdir -p download && ( cd download diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 5e91f7017..54e4d7228 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -4,13 +4,8 @@ "deploy_start" = "Deploying Rime input method engine."; "deploy_success" = "Squirrel is ready."; "deploy_failure" = "Error occurred. See log file $TMPDIR/rime.squirrel.INFO."; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "Full shape"; -"!full_shape" = "Half shape"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "Simplified"; -"!simplification" = "Traditional"; -"extended_charset" = "CJK extended"; -"!extended_charset" = "CJK baseset"; + +"problematic_launch" = "Problematic launch detected! \ + Squirrel may be suffering a crash due to imporper configuration. \ + Revert previous modifications to see if the problem recurs."; +"say_voice" = "Alex"; diff --git a/input_source.m b/input_source.m index 9118fe62f..58b659a44 100644 --- a/input_source.m +++ b/input_source.m @@ -1,18 +1,18 @@ #import static const unsigned char kInstallLocation[] = - "/Library/Input Methods/Squirrel.app"; + "/Library/Input Methods/Squirrel.app"; static NSString *const kHansInputModeID = - @"im.rime.inputmethod.Squirrel.Hans"; + @"im.rime.inputmethod.Squirrel.Hans"; static NSString *const kHantInputModeID = - @"im.rime.inputmethod.Squirrel.Hant"; + @"im.rime.inputmethod.Squirrel.Hant"; #define HANS_INPUT_MODE (1 << 0) #define HANT_INPUT_MODE (1 << 1) void RegisterInputSource(void) { CFURLRef installedLocationURL = CFURLCreateFromFileSystemRepresentation( - NULL, kInstallLocation, strlen((const char *)kInstallLocation), NO); + NULL, kInstallLocation, strlen((const char *)kInstallLocation), NO); if (installedLocationURL) { TISRegisterInputSource(installedLocationURL); CFRelease(installedLocationURL); @@ -29,13 +29,13 @@ void ActivateInputSource(int enabled_modes) { inputSource, kTISPropertyInputSourceID)); //NSLog(@"Examining input source: %@", sourceID); if (([sourceID isEqualToString:kHansInputModeID] && - ((enabled_modes & HANS_INPUT_MODE) != 0)) || + ((enabled_modes & HANS_INPUT_MODE) != 0)) || ([sourceID isEqualToString:kHantInputModeID] && - ((enabled_modes & HANT_INPUT_MODE) != 0))) { + ((enabled_modes & HANT_INPUT_MODE) != 0))) { TISEnableInputSource(inputSource); NSLog(@"Enabled input source: %@", sourceID); CFBooleanRef isSelectable = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsSelectCapable); + inputSource, kTISPropertyInputSourceIsSelectCapable); if (CFBooleanGetValue(isSelectable)) { TISSelectInputSource(inputSource); NSLog(@"Selected input source: %@", sourceID); @@ -80,10 +80,11 @@ int GetEnabledInputModes(void) { CFBooleanRef isEnabled = (CFBooleanRef)(TISGetInputSourceProperty( inputSource, kTISPropertyInputSourceIsEnabled)); if (CFBooleanGetValue(isEnabled)) { - if ([sourceID isEqualToString:kHansInputModeID]) + if ([sourceID isEqualToString:kHansInputModeID]) { input_modes |= HANS_INPUT_MODE; - else if ([sourceID isEqualToString:kHantInputModeID]) + } else if ([sourceID isEqualToString:kHantInputModeID]) { input_modes |= HANT_INPUT_MODE; + } } } } diff --git a/librime b/librime index 8911db66b..434e89835 160000 --- a/librime +++ b/librime @@ -1 +1 @@ -Subproject commit 8911db66b0e5d43c2c3e47ffec06bfd10201daae +Subproject commit 434e89835214d5522c2bb10678fb15f75236d4f4 diff --git a/macos_keycode.h b/macos_keycode.h index 839bc7605..ef88b3dbd 100644 --- a/macos_keycode.h +++ b/macos_keycode.h @@ -4,11 +4,11 @@ // masks -#define OSX_CAPITAL_MASK 1 << 16 -#define OSX_SHIFT_MASK 1 << 17 -#define OSX_CTRL_MASK 1 << 18 -#define OSX_ALT_MASK 1 << 19 -#define OSX_COMMAND_MASK 1 << 20 +#define OSX_CAPITAL_MASK 1 << 16 +#define OSX_SHIFT_MASK 1 << 17 +#define OSX_CTRL_MASK 1 << 18 +#define OSX_ALT_MASK 1 << 19 +#define OSX_COMMAND_MASK 1 << 20 // key codes // @@ -18,218 +18,218 @@ // ---------------------------------------- // alphabet -#define OSX_VK_A 0x0 -#define OSX_VK_B 0xb -#define OSX_VK_C 0x8 -#define OSX_VK_D 0x2 -#define OSX_VK_E 0xe -#define OSX_VK_F 0x3 -#define OSX_VK_G 0x5 -#define OSX_VK_H 0x4 -#define OSX_VK_I 0x22 -#define OSX_VK_J 0x26 -#define OSX_VK_K 0x28 -#define OSX_VK_L 0x25 -#define OSX_VK_M 0x2e -#define OSX_VK_N 0x2d -#define OSX_VK_O 0x1f -#define OSX_VK_P 0x23 -#define OSX_VK_Q 0xc -#define OSX_VK_R 0xf -#define OSX_VK_S 0x1 -#define OSX_VK_T 0x11 -#define OSX_VK_U 0x20 -#define OSX_VK_V 0x9 -#define OSX_VK_W 0xd -#define OSX_VK_X 0x7 -#define OSX_VK_Y 0x10 -#define OSX_VK_Z 0x6 +#define OSX_VK_A 0x0 +#define OSX_VK_B 0xb +#define OSX_VK_C 0x8 +#define OSX_VK_D 0x2 +#define OSX_VK_E 0xe +#define OSX_VK_F 0x3 +#define OSX_VK_G 0x5 +#define OSX_VK_H 0x4 +#define OSX_VK_I 0x22 +#define OSX_VK_J 0x26 +#define OSX_VK_K 0x28 +#define OSX_VK_L 0x25 +#define OSX_VK_M 0x2e +#define OSX_VK_N 0x2d +#define OSX_VK_O 0x1f +#define OSX_VK_P 0x23 +#define OSX_VK_Q 0xc +#define OSX_VK_R 0xf +#define OSX_VK_S 0x1 +#define OSX_VK_T 0x11 +#define OSX_VK_U 0x20 +#define OSX_VK_V 0x9 +#define OSX_VK_W 0xd +#define OSX_VK_X 0x7 +#define OSX_VK_Y 0x10 +#define OSX_VK_Z 0x6 // ---------------------------------------- // number -#define OSX_VK_KEY_0 0x1d -#define OSX_VK_KEY_1 0x12 -#define OSX_VK_KEY_2 0x13 -#define OSX_VK_KEY_3 0x14 -#define OSX_VK_KEY_4 0x15 -#define OSX_VK_KEY_5 0x17 -#define OSX_VK_KEY_6 0x16 -#define OSX_VK_KEY_7 0x1a -#define OSX_VK_KEY_8 0x1c -#define OSX_VK_KEY_9 0x19 +#define OSX_VK_KEY_0 0x1d +#define OSX_VK_KEY_1 0x12 +#define OSX_VK_KEY_2 0x13 +#define OSX_VK_KEY_3 0x14 +#define OSX_VK_KEY_4 0x15 +#define OSX_VK_KEY_5 0x17 +#define OSX_VK_KEY_6 0x16 +#define OSX_VK_KEY_7 0x1a +#define OSX_VK_KEY_8 0x1c +#define OSX_VK_KEY_9 0x19 // ---------------------------------------- // symbol // BACKQUOTE is also known as grave accent or backtick. -#define OSX_VK_BACKQUOTE 0x32 -#define OSX_VK_BACKSLASH 0x2a -#define OSX_VK_BRACKET_LEFT 0x21 -#define OSX_VK_BRACKET_RIGHT 0x1e -#define OSX_VK_COMMA 0x2b -#define OSX_VK_DOT 0x2f -#define OSX_VK_EQUAL 0x18 -#define OSX_VK_MINUS 0x1b -#define OSX_VK_QUOTE 0x27 -#define OSX_VK_SEMICOLON 0x29 -#define OSX_VK_SLASH 0x2c +#define OSX_VK_BACKQUOTE 0x32 +#define OSX_VK_BACKSLASH 0x2a +#define OSX_VK_BRACKET_LEFT 0x21 +#define OSX_VK_BRACKET_RIGHT 0x1e +#define OSX_VK_COMMA 0x2b +#define OSX_VK_DOT 0x2f +#define OSX_VK_EQUAL 0x18 +#define OSX_VK_MINUS 0x1b +#define OSX_VK_QUOTE 0x27 +#define OSX_VK_SEMICOLON 0x29 +#define OSX_VK_SLASH 0x2c // ---------------------------------------- // keypad -#define OSX_VK_KEYPAD_0 0x52 -#define OSX_VK_KEYPAD_1 0x53 -#define OSX_VK_KEYPAD_2 0x54 -#define OSX_VK_KEYPAD_3 0x55 -#define OSX_VK_KEYPAD_4 0x56 -#define OSX_VK_KEYPAD_5 0x57 -#define OSX_VK_KEYPAD_6 0x58 -#define OSX_VK_KEYPAD_7 0x59 -#define OSX_VK_KEYPAD_8 0x5b -#define OSX_VK_KEYPAD_9 0x5c -#define OSX_VK_KEYPAD_CLEAR 0x47 -#define OSX_VK_KEYPAD_COMMA 0x5f -#define OSX_VK_KEYPAD_DOT 0x41 -#define OSX_VK_KEYPAD_EQUAL 0x51 -#define OSX_VK_KEYPAD_MINUS 0x4e -#define OSX_VK_KEYPAD_MULTIPLY 0x43 -#define OSX_VK_KEYPAD_PLUS 0x45 -#define OSX_VK_KEYPAD_SLASH 0x4b +#define OSX_VK_KEYPAD_0 0x52 +#define OSX_VK_KEYPAD_1 0x53 +#define OSX_VK_KEYPAD_2 0x54 +#define OSX_VK_KEYPAD_3 0x55 +#define OSX_VK_KEYPAD_4 0x56 +#define OSX_VK_KEYPAD_5 0x57 +#define OSX_VK_KEYPAD_6 0x58 +#define OSX_VK_KEYPAD_7 0x59 +#define OSX_VK_KEYPAD_8 0x5b +#define OSX_VK_KEYPAD_9 0x5c +#define OSX_VK_KEYPAD_CLEAR 0x47 +#define OSX_VK_KEYPAD_COMMA 0x5f +#define OSX_VK_KEYPAD_DOT 0x41 +#define OSX_VK_KEYPAD_EQUAL 0x51 +#define OSX_VK_KEYPAD_MINUS 0x4e +#define OSX_VK_KEYPAD_MULTIPLY 0x43 +#define OSX_VK_KEYPAD_PLUS 0x45 +#define OSX_VK_KEYPAD_SLASH 0x4b // ---------------------------------------- // special -#define OSX_VK_DELETE 0x33 -#define OSX_VK_ENTER 0x4c -#define OSX_VK_ENTER_POWERBOOK 0x34 -#define OSX_VK_ESCAPE 0x35 -#define OSX_VK_FORWARD_DELETE 0x75 -#define OSX_VK_HELP 0x72 -#define OSX_VK_RETURN 0x24 -#define OSX_VK_SPACE 0x31 -#define OSX_VK_TAB 0x30 +#define OSX_VK_DELETE 0x33 +#define OSX_VK_ENTER 0x4c +#define OSX_VK_ENTER_POWERBOOK 0x34 +#define OSX_VK_ESCAPE 0x35 +#define OSX_VK_FORWARD_DELETE 0x75 +#define OSX_VK_HELP 0x72 +#define OSX_VK_RETURN 0x24 +#define OSX_VK_SPACE 0x31 +#define OSX_VK_TAB 0x30 // ---------------------------------------- // function -#define OSX_VK_F1 0x7a -#define OSX_VK_F2 0x78 -#define OSX_VK_F3 0x63 -#define OSX_VK_F4 0x76 -#define OSX_VK_F5 0x60 -#define OSX_VK_F6 0x61 -#define OSX_VK_F7 0x62 -#define OSX_VK_F8 0x64 -#define OSX_VK_F9 0x65 -#define OSX_VK_F10 0x6d -#define OSX_VK_F11 0x67 -#define OSX_VK_F12 0x6f -#define OSX_VK_F13 0x69 -#define OSX_VK_F14 0x6b -#define OSX_VK_F15 0x71 -#define OSX_VK_F16 0x6a -#define OSX_VK_F17 0x40 -#define OSX_VK_F18 0x4f -#define OSX_VK_F19 0x50 +#define OSX_VK_F1 0x7a +#define OSX_VK_F2 0x78 +#define OSX_VK_F3 0x63 +#define OSX_VK_F4 0x76 +#define OSX_VK_F5 0x60 +#define OSX_VK_F6 0x61 +#define OSX_VK_F7 0x62 +#define OSX_VK_F8 0x64 +#define OSX_VK_F9 0x65 +#define OSX_VK_F10 0x6d +#define OSX_VK_F11 0x67 +#define OSX_VK_F12 0x6f +#define OSX_VK_F13 0x69 +#define OSX_VK_F14 0x6b +#define OSX_VK_F15 0x71 +#define OSX_VK_F16 0x6a +#define OSX_VK_F17 0x40 +#define OSX_VK_F18 0x4f +#define OSX_VK_F19 0x50 // ---------------------------------------- // functional -#define OSX_VK_BRIGHTNESS_DOWN 0x91 -#define OSX_VK_BRIGHTNESS_UP 0x90 -#define OSX_VK_DASHBOARD 0x82 -#define OSX_VK_EXPOSE_ALL 0xa0 -#define OSX_VK_LAUNCHPAD 0x83 -#define OSX_VK_MISSION_CONTROL 0xa0 +#define OSX_VK_BRIGHTNESS_DOWN 0x91 +#define OSX_VK_BRIGHTNESS_UP 0x90 +#define OSX_VK_DASHBOARD 0x82 +#define OSX_VK_EXPOSE_ALL 0xa0 +#define OSX_VK_LAUNCHPAD 0x83 +#define OSX_VK_MISSION_CONTROL 0xa0 // ---------------------------------------- // cursor -#define OSX_VK_CURSOR_UP 0x7e -#define OSX_VK_CURSOR_DOWN 0x7d -#define OSX_VK_CURSOR_LEFT 0x7b -#define OSX_VK_CURSOR_RIGHT 0x7c +#define OSX_VK_CURSOR_UP 0x7e +#define OSX_VK_CURSOR_DOWN 0x7d +#define OSX_VK_CURSOR_LEFT 0x7b +#define OSX_VK_CURSOR_RIGHT 0x7c -#define OSX_VK_PAGEUP 0x74 -#define OSX_VK_PAGEDOWN 0x79 -#define OSX_VK_HOME 0x73 -#define OSX_VK_END 0x77 +#define OSX_VK_PAGEUP 0x74 +#define OSX_VK_PAGEDOWN 0x79 +#define OSX_VK_HOME 0x73 +#define OSX_VK_END 0x77 // ---------------------------------------- // modifiers -#define OSX_VK_CAPSLOCK 0x39 -#define OSX_VK_COMMAND_L 0x37 -#define OSX_VK_COMMAND_R 0x36 -#define OSX_VK_CONTROL_L 0x3b -#define OSX_VK_CONTROL_R 0x3e -#define OSX_VK_FN 0x3f -#define OSX_VK_OPTION_L 0x3a -#define OSX_VK_OPTION_R 0x3d -#define OSX_VK_SHIFT_L 0x38 -#define OSX_VK_SHIFT_R 0x3c +#define OSX_VK_CAPSLOCK 0x39 +#define OSX_VK_COMMAND_L 0x37 +#define OSX_VK_COMMAND_R 0x36 +#define OSX_VK_CONTROL_L 0x3b +#define OSX_VK_CONTROL_R 0x3e +#define OSX_VK_FN 0x3f +#define OSX_VK_OPTION_L 0x3a +#define OSX_VK_OPTION_R 0x3d +#define OSX_VK_SHIFT_L 0x38 +#define OSX_VK_SHIFT_R 0x3c // ---------------------------------------- // pc keyboard -#define OSX_VK_PC_APPLICATION 0x6e -#define OSX_VK_PC_BS 0x33 -#define OSX_VK_PC_DEL 0x75 -#define OSX_VK_PC_INSERT 0x72 -#define OSX_VK_PC_KEYPAD_NUMLOCK 0x47 -#define OSX_VK_PC_PAUSE 0x71 -#define OSX_VK_PC_POWER 0x7f -#define OSX_VK_PC_PRINTSCREEN 0x69 -#define OSX_VK_PC_SCROLLLOCK 0x6b +#define OSX_VK_PC_APPLICATION 0x6e +#define OSX_VK_PC_BS 0x33 +#define OSX_VK_PC_DEL 0x75 +#define OSX_VK_PC_INSERT 0x72 +#define OSX_VK_PC_KEYPAD_NUMLOCK 0x47 +#define OSX_VK_PC_PAUSE 0x71 +#define OSX_VK_PC_POWER 0x7f +#define OSX_VK_PC_PRINTSCREEN 0x69 +#define OSX_VK_PC_SCROLLLOCK 0x6b // ---------------------------------------- // international -#define OSX_VK_DANISH_DOLLAR 0xa -#define OSX_VK_DANISH_LESS_THAN 0x32 - -#define OSX_VK_FRENCH_DOLLAR 0x1e -#define OSX_VK_FRENCH_EQUAL 0x2c -#define OSX_VK_FRENCH_HAT 0x21 -#define OSX_VK_FRENCH_MINUS 0x18 -#define OSX_VK_FRENCH_RIGHT_PAREN 0x1b - -#define OSX_VK_GERMAN_CIRCUMFLEX 0xa -#define OSX_VK_GERMAN_LESS_THAN 0x32 -#define OSX_VK_GERMAN_PC_LESS_THAN 0x80 -#define OSX_VK_GERMAN_QUOTE 0x18 -#define OSX_VK_GERMAN_A_UMLAUT 0x27 -#define OSX_VK_GERMAN_O_UMLAUT 0x29 -#define OSX_VK_GERMAN_U_UMLAUT 0x21 - -#define OSX_VK_ITALIAN_BACKSLASH 0xa -#define OSX_VK_ITALIAN_LESS_THAN 0x32 - -#define OSX_VK_JIS_ATMARK 0x21 -#define OSX_VK_JIS_BRACKET_LEFT 0x1e -#define OSX_VK_JIS_BRACKET_RIGHT 0x2a -#define OSX_VK_JIS_COLON 0x27 -#define OSX_VK_JIS_DAKUON 0x21 -#define OSX_VK_JIS_EISUU 0x66 -#define OSX_VK_JIS_HANDAKUON 0x1e -#define OSX_VK_JIS_HAT 0x18 -#define OSX_VK_JIS_KANA 0x68 -#define OSX_VK_JIS_PC_HAN_ZEN 0x32 -#define OSX_VK_JIS_UNDERSCORE 0x5e -#define OSX_VK_JIS_YEN 0x5d - -#define OSX_VK_RUSSIAN_PARAGRAPH 0xa -#define OSX_VK_RUSSIAN_TILDE 0x32 +#define OSX_VK_DANISH_DOLLAR 0xa +#define OSX_VK_DANISH_LESS_THAN 0x32 + +#define OSX_VK_FRENCH_DOLLAR 0x1e +#define OSX_VK_FRENCH_EQUAL 0x2c +#define OSX_VK_FRENCH_HAT 0x21 +#define OSX_VK_FRENCH_MINUS 0x18 +#define OSX_VK_FRENCH_RIGHT_PAREN 0x1b + +#define OSX_VK_GERMAN_CIRCUMFLEX 0xa +#define OSX_VK_GERMAN_LESS_THAN 0x32 +#define OSX_VK_GERMAN_PC_LESS_THAN 0x80 +#define OSX_VK_GERMAN_QUOTE 0x18 +#define OSX_VK_GERMAN_A_UMLAUT 0x27 +#define OSX_VK_GERMAN_O_UMLAUT 0x29 +#define OSX_VK_GERMAN_U_UMLAUT 0x21 + +#define OSX_VK_ITALIAN_BACKSLASH 0xa +#define OSX_VK_ITALIAN_LESS_THAN 0x32 + +#define OSX_VK_JIS_ATMARK 0x21 +#define OSX_VK_JIS_BRACKET_LEFT 0x1e +#define OSX_VK_JIS_BRACKET_RIGHT 0x2a +#define OSX_VK_JIS_COLON 0x27 +#define OSX_VK_JIS_DAKUON 0x21 +#define OSX_VK_JIS_EISUU 0x66 +#define OSX_VK_JIS_HANDAKUON 0x1e +#define OSX_VK_JIS_HAT 0x18 +#define OSX_VK_JIS_KANA 0x68 +#define OSX_VK_JIS_PC_HAN_ZEN 0x32 +#define OSX_VK_JIS_UNDERSCORE 0x5e +#define OSX_VK_JIS_YEN 0x5d + +#define OSX_VK_RUSSIAN_PARAGRAPH 0xa +#define OSX_VK_RUSSIAN_TILDE 0x32 #define OSX_VK_SPANISH_LESS_THAN 0x32 #define OSX_VK_SPANISH_ORDINAL_INDICATOR 0xa -#define OSX_VK_SWEDISH_LESS_THAN 0x32 -#define OSX_VK_SWEDISH_SECTION 0xa +#define OSX_VK_SWEDISH_LESS_THAN 0x32 +#define OSX_VK_SWEDISH_SECTION 0xa -#define OSX_VK_SWISS_LESS_THAN 0x32 -#define OSX_VK_SWISS_SECTION 0xa +#define OSX_VK_SWISS_LESS_THAN 0x32 +#define OSX_VK_SWISS_SECTION 0xa -#define OSX_VK_UK_SECTION 0xa +#define OSX_VK_UK_SECTION 0xa // conversion functions diff --git a/macos_keycode.m b/macos_keycode.m index fc7f428a9..9912525e8 100644 --- a/macos_keycode.m +++ b/macos_keycode.m @@ -3,8 +3,7 @@ #import -int osx_modifiers_to_rime_modifiers(unsigned long modifiers) -{ +int osx_modifiers_to_rime_modifiers(unsigned long modifiers) { int ret = 0; if (modifiers & OSX_CAPITAL_MASK) @@ -25,93 +24,92 @@ int osx_modifiers_to_rime_modifiers(unsigned long modifiers) int osx_keycode, rime_keycode; } keycode_mappings[] = { // modifiers - {OSX_VK_CAPSLOCK, XK_Caps_Lock}, - {OSX_VK_COMMAND_L, XK_Super_L}, // XK_Meta_L? - {OSX_VK_COMMAND_R, XK_Super_R}, // XK_Meta_R? - {OSX_VK_CONTROL_L, XK_Control_L}, - {OSX_VK_CONTROL_R, XK_Control_R}, - {OSX_VK_FN, XK_Hyper_L}, - {OSX_VK_OPTION_L, XK_Alt_L}, - {OSX_VK_OPTION_R, XK_Alt_R}, - {OSX_VK_SHIFT_L, XK_Shift_L}, - {OSX_VK_SHIFT_R, XK_Shift_R}, + { OSX_VK_CAPSLOCK, XK_Caps_Lock }, + { OSX_VK_COMMAND_L, XK_Super_L }, // XK_Meta_L? + { OSX_VK_COMMAND_R, XK_Super_R }, // XK_Meta_R? + { OSX_VK_CONTROL_L, XK_Control_L }, + { OSX_VK_CONTROL_R, XK_Control_R }, + { OSX_VK_FN, XK_Hyper_L }, + { OSX_VK_OPTION_L, XK_Alt_L }, + { OSX_VK_OPTION_R, XK_Alt_R }, + { OSX_VK_SHIFT_L, XK_Shift_L }, + { OSX_VK_SHIFT_R, XK_Shift_R }, // special - {OSX_VK_DELETE, XK_BackSpace}, - {OSX_VK_ENTER, XK_KP_Enter}, + { OSX_VK_DELETE, XK_BackSpace }, + { OSX_VK_ENTER, XK_KP_Enter }, //OSX_VK_ENTER_POWERBOOK -> ? - {OSX_VK_ESCAPE, XK_Escape}, - {OSX_VK_FORWARD_DELETE, XK_Delete}, + { OSX_VK_ESCAPE, XK_Escape }, + { OSX_VK_FORWARD_DELETE, XK_Delete }, //{OSX_VK_HELP, XK_Help}, // the same keycode with OSX_VK_PC_INSERT - {OSX_VK_RETURN, XK_Return}, - {OSX_VK_SPACE, XK_space}, - {OSX_VK_TAB, XK_Tab}, + { OSX_VK_RETURN, XK_Return }, + { OSX_VK_SPACE, XK_space }, + { OSX_VK_TAB, XK_Tab }, // function - {OSX_VK_F1, XK_F1}, - {OSX_VK_F2, XK_F2}, - {OSX_VK_F3, XK_F3}, - {OSX_VK_F4, XK_F4}, - {OSX_VK_F5, XK_F5}, - {OSX_VK_F6, XK_F6}, - {OSX_VK_F7, XK_F7}, - {OSX_VK_F8, XK_F8}, - {OSX_VK_F9, XK_F9}, - {OSX_VK_F10, XK_F10}, - {OSX_VK_F11, XK_F11}, - {OSX_VK_F12, XK_F12}, - {OSX_VK_F13, XK_F13}, - {OSX_VK_F14, XK_F14}, - {OSX_VK_F15, XK_F15}, - {OSX_VK_F16, XK_F16}, - {OSX_VK_F17, XK_F17}, - {OSX_VK_F18, XK_F18}, - {OSX_VK_F19, XK_F19}, + { OSX_VK_F1, XK_F1 }, + { OSX_VK_F2, XK_F2 }, + { OSX_VK_F3, XK_F3 }, + { OSX_VK_F4, XK_F4 }, + { OSX_VK_F5, XK_F5 }, + { OSX_VK_F6, XK_F6 }, + { OSX_VK_F7, XK_F7 }, + { OSX_VK_F8, XK_F8 }, + { OSX_VK_F9, XK_F9 }, + { OSX_VK_F10, XK_F10 }, + { OSX_VK_F11, XK_F11 }, + { OSX_VK_F12, XK_F12 }, + { OSX_VK_F13, XK_F13 }, + { OSX_VK_F14, XK_F14 }, + { OSX_VK_F15, XK_F15 }, + { OSX_VK_F16, XK_F16 }, + { OSX_VK_F17, XK_F17 }, + { OSX_VK_F18, XK_F18 }, + { OSX_VK_F19, XK_F19 }, // cursor - {OSX_VK_CURSOR_UP, XK_Up}, - {OSX_VK_CURSOR_DOWN, XK_Down}, - {OSX_VK_CURSOR_LEFT, XK_Left}, - {OSX_VK_CURSOR_RIGHT, XK_Right}, - {OSX_VK_PAGEUP, XK_Page_Up}, - {OSX_VK_PAGEDOWN, XK_Page_Down}, - {OSX_VK_HOME, XK_Home}, - {OSX_VK_END, XK_End}, + { OSX_VK_CURSOR_UP, XK_Up }, + { OSX_VK_CURSOR_DOWN, XK_Down }, + { OSX_VK_CURSOR_LEFT, XK_Left }, + { OSX_VK_CURSOR_RIGHT, XK_Right }, + { OSX_VK_PAGEUP, XK_Page_Up }, + { OSX_VK_PAGEDOWN, XK_Page_Down }, + { OSX_VK_HOME, XK_Home }, + { OSX_VK_END, XK_End }, // keypad - {OSX_VK_KEYPAD_0, XK_KP_0}, - {OSX_VK_KEYPAD_1, XK_KP_1}, - {OSX_VK_KEYPAD_2, XK_KP_2}, - {OSX_VK_KEYPAD_3, XK_KP_3}, - {OSX_VK_KEYPAD_4, XK_KP_4}, - {OSX_VK_KEYPAD_5, XK_KP_5}, - {OSX_VK_KEYPAD_6, XK_KP_6}, - {OSX_VK_KEYPAD_7, XK_KP_7}, - {OSX_VK_KEYPAD_8, XK_KP_8}, - {OSX_VK_KEYPAD_9, XK_KP_9}, + { OSX_VK_KEYPAD_0, XK_KP_0 }, + { OSX_VK_KEYPAD_1, XK_KP_1 }, + { OSX_VK_KEYPAD_2, XK_KP_2 }, + { OSX_VK_KEYPAD_3, XK_KP_3 }, + { OSX_VK_KEYPAD_4, XK_KP_4 }, + { OSX_VK_KEYPAD_5, XK_KP_5 }, + { OSX_VK_KEYPAD_6, XK_KP_6 }, + { OSX_VK_KEYPAD_7, XK_KP_7 }, + { OSX_VK_KEYPAD_8, XK_KP_8 }, + { OSX_VK_KEYPAD_9, XK_KP_9 }, //OSX_VK_KEYPAD_CLEAR -> ? - {OSX_VK_KEYPAD_COMMA, XK_KP_Separator}, - {OSX_VK_KEYPAD_DOT, XK_KP_Decimal}, - {OSX_VK_KEYPAD_EQUAL, XK_KP_Equal}, - {OSX_VK_KEYPAD_MINUS, XK_KP_Subtract}, - {OSX_VK_KEYPAD_MULTIPLY, XK_KP_Multiply}, - {OSX_VK_KEYPAD_PLUS, XK_KP_Add}, - {OSX_VK_KEYPAD_SLASH, XK_KP_Divide}, + { OSX_VK_KEYPAD_COMMA, XK_KP_Separator }, + { OSX_VK_KEYPAD_DOT, XK_KP_Decimal }, + { OSX_VK_KEYPAD_EQUAL, XK_KP_Equal }, + { OSX_VK_KEYPAD_MINUS, XK_KP_Subtract }, + { OSX_VK_KEYPAD_MULTIPLY, XK_KP_Multiply }, + { OSX_VK_KEYPAD_PLUS, XK_KP_Add }, + { OSX_VK_KEYPAD_SLASH, XK_KP_Divide }, // pc keyboard - {OSX_VK_PC_APPLICATION, XK_Menu}, - {OSX_VK_PC_INSERT, XK_Insert}, - {OSX_VK_PC_KEYPAD_NUMLOCK, XK_Num_Lock}, - {OSX_VK_PC_PAUSE, XK_Pause}, + { OSX_VK_PC_APPLICATION, XK_Menu }, + { OSX_VK_PC_INSERT, XK_Insert }, + { OSX_VK_PC_KEYPAD_NUMLOCK, XK_Num_Lock }, + { OSX_VK_PC_PAUSE, XK_Pause }, //OSX_VK_PC_POWER -> ? - {OSX_VK_PC_PRINTSCREEN, XK_Print}, - {OSX_VK_PC_SCROLLLOCK, XK_Scroll_Lock}, + { OSX_VK_PC_PRINTSCREEN, XK_Print }, + { OSX_VK_PC_SCROLLLOCK, XK_Scroll_Lock }, - {-1, -1} + { -1, -1 } }; -int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) -{ +int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) { for (struct keycode_mapping_t *mapping = keycode_mappings; mapping->osx_keycode >= 0; ++mapping) { @@ -128,20 +126,15 @@ int osx_keycode_to_rime_keycode(int keycode, int keychar, int shift, int caps) if (keychar >= 0x20 && keychar <= 0x7e) { return keychar; - } - else if (keychar == 0x1b) { // ^[ + } else if (keychar == 0x1b) { // ^[ return XK_bracketleft; - } - else if (keychar == 0x1c) { // ^\ + } else if (keychar == 0x1c) { // ^\ return XK_backslash; - } - else if (keychar == 0x1d) { // ^] + } else if (keychar == 0x1d) { // ^] return XK_bracketright; - } - else if (keychar == 0x1f) { // ^_ + } else if (keychar == 0x1f) { // ^_ return XK_minus; } return XK_VoidSymbol; } - diff --git a/main.m b/main.m index 2a1a556f3..fba429e0b 100644 --- a/main.m +++ b/main.m @@ -20,7 +20,7 @@ int main(int argc, char *argv[]) { if (argc > 1 && !strcmp("--quit", argv[1])) { NSString *bundleId = [NSBundle mainBundle].bundleIdentifier; NSArray *runningSquirrels = - [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId]; + [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleId]; for (NSRunningApplication *squirrelApp in runningSquirrels) { [squirrelApp terminate]; } @@ -29,8 +29,8 @@ int main(int argc, char *argv[]) { if (argc > 1 && !strcmp("--reload", argv[1])) { [[NSDistributedNotificationCenter defaultCenter] - postNotificationName:@"SquirrelReloadNotification" - object:nil]; + postNotificationName:@"SquirrelReloadNotification" + object:nil]; return 0; } @@ -56,8 +56,8 @@ int main(int argc, char *argv[]) { if (argc > 1 && !strcmp("--sync", argv[1])) { [[NSDistributedNotificationCenter defaultCenter] - postNotificationName:@"SquirrelSyncNotification" - object:nil]; + postNotificationName:@"SquirrelSyncNotification" + object:nil]; return 0; } @@ -65,8 +65,8 @@ int main(int argc, char *argv[]) { // find the bundle identifier and then initialize the input method server NSBundle *main = [NSBundle mainBundle]; IMKServer *server __unused = - [[IMKServer alloc] initWithName:(NSString *)kConnectionName - bundleIdentifier:main.bundleIdentifier]; + [[IMKServer alloc] initWithName:(NSString *)kConnectionName + bundleIdentifier:main.bundleIdentifier]; // load the bundle explicitly because in this case the input method is a // background only application @@ -74,16 +74,18 @@ int main(int argc, char *argv[]) { // opencc will be configured with relative dictionary paths [[NSFileManager defaultManager] - changeCurrentDirectoryPath:main.sharedSupportPath]; + changeCurrentDirectoryPath:main.sharedSupportPath]; if (NSApp.squirrelAppDelegate.problematicLaunchDetected) { NSLog(@"Problematic launch detected!"); - NSArray *args = @[ - @"Problematic launch detected! \ - Squirrel may be suffering a crash due to imporper configuration. \ - Revert previous modifications to see if the problem recurs." - ]; - [NSTask launchedTaskWithLaunchPath:@"/usr/bin/say" arguments:args]; + NSArray *args = @[@"-v", NSLocalizedString(@"say_voice", nil), + NSLocalizedString(@"problematic_launch", nil)]; + if (@available(macOS 10.13, *)) { + [NSTask launchedTaskWithExecutableURL:[NSURL fileURLWithPath:@"/usr/bin/say"] + arguments:args error:nil terminationHandler:nil]; + } else { + [NSTask launchedTaskWithLaunchPath:@"/usr/bin/say" arguments:args]; + } } else { [NSApp.squirrelAppDelegate setupRime]; [NSApp.squirrelAppDelegate startRimeWithFullCheck:NO]; diff --git a/plum b/plum index 3d06432dd..6f502ff6f 160000 --- a/plum +++ b/plum @@ -1 +1 @@ -Subproject commit 3d06432dda5fd1738bebda298c574b6ed2d512d8 +Subproject commit 6f502ff6fa87789847fa18200415318e705bffa4 diff --git a/rime.pdf b/rime.pdf index 48c079546..be69811f2 100644 Binary files a/rime.pdf and b/rime.pdf differ diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 7f49d70d4..26a54a014 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -11,13 +11,8 @@ "deploy_start" = "部署输入法引擎…"; "deploy_success" = "部署完成。"; "deploy_failure" = "有错误!请查看日志 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增广"; -"!extended_charset" = "通用"; + +"problematic_launch" = "检测到启动有问题!\ + “鼠须管”可能因错误设置而崩溃。\ + 请尝试撤销之前的修改,然后查看问题是否仍旧存在。"; +"say_voice" = "TingTing"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings index 69ea17b32..300d27b8b 100644 --- a/zh-Hant.lproj/Localizable.strings +++ b/zh-Hant.lproj/Localizable.strings @@ -11,13 +11,8 @@ "deploy_start" = "部署輸入法引擎…"; "deploy_success" = "部署完成。"; "deploy_failure" = "有錯誤!請查看日誌 $TMPDIR/rime.squirrel.INFO"; -"ascii_mode" = "A"; -"!ascii_mode" = "中"; -"full_shape" = "全角"; -"!full_shape" = "半角"; -"ascii_punct" = ".,"; -"!ascii_punct" = "。,"; -"simplification" = "汉字"; -"!simplification" = "漢字"; -"extended_charset" = "增廣"; -"!extended_charset" = "通用"; + +"problematic_launch" = "啟動時偵測到問題!\ + 「鼠鬚管」可能因設定不當而崩潰。\ + 請嘗試回退先前的修改,然後查看問題是否依然存在。"; +"say_voice" = "MeiJia";