From 55bbde24c1b98d4d89ae1abd974c580584528252 Mon Sep 17 00:00:00 2001 From: groverlynn Date: Mon, 26 Feb 2024 04:58:08 +0100 Subject: [PATCH] expandable tabular --- .../chevron.down.symbolset/Contents.json | 15 + .../chevron.down.symbolset/chevron.down.svg | 160 +++ .../chevron.up.symbolset/Contents.json | 15 + .../chevron.up.symbolset/chevron.up.svg | 160 +++ .../Symbols/lock.fill.symbolset/Contents.json | 12 + .../Symbols/lock.fill.symbolset/lock.fill.svg | 160 +++ SquirrelApplicationDelegate.m | 21 +- SquirrelConfig.h | 38 +- SquirrelConfig.m | 86 +- SquirrelInputController.h | 37 +- SquirrelInputController.m | 142 +- SquirrelPanel.h | 12 +- SquirrelPanel.m | 1222 ++++++++++------- en.lproj/Localizable.strings | 5 +- zh-HK.lproj/Localizable.strings | 5 +- zh-Hans.lproj/Localizable.strings | 5 +- zh-Hant.lproj/Localizable.strings | 5 +- 17 files changed, 1480 insertions(+), 620 deletions(-) create mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg create mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg create mode 100644 Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json create mode 100644 Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json new file mode 100644 index 000000000..e599b1e46 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.down.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg new file mode 100644 index 000000000..26086ef64 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.down.symbolset/chevron.down.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.down + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json b/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json new file mode 100644 index 000000000..3f9c92e18 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.symbolset/Contents.json @@ -0,0 +1,15 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "symbol-rendering-intent" : "template" + }, + "symbols" : [ + { + "filename" : "chevron.up.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg new file mode 100644 index 000000000..e35a6e2d0 --- /dev/null +++ b/Assets.xcassets/Symbols/chevron.up.symbolset/chevron.up.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from chevron.up + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json b/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json new file mode 100644 index 000000000..8d2370a08 --- /dev/null +++ b/Assets.xcassets/Symbols/lock.fill.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "lock.fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg new file mode 100644 index 000000000..56b1c665e --- /dev/null +++ b/Assets.xcassets/Symbols/lock.fill.symbolset/lock.fill.svg @@ -0,0 +1,160 @@ + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from lock.fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m index 81bd62669..b35580828 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.m @@ -174,21 +174,21 @@ - (void)shutdownRime { } NSArray *getScriptOptionsForSchema(SquirrelConfig *schema) { - NSUInteger numSwitches = [schema getListSize:@"switches"]; + NSUInteger numSwitches = [schema getListSizeForOption:@"switches"]; if (numSwitches == 0) { return nil; } for (NSUInteger i = 0; i < numSwitches; ++i) { - NSString *name = [schema getString:[NSString stringWithFormat: - @"switches/@%lu/name", i]]; + NSString *name = [schema getStringForOption:[NSString stringWithFormat: + @"switches/@%lu/name", i]]; if (name) { if ([name isEqualToString:@"simplification"] || [name isEqualToString:@"traditional"]) { return @[name]; } } else { - NSArray *options = [schema getList:[NSString stringWithFormat: - @"switches/@%lu/options", i]]; + NSArray *options = [schema getListForOption:[NSString stringWithFormat: + @"switches/@%lu/options", i]]; if ([options containsObject:@"simplification"] || [options containsObject:@"traditional"]) { return options; @@ -228,7 +228,7 @@ - (void)loadSettings { return; } - NSString *showNotificationsWhen = [_config getString:@"show_notifications_when"]; + NSString *showNotificationsWhen = [_config getStringForOption:@"show_notifications_when"]; if ([showNotificationsWhen isEqualToString:@"never"]) { _showNotifications = kShowNotificationsNever; } else if ([showNotificationsWhen isEqualToString:@"appropriate"]) { @@ -236,8 +236,7 @@ - (void)loadSettings { } else { _showNotifications = kShowNotificationsAlways; } - [self.panel loadConfig:_config forAppearance:defaultAppear]; - [self.panel loadConfig:_config forAppearance:darkAppear]; + [self.panel loadConfig:_config]; } - (void)loadSchemaSpecificSettings:(NSString *)schemaId @@ -251,12 +250,10 @@ - (void)loadSchemaSpecificSettings:(NSString *)schemaId [schema hasSection:@"style"]) { SquirrelOptionSwitcher *optionSwitcher = [schema getOptionSwitcher]; self.panel.optionSwitcher = updateOptionSwitcher(optionSwitcher, sessionId); - [self.panel loadConfig:schema forAppearance:defaultAppear]; - [self.panel loadConfig:schema forAppearance:darkAppear]; + [self.panel loadConfig:schema]; } else { self.panel.optionSwitcher = [[SquirrelOptionSwitcher alloc] initWithSchemaId:schemaId]; - [self.panel loadConfig:self.config forAppearance:defaultAppear]; - [self.panel loadConfig:self.config forAppearance:darkAppear]; + [self.panel loadConfig:self.config]; } [schema close]; } diff --git a/SquirrelConfig.h b/SquirrelConfig.h index 9304a8057..8d2ac7f5e 100644 --- a/SquirrelConfig.h +++ b/SquirrelConfig.h @@ -45,25 +45,31 @@ typedef NSMutableDictionary SquirrelMutableAppOptions; - (BOOL)hasSection:(NSString *)section; -- (BOOL)getBool:(NSString *)option; -- (int)getInt:(NSString *)option; -- (double)getDouble:(NSString *)option; -- (double)getDouble:(NSString *)option - applyConstraint:(double(*)(double param))func; -- (NSNumber *)getOptionalBool:(NSString *)option; -- (NSNumber *)getOptionalInt:(NSString *)option; -- (NSNumber *)getOptionalDouble:(NSString *)option; -- (NSNumber *)getOptionalDouble:(NSString *)option - applyConstraint:(double(*)(double param))func; - -- (NSString *)getString:(NSString *)option; +- (BOOL)setBool:(bool)value forOption:(NSString *)option; +- (BOOL)setInt:(int)value forOption:(NSString *)option; +- (BOOL)setDouble:(double)value forOption:(NSString *)option; +- (BOOL)setString:(NSString *)value forOption:(NSString *)option; + +- (BOOL)getBoolForOption:(NSString *)option; +- (int)getIntForOption:(NSString *)option; +- (double)getDoubleForOption:(NSString *)option; +- (double)getDoubleForOption:(NSString *)option + applyConstraint:(double(*)(double param))func; + +- (NSNumber *)getOptionalBoolForOption:(NSString *)option; +- (NSNumber *)getOptionalIntForOption:(NSString *)option; +- (NSNumber *)getOptionalDoubleForOption:(NSString *)option; +- (NSNumber *)getOptionalDoubleForOption:(NSString *)option + applyConstraint:(double(*)(double param))func; + +- (NSString *)getStringForOption:(NSString *)option; // 0xaabbggrr or 0xbbggrr -- (NSColor *)getColor:(NSString *)option; +- (NSColor *)getColorForOption:(NSString *)option; // file path (absolute or relative to ~/Library/Rime) -- (NSImage *)getImage:(NSString *)option; +- (NSImage *)getImageForOption:(NSString *)option; -- (NSUInteger)getListSize:(NSString *)option; -- (NSArray *)getList:(NSString *)option; +- (NSUInteger)getListSizeForOption:(NSString *)option; +- (NSArray *)getListForOption:(NSString *)option; - (SquirrelOptionSwitcher *)getOptionSwitcher; - (SquirrelAppOptions *)getAppOptions:(NSString *)appName; diff --git a/SquirrelConfig.m b/SquirrelConfig.m index 016880659..2a0a9721d 100644 --- a/SquirrelConfig.m +++ b/SquirrelConfig.m @@ -137,25 +137,41 @@ - (BOOL)hasSection:(NSString *)section { return NO; } -- (BOOL)getBool:(NSString *)option { - return [self getOptionalBool:option].boolValue; +- (BOOL)setBool:(bool)value forOption:(NSString *)option { + return (BOOL)(rime_get_api()->config_set_bool(&_config, option.UTF8String, value)); } -- (int)getInt:(NSString *)option { - return [self getOptionalInt:option].intValue; +- (BOOL)setInt:(int)value forOption:(NSString *)option { + return (BOOL)(rime_get_api()->config_set_int(&_config, option.UTF8String, value)); } -- (double)getDouble:(NSString *)option { - return [self getOptionalDouble:option].doubleValue; +- (BOOL)setDouble:(double)value forOption:(NSString *)option { + return (BOOL)(rime_get_api()->config_set_double(&_config, option.UTF8String, value)); } -- (double)getDouble:(NSString *)option - applyConstraint:(double(*)(double param))func { - NSNumber *value = [self getOptionalDouble:option]; +- (BOOL)setString:(NSString *)value forOption:(NSString *)option { + return (BOOL)(rime_get_api()->config_set_string(&_config, option.UTF8String, value.UTF8String)); +} + +- (BOOL)getBoolForOption:(NSString *)option { + return [self getOptionalBoolForOption:option].boolValue; +} + +- (int)getIntForOption:(NSString *)option { + return [self getOptionalIntForOption:option].intValue; +} + +- (double)getDoubleForOption:(NSString *)option { + return [self getOptionalDoubleForOption:option].doubleValue; +} + +- (double)getDoubleForOption:(NSString *)option + applyConstraint:(double(*)(double param))func { + NSNumber *value = [self getOptionalDoubleForOption:option]; return func(value.doubleValue); } -- (NSNumber *)getOptionalBool:(NSString *)option { +- (NSNumber *)getOptionalBoolForOption:(NSString *)option { NSNumber *cachedValue = [self cachedValueOfObjCType:@encode(BOOL) forKey:option]; if (cachedValue) { return cachedValue; @@ -166,10 +182,10 @@ - (NSNumber *)getOptionalBool:(NSString *)option { [_cache setObject:number forKey:option]; return number; } - return [_baseConfig getOptionalBool:option]; + return [_baseConfig getOptionalBoolForOption:option]; } -- (NSNumber *)getOptionalInt:(NSString *)option { +- (NSNumber *)getOptionalIntForOption:(NSString *)option { NSNumber *cachedValue = [self cachedValueOfObjCType:@encode(int) forKey:option]; if (cachedValue) { return cachedValue; @@ -180,10 +196,10 @@ - (NSNumber *)getOptionalInt:(NSString *)option { [_cache setObject:number forKey:option]; return number; } - return [_baseConfig getOptionalInt:option]; + return [_baseConfig getOptionalIntForOption:option]; } -- (NSNumber *)getOptionalDouble:(NSString *)option { +- (NSNumber *)getOptionalDoubleForOption:(NSString *)option { NSNumber *cachedValue = [self cachedValueOfObjCType:@encode(double) forKey:option]; if (cachedValue) { return cachedValue; @@ -194,16 +210,16 @@ - (NSNumber *)getOptionalDouble:(NSString *)option { [_cache setObject:number forKey:option]; return number; } - return [_baseConfig getOptionalDouble:option]; + return [_baseConfig getOptionalDoubleForOption:option]; } -- (NSNumber *)getOptionalDouble:(NSString *)option - applyConstraint:(double(*)(double param))func { - NSNumber *value = [self getOptionalDouble:option]; +- (NSNumber *)getOptionalDoubleForOption:(NSString *)option + applyConstraint:(double(*)(double param))func { + NSNumber *value = [self getOptionalDoubleForOption:option]; return value ? [NSNumber numberWithDouble:func(value.doubleValue)] : nil; } -- (NSString *)getString:(NSString *)option { +- (NSString *)getStringForOption:(NSString *)option { NSString *cachedValue = [self cachedValueOfClass:NSString.class forKey:option]; if (cachedValue) { return cachedValue; @@ -216,47 +232,47 @@ - (NSString *)getString:(NSString *)option { [_cache setObject:string forKey:option]; return string; } - return [_baseConfig getString:option]; + return [_baseConfig getStringForOption:option]; } -- (NSColor *)getColor:(NSString *)option { +- (NSColor *)getColorForOption:(NSString *)option { NSColor *cachedValue = [self cachedValueOfClass:NSColor.class forKey:option]; if (cachedValue) { return cachedValue; } - NSColor *color = [self colorFromString:[self getString:option]]; + NSColor *color = [self colorFromString:[self getStringForOption:option]]; if (color) { [_cache setObject:color forKey:option]; return color; } - return [_baseConfig getColor:option]; + return [_baseConfig getColorForOption:option]; } -- (NSImage *)getImage:(NSString *)option { +- (NSImage *)getImageForOption:(NSString *)option { NSImage *cachedValue = [self cachedValueOfClass:NSImage.class forKey:option]; if (cachedValue) { return cachedValue; } - NSImage *image = [self imageFromFile:[self getString:option]]; + NSImage *image = [self imageFromFile:[self getStringForOption:option]]; if (image) { [_cache setObject:image forKey:option]; return image; } - return [_baseConfig getImage:option]; + return [_baseConfig getImageForOption:option]; } -- (NSUInteger)getListSize:(NSString *)option { +- (NSUInteger)getListSizeForOption:(NSString *)option { return rime_get_api()->config_list_size(&_config, option.UTF8String); } -- (NSArray *)getList:(NSString *)option { +- (NSArray *)getListForOption:(NSString *)option { RimeConfigIterator iterator; if (!rime_get_api()->config_begin_list(&iterator, &_config, option.UTF8String)) { return nil; } NSMutableArray *strList = [[NSMutableArray alloc] init]; while (rime_get_api()->config_next(&iterator)) { - [strList addObject:[self getString:@(iterator.path)]]; + [strList addObject:[self getStringForOption:@(iterator.path)]]; } rime_get_api()->config_end(&iterator); return strList; @@ -270,8 +286,8 @@ - (SquirrelOptionSwitcher *)getOptionSwitcher { NSMutableDictionary *switcher = [[NSMutableDictionary alloc] init]; NSMutableDictionary *optionGroups = [[NSMutableDictionary alloc] init]; while (rime_get_api()->config_next(&switchIter)) { - int reset = [self getInt:[@(switchIter.path) stringByAppendingString:@"/reset"]]; - NSString *name = [self getString:[@(switchIter.path) stringByAppendingString:@"/name"]]; + int reset = [self getIntForOption:[@(switchIter.path) stringByAppendingString:@"/reset"]]; + NSString *name = [self getStringForOption:[@(switchIter.path) stringByAppendingString:@"/name"]]; if (name) { if ([self hasSection:[@"style/!" stringByAppendingString:name]] || [self hasSection:[@"style/" stringByAppendingString:name]]) { @@ -287,7 +303,7 @@ - (SquirrelOptionSwitcher *)getOptionSwitcher { NSMutableArray *optionGroup = [[NSMutableArray alloc] init]; BOOL hasStyleSection = NO; while (rime_get_api()->config_next(&optionIter)) { - NSString *option = [self getString:@(optionIter.path)]; + NSString *option = [self getStringForOption:@(optionIter.path)]; [optionGroup addObject:option]; hasStyleSection |= [self hasSection:[@"style/" stringByAppendingString:option]]; } @@ -315,9 +331,9 @@ - (SquirrelAppOptions *)getAppOptions:(NSString *)appName { } while (rime_get_api()->config_next(&iterator)) { //NSLog(@"DEBUG option[%d]: %s (%s)", iterator.index, iterator.key, iterator.path); - NSNumber *value = [self getOptionalBool:@(iterator.path)] ? : - [self getOptionalInt:@(iterator.path)] ? : - [self getOptionalDouble:@(iterator.path)]; + NSNumber *value = [self getOptionalBoolForOption:@(iterator.path)] ? : + [self getOptionalIntForOption:@(iterator.path)] ? : + [self getOptionalDoubleForOption:@(iterator.path)]; if (value) { appOptions[@(iterator.key)] = value; } diff --git a/SquirrelInputController.h b/SquirrelInputController.h index 92c7bd6f3..30dc575c6 100644 --- a/SquirrelInputController.h +++ b/SquirrelInputController.h @@ -3,24 +3,33 @@ @interface SquirrelInputController : IMKInputController +// kPROCESS accepts miscellaneous / function keys (e.g. XK_Escape) +// The remaining 3 actions accept candidate indices (int), starting from item 0 on page 0 typedef NS_ENUM(NSInteger, SquirrelAction) { - kSELECT = 1, // accepts indices in digits, selection keys, and keycodes (XK_Escape) - kHILITE = 2, // accepts indices in digits and selection keys (char '1' / 'A') - kDELETE = 3 // only accepts indices in digits (int 1) + kPROCESS = 0, + kSELECT = 1, + kHIGHLIGHT = 2, + kDELETE = 3 }; typedef NS_ENUM(NSUInteger, SquirrelIndex) { - // 0 ... 9 are ordinal digits, used as (int) index - // 0x21 ... 0x7e are ASCII chars (as selection keys) - // other rime keycodes (as function keys), for paging etc. - kBackSpace = 0xff08, // XK_BackSpace - kEscape = 0xff1b, // XK_Escape - kCodeInput = 0xff37, // XK_Codeinput - kHome = 0xff50, // XK_Home - kPageUp = 0xff55, // XK_Page_Up - kPageDown = 0xff56, // XK_Page_Down - kEnd = 0xff57, // XK_End - kVoidSymbol = 0xffffff // XK_VoidSymbol + // 0, 1, 2 ... are ordinal digits, used as (int) indices + // 0xFFXX are rime keycodes (as function keys), for paging etc. + kBackSpaceKey = 0xff08, // XK_BackSpace + kEscapeKey = 0xff1b, // XK_Escape + kCodeInputArea = 0xff37, // XK_Codeinput + kHomeKey = 0xff50, // XK_Home + kLeftKey = 0xff51, // XK_Left + kUpKey = 0xff52, // XK_Up + kRightKey = 0xff53, // XK_Right + kDownKey = 0xff54, // XK_Down + kPageUpKey = 0xff55, // XK_Page_Up + kPageDownKey = 0xff56, // XK_Page_Down + kEndKey = 0xff57, // XK_End + kExpandButton = 0xff04, + kCompressButton = 0xff05, + kLockButton = 0xff06, + kVoidSymbol = 0xffffff // XK_VoidSymbol }; @property(class, weak, readonly) SquirrelInputController *currentController; diff --git a/SquirrelInputController.m b/SquirrelInputController.m index 48240825b..d4d610f29 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.m @@ -10,7 +10,7 @@ #import #import -const int N_KEY_ROLL_OVER = 50; +static const int N_KEY_ROLL_OVER = 50; static NSString *const kFullWidthSpace = @" "; @implementation SquirrelInputController { @@ -22,6 +22,7 @@ @implementation SquirrelInputController { NSArray *_candidates; NSEventModifierFlags _lastModifiers; uint _lastEventCount; + NSUInteger _lastPageNum; RimeSessionId _session; NSString *_schemaId; BOOL _inlinePreedit; @@ -41,7 +42,8 @@ @implementation SquirrelInputController { } static SquirrelInputController *_currentController = nil; -static NSMapTable *_controllerDeactivationTime = NSMapTable.weakToWeakObjectsMapTable; +static NSMapTable *_controllerDeactivationTime = + NSMapTable.weakToWeakObjectsMapTable; + (void)setCurrentController:(SquirrelInputController *)controller { _currentController = controller; @@ -212,9 +214,9 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index if (_inlineCandidate && !_inlinePreedit) { return NO; } - [self perform:kSELECT onIndex:kEnd]; + [self perform:kPROCESS onIndex:kEndKey]; } else if (point.x < head.x || index <= 0) { - [self perform:kSELECT onIndex:kHome]; + [self perform:kPROCESS onIndex:kHomeKey]; } else { [self moveCursor:_caretPos toPosition:index @@ -240,17 +242,30 @@ void set_CapsLock_LED_state(bool target_state) { - (BOOL)processKey:(int)rime_keycode modifiers:(int)rime_modifiers { + SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; // with linear candidate list, arrow keys may behave differently. - Bool is_linear = (Bool)NSApp.squirrelAppDelegate.panel.linear; + Bool is_linear = (Bool)panel.linear; if (is_linear != rime_get_api()->get_option(_session, "_linear")) { rime_get_api()->set_option(_session, "_linear", is_linear); } // with vertical text, arrow keys may behave differently. - Bool is_vertical = (Bool)NSApp.squirrelAppDelegate.panel.vertical; + Bool is_vertical = (Bool)panel.vertical; if (is_vertical != rime_get_api()->get_option(_session, "_vertical")) { rime_get_api()->set_option(_session, "_vertical", is_vertical); } + if (panel.tabular && !rime_modifiers && + (is_vertical ? rime_keycode == XK_Left || rime_keycode == XK_Right + : rime_keycode == XK_Up || rime_keycode == XK_Down)) { + NSUInteger newIndex = [panel candidateIndexOnDirection:(SquirrelIndex)rime_keycode]; + if (newIndex != NSNotFound) { + if (!panel.locked && !panel.expanded && rime_keycode == (is_vertical ? XK_Left : XK_Down)) { + panel.expanded = YES; + } + return rime_get_api()->highlight_candidate(_session, newIndex); + } + } + BOOL handled = (BOOL)rime_get_api()->process_key(_session, rime_keycode, rime_modifiers); //NSLog(@"rime_keycode: 0x%x, rime_modifiers: 0x%x, handled = %d", rime_keycode, rime_modifiers, handled); @@ -337,24 +352,26 @@ - (void)perform:(SquirrelAction)action onIndex:(SquirrelIndex)index { //NSLog(@"perform action: %lu on index: %lu", action, index); bool handled = false; - if (index >= '!' && index <= '~' && (action == kSELECT || action == kHILITE)) { - handled = rime_get_api()->process_key(_session, (int)index, action == kHILITE ? kAltMask : 0); - } else if (index >= 0xff08 && index <= 0xffff && action == kSELECT) { - handled = rime_get_api()->process_key(_session, (int)index, 0); - } else if (index >= 0 && index < 10) { - switch (action) { - case kDELETE: - handled = rime_get_api()->delete_candidate_on_current_page(_session, (size_t)index); - break; - case kSELECT: - handled = rime_get_api()->select_candidate_on_current_page(_session, (size_t)index); - break; - case kHILITE: - handled = rime_get_api()->highlight_candidate_on_current_page(_session, (size_t)index); - break; - } + switch (action) { + case kPROCESS: + if (index >= 0xff08 && index <= 0xffff) { + handled = rime_get_api()->process_key(_session, (int)index, 0); + } else if (index >= kExpandButton && index <= kLockButton) { + handled = true; + } + break; + case kSELECT: + handled = rime_get_api()->select_candidate(_session, index); + break; + case kHIGHLIGHT: + handled = rime_get_api()->highlight_candidate(_session, index); + break; + case kDELETE: + handled = rime_get_api()->delete_candidate(_session, index); + break; } if (handled) { + _lastPageNum = NSNotFound; [self rimeUpdate]; } } @@ -398,7 +415,7 @@ - (void)updateChord:(int)keycode } _chordDuration = 0.1; NSNumber *duration = [NSApp.squirrelAppDelegate.config - getOptionalDouble:@"chord_duration"]; + getOptionalDoubleForOption:@"chord_duration"]; if (duration.doubleValue > 0) { _chordDuration = duration.doubleValue; } @@ -467,7 +484,7 @@ - (void)showInitialStatus { - (void)activateServer:(id)sender { //NSLog(@"activateServer:"); NSString *keyboardLayout = [NSApp.squirrelAppDelegate.config - getString:@"keyboard_layout"]; + getStringForOption:@"keyboard_layout"]; if ([keyboardLayout isEqualToString:@"last"] || [keyboardLayout isEqualToString:@""]) { keyboardLayout = nil; @@ -493,7 +510,7 @@ - (void)activateServer:(id)sender { SquirrelConfig *defaultConfig = [[SquirrelConfig alloc] init]; if ([defaultConfig openWithConfigId:@"default"] && [defaultConfig hasSection:@"ascii_composer"]) { - _goodOldCapsLock = [defaultConfig getBool: + _goodOldCapsLock = [defaultConfig getBoolForOption: @"ascii_composer/good_old_caps_lock"]; } [defaultConfig close]; @@ -733,6 +750,7 @@ - (void)showPanelWithPreedit:(NSString *)preedit lastPage:(BOOL)lastPage { //NSLog(@"showPanelWithPreedit:...:"); _candidates = candidates; + _lastPageNum = pageNum; SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; panel.IbeamRect = [self getIbeamRect]; if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) { @@ -819,6 +837,7 @@ - (void)rimeUpdate { //NSLog(@"rimeUpdate"); BOOL didCommit = [self rimeConsumeCommittedText]; + SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; RIME_STRUCT(RimeStatus, status); if (rime_get_api()->get_status(_session, &status)) { // enable schema specific ui style @@ -830,11 +849,9 @@ - (void)rimeUpdate { [NSApp.squirrelAppDelegate loadSchemaSpecificSettings:_schemaId withRimeSession:_session]; // inline preedit - _inlinePreedit = (NSApp.squirrelAppDelegate.panel.inlinePreedit && - !rime_get_api()->get_option(_session, "no_inline")) || + _inlinePreedit = (panel.inlinePreedit && !rime_get_api()->get_option(_session, "no_inline")) || rime_get_api()->get_option(_session, "inline"); - _inlineCandidate = (NSApp.squirrelAppDelegate.panel.inlineCandidate && - !rime_get_api()->get_option(_session, "no_inline")); + _inlineCandidate = panel.inlineCandidate && !rime_get_api()->get_option(_session, "no_inline"); // if not inline, embed soft cursor in preedit string rime_get_api()->set_option(_session, "soft_cursor", !_inlinePreedit); } else { @@ -846,9 +863,10 @@ - (void)rimeUpdate { RIME_STRUCT(RimeContext, ctx); if (rime_get_api()->get_context(_session, &ctx)) { - BOOL showingStatus = NSApp.squirrelAppDelegate.panel.statusMessage.length > 0; + BOOL showingStatus = panel.statusMessage.length > 0; // update raw input const char *raw_input = rime_get_api()->get_input(_session); + BOOL didCompose = ![_originalString isEqualToString:raw_input ? @(raw_input) : @""]; _originalString = raw_input ? @(raw_input) : @""; // update preedit text @@ -873,7 +891,31 @@ - (void)rimeUpdate { NSUInteger end = UTF8LengthToUTF16Length(preedit, ctx.composition.sel_end); NSUInteger caretPos = UTF8LengthToUTF16Length(preedit, ctx.composition.cursor_pos); NSUInteger length = UTF8LengthToUTF16Length(preedit, ctx.composition.length); - NSUInteger numCandidate = (NSUInteger)ctx.menu.num_candidates; + NSUInteger numCandidates = (NSUInteger)ctx.menu.num_candidates; + NSUInteger pageNum = (NSUInteger)ctx.menu.page_no; + NSUInteger pageSize = (NSUInteger)ctx.menu.page_size; + NSUInteger highlightedIndex = numCandidates == 0 ? NSNotFound : + (NSUInteger)ctx.menu.highlighted_candidate_index; + BOOL isLastPage = (BOOL)ctx.menu.is_last_page; + + // update discloser and active line status in gridded layout + if (panel.tabular && !showingStatus && numCandidates > 0) { + if (didCompose) { + panel.activePage = 0; + } else if (_lastPageNum != NSNotFound) { + if (!panel.locked && panel.expanded && (pageNum | _lastPageNum | highlightedIndex) == 0) { + panel.expanded = NO; + } else if (!panel.locked && !panel.expanded && pageNum > 0 && pageNum > _lastPageNum) { + panel.expanded = YES; + } + if (panel.expanded && pageNum > _lastPageNum && panel.activePage < 4) { + panel.activePage = MIN(panel.activePage + pageNum - _lastPageNum, isLastPage ? 4UL : 3UL); + } else if (panel.expanded && pageNum < _lastPageNum && panel.activePage > 0) { + panel.activePage = MAX(panel.activePage + pageNum - _lastPageNum, pageNum == 0 ? 0UL : 1UL); + } + } + highlightedIndex += pageSize * panel.activePage; + } if (showingStatus) { [self clearBuffer]; @@ -909,7 +951,7 @@ - (void)rimeUpdate { } } else { if (_inlinePreedit && !_showingSwitcherMenu) { - if (_inlinePlaceholder && preeditText.length == 0 && numCandidate > 0) { + if (_inlinePlaceholder && preeditText.length == 0 && numCandidates > 0) { [self showPlaceholder:kFullWidthSpace]; } else if (!didCommit || preeditText.length > 0) { [self showPreeditString:preeditText @@ -925,20 +967,44 @@ - (void)rimeUpdate { } } // update candidates - NSMutableArray *candidates = [[NSMutableArray alloc] initWithCapacity:numCandidate]; - NSMutableArray *comments = [[NSMutableArray alloc] initWithCapacity:numCandidate]; - for (NSUInteger i = 0; i < numCandidate; ++i) { + NSMutableArray *candidates = numCandidates ? [[NSMutableArray alloc] init] : nil; + NSMutableArray *comments = numCandidates ? [[NSMutableArray alloc] init] : nil; + if (numCandidates > 0 && panel.expanded && panel.activePage > 0) { + NSUInteger index = pageSize * (pageNum - panel.activePage); + RimeCandidateListIterator iterator; + if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { + NSUInteger endIndex = pageSize * pageNum; + while (index++ < endIndex && rime_get_api()->candidate_list_next(&iterator)) { + [candidates addObject:@(iterator.candidate.text)]; + [comments addObject:@(iterator.candidate.comment ? : "")]; + } + rime_get_api()->candidate_list_end(&iterator); + } + } + for (NSUInteger i = 0; i < numCandidates; ++i) { [candidates addObject:@(ctx.menu.candidates[i].text)]; [comments addObject:@(ctx.menu.candidates[i].comment ? : "")]; } + if (numCandidates > 0 && panel.expanded && panel.activePage < 5) { + NSUInteger index = pageSize * (pageNum + 1); + RimeCandidateListIterator iterator; + if (rime_get_api()->candidate_list_from_index(_session, &iterator, (int)index)) { + NSUInteger endIndex = pageSize * (pageNum + 5 - panel.activePage); + while (index++ < endIndex && rime_get_api()->candidate_list_next(&iterator)) { + [candidates addObject:@(iterator.candidate.text)]; + [comments addObject:@(iterator.candidate.comment ? : "")]; + } + rime_get_api()->candidate_list_end(&iterator); + } + } [self showPanelWithPreedit:_inlinePreedit && !_showingSwitcherMenu ? nil : preeditText selRange:NSMakeRange(start, end - start) caretPos:_showingSwitcherMenu ? NSNotFound : caretPos candidates:candidates comments:comments - highlightedIndex:(NSUInteger)ctx.menu.highlighted_candidate_index - pageNum:(NSUInteger)ctx.menu.page_no - lastPage:(BOOL)ctx.menu.is_last_page]; + highlightedIndex:highlightedIndex + pageNum:pageNum + lastPage:isLastPage]; rime_get_api()->free_context(&ctx); } else { [self hidePalettes]; diff --git a/SquirrelPanel.h b/SquirrelPanel.h index aed2cab76..2b79e84e8 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.h @@ -14,8 +14,11 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { // Linear candidate list layout, as opposed to stacked candidate list layout. @property(nonatomic, readonly) BOOL linear; -// Tabled candidate list layout, a subtype of linear candidate list with tabled layout. -@property(nonatomic, readonly) BOOL tabled; +// Tabular candidate list layout, initializes as tab-aligned linear layout, expandable to stack more candidates +@property(nonatomic, readonly) BOOL tabular; +@property(nonatomic, readonly) BOOL locked; +@property(nonatomic, assign) BOOL expanded; +@property(nonatomic, assign) NSUInteger activePage; // Vertical text orientation, as opposed to horizontal text orientation. @property(nonatomic, readonly) BOOL vertical; // Show preedit text inline. @@ -29,6 +32,8 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { // position of the text input I-beam cursor on screen. @property(nonatomic, assign) NSRect IbeamRect; +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey; + - (void)showPreedit:(NSString *)preedit selRange:(NSRange)selRange caretPos:(NSUInteger)caretPos @@ -43,8 +48,7 @@ typedef NS_ENUM(NSUInteger, SquirrelAppear) { - (void)updateStatusLong:(NSString *)messageLong statusShort:(NSString *)messageShort; -- (void)loadConfig:(SquirrelConfig *)config - forAppearance:(SquirrelAppear)appear; +- (void)loadConfig:(SquirrelConfig *)config; - (void)loadLabelConfig:(SquirrelConfig *)config directUpdate:(BOOL)update; diff --git a/SquirrelPanel.m b/SquirrelPanel.m index b171b905b..8c64d2af5 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.m @@ -1,5 +1,6 @@ #import "SquirrelPanel.h" +#import "SquirrelApplicationDelegate.h" #import "SquirrelConfig.h" #import @@ -244,6 +245,25 @@ + (NSColorSpace *)labColorSpace { @end // NSColorSpace (labColorSpace) +@implementation NSColor (semanticColors) + ++ (NSColor *)secondaryTextColor { + if (@available(macOS 10.10, *)) { + return NSColor.secondaryLabelColor; + } else { + return NSColor.disabledControlTextColor; + } +} + ++ (NSColor *)accentColor { + if (@available(macOS 10.14, *)) { + return NSColor.controlAccentColor; + } else { + return [NSColor colorForControlTint:NSColor.currentControlTint]; + } +} + +@end @implementation NSColor (colorWithLabColorSpace) @@ -322,20 +342,19 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, readonly) CGFloat cornerRadius; @property(nonatomic, readonly) CGFloat highlightedCornerRadius; @property(nonatomic, readonly) CGFloat separatorWidth; -@property(nonatomic, readonly) NSSize borderInset; @property(nonatomic, readonly) CGFloat linespace; @property(nonatomic, readonly) CGFloat preeditLinespace; @property(nonatomic, readonly) CGFloat alpha; @property(nonatomic, readonly) CGFloat translucency; @property(nonatomic, readonly) CGFloat lineLength; +@property(nonatomic, readonly) NSSize borderInset; @property(nonatomic, readonly) BOOL showPaging; @property(nonatomic, readonly) BOOL rememberSize; -@property(nonatomic, readonly) BOOL tabled; +@property(nonatomic, readonly) BOOL tabular; @property(nonatomic, readonly) BOOL linear; @property(nonatomic, readonly) BOOL vertical; @property(nonatomic, readonly) BOOL inlinePreedit; @property(nonatomic, readonly) BOOL inlineCandidate; -@property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; @property(nonatomic, strong, readonly) NSDictionary *attrs; @property(nonatomic, strong, readonly) NSDictionary *highlightedAttrs; @@ -360,14 +379,17 @@ typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { @property(nonatomic, strong, readonly) NSAttributedString *symbolForwardStroke; @property(nonatomic, strong, readonly) NSAttributedString *symbolDeleteFill; @property(nonatomic, strong, readonly) NSAttributedString *symbolDeleteStroke; +@property(nonatomic, strong, readonly) NSAttributedString *symbolCompress; +@property(nonatomic, strong, readonly) NSAttributedString *symbolExpand; +@property(nonatomic, strong, readonly) NSAttributedString *symbolLock; @property(nonatomic, strong, readonly) NSString *selectKeys; @property(nonatomic, strong, readonly) NSString *candidateFormat; @property(nonatomic, strong, readonly) NSArray *labels; @property(nonatomic, strong, readonly) NSArray *candidateFormats; @property(nonatomic, strong, readonly) NSArray *candidateHighlightedFormats; - -- (instancetype)init; +@property(nonatomic, readonly) SquirrelStatusMessageType statusMessageType; +@property(nonatomic, readonly) NSUInteger pageSize; - (void) setBackColor:(NSColor *)backColor highlightedCandidateBackColor:(NSColor *)highlightedCandidateBackColor @@ -379,15 +401,15 @@ - (void) setBackColor:(NSColor *)backColor - (void) setCornerRadius:(CGFloat)cornerRadius highlightedCornerRadius:(CGFloat)highlightedCornerRadius separatorWidth:(CGFloat)separatorWidth - borderInset:(NSSize)borderInset linespace:(CGFloat)linespace preeditLinespace:(CGFloat)preeditLinespace alpha:(CGFloat)alpha translucency:(CGFloat)translucency lineLength:(CGFloat)lineLength + borderInset:(NSSize)borderInset showPaging:(BOOL)showPaging rememberSize:(BOOL)rememberSize - tabled:(BOOL)tabled + tabular:(BOOL)tabular linear:(BOOL)linear vertical:(BOOL)vertical inlinePreedit:(BOOL)inlinePreedit @@ -597,22 +619,6 @@ static CGFloat getLineHeight(NSFont *font, BOOL vertical) { return formatted; } -+ (NSColor *)secondaryTextColor { - if (@available(macOS 10.10, *)) { - return NSColor.secondaryLabelColor; - } else { - return NSColor.disabledControlTextColor; - } -} - -+ (NSColor *)accentColor { - if (@available(macOS 10.14, *)) { - return NSColor.controlAccentColor; - } else { - return [NSColor colorForControlTint:NSColor.currentControlTint]; - } -} - - (instancetype)init { if (self = [super init]) { NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; @@ -647,14 +653,14 @@ - (instancetype)init { highlightedAttrs[NSForegroundColorAttributeName] = NSColor.selectedMenuItemTextColor; NSMutableDictionary *labelAttrs = attrs.mutableCopy; - labelAttrs[NSForegroundColorAttributeName] = SquirrelTheme.accentColor; + labelAttrs[NSForegroundColorAttributeName] = NSColor.accentColor; labelAttrs[NSFontAttributeName] = userMonoFont; NSMutableDictionary *labelHighlightedAttrs = labelAttrs.mutableCopy; labelHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.alternateSelectedControlTextColor; NSMutableDictionary *commentAttrs = [[NSMutableDictionary alloc] init]; - commentAttrs[NSForegroundColorAttributeName] = SquirrelTheme.secondaryTextColor; + commentAttrs[NSForegroundColorAttributeName] = NSColor.secondaryTextColor; commentAttrs[NSFontAttributeName] = userFont; NSMutableDictionary *commentHighlightedAttrs = commentAttrs.mutableCopy; @@ -719,15 +725,15 @@ - (void) setBackColor:(NSColor *)backColor - (void) setCornerRadius:(CGFloat)cornerRadius highlightedCornerRadius:(CGFloat)highlightedCornerRadius separatorWidth:(CGFloat)separatorWidth - borderInset:(NSSize)borderInset linespace:(CGFloat)linespace preeditLinespace:(CGFloat)preeditLinespace alpha:(CGFloat)alpha translucency:(CGFloat)translucency lineLength:(CGFloat)lineLength + borderInset:(NSSize)borderInset showPaging:(BOOL)showPaging rememberSize:(BOOL)rememberSize - tabled:(BOOL)tabled + tabular:(BOOL)tabular linear:(BOOL)linear vertical:(BOOL)vertical inlinePreedit:(BOOL)inlinePreedit @@ -735,15 +741,15 @@ - (void) setCornerRadius:(CGFloat)cornerRadius _cornerRadius = cornerRadius; _highlightedCornerRadius = highlightedCornerRadius; _separatorWidth = separatorWidth; - _borderInset = borderInset; _linespace = linespace; _preeditLinespace = preeditLinespace; _alpha = alpha; _translucency = translucency; _lineLength = lineLength; + _borderInset = borderInset; _showPaging = showPaging; _rememberSize = rememberSize; - _tabled = tabled; + _tabular = tabular; _linear = linear; _vertical = vertical; _inlinePreedit = inlinePreedit; @@ -777,44 +783,12 @@ - (void) setAttrs:(NSDictionary *)attrs sepAttrs[NSVerticalGlyphFormAttributeName] = @(NO); sepAttrs[NSKernAttributeName] = @(0.0); _separator = [[NSAttributedString alloc] initWithString: - _linear ? (_tabled ? [kFullWidthSpace stringByAppendingString:@"\t"] - : kFullWidthSpace) : @"\n" attributes:sepAttrs]; + _linear ? (_tabular ? [kFullWidthSpace stringByAppendingString:@"\t"] + : kFullWidthSpace) : @"\n" attributes:sepAttrs]; // Symbols for function buttons NSString *attmCharacter = [NSString stringWithFormat:@"%C", (unichar)NSAttachmentCharacter]; - NSTextAttachment *attmBackFill = [[NSTextAttachment alloc] init]; - attmBackFill.image = [NSImage imageNamed:[NSString stringWithFormat: - @"Symbols/chevron.%@.circle.fill", _linear ? @"up" : @"left"]]; - NSMutableDictionary *attrsBackFill = pagingAttrs.mutableCopy; - attrsBackFill[NSAttachmentAttributeName] = attmBackFill; - _symbolBackFill = [[NSAttributedString alloc] initWithString:attmCharacter - attributes:attrsBackFill]; - - NSTextAttachment *attmBackStroke = [[NSTextAttachment alloc] init]; - attmBackStroke.image = [NSImage imageNamed:[NSString stringWithFormat: - @"Symbols/chevron.%@.circle", _linear ? @"up" : @"left"]]; - NSMutableDictionary *attrsBackStroke = pagingAttrs.mutableCopy; - attrsBackStroke[NSAttachmentAttributeName] = attmBackStroke; - _symbolBackStroke = [[NSAttributedString alloc] initWithString:attmCharacter - attributes:attrsBackStroke]; - - NSTextAttachment *attmForwardFill = [[NSTextAttachment alloc] init]; - attmForwardFill.image = [NSImage imageNamed:[NSString stringWithFormat: - @"Symbols/chevron.%@.circle.fill", _linear ? @"down" : @"right"]]; - NSMutableDictionary *attrsForwardFill = pagingAttrs.mutableCopy; - attrsForwardFill[NSAttachmentAttributeName] = attmForwardFill; - _symbolForwardFill = [[NSAttributedString alloc] initWithString:attmCharacter - attributes:attrsForwardFill]; - - NSTextAttachment *attmForwardStroke = [[NSTextAttachment alloc] init]; - attmForwardStroke.image = [NSImage imageNamed:[NSString stringWithFormat: - @"Symbols/chevron.%@.circle", _linear ? @"down" : @"right"]]; - NSMutableDictionary *attrsForwardStroke = pagingAttrs.mutableCopy; - attrsForwardStroke[NSAttachmentAttributeName] = attmForwardStroke; - _symbolForwardStroke = [[NSAttributedString alloc] initWithString:attmCharacter - attributes:attrsForwardStroke]; - NSTextAttachment *attmDeleteFill = [[NSTextAttachment alloc] init]; attmDeleteFill.image = [NSImage imageNamed:@"Symbols/delete.backward.fill"]; NSMutableDictionary *attrsDeleteFill = preeditAttrs.mutableCopy; @@ -830,6 +804,60 @@ - (void) setAttrs:(NSDictionary *)attrs attrsDeleteStroke[NSVerticalGlyphFormAttributeName] = @(NO); _symbolDeleteStroke = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsDeleteStroke]; + if (_tabular) { + NSTextAttachment *attmCompress = [[NSTextAttachment alloc] init]; + attmCompress.image = [NSImage imageNamed:@"Symbols/chevron.up"]; + NSMutableDictionary *attrsCompress = pagingAttrs.mutableCopy; + attrsCompress[NSAttachmentAttributeName] = attmCompress; + _symbolCompress = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsCompress]; + + NSTextAttachment *attmExpand = [[NSTextAttachment alloc] init]; + attmExpand.image = [NSImage imageNamed:@"Symbols/chevron.down"]; + NSMutableDictionary *attrsExpand = pagingAttrs.mutableCopy; + attrsExpand[NSAttachmentAttributeName] = attmExpand; + _symbolExpand = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsExpand]; + + NSTextAttachment *attmLock = [[NSTextAttachment alloc] init]; + attmLock.image = [NSImage imageNamed:@"Symbols/lock.fill"]; + NSMutableDictionary *attrsLock = pagingAttrs.mutableCopy; + attrsLock[NSAttachmentAttributeName] = attmLock; + _symbolLock = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsLock]; + } else if (_showPaging) { + NSTextAttachment *attmBackFill = [[NSTextAttachment alloc] init]; + attmBackFill.image = [NSImage imageNamed:[NSString stringWithFormat: + @"Symbols/chevron.%@.circle.fill", _linear ? @"up" : @"left"]]; + NSMutableDictionary *attrsBackFill = pagingAttrs.mutableCopy; + attrsBackFill[NSAttachmentAttributeName] = attmBackFill; + _symbolBackFill = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsBackFill]; + + NSTextAttachment *attmBackStroke = [[NSTextAttachment alloc] init]; + attmBackStroke.image = [NSImage imageNamed:[NSString stringWithFormat: + @"Symbols/chevron.%@.circle", _linear ? @"up" : @"left"]]; + NSMutableDictionary *attrsBackStroke = pagingAttrs.mutableCopy; + attrsBackStroke[NSAttachmentAttributeName] = attmBackStroke; + _symbolBackStroke = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsBackStroke]; + + NSTextAttachment *attmForwardFill = [[NSTextAttachment alloc] init]; + attmForwardFill.image = [NSImage imageNamed:[NSString stringWithFormat: + @"Symbols/chevron.%@.circle.fill", _linear ? @"down" : @"right"]]; + NSMutableDictionary *attrsForwardFill = pagingAttrs.mutableCopy; + attrsForwardFill[NSAttachmentAttributeName] = attmForwardFill; + _symbolForwardFill = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsForwardFill]; + + NSTextAttachment *attmForwardStroke = [[NSTextAttachment alloc] init]; + attmForwardStroke.image = [NSImage imageNamed:[NSString stringWithFormat: + @"Symbols/chevron.%@.circle", _linear ? @"down" : @"right"]]; + NSMutableDictionary *attrsForwardStroke = pagingAttrs.mutableCopy; + attrsForwardStroke[NSAttachmentAttributeName] = attmForwardStroke; + _symbolForwardStroke = [[NSAttributedString alloc] initWithString:attmCharacter + attributes:attrsForwardStroke]; + } } - (void)setParagraphStyle:(NSParagraphStyle *)paragraphStyle @@ -847,6 +875,7 @@ - (void)setSelectKeys:(NSString *)selectKeys directUpdate:(BOOL)update { _selectKeys = selectKeys; _labels = labels; + _pageSize = labels.count; if (update && _candidateFormat) { [self updateCandidateFormats]; } @@ -869,7 +898,7 @@ - (void)updateCandidateFormats { } NSRange candidateRange = [candidateFormat rangeOfString:@"%@"]; if (labelRange.location > candidateRange.location) { - [candidateFormat setString:kDefaultCandidateFormat]; + candidateFormat.string = kDefaultCandidateFormat; candidateRange = [candidateFormat rangeOfString:@"%@"]; } labelRange = NSMakeRange(0, candidateRange.location); @@ -1153,12 +1182,19 @@ - (NSTextLayoutFragment *)textLayoutManager:(NSTextLayoutManager *)textLayoutMan @interface SquirrelView : NSView +typedef struct { + NSUInteger index; + NSUInteger row; + NSUInteger tabColumn; +} SquirrelTabularPosition; + @property(nonatomic, strong, readonly) NSTextView *textView; @property(nonatomic, strong, readonly) NSTextStorage *textStorage; @property(nonatomic, strong, readonly) SquirrelTheme *currentTheme; @property(nonatomic, strong, readonly) CAShapeLayer *shape; @property(nonatomic, strong, readonly) NSMutableArray *candidatePaths; @property(nonatomic, strong, readonly) NSMutableArray *pagingPaths; +@property(nonatomic, strong, readonly) NSBezierPath *expanderPath; @property(nonatomic, strong, readonly) NSBezierPath *deleteBackPath; @property(nonatomic, strong, readonly) NSArray *candidateRanges; @property(nonatomic, readonly) NSRange preeditRange; @@ -1172,6 +1208,8 @@ @interface SquirrelView : NSView @property(nonatomic, readonly) NSRect pagingBlock; @property(nonatomic, readonly) NSEdgeInsets alignmentRectInsets; @property(nonatomic, readonly) SquirrelAppear appear; +@property(nonatomic, readonly) SquirrelTabularPosition *tabularPositions; +@property(nonatomic) BOOL expanded; - (NSTextRange *)getTextRangeFromCharRange:(NSRange)charRange API_AVAILABLE(macos(12.0)); @@ -1238,7 +1276,6 @@ - (instancetype)initWithFrame:(NSRect)frameRect { self.wantsLayer = YES; self.layer.geometryFlipped = YES; self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; - self.layerUsesCoreImageFilters = YES; if (@available(macOS 12.0, *)) { SquirrelTextLayoutManager *textLayoutManager = [[SquirrelTextLayoutManager alloc] init]; @@ -1270,7 +1307,6 @@ - (instancetype)initWithFrame:(NSRect)frameRect { textContainer:textContainer]; } _textView.drawsBackground = NO; - _textView.editable = NO; _textView.selectable = NO; _textView.wantsLayer = NO; @@ -1520,32 +1556,39 @@ - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets _preeditRange = preeditRange; _highlightedPreeditRange = highlightedPreeditRange; _pagingRange = pagingRange; + _tabularPositions = candidateRanges.count > 0 ? new SquirrelTabularPosition[candidateRanges.count] : NULL; + _expanderPath = nil; _deleteBackPath = nil; _candidatePaths = [[NSMutableArray alloc] initWithCapacity:candidateRanges.count]; - _pagingPaths = [[NSMutableArray alloc] initWithCapacity:pagingRange.length > 0 ? 2 : 0]; + _pagingPaths = [[NSMutableArray alloc] initWithCapacity: + pagingRange.length > 0 && !self.currentTheme.tabular ? 2 : 0]; _functionButton = kVoidSymbol; // invalidate Rect beyond bound of textview to clear any out-of-bound drawing from last round - [self setNeedsDisplayInRect:self.bounds]; - [self.textView setNeedsDisplayInRect:self.bounds]; + self.needsDisplayInRect = self.bounds; + _textView.needsDisplayInRect = self.bounds; } - (void)highlightFunctionButton:(SquirrelIndex)functionButton { _functionButton = functionButton; + if (_expanderPath) { + self.needsDisplayInRect = _expanderPath.bounds; + _textView.needsDisplayInRect = _expanderPath.bounds; + } if (_deleteBackPath) { - [self setNeedsDisplayInRect:_deleteBackPath.bounds]; - [self.textView setNeedsDisplayInRect:_deleteBackPath.bounds]; + self.needsDisplayInRect = _deleteBackPath.bounds; + _textView.needsDisplayInRect = _deleteBackPath.bounds; } if (_pagingPaths.count > 0) { - [self setNeedsDisplayInRect:_pagingPaths[0].bounds]; - [self setNeedsDisplayInRect:_pagingPaths[1].bounds]; - [self.textView setNeedsDisplayInRect:_pagingPaths[0].bounds]; - [self.textView setNeedsDisplayInRect:_pagingPaths[1].bounds]; + self.needsDisplayInRect = _pagingPaths[0].bounds; + self.needsDisplayInRect = _pagingPaths[1].bounds; + _textView.needsDisplayInRect = _pagingPaths[0].bounds; + _textView.needsDisplayInRect = _pagingPaths[1].bounds; } } // Bezier cubic curve, which has continuous roundness static NSBezierPath * squirclePath(NSArray *vertex, CGFloat radius) { - if (vertex.count < 1) { + if (vertex.count == 0) { return nil; } NSBezierPath *path = [NSBezierPath bezierPath]; @@ -1559,9 +1602,9 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { CGVector nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); CGVector lastDiff; if (ABS(nextDiff.dx) >= ABS(nextDiff.dy)) { - endPoint = NSMakePoint(point.x + nextDiff.dx / 2, nextPoint.y); + endPoint = NSMakePoint(point.x + nextDiff.dx * 0.5, nextPoint.y); } else { - endPoint = NSMakePoint(nextPoint.x, point.y + nextDiff.dy / 2); + endPoint = NSMakePoint(nextPoint.x, point.y + nextDiff.dy * 0.5); } [path moveToPoint:endPoint]; for (NSUInteger i = 0; i < vertex.count; ++i) { @@ -1570,14 +1613,14 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { nextPoint = vertex[(i + 1) % vertex.count].pointValue; nextDiff = CGVectorMake(nextPoint.x - point.x, nextPoint.y - point.y); if (ABS(nextDiff.dx) >= ABS(nextDiff.dy)) { - arcRadius = MIN(radius * 1.5, MIN(ABS(nextDiff.dx), ABS(lastDiff.dy)) / 2); + arcRadius = MIN(radius * 1.5, MIN(ABS(nextDiff.dx), ABS(lastDiff.dy)) * 0.5); point.y = nextPoint.y; startPoint = NSMakePoint(point.x, point.y - copysign(arcRadius, lastDiff.dy)); controlPoint1 = NSMakePoint(point.x, point.y - copysign(arcRadius * 0.1, lastDiff.dy)); endPoint = NSMakePoint(point.x + copysign(arcRadius, nextDiff.dx), nextPoint.y); controlPoint2 = NSMakePoint(point.x + copysign(arcRadius * 0.1, nextDiff.dx), nextPoint.y); } else { - arcRadius = MIN(radius * 1.5, MIN(ABS(nextDiff.dy), ABS(lastDiff.dx)) / 2); + arcRadius = MIN(radius * 1.5, MIN(ABS(nextDiff.dy), ABS(lastDiff.dx)) * 0.5); point.x = nextPoint.x; startPoint = NSMakePoint(point.x - copysign(arcRadius, lastDiff.dx), point.y); controlPoint1 = NSMakePoint(point.x - copysign(arcRadius * 0.1, lastDiff.dx), point.y); @@ -1631,7 +1674,7 @@ - (void)highlightFunctionButton:(SquirrelIndex)functionButton { return @[leadingVertex[0], leadingVertex[1], bodyVertex[0], trailingVertex[1], trailingVertex[2], trailingVertex[3], bodyVertex[2], leadingVertex[3]]; } else { - return @[]; + return nil; } } @@ -1662,31 +1705,37 @@ - (CAShapeLayer *)getFunctionButtonLayer { NSColor *buttonColor; NSBezierPath *buttonPath; switch (_functionButton) { - case kPageUp: + case kPageUpKey: buttonColor = hooverColor(theme.linear ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); buttonPath = _pagingPaths[0]; break; - case kHome: + case kHomeKey: buttonColor = disabledColor(theme.linear ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); buttonPath = _pagingPaths[0]; break; - case kPageDown: + case kPageDownKey: buttonColor = hooverColor(theme.linear ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); buttonPath = _pagingPaths[1]; break; - case kEnd: + case kEndKey: buttonColor = disabledColor(theme.linear ? theme.highlightedCandidateBackColor : theme.highlightedPreeditBackColor, self.appear); buttonPath = _pagingPaths[1]; break; - case kBackSpace: + case kExpandButton: + case kCompressButton: + case kLockButton: + buttonColor = hooverColor(theme.highlightedCandidateBackColor, self.appear); + buttonPath = _expanderPath; + break; + case kBackSpaceKey: buttonColor = hooverColor(theme.highlightedPreeditBackColor, self.appear); buttonPath = _deleteBackPath; break; - case kEscape: + case kEscapeKey: buttonColor = disabledColor(theme.highlightedPreeditBackColor, self.appear); buttonPath= _deleteBackPath; break; @@ -1809,6 +1858,7 @@ - (void)updateLayer { _candidateBlock = NSZeroRect; NSBezierPath *candidateBlockPath; NSBezierPath *gridPath; + NSBezierPath *activePagePath; if (candidateBlockRange.length > 0) { _candidateBlock = [self blockRectForRange:candidateBlockRange]; _candidateBlock.size.width = backgroundRect.size.width; @@ -1830,8 +1880,9 @@ - (void)updateLayer { if (theme.linear) { CGFloat gridOriginY; CGFloat tabInterval; - CGFloat kerning = [theme.attrs[NSKernAttributeName] doubleValue]; - if (theme.tabled) { + NSUInteger rowNum = 0; + NSRect activePageBlock = NSZeroRect; + if (theme.tabular) { gridPath = [NSBezierPath bezierPath]; gridOriginY = NSMinY(_candidateBlock); tabInterval = theme.separatorWidth * 2; @@ -1876,31 +1927,42 @@ - (void)updateLayer { bodyRect = [self backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) options:NSAlignAllEdgesNearest]; } - if (theme.tabled) { + if (theme.tabular) { + if (self.expanded && i / theme.pageSize == _highlightedIndex / theme.pageSize) { + if (i % theme.pageSize == 0) { + activePageBlock.origin = NSIsEmptyRect(leadingRect) ? bodyRect.origin : leadingRect.origin; + } else if (i % theme.pageSize == theme.pageSize - 1) { + activePageBlock.size.height = NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect) - activePageBlock.origin.y; + activePageBlock.size.width = _candidateBlock.size.width - theme.symbolExpand.size.width - floor(theme.separatorWidth * 0.5); + activePagePath = squirclePath(rectVertex(activePageBlock), + MIN(theme.highlightedCornerRadius, NSHeight(activePageBlock) * 0.5)); + } + } CGFloat bottomEdge = NSMaxY(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); if (ABS(bottomEdge - gridOriginY) > 2 && ABS(bottomEdge - NSMaxY(_candidateBlock)) > 2) { // horizontal border - [gridPath moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + - theme.separatorWidth * 0.5, bottomEdge)]; - [gridPath lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - - theme.separatorWidth * 0.5, bottomEdge)]; + [gridPath moveToPoint:NSMakePoint(NSMinX(_candidateBlock) + ceil(theme.separatorWidth * 0.5), bottomEdge)]; + [gridPath lineToPoint:NSMakePoint(NSMaxX(_candidateBlock) - theme.symbolExpand.size.width - theme.separatorWidth, bottomEdge)]; gridOriginY = bottomEdge; + ++rowNum; } CGPoint headOrigin = (NSIsEmptyRect(leadingRect) ? bodyRect : leadingRect).origin; - if (headOrigin.x > NSMinX(_candidateBlock) + theme.separatorWidth + kerning) { // vertical bar + NSUInteger headTabColumn = (NSUInteger)floor((headOrigin.x - theme.borderInset.width) / tabInterval); + if (headOrigin.x > NSMinX(_candidateBlock) + theme.separatorWidth) { // vertical bar [gridPath moveToPoint:NSMakePoint(headOrigin.x, headOrigin.y + cornerRadius * 0.8)]; [gridPath lineToPoint:NSMakePoint(headOrigin.x, NSMaxY(NSIsEmptyRect(leadingRect) ? bodyRect : leadingRect) - cornerRadius * 0.8)]; } CGFloat tailEdge = NSMaxX(NSIsEmptyRect(trailingRect) ? bodyRect : trailingRect); - CGFloat tabPosition = ceil((tailEdge + kerning * 0.5 - theme.borderInset.width) / tabInterval) * tabInterval + theme.borderInset.width; + CGFloat tailTabPosition = ceil((tailEdge - theme.borderInset.width) / tabInterval) * tabInterval + theme.borderInset.width; if (!NSIsEmptyRect(trailingRect)) { - trailingRect.size.width += tabPosition - tailEdge; + trailingRect.size.width += tailTabPosition - tailEdge; trailingRect = [self backingAlignedRect:NSIntersectionRect(trailingRect, _candidateBlock) options:NSAlignAllEdgesNearest]; } else if (NSIsEmptyRect(leadingRect)) { - bodyRect.size.width += tabPosition - tailEdge; + bodyRect.size.width += tailTabPosition - tailEdge; bodyRect = [self backingAlignedRect:NSIntersectionRect(bodyRect, _candidateBlock) options:NSAlignAllEdgesNearest]; } + _tabularPositions[i] = (SquirrelTabularPosition){i, rowNum, headTabColumn}; } NSBezierPath *candidatePath; @@ -1935,39 +1997,50 @@ - (void)updateLayer { // Draw paging Rect _pagingBlock = NSZeroRect; if (pagingRange.length > 0) { - NSRect pageUpRect = [self blockRectForRange: - NSMakeRange(pagingRange.location, 1)]; - NSRect pageDownRect = [self blockRectForRange: - NSMakeRange(NSMaxRange(pagingRange) - 1, 1)]; - pageDownRect.origin.x += _alignmentRectInsets.left; - pageDownRect.size.width += ceil(theme.separatorWidth * 0.5); - pageDownRect.origin.y += _alignmentRectInsets.top; - pageUpRect.origin.x += theme.borderInset.width; - pageUpRect.size.width = NSWidth(pageDownRect); // bypass the bug of getting wrong glyph position when tab is presented - pageUpRect.origin.y += _alignmentRectInsets.top; - if (theme.linear) { - pageUpRect.origin.y -= ceil(theme.linespace * 0.5); - pageUpRect.size.height += theme.linespace; - pageDownRect.origin.y -= ceil(theme.linespace * 0.5); - pageDownRect.size.height += theme.linespace; - pageUpRect = NSIntersectionRect(pageUpRect, _candidateBlock); - pageDownRect = NSIntersectionRect(pageDownRect, _candidateBlock); + if (theme.tabular) { + NSRect expanderRect = [self blockRectForRange:pagingRange]; + expanderRect.size.width += floor(theme.separatorWidth * 0.5); + expanderRect.origin.x = NSMaxX(backgroundRect) - NSWidth(expanderRect); + expanderRect.size.height += theme.linespace; + expanderRect.origin.y += _alignmentRectInsets.top - ceil(theme.linespace * 0.5); + expanderRect = [self backingAlignedRect:NSIntersectionRect(expanderRect, _candidateBlock) + options:NSAlignAllEdgesNearest]; + _expanderPath = squirclePath(rectVertex(expanderRect), + MIN(theme.highlightedCornerRadius, theme.paragraphStyle.minimumLineHeight * 0.5)); } else { - _pagingBlock = NSMakeRect(NSMinX(backgroundRect), - NSMaxY(_candidateBlock), - NSWidth(backgroundRect), - NSMaxY(backgroundRect) - NSMaxY(_candidateBlock)); - pageUpRect = NSIntersectionRect(pageUpRect, _pagingBlock); - pageDownRect = NSIntersectionRect(pageDownRect, _pagingBlock); - } - pageUpRect = [self backingAlignedRect:pageUpRect - options:NSAlignAllEdgesNearest]; - pageDownRect = [self backingAlignedRect:pageDownRect + NSRect pageUpRect = [self blockRectForRange:NSMakeRange(pagingRange.location, 1)]; + NSRect pageDownRect = [self blockRectForRange:NSMakeRange(NSMaxRange(pagingRange) - 1, 1)]; + pageDownRect.origin.x += _alignmentRectInsets.left; + pageDownRect.size.width += ceil(theme.separatorWidth * 0.5); + pageDownRect.origin.y += _alignmentRectInsets.top; + pageUpRect.origin.x += theme.borderInset.width; + // bypass the bug of getting wrong glyph position when tab is presented + pageUpRect.size.width = NSWidth(pageDownRect); + pageUpRect.origin.y += _alignmentRectInsets.top; + if (theme.linear) { + pageUpRect.origin.y -= ceil(theme.linespace * 0.5); + pageUpRect.size.height += theme.linespace; + pageDownRect.origin.y -= ceil(theme.linespace * 0.5); + pageDownRect.size.height += theme.linespace; + pageUpRect = NSIntersectionRect(pageUpRect, _candidateBlock); + pageDownRect = NSIntersectionRect(pageDownRect, _candidateBlock); + } else { + _pagingBlock = NSMakeRect(NSMinX(backgroundRect), + NSMaxY(_candidateBlock), + NSWidth(backgroundRect), + NSMaxY(backgroundRect) - NSMaxY(_candidateBlock)); + pageUpRect = NSIntersectionRect(pageUpRect, _pagingBlock); + pageDownRect = NSIntersectionRect(pageDownRect, _pagingBlock); + } + pageUpRect = [self backingAlignedRect:pageUpRect options:NSAlignAllEdgesNearest]; - CGFloat cornerRadius = MIN(theme.highlightedCornerRadius, - MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); - _pagingPaths[0] = squirclePath(rectVertex(pageUpRect), cornerRadius); - _pagingPaths[1] = squirclePath(rectVertex(pageDownRect), cornerRadius); + pageDownRect = [self backingAlignedRect:pageDownRect + options:NSAlignAllEdgesNearest]; + CGFloat cornerRadius = MIN(theme.highlightedCornerRadius, + MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); + _pagingPaths[0] = squirclePath(rectVertex(pageUpRect), cornerRadius); + _pagingPaths[1] = squirclePath(rectVertex(pageDownRect), cornerRadius); + } } // Set layers @@ -2024,15 +2097,6 @@ - (void)updateLayer { backColorLayer.fillColor = theme.backColor.CGColor; [BackLayers addSublayer:backColorLayer]; } - // grids (in candidate block) layer - if (gridPath) { - CAShapeLayer *gridLayer = [[CAShapeLayer alloc] init]; - gridLayer.path = gridPath.quartzPath; - gridLayer.lineWidth = 1.0; - gridLayer.strokeColor = [theme.commentAttrs[NSForegroundColorAttributeName] - blendedColorWithFraction:0.5 ofColor:theme.backColor].CGColor; - [BackLayers addSublayer:gridLayer]; - } // border layer CAShapeLayer *borderLayer = [[CAShapeLayer alloc] init]; borderLayer.path = borderPath.quartzPath; @@ -2055,6 +2119,15 @@ - (void)updateLayer { } // highlighted candidate layer if (_highlightedIndex < _candidatePaths.count && theme.highlightedCandidateBackColor) { + if (activePagePath) { + CAShapeLayer *activePageLayer = [[CAShapeLayer alloc] init]; + activePageLayer.path = activePagePath.quartzPath; + activePageLayer.fillColor = [[theme.highlightedCandidateBackColor + blendedColorWithFraction:0.8 + ofColor:[theme.backColor colorWithAlphaComponent:1.0]] + colorWithAlphaComponent:theme.backColor.alphaComponent].CGColor; + [BackLayers addSublayer:activePageLayer]; + } CAShapeLayer *highlightedCandidateLayer = [[CAShapeLayer alloc] init]; highlightedCandidateLayer.path = _candidatePaths[_highlightedIndex].quartzPath; highlightedCandidateLayer.fillColor = theme.highlightedCandidateBackColor.CGColor; @@ -2067,6 +2140,15 @@ - (void)updateLayer { [ForeLayers addSublayer:functionButtonLayer]; } } + // grids (in candidate block) layer + if (gridPath) { + CAShapeLayer *gridLayer = [[CAShapeLayer alloc] init]; + gridLayer.path = gridPath.quartzPath; + gridLayer.lineWidth = 1.0; + gridLayer.strokeColor = [theme.commentAttrs[NSForegroundColorAttributeName] + blendedColorWithFraction:0.5 ofColor:theme.backColor].CGColor; + [ForeLayers addSublayer:gridLayer]; + } // logo at the beginning for status message if (NSIsEmptyRect(_preeditBlock) && NSIsEmptyRect(_candidateBlock)) { CALayer *logoLayer = [[CALayer alloc] init]; @@ -2090,14 +2172,16 @@ - (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot { NSPoint point = [self convertPoint:spot fromView:nil]; if (NSPointInRect(point, self.bounds)) { if (NSPointInRect(point, _preeditBlock)) { - return [_deleteBackPath containsPoint:point] ? kBackSpace : kCodeInput; + return [_deleteBackPath containsPoint:point] ? kBackSpaceKey : kCodeInputArea; } - if (_pagingPaths.count > 0) { + if ([_expanderPath containsPoint:point]) { + return kExpandButton; + } else if (_pagingPaths.count > 0) { if ([_pagingPaths[0] containsPoint:point]) { - return kPageUp; + return kPageUpKey; } if ([_pagingPaths[1] containsPoint:point]) { - return kPageDown; + return kPageDownKey; } } for (NSUInteger i = 0; i < _candidatePaths.count; ++i) { @@ -2215,7 +2299,6 @@ @implementation SquirrelPanel { NSSize _maxSize; CGFloat _textWidthLimit; - NSPoint _scrollLocus; BOOL _initPosition; NSTimer *_statusTimer; @@ -2223,8 +2306,8 @@ @implementation SquirrelPanel { NSUInteger _highlightedIndex; NSUInteger _functionButton; NSUInteger _caretPos; + NSUInteger _pageNum; BOOL _caretAtHome; - BOOL _firstPage; BOOL _lastPage; } @@ -2232,8 +2315,8 @@ - (BOOL)linear { return _view.currentTheme.linear; } -- (BOOL)tabled { - return _view.currentTheme.tabled; +- (BOOL)tabular { + return _view.currentTheme.tabular; } - (BOOL)vertical { @@ -2248,6 +2331,43 @@ - (BOOL)inlineCandidate { return _view.currentTheme.inlineCandidate; } +- (BOOL)expanded { + return _view.expanded; +} + +- (void)setExpanded:(BOOL)expanded { + if (_view.currentTheme.tabular && !_locked) { + _view.expanded = expanded; + } +} + +- (void)setActivePage:(NSUInteger)activePage { + if (_view.currentTheme.tabular && _view.expanded) { + _activePage = MAX(MIN(activePage, 5UL), 0UL); + } +} + +- (void)setLocked:(BOOL)locked { + if (_view.currentTheme.tabular) { + _locked = locked; + SquirrelConfig *userConfig = [[SquirrelConfig alloc] init]; + if ([userConfig openUserConfig:@"user"]) { + [userConfig setBool:locked forOption:@"var/option/_lock_tabular"]; + } + [userConfig close]; + } +} + +- (BOOL)getLocked { + BOOL locked = NO; + SquirrelConfig *userConfig = [[SquirrelConfig alloc] init]; + if ([userConfig openUserConfig:@"user"]) { + locked = [userConfig getBoolForOption:@"var/option/_lock_tabular"]; + } + [userConfig close]; + return locked; +} + - (SquirrelInputController *)inputController { return SquirrelInputController.currentController; } @@ -2291,157 +2411,240 @@ - (void)windowDidChangeBackingProperties:(NSNotification *)notification { } } -- (void)sendEvent:(NSEvent *)event { - NSUInteger cursorIndex; - switch (event.type) { - case NSEventTypeLeftMouseDown: - if (event.clickCount == 1 && _functionButton == kCodeInput) { - NSPoint spot = [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream - fromView:nil]; - NSUInteger inputIndex = [_view.textView characterIndexForInsertionAtPoint:spot]; - if (inputIndex == 0) { - [self.inputController perform:kSELECT onIndex:kHome]; - } else if (inputIndex < _caretPos) { - [self.inputController moveCursor:_caretPos - toPosition:inputIndex - inlinePreedit:NO - inlineCandidate:NO]; - } else if (inputIndex >= _view.preeditRange.length) { - [self.inputController perform:kSELECT onIndex:kEnd]; - } else if (inputIndex > _caretPos + 1) { - [self.inputController moveCursor:_caretPos - toPosition:inputIndex - 1 - inlinePreedit:NO - inlineCandidate:NO]; - } - } - break; - case NSEventTypeLeftMouseUp: - cursorIndex = [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; - if (event.clickCount == 1 && cursorIndex != NSNotFound && - (cursorIndex == _highlightedIndex || cursorIndex == _functionButton)) { - [self.inputController perform:kSELECT onIndex:cursorIndex]; +- (NSUInteger)candidateIndexOnDirection:(SquirrelIndex)arrowKey { + if (!self.tabular || _numCandidates == 0 || _highlightedIndex == NSNotFound) { + return NSNotFound; + } + NSUInteger pageSize = _view.currentTheme.pageSize; + NSUInteger currentTabColumn = _view.tabularPositions[_highlightedIndex].tabColumn; + NSUInteger currentRow = _view.tabularPositions[_highlightedIndex].row; + if ((arrowKey == kLeftKey && self.vertical) || + (arrowKey == kDownKey && !self.vertical)) { + NSUInteger newIndex = _highlightedIndex + 1; + while (newIndex < _numCandidates && (_view.tabularPositions[newIndex].row == currentRow || + (_view.tabularPositions[newIndex].row == currentRow + 1 && + _view.tabularPositions[newIndex].tabColumn <= currentTabColumn))) { + ++newIndex; + } + if (newIndex == _numCandidates) { + return _numCandidates < pageSize * 5 ? NSNotFound : _numCandidates + pageSize * (_pageNum - _activePage); + } else { + return newIndex - 1 + pageSize * (_pageNum - _activePage); + } + } else if ((arrowKey == kRightKey && self.vertical) || + (arrowKey == kUpKey && !self.vertical)) { + NSInteger newIndex = (NSInteger)_highlightedIndex - 1; + while (newIndex >= 0 && (_view.tabularPositions[newIndex].row == currentRow || + (_view.tabularPositions[newIndex].row == currentRow - 1 && + _view.tabularPositions[newIndex].tabColumn > currentTabColumn))) { + --newIndex; + } + if (newIndex == -1) { + return _pageNum == 0 ? NSNotFound : pageSize * (_pageNum - _activePage) - 1; + } else { + return (NSUInteger)newIndex + pageSize * (_pageNum - _activePage); + } + } + return NSNotFound; +} + +// handle mouse interaction events +- (void)mouseDown:(NSEvent *)event { + if (event.clickCount > 1 || _functionButton != kCodeInputArea) { + return; + } + NSPoint spot = [_view.textView convertPoint:self.mouseLocationOutsideOfEventStream + fromView:nil]; + NSUInteger inputIndex = [_view.textView characterIndexForInsertionAtPoint:spot]; + if (inputIndex == 0) { + [self.inputController perform:kPROCESS onIndex:kHomeKey]; + } else if (inputIndex < _caretPos) { + [self.inputController moveCursor:_caretPos + toPosition:inputIndex + inlinePreedit:NO + inlineCandidate:NO]; + } else if (inputIndex >= _view.preeditRange.length) { + [self.inputController perform:kPROCESS onIndex:kEndKey]; + } else if (inputIndex > _caretPos + 1) { + [self.inputController moveCursor:_caretPos + toPosition:inputIndex - 1 + inlinePreedit:NO + inlineCandidate:NO]; + } +} + +- (void)mouseUp:(NSEvent *)event { + NSUInteger cursorIndex = [_view getIndexFromMouseSpot: + self.mouseLocationOutsideOfEventStream]; + if (event.clickCount > 1 || cursorIndex == NSNotFound) { + return; + } + if (cursorIndex == _highlightedIndex) { + cursorIndex += (_pageNum - _activePage) * _view.currentTheme.pageSize; + [self.inputController perform:kSELECT + onIndex:cursorIndex]; + } else if (cursorIndex == _functionButton) { + if (cursorIndex == kExpandButton) { + if (_locked) { + [self setLocked:NO]; + } else { + _view.expanded = !_view.expanded; + _activePage = 0; } - break; - case NSEventTypeRightMouseUp: - cursorIndex = [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; - if (event.clickCount == 1 && cursorIndex != NSNotFound) { - if (cursorIndex == _highlightedIndex) { - [self.inputController perform:kDELETE onIndex:cursorIndex]; - } else if (cursorIndex == _functionButton) { - switch (cursorIndex) { - case kPageUp: - [self.inputController perform:kSELECT onIndex:kHome]; - break; - case kPageDown: - [self.inputController perform:kSELECT onIndex:kEnd]; - break; - case kBackSpace: - [self.inputController perform:kSELECT onIndex:kEscape]; - break; - } + } + [self.inputController perform:kPROCESS onIndex:cursorIndex]; + } +} + +- (void)rightMouseUp:(NSEvent *)event { + NSUInteger cursorIndex = [_view getIndexFromMouseSpot: + self.mouseLocationOutsideOfEventStream]; + if (event.clickCount > 1 || cursorIndex == NSNotFound) { + return; + } + if (cursorIndex == _highlightedIndex) { + cursorIndex += (_pageNum - _activePage) * _view.currentTheme.pageSize; + [self.inputController perform:kDELETE + onIndex:cursorIndex]; + } else if (cursorIndex == _functionButton) { + switch (cursorIndex) { + case kPageUpKey: + [self.inputController perform:kPROCESS onIndex:kHomeKey]; + break; + case kPageDownKey: + [self.inputController perform:kPROCESS onIndex:kEndKey]; + break; + case kExpandButton: + [self setLocked:!_locked]; + [self.inputController perform:kPROCESS onIndex:kLockButton]; + break; + case kBackSpaceKey: + [self.inputController perform:kPROCESS onIndex:kEscapeKey]; + break; + } + } +} + +- (void)mouseMoved:(NSEvent *)event { + if (event.modifierFlags & NSEventModifierFlagOption) { + return; + } + SquirrelTheme *theme = _view.currentTheme; + NSUInteger cursorIndex = [_view getIndexFromMouseSpot: + self.mouseLocationOutsideOfEventStream]; + if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { + [_toolTip hide]; + } + if (cursorIndex >= 0 && cursorIndex < _numCandidates && _highlightedIndex != cursorIndex) { + _highlightedIndex = cursorIndex; + cursorIndex += (_pageNum - _activePage) * theme.pageSize; + _activePage = _highlightedIndex / theme.pageSize; + _pageNum = cursorIndex / theme.pageSize; + [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil)]; + [self.inputController perform:kHIGHLIGHT onIndex:cursorIndex]; + } else if ((cursorIndex == kPageUpKey || cursorIndex == kPageDownKey || cursorIndex == kExpandButton || + cursorIndex == kBackSpaceKey) && _functionButton != cursorIndex) { + _functionButton = cursorIndex; + switch (_functionButton) { + case kPageUpKey: + [_view.textStorage addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage addAttributes:theme.pagingAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; + if (_view.preeditRange.length > 0) { + [_view.textStorage addAttributes:theme.preeditAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; } - } - break; - case NSEventTypeMouseMoved: - cursorIndex = [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; - if (cursorIndex != _highlightedIndex && cursorIndex != _functionButton) { - [_toolTip hide]; - } - if (cursorIndex >= 0 && cursorIndex < _numCandidates && _highlightedIndex != cursorIndex) { - _highlightedIndex = cursorIndex; - [_toolTip showWithToolTip:NSLocalizedString(@"candidate", nil)]; - [self.inputController perform:kHILITE onIndex: - [_view.currentTheme.selectKeys characterAtIndex:cursorIndex]]; - } else if ((cursorIndex == kPageUp || cursorIndex == kPageDown || - cursorIndex == kBackSpace) && _functionButton != cursorIndex) { - _functionButton = cursorIndex; - SquirrelTheme *theme = _view.currentTheme; - switch (_functionButton) { - case kPageUp: - [_view.textStorage addAttributes:theme.pagingHighlightedAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; + cursorIndex = _pageNum == 0 ? kHomeKey : kPageUpKey; + [_toolTip showWithToolTip:NSLocalizedString(_pageNum == 0 ? @"home" : @"page_up", nil)]; + break; + case kPageDownKey: + [_view.textStorage addAttributes:theme.pagingAttrs + range:NSMakeRange(_view.pagingRange.location, 1)]; + [_view.textStorage addAttributes:theme.pagingHighlightedAttrs + range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; + if (_view.preeditRange.length > 0) { + [_view.textStorage addAttributes:theme.preeditAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + } + cursorIndex = _lastPage ? kEndKey : kPageDownKey; + [_toolTip showWithToolTip:NSLocalizedString(_lastPage ? @"end" : @"page_down", nil)]; + break; + case kExpandButton: + [_view.textStorage addAttributes:theme.pagingHighlightedAttrs + range:_view.pagingRange]; + if (_view.preeditRange.length > 0) { + [_view.textStorage addAttributes:theme.preeditAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + } + cursorIndex = _locked ? kLockButton : _view.expanded ? kCompressButton : kExpandButton; + [_toolTip showWithToolTip:NSLocalizedString(_locked ? @"unlock" : _view.expanded ? @"compress" : @"expand", nil)]; + break; + case kBackSpaceKey: + [_view.textStorage addAttributes:theme.preeditHighlightedAttrs + range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; + if (_view.pagingRange.length > 0) { + if (theme.tabular) { [_view.textStorage addAttributes:theme.pagingAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; - if (_view.preeditRange.length > 0) { - [_view.textStorage addAttributes:theme.preeditAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - } - cursorIndex = _firstPage ? kHome : kPageUp; - [_toolTip showWithToolTip:NSLocalizedString(_firstPage ? @"home" : @"page_up", nil)]; - break; - case kPageDown: + range:_view.pagingRange]; + } else { [_view.textStorage addAttributes:theme.pagingAttrs range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage addAttributes:theme.pagingHighlightedAttrs + [_view.textStorage addAttributes:theme.pagingAttrs range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; - if (_view.preeditRange.length > 0) { - [_view.textStorage addAttributes:theme.preeditAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - } - cursorIndex = _lastPage ? kEnd : kPageDown; - [_toolTip showWithToolTip:NSLocalizedString(_lastPage ? @"end" : @"page_down", nil)]; - break; - case kBackSpace: - [_view.textStorage addAttributes:theme.preeditHighlightedAttrs - range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; - if (_view.pagingRange.length > 0) { - [_view.textStorage addAttributes:theme.pagingAttrs - range:NSMakeRange(_view.pagingRange.location, 1)]; - [_view.textStorage addAttributes:theme.pagingAttrs - range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; - } - cursorIndex = _caretAtHome ? kEscape : kBackSpace; - [_toolTip showWithToolTip:NSLocalizedString(_caretAtHome ? @"escape" : @"delete", nil)]; - break; - } - [_view highlightFunctionButton:cursorIndex]; - [self display]; - } else if (cursorIndex == kCodeInput && _functionButton != cursorIndex) { - _functionButton = cursorIndex; - } - break; - case NSEventTypeLeftMouseDragged: - _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] minimumLineHeight] + - [theme.attrs[NSParagraphStyleAttributeName] lineSpacing]; - if (event.phase == NSEventPhaseBegan) { - _scrollLocus = NSZeroPoint; - } else if ((event.phase == NSEventPhaseNone || event.momentumPhase == NSEventPhaseNone) && - !isnan(_scrollLocus.x) && !isnan(_scrollLocus.y)) { - // determine scrolling direction by confining to sectors within ±30º of any axis - if (ABS(event.scrollingDeltaX) > ABS(event.scrollingDeltaY) * sqrt(3.0)) { - _scrollLocus.x += event.scrollingDeltaX * (event.hasPreciseScrollingDeltas ? 1 : 10); - } else if (ABS(event.scrollingDeltaY) > ABS(event.scrollingDeltaX) * sqrt(3.0)) { - _scrollLocus.y += event.scrollingDeltaY * (event.hasPreciseScrollingDeltas ? 1 : 10); - } - // compare accumulated locus length against threshold and limit paging to max once - if (_scrollLocus.x > scrollThreshold) { - [self.inputController perform:kSELECT - onIndex:(theme.vertical ? kPageDown : kPageUp)]; - _scrollLocus = NSMakePoint(NAN, NAN); - } else if (_scrollLocus.y > scrollThreshold) { - [self.inputController perform:kSELECT - onIndex:kPageUp]; - _scrollLocus = NSMakePoint(NAN, NAN); - } else if (_scrollLocus.x < -scrollThreshold) { - [self.inputController perform:kSELECT - onIndex:(theme.vertical ? kPageUp : kPageDown)]; - _scrollLocus = NSMakePoint(NAN, NAN); - } else if (_scrollLocus.y < -scrollThreshold) { - [self.inputController perform:kSELECT - onIndex:kPageDown]; - _scrollLocus = NSMakePoint(NAN, NAN); + } } - } - } break; - default: - [super sendEvent:event]; - break; + cursorIndex = _caretAtHome ? kEscapeKey : kBackSpaceKey; + [_toolTip showWithToolTip:NSLocalizedString(_caretAtHome ? @"escape" : @"delete", nil)]; + break; + } + [_view highlightFunctionButton:cursorIndex]; + [self display]; + } else if (cursorIndex == kCodeInputArea && _functionButton != cursorIndex) { + _functionButton = cursorIndex; + } +} + +- (void)mouseDragged:(NSEvent *)event { + // reset the remember_size references after moving the panel + _maxSize = NSZeroSize; + [self performWindowDragWithEvent:event]; +} + +- (void)scrollWheel:(NSEvent *)event { + SquirrelTheme *theme = _view.currentTheme; + CGFloat scrollThreshold = [theme.attrs[NSParagraphStyleAttributeName] minimumLineHeight] + + [theme.attrs[NSParagraphStyleAttributeName] lineSpacing]; + + static NSPoint scrollLocus = NSZeroPoint; + if (event.phase == NSEventPhaseBegan) { + scrollLocus = NSZeroPoint; + } else if ((event.phase == NSEventPhaseNone || event.momentumPhase == NSEventPhaseNone) && + !isnan(scrollLocus.x) && !isnan(scrollLocus.y)) { + // determine scrolling direction by confining to sectors within ±30º of any axis + if (ABS(event.scrollingDeltaX) > ABS(event.scrollingDeltaY) * sqrt(3.0)) { + scrollLocus.x += event.scrollingDeltaX * (event.hasPreciseScrollingDeltas ? 1 : 10); + } else if (ABS(event.scrollingDeltaY) > ABS(event.scrollingDeltaX) * sqrt(3.0)) { + scrollLocus.y += event.scrollingDeltaY * (event.hasPreciseScrollingDeltas ? 1 : 10); + } + // compare accumulated locus length against threshold and limit paging to max once + if (scrollLocus.x > scrollThreshold) { + [self.inputController perform:kPROCESS + onIndex:(theme.vertical ? kPageDownKey : kPageUpKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.y > scrollThreshold) { + [self.inputController perform:kPROCESS + onIndex:kPageUpKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.x < -scrollThreshold) { + [self.inputController perform:kPROCESS + onIndex:(theme.vertical ? kPageUpKey : kPageDownKey)]; + scrollLocus = NSMakePoint(NAN, NAN); + } else if (scrollLocus.y < -scrollThreshold) { + [self.inputController perform:kPROCESS + onIndex:kPageDownKey]; + scrollLocus = NSMakePoint(NAN, NAN); + } } } @@ -2474,9 +2677,10 @@ - (void)updateDisplayParameters { 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; + if (theme.tabular) { + CGFloat doubleTabInterval = theme.separatorWidth * 4; + CGFloat expanderWidth = theme.symbolExpand.size.width - ceil(theme.separatorWidth * 0.5); + _textWidthLimit = floor((_textWidthLimit - expanderWidth) / doubleTabInterval) * doubleTabInterval + expanderWidth; } CGFloat textHeightLimit = (theme.vertical ? NSWidth(screenRect) : NSHeight(screenRect)) * 0.8 - theme.borderInset.height * 2 - (theme.inlinePreedit ? ceil(theme.linespace * 0.5) : 0.0) - @@ -2648,19 +2852,19 @@ - (void)show { // rotate the view, the core in vertical mode! self.contentView.boundsRotation = theme.vertical ? -90.0 : 0.0; - [self.contentView setBoundsOrigin:theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint]; + self.contentView.boundsOrigin = theme.vertical ? NSMakePoint(0.0, NSWidth(windowRect)) : NSZeroPoint; NSRect viewRect = self.contentView.bounds; - [_view setBoundsOrigin:NSZeroPoint]; + _view.boundsOrigin = NSZeroPoint; _view.frame = viewRect; _view.textView.boundsRotation = 0.0; - [_view.textView setBoundsOrigin:NSZeroPoint]; + _view.textView.boundsOrigin = NSZeroPoint; _view.textView.frame = NSOffsetRect(viewRect, insets.left - _view.textView.textContainerOrigin.x, insets.top - _view.textView.textContainerOrigin.y); if (theme.translucency > 0) { - [_back setBoundsOrigin:NSZeroPoint]; + _back.boundsOrigin = NSZeroPoint; _back.frame = viewRect; _back.hidden = NO; } else { @@ -2683,10 +2887,16 @@ - (void)hide { [self orderOut:nil]; _maxSize = NSZeroSize; _initPosition = YES; + _view.expanded = NO; + _activePage = 0; } - (BOOL)shouldBreakLineInsideRange:(NSRange)range { [_view.textStorage fixFontAttributeInRange:range]; + CGFloat maxTextWidth = _textWidthLimit; + if (self.tabular) { + maxTextWidth -= _view.currentTheme.symbolExpand.size.width + floor(_view.currentTheme.separatorWidth * 0.5); + } NSUInteger __block lineCount = 0; if (@available(macOS 12.0, *)) { NSTextRange *textRange = [_view getTextRangeFromCharRange:range]; @@ -2696,7 +2906,7 @@ - (BOOL)shouldBreakLineInsideRange:(NSRange)range { options:NSTextLayoutManagerSegmentOptionsRangeNotRequired usingBlock:^BOOL(NSTextRange *segRange, CGRect segFrame, CGFloat baseline, NSTextContainer *textContainer) { - lineCount += 1 + (NSMaxX(segFrame) > _textWidthLimit); + lineCount += 1 + (NSMaxX(segFrame) > maxTextWidth); return YES; }]; } else { @@ -2707,7 +2917,7 @@ - (BOOL)shouldBreakLineInsideRange:(NSRange)range { enumerateLineFragmentsForGlyphRange:glyphRange usingBlock:^(NSRect rect, NSRect usedRect, NSTextContainer *textContainer, NSRange lineRange, BOOL *stop) { - lineCount += 1 + (NSMaxX(usedRect) > self->_textWidthLimit); + lineCount += 1 + (NSMaxX(usedRect) > maxTextWidth); }]; } return lineCount > 1; @@ -2780,9 +2990,10 @@ - (NSMutableAttributedString *)getPageNumString:(NSUInteger)pageNum { NSTextAttachment *pageNumAttm = [[NSTextAttachment alloc] init]; pageNumAttm.image = pageNumImage; pageNumAttm.bounds = NSMakeRect(0, font.ascender * 0.5 + font.descender * 0.5 - lineHeight * 0.5, lineHeight, lineHeight); - NSMutableAttributedString *attmString = [[NSMutableAttributedString alloc] - initWithString:[NSString stringWithFormat:@" %C ", (unichar)NSAttachmentCharacter] - attributes:pageNumAttrs]; + NSMutableAttributedString *attmString = + [[NSMutableAttributedString alloc] + initWithString:[NSString stringWithFormat:@" %C ", (unichar)NSAttachmentCharacter] + attributes:pageNumAttrs]; [attmString addAttribute:NSAttachmentAttributeName value:pageNumAttm range:NSMakeRange(1, 1)]; @@ -2803,10 +3014,10 @@ - (void)showPreedit:(NSString *)preedit [self updateDisplayParameters]; } _numCandidates = candidates.count; - _highlightedIndex = _numCandidates == 0 ? NSNotFound : highlightedIndex; + _highlightedIndex = highlightedIndex; _caretPos = caretPos; _caretAtHome = caretPos == NSNotFound || (caretPos == selRange.location && selRange.location == 1); - _firstPage = pageNum == 0; + _pageNum = pageNum; _lastPage = lastPage; _functionButton = kVoidSymbol; if (_numCandidates > 0 || preedit.length > 0) { @@ -2832,7 +3043,7 @@ - (void)showPreedit:(NSString *)preedit } NSTextStorage *text = _view.textStorage; - [text setAttributedString:[[NSMutableAttributedString alloc] init]]; + text.attributedString = [[NSAttributedString alloc] init]; NSRange preeditRange = NSMakeRange(NSNotFound, 0); NSRange highlightedPreeditRange = NSMakeRange(NSNotFound, 0); NSMutableArray *candidateRanges = [[NSMutableArray alloc] @@ -2840,10 +3051,6 @@ - (void)showPreedit:(NSString *)preedit NSRange pagingRange = NSMakeRange(NSNotFound, 0); NSUInteger candidateBlockStart; NSUInteger lineStart; - - CGFloat topMargin = preedit ? 0.0 : ceil(theme.linespace * 0.5); - CGFloat bottomMargin = _numCandidates > 0 && (theme.linear || !theme.showPaging) ? - floor(theme.linespace * 0.5) : 0.0; NSMutableParagraphStyle *paragraphStyleCandidate; CGFloat tabInterval = theme.separatorWidth * 2; CGFloat maxLineLength = 0.0; @@ -2887,22 +3094,9 @@ - (void)showPreedit:(NSString *)preedit initWithString:@"\n" attributes:theme.preeditAttrs]]; } else { - if ([self shouldUseTabInRange:NSMakeRange(preeditLine.length - 2, 2) - maxLineLength:&maxLineLength]) { - if (theme.tabled) { - maxLineLength = ceil((maxLineLength + theme.separatorWidth) / tabInterval) * tabInterval - theme.separatorWidth; - } - [text replaceCharactersInRange:NSMakeRange(preeditLine.length - 2, 1) - withString:@"\t"]; - NSMutableParagraphStyle *paragraphStylePreedit = theme.preeditParagraphStyle.mutableCopy; - paragraphStylePreedit.tabStops = @[[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight - location:maxLineLength - options:@{}]]; - [text addAttribute:NSParagraphStyleAttributeName - value:paragraphStylePreedit - range:preeditRange]; - } - goto drawPanel; + _view.expanded = NO; + _activePage = 0; + goto alignDelete; } } @@ -2910,26 +3104,32 @@ - (void)showPreedit:(NSString *)preedit candidateBlockStart = text.length; lineStart = text.length; if (theme.linear) { - paragraphStyleCandidate = [theme.paragraphStyle copy]; + paragraphStyleCandidate = theme.paragraphStyle.copy; } for (NSUInteger idx = 0; idx < _numCandidates; ++idx) { + NSUInteger col = idx % theme.pageSize; // attributed labels are already included in candidateFormats - NSMutableAttributedString *item = (idx == highlightedIndex) ? [theme.candidateHighlightedFormats[idx] mutableCopy] - : [theme.candidateFormats[idx] mutableCopy]; - NSRange candidateRange = [item.string rangeOfString:@"%@"]; + NSMutableAttributedString *item = idx == highlightedIndex ? theme.candidateHighlightedFormats[col].mutableCopy + : theme.candidateFormats[col].mutableCopy; + NSRange candidateField = [item.string rangeOfString:@"%@"]; // get the label size for indent - CGFloat labelWidth = theme.linear ? 0.0 : ceil([item attributedSubstringFromRange: - NSMakeRange(0, candidateRange.location)].size.width); + NSRange labelRange = NSMakeRange(0, candidateField.location); + CGFloat labelWidth = theme.linear ? 0.0 : ceil([item attributedSubstringFromRange:labelRange].size.width); + // hide labels in non-highlighted pages (no selection keys) + if (idx / theme.pageSize != _activePage) { + [item addAttribute:NSForegroundColorAttributeName + value:NSColor.clearColor range:labelRange]; + } // plug in candidate texts and comments into the template - [item replaceCharactersInRange:candidateRange + [item replaceCharactersInRange:candidateField withString:candidates[idx]]; - NSRange commentRange = [item.string rangeOfString:kTipSpecifier]; - if (comments[idx].length != 0) { - [item replaceCharactersInRange:commentRange + NSRange commentField = [item.string rangeOfString:kTipSpecifier]; + if (comments[idx].length > 0) { + [item replaceCharactersInRange:commentField withString:[@" " stringByAppendingString:comments[idx]]]; } else { - [item deleteCharactersInRange:commentRange]; + [item deleteCharactersInRange:commentField]; } [item formatMarkDown]; @@ -2948,10 +3148,8 @@ - (void)showPreedit:(NSString *)preedit style.paragraphSpacingBefore = annotationHeight; [text addAttribute:NSParagraphStyleAttributeName value:style range:range]; }]; - topMargin = preedit ? 0.0 : annotationHeight; - bottomMargin = (theme.linear || !theme.showPaging) ? annotationHeight : 0.0; } - if (comments[idx].length != 0 && [item.string hasSuffix:@" "]) { + if (comments[idx].length > 0 && [item.string hasSuffix:@" "]) { [item deleteCharactersInRange:NSMakeRange(item.length - 1, 1)]; } if (!theme.linear) { @@ -2959,16 +3157,17 @@ - (void)showPreedit:(NSString *)preedit paragraphStyleCandidate.headIndent = labelWidth; } [item addAttribute:NSParagraphStyleAttributeName - value:paragraphStyleCandidate range:NSMakeRange(0, item.length)]; + 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; - // separator: linear = " "; tabled = " \t"; stacked = "\n" + // separator: linear = " "; tabular = " \t"; stacked = "\n" NSMutableAttributedString *separator = theme.separator.mutableCopy; [text appendAttributedString:separator]; [text appendAttributedString:item]; - if (theme.linear && (ceil(item.size.width) > _textWidthLimit || + if (theme.linear && (col == 0 || ceil(item.size.width) > _textWidthLimit || [self shouldBreakLineInsideRange:NSMakeRange(lineStart, text.length - lineStart)])) { [text replaceCharactersInRange:NSMakeRange(separatorStart, separator.length) withString:@"\n"]; @@ -2979,7 +3178,7 @@ - (void)showPreedit:(NSString *)preedit } // 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) { + if (idx < _numCandidates - 1 || theme.showPaging || theme.tabular) { [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:theme.commentAttrs]]; } @@ -2998,10 +3197,37 @@ - (void)showPreedit:(NSString *)preedit } // paging indication - if (theme.showPaging) { + if (theme.tabular) { + [text appendAttributedString:[[NSAttributedString alloc] initWithString:@"\t" + attributes:theme.commentAttrs]]; + NSUInteger pagingStart = text.length; + NSAttributedString *expander = _locked ? theme.symbolLock : _view.expanded ? theme.symbolCompress : theme.symbolExpand; + [text appendAttributedString:expander]; + paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; + [self shouldUseTabInRange:NSMakeRange(pagingStart, 1) maxLineLength:&maxLineLength]; + paragraphStyleCandidate.tabStops = @[]; + CGFloat candidateEndPosition = NSMaxX([_view blockRectForRange: + NSMakeRange(lineStart, pagingStart - 1 - lineStart)]); + for (NSUInteger i = 1; i * tabInterval < candidateEndPosition; ++i) { + [paragraphStyleCandidate addTabStop: + [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft + location:i * tabInterval + options:@{}]]; + } + CGFloat expanderPosition = floor((maxLineLength - theme.symbolExpand.size.width + ceil(theme.separatorWidth * 0.5)) / + (tabInterval * 2)) * tabInterval * 2 - ceil(theme.separatorWidth * 0.5); + [paragraphStyleCandidate addTabStop: + [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft + location:expanderPosition + options:@{}]]; + pagingRange = NSMakeRange(text.length - 1, 1); + [text addAttribute:NSParagraphStyleAttributeName + value:paragraphStyleCandidate + range:NSMakeRange(lineStart, text.length - lineStart)]; + + } else if (theme.showPaging) { NSMutableAttributedString *paging = [self getPageNumString:pageNum]; - [paging insertAttributedString:pageNum > 0 ? theme.symbolBackFill : theme.symbolBackStroke - atIndex:0]; + [paging insertAttributedString:pageNum > 0 ? theme.symbolBackFill : theme.symbolBackStroke atIndex:0]; [paging appendAttributedString:lastPage ? theme.symbolForwardStroke : theme.symbolForwardFill]; [text appendAttributedString:theme.separator]; NSUInteger pagingStart = text.length; @@ -3013,30 +3239,15 @@ - (void)showPreedit:(NSString *)preedit lineStart = pagingStart; pagingStart += 1; } - if ([self shouldUseTabInRange:NSMakeRange(pagingStart, paging.length) - maxLineLength:&maxLineLength] || - lineStart != candidateBlockStart) { - if (theme.tabled) { - maxLineLength = ceil((maxLineLength + theme.separatorWidth) / tabInterval) * tabInterval - theme.separatorWidth; - } else { - [text replaceCharactersInRange:NSMakeRange(pagingStart - 1, 1) - withString:@"\t"]; - } + if ([self shouldUseTabInRange:NSMakeRange(pagingStart, paging.length) + maxLineLength:&maxLineLength] || lineStart != candidateBlockStart) { + [text replaceCharactersInRange:NSMakeRange(pagingStart - 1, 1) + withString:@"\t"]; paragraphStyleCandidate = theme.paragraphStyle.mutableCopy; - paragraphStyleCandidate.tabStops = @[]; - NSUInteger candidateEnd = pagingStart - 1; - CGFloat candidateEndPosition = NSMaxX([_view blockRectForRange: - NSMakeRange(lineStart, candidateEnd - lineStart)]); - for (NSUInteger i = 1; i * tabInterval < candidateEndPosition; ++i) { - [paragraphStyleCandidate addTabStop: - [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft - location:i * tabInterval - options:@{}]]; - } - [paragraphStyleCandidate addTabStop: - [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight - location:maxLineLength - options:@{}]]; + paragraphStyleCandidate.tabStops = + @[[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight + location:maxLineLength + options:@{}]]; } [text addAttribute:NSParagraphStyleAttributeName value:paragraphStyleCandidate @@ -3049,12 +3260,13 @@ - (void)showPreedit:(NSString *)preedit withString:@"\t"]; [text replaceCharactersInRange:NSMakeRange(pagingStart + paging.length - 2, 1) withString:@"\t"]; - paragraphStylePaging.tabStops = @[[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentCenter - location:maxLineLength / 2 - options:@{}], - [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight - location:maxLineLength - options:@{}]]; + paragraphStylePaging.tabStops = + @[[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentCenter + location:maxLineLength * 0.5 + options:@{}], + [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight + location:maxLineLength + options:@{}]]; } [text addAttribute:NSParagraphStyleAttributeName value:paragraphStylePaging @@ -3062,9 +3274,15 @@ - (void)showPreedit:(NSString *)preedit } pagingRange = NSMakeRange(text.length - paging.length, paging.length); } + +alignDelete: // right-align the backward delete symbol if (preedit && [self shouldUseTabInRange:NSMakeRange(preeditRange.length - 2, 2) maxLineLength:&maxLineLength]) { + if (theme.tabular && _numCandidates == 0) { + CGFloat expanderWidth = theme.symbolExpand.size.width - ceil(theme.separatorWidth * 0.5); + maxLineLength = floor((maxLineLength - expanderWidth) / tabInterval) * tabInterval + expanderWidth; + } [text replaceCharactersInRange:NSMakeRange(preeditRange.length - 2, 1) withString:@"\t"]; NSMutableParagraphStyle *paragraphStylePreedit = theme.preeditParagraphStyle.mutableCopy; @@ -3076,8 +3294,9 @@ - (void)showPreedit:(NSString *)preedit } // text done! -drawPanel: [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; + CGFloat topMargin = preedit ? 0.0 : ceil(theme.linespace * 0.5); + CGFloat bottomMargin = _numCandidates > 0 && (theme.linear || !theme.showPaging) ? floor(theme.linespace * 0.5) : 0.0; NSEdgeInsets insets = NSEdgeInsetsMake(theme.borderInset.height + topMargin, theme.borderInset.width + ceil(theme.separatorWidth * 0.5), theme.borderInset.height + bottomMargin, @@ -3117,9 +3336,9 @@ - (void)showStatus:(NSString *)message { _view.textView.layoutOrientation = (NSTextLayoutOrientation)theme.vertical; NSTextStorage *text = _view.textStorage; - [text setAttributedString:[[NSAttributedString alloc] - initWithString:[NSString stringWithFormat:@"%@ %@", kFullWidthSpace, message] - attributes:theme.statusAttrs]]; + text.attributedString = [[NSAttributedString alloc] + initWithString:[NSString stringWithFormat:@"%@ %@", kFullWidthSpace, message] + attributes:theme.statusAttrs]; [text ensureAttributesAreFixedInRange:NSMakeRange(0, text.length)]; NSEdgeInsets insets = NSEdgeInsetsMake(theme.borderInset.height, @@ -3137,7 +3356,7 @@ - (void)showStatus:(NSString *)message { } self.animationBehavior = NSWindowAnimationBehaviorUtilityWindow; [_view drawViewWithInsets:insets - candidateRanges:@[] + candidateRanges:nil highlightedIndex:NSNotFound preeditRange:NSMakeRange(NSNotFound, 0) highlightedPreeditRange:NSMakeRange(NSNotFound, 0) @@ -3154,40 +3373,40 @@ - (void)hideStatus:(NSTimer *)timer { [self hide]; } -static void updateCandidateListLayout(BOOL *isLinear, BOOL *isTabled, +static void updateCandidateListLayout(BOOL *isLinear, BOOL *isTabular, SquirrelConfig *config, NSString *prefix) { - NSString *candidateListLayout = [config getString: + NSString *candidateListLayout = [config getStringForOption: [prefix stringByAppendingString:@"/candidate_list_layout"]]; if ([candidateListLayout isEqualToString:@"stacked"]) { *isLinear = NO; - *isTabled = NO; + *isTabular = NO; } else if ([candidateListLayout isEqualToString:@"linear"]) { *isLinear = YES; - *isTabled = NO; - } else if ([candidateListLayout isEqualToString:@"tabled"]) { - // `tabled` is a derived layout of `linear`; tabled implies linear + *isTabular = NO; + } else if ([candidateListLayout isEqualToString:@"tabular"]) { + // `tabular` is a derived layout of `linear`; tabular implies linear *isLinear = YES; - *isTabled = YES; + *isTabular = YES; } else { // Deprecated. Not to be confused with text_orientation: horizontal - NSNumber *horizontal = [config getOptionalBool: + NSNumber *horizontal = [config getOptionalBoolForOption: [prefix stringByAppendingString:@"/horizontal"]]; if (horizontal) { *isLinear = horizontal.boolValue; - *isTabled = NO; + *isTabular = NO; } } } static void updateTextOrientation(BOOL *isVertical, SquirrelConfig *config, NSString *prefix) { - NSString *textOrientation = [config getString: + NSString *textOrientation = [config getStringForOption: [prefix stringByAppendingString:@"/text_orientation"]]; if ([textOrientation isEqualToString:@"horizontal"]) { *isVertical = NO; } else if ([textOrientation isEqualToString:@"vertical"]) { *isVertical = YES; } else { - NSNumber *vertical = [config getOptionalBool: + NSNumber *vertical = [config getOptionalBoolForOption: [prefix stringByAppendingString:@"/vertical"]]; if (vertical) { *isVertical = vertical.boolValue; @@ -3218,10 +3437,10 @@ - (void)loadLabelConfig:(SquirrelConfig *)config + (void)updateTheme:(SquirrelTheme *)theme withLabelConfig:(SquirrelConfig *)config directUpdate:(BOOL)update { - NSUInteger menuSize = (NSUInteger)[config getInt:@"menu/page_size"] ? : 5; + NSUInteger menuSize = (NSUInteger)[config getIntForOption:@"menu/page_size"] ? : 5; NSMutableArray *labels = [[NSMutableArray alloc] initWithCapacity:menuSize]; - NSString *selectKeys = [config getString:@"menu/alternative_select_keys"]; - NSArray *selectLabels = [config getList:@"menu/alternative_select_labels"]; + NSString *selectKeys = [config getStringForOption:@"menu/alternative_select_keys"]; + NSArray *selectLabels = [config getListForOption:@"menu/alternative_select_labels"]; if (selectLabels.count > 0) { for (NSUInteger i = 0; i < menuSize; ++i) { labels[i] = selectLabels[i]; @@ -3250,14 +3469,23 @@ + (void)updateTheme:(SquirrelTheme *)theme directUpdate:update]; } -- (void)loadConfig:(SquirrelConfig *)config - forAppearance:(SquirrelAppear)appear { - SquirrelTheme *theme = [_view selectTheme:appear]; +- (void)loadConfig:(SquirrelConfig *)config { NSSet *styleOptions = [NSSet setWithArray:self.optionSwitcher.optionStates]; - [SquirrelPanel updateTheme:theme + SquirrelTheme *defaultTheme = [_view selectTheme:defaultAppear]; + [SquirrelPanel updateTheme:defaultTheme withConfig:config styleOptions:styleOptions - forAppearance:appear]; + forAppearance:defaultAppear]; + if (@available(macOS 10.14, *)) { + SquirrelTheme *darkTheme = [_view selectTheme:darkAppear]; + [SquirrelPanel updateTheme:darkTheme + withConfig:config + styleOptions:styleOptions + forAppearance:darkAppear]; + } + if (_view.currentTheme.tabular) { + _locked = [self getLocked]; + } [self updateDisplayParameters]; } @@ -3273,44 +3501,44 @@ + (void)updateTheme:(SquirrelTheme *)theme forAppearance:(SquirrelAppear)appear { // INTERFACE BOOL linear = NO; - BOOL tabled = NO; + BOOL tabular = NO; BOOL vertical = NO; - updateCandidateListLayout(&linear, &tabled, config, @"style"); + updateCandidateListLayout(&linear, &tabular, config, @"style"); updateTextOrientation(&vertical, config, @"style"); - 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"]; + NSNumber *inlinePreedit = [config getOptionalBoolForOption:@"style/inline_preedit"]; + NSNumber *inlineCandidate = [config getOptionalBoolForOption:@"style/inline_candidate"]; + NSNumber *showPaging = [config getOptionalBoolForOption:@"style/show_paging"]; + NSNumber *rememberSize = [config getOptionalBoolForOption:@"style/remember_size"]; + NSString *statusMessageType = [config getStringForOption:@"style/status_message_type"]; + NSString *candidateFormat = [config getStringForOption:@"style/candidate_format"]; // TYPOGRAPHY - NSString *fontName = [config getString:@"style/font_face"]; - NSNumber *fontSize = [config getOptionalDouble:@"style/font_point" - applyConstraint:pos_round]; - NSString *labelFontName = [config getString:@"style/label_font_face"]; - NSNumber *labelFontSize = [config getOptionalDouble:@"style/label_font_point" - applyConstraint:pos_round]; - NSString *commentFontName = [config getString:@"style/comment_font_face"]; - NSNumber *commentFontSize = [config getOptionalDouble:@"style/comment_font_point" - applyConstraint:pos_round]; - NSNumber *alpha = [config getOptionalDouble:@"style/alpha" - applyConstraint:clamp_uni]; - NSNumber *translucency = [config getOptionalDouble:@"style/translucency" - applyConstraint:clamp_uni]; - NSNumber *cornerRadius = [config getOptionalDouble:@"style/corner_radius" - applyConstraint:positive]; - NSNumber *highlightedCornerRadius = [config getOptionalDouble:@"style/hilited_corner_radius" - applyConstraint:positive]; - NSNumber *borderHeight = [config getOptionalDouble:@"style/border_height" - applyConstraint:pos_ceil]; - NSNumber *borderWidth = [config getOptionalDouble:@"style/border_width" - applyConstraint:pos_ceil]; - NSNumber *lineSpacing = [config getOptionalDouble:@"style/line_spacing" - applyConstraint:pos_round]; - NSNumber *spacing = [config getOptionalDouble:@"style/spacing" - applyConstraint:pos_round]; - NSNumber *baseOffset = [config getOptionalDouble:@"style/base_offset"]; - NSNumber *lineLength = [config getOptionalDouble:@"style/line_length"]; + NSString *fontName = [config getStringForOption:@"style/font_face"]; + NSNumber *fontSize = [config getOptionalDoubleForOption:@"style/font_point" + applyConstraint:pos_round]; + NSString *labelFontName = [config getStringForOption:@"style/label_font_face"]; + NSNumber *labelFontSize = [config getOptionalDoubleForOption:@"style/label_font_point" + applyConstraint:pos_round]; + NSString *commentFontName = [config getStringForOption:@"style/comment_font_face"]; + NSNumber *commentFontSize = [config getOptionalDoubleForOption:@"style/comment_font_point" + applyConstraint:pos_round]; + NSNumber *alpha = [config getOptionalDoubleForOption:@"style/alpha" + applyConstraint:clamp_uni]; + NSNumber *translucency = [config getOptionalDoubleForOption:@"style/translucency" + applyConstraint:clamp_uni]; + NSNumber *cornerRadius = [config getOptionalDoubleForOption:@"style/corner_radius" + applyConstraint:positive]; + NSNumber *highlightedCornerRadius = [config getOptionalDoubleForOption:@"style/hilited_corner_radius" + applyConstraint:positive]; + NSNumber *borderHeight = [config getOptionalDoubleForOption:@"style/border_height" + applyConstraint:pos_ceil]; + NSNumber *borderWidth = [config getOptionalDoubleForOption:@"style/border_width" + applyConstraint:pos_ceil]; + NSNumber *lineSpacing = [config getOptionalDoubleForOption:@"style/line_spacing" + applyConstraint:pos_round]; + NSNumber *spacing = [config getOptionalDoubleForOption:@"style/spacing" + applyConstraint:pos_round]; + NSNumber *baseOffset = [config getOptionalDoubleForOption:@"style/base_offset"]; + NSNumber *lineLength = [config getOptionalDoubleForOption:@"style/line_length"]; // CHROMATICS NSColor *backColor; NSColor *borderColor; @@ -3330,17 +3558,17 @@ + (void)updateTheme:(SquirrelTheme *)theme NSString *colorScheme; if (appear == darkAppear) { for (NSString *option in styleOptions) { - if ((colorScheme = [config getString:[NSString stringWithFormat: - @"style/%@/color_scheme_dark", option]])) break; + if ((colorScheme = [config getStringForOption:[NSString stringWithFormat: + @"style/%@/color_scheme_dark", option]])) break; } - colorScheme = colorScheme ? : [config getString:@"style/color_scheme_dark"]; + colorScheme = colorScheme ? : [config getStringForOption:@"style/color_scheme_dark"]; } if (!colorScheme) { for (NSString *option in styleOptions) { - if ((colorScheme = [config getString:[NSString stringWithFormat: - @"style/%@/color_scheme", option]])) break; + if ((colorScheme = [config getStringForOption:[NSString stringWithFormat: + @"style/%@/color_scheme", option]])) break; } - colorScheme = colorScheme ? : [config getString:@"style/color_scheme"]; + colorScheme = colorScheme ? : [config getStringForOption:@"style/color_scheme"]; } BOOL isNative = !colorScheme || [colorScheme isEqualToString:@"native"]; NSArray *configPrefixes = isNative @@ -3351,66 +3579,66 @@ + (void)updateTheme:(SquirrelTheme *)theme // get color scheme and then check possible overrides from styleSwitcher for (NSString *prefix in configPrefixes) { // CHROMATICS override - config.colorSpace = [config getString:[prefix stringByAppendingString:@"/color_space"]] ? : config.colorSpace; - backColor = [config getColor:[prefix stringByAppendingString:@"/back_color"]] ? : backColor; - borderColor = [config getColor:[prefix stringByAppendingString:@"/border_color"]] ? : borderColor; - preeditBackColor = [config getColor:[prefix stringByAppendingString:@"/preedit_back_color"]] ? : preeditBackColor; - textColor = [config getColor:[prefix stringByAppendingString:@"/text_color"]] ? : textColor; - candidateTextColor = [config getColor:[prefix stringByAppendingString:@"/candidate_text_color"]] ? : candidateTextColor; - commentTextColor = [config getColor:[prefix stringByAppendingString:@"/comment_text_color"]] ? : commentTextColor; - candidateLabelColor = [config getColor:[prefix stringByAppendingString:@"/label_color"]] ? : candidateLabelColor; - highlightedBackColor = [config getColor:[prefix stringByAppendingString:@"/hilited_back_color"]] ? : highlightedBackColor; - highlightedTextColor = [config getColor:[prefix stringByAppendingString:@"/hilited_text_color"]] ? : highlightedTextColor; - highlightedCandidateBackColor = [config getColor:[prefix stringByAppendingString: - @"/hilited_candidate_back_color"]] ? : highlightedCandidateBackColor; - highlightedCandidateTextColor = [config getColor:[prefix stringByAppendingString: - @"/hilited_candidate_text_color"]] ? : highlightedCandidateTextColor; - highlightedCommentTextColor = [config getColor:[prefix stringByAppendingString: - @"/hilited_comment_text_color"]] ? : highlightedCommentTextColor; + config.colorSpace = [config getStringForOption:[prefix stringByAppendingString:@"/color_space"]] ? : config.colorSpace; + backColor = [config getColorForOption:[prefix stringByAppendingString:@"/back_color"]] ? : backColor; + borderColor = [config getColorForOption:[prefix stringByAppendingString:@"/border_color"]] ? : borderColor; + preeditBackColor = [config getColorForOption:[prefix stringByAppendingString:@"/preedit_back_color"]] ? : preeditBackColor; + textColor = [config getColorForOption:[prefix stringByAppendingString:@"/text_color"]] ? : textColor; + candidateTextColor = [config getColorForOption:[prefix stringByAppendingString:@"/candidate_text_color"]] ? : candidateTextColor; + commentTextColor = [config getColorForOption:[prefix stringByAppendingString:@"/comment_text_color"]] ? : commentTextColor; + candidateLabelColor = [config getColorForOption:[prefix stringByAppendingString:@"/label_color"]] ? : candidateLabelColor; + highlightedBackColor = [config getColorForOption:[prefix stringByAppendingString:@"/hilited_back_color"]] ? : highlightedBackColor; + highlightedTextColor = [config getColorForOption:[prefix stringByAppendingString:@"/hilited_text_color"]] ? : highlightedTextColor; + highlightedCandidateBackColor = [config getColorForOption:[prefix stringByAppendingString: + @"/hilited_candidate_back_color"]] ? : highlightedCandidateBackColor; + highlightedCandidateTextColor = [config getColorForOption:[prefix stringByAppendingString: + @"/hilited_candidate_text_color"]] ? : highlightedCandidateTextColor; + highlightedCommentTextColor = [config getColorForOption:[prefix stringByAppendingString: + @"/hilited_comment_text_color"]] ? : highlightedCommentTextColor; // 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; - backImage = [config getImage:[prefix stringByAppendingString:@"/back_image"]] ? : backImage; + highlightedCandidateLabelColor = [config getColorForOption:[prefix stringByAppendingString:@"/label_hilited_color"]] ? : + [config getColorForOption:[prefix stringByAppendingString:@"/hilited_candidate_label_color"]] ? : highlightedCandidateLabelColor; + backImage = [config getImageForOption:[prefix stringByAppendingString:@"/back_image"]] ? : backImage; // the following per-color-scheme configurations, if exist, will // override configurations with the same name under the global 'style' section // INTERFACE override - updateCandidateListLayout(&linear, &tabled, config, prefix); + updateCandidateListLayout(&linear, &tabular, config, prefix); updateTextOrientation(&vertical, config, prefix); - 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:@"/status_message_type"]] ? : statusMessageType; - candidateFormat = [config getString:[prefix stringByAppendingString:@"/candidate_format"]] ? : candidateFormat; + inlinePreedit = [config getOptionalBoolForOption:[prefix stringByAppendingString:@"/inline_preedit"]] ? : inlinePreedit; + inlineCandidate = [config getOptionalBoolForOption:[prefix stringByAppendingString:@"/inline_candidate"]] ? : inlineCandidate; + showPaging = [config getOptionalBoolForOption:[prefix stringByAppendingString:@"/show_paging"]] ? : showPaging; + rememberSize = [config getOptionalBoolForOption:[prefix stringByAppendingString:@"/remember_size"]] ? : rememberSize; + statusMessageType = [config getStringForOption:[prefix stringByAppendingString:@"/status_message_type"]] ? : statusMessageType; + candidateFormat = [config getStringForOption:[prefix stringByAppendingString:@"/candidate_format"]] ? : candidateFormat; // TYPOGRAPHY override - fontName = [config getString:[prefix stringByAppendingString:@"/font_face"]] ? : fontName; - fontSize = [config getOptionalDouble:[prefix stringByAppendingString:@"/font_point"] - applyConstraint:pos_round] ? : fontSize; - labelFontName = [config getString:[prefix stringByAppendingString:@"/label_font_face"]] ? : labelFontName; - labelFontSize = [config getOptionalDouble:[prefix stringByAppendingString:@"/label_font_point"] - applyConstraint:pos_round] ? : labelFontSize; - commentFontName = [config getString:[prefix stringByAppendingString:@"/comment_font_face"]] ? : commentFontName; - commentFontSize = [config getOptionalDouble:[prefix stringByAppendingString:@"/comment_font_point"] - applyConstraint:pos_round] ? : commentFontSize; - alpha = [config getOptionalDouble:[prefix stringByAppendingString:@"/alpha"] - applyConstraint:clamp_uni] ? : alpha; - translucency = [config getOptionalDouble:[prefix stringByAppendingString:@"/translucency"] - applyConstraint:clamp_uni] ? : translucency; - cornerRadius = [config getOptionalDouble:[prefix stringByAppendingString:@"/corner_radius"] - applyConstraint:positive] ? : cornerRadius; - highlightedCornerRadius = [config getOptionalDouble:[prefix stringByAppendingString:@"/hilited_corner_radius"] - applyConstraint:positive] ? : highlightedCornerRadius; - borderHeight = [config getOptionalDouble:[prefix stringByAppendingString:@"/border_height"] - applyConstraint:pos_ceil] ? : borderHeight; - borderWidth = [config getOptionalDouble:[prefix stringByAppendingString:@"/border_width"] - applyConstraint:pos_ceil] ? : borderWidth; - lineSpacing = [config getOptionalDouble:[prefix stringByAppendingString:@"/line_spacing"] - applyConstraint:pos_round] ? : lineSpacing; - spacing = [config getOptionalDouble:[prefix stringByAppendingString:@"/spacing"] - applyConstraint:pos_round] ? : spacing; - baseOffset = [config getOptionalDouble:[prefix stringByAppendingString:@"/base_offset"]] ? : baseOffset; - lineLength = [config getOptionalDouble:[prefix stringByAppendingString:@"/line_length"]] ? : lineLength; + fontName = [config getStringForOption:[prefix stringByAppendingString:@"/font_face"]] ? : fontName; + fontSize = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/font_point"] + applyConstraint:pos_round] ? : fontSize; + labelFontName = [config getStringForOption:[prefix stringByAppendingString:@"/label_font_face"]] ? : labelFontName; + labelFontSize = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/label_font_point"] + applyConstraint:pos_round] ? : labelFontSize; + commentFontName = [config getStringForOption:[prefix stringByAppendingString:@"/comment_font_face"]] ? : commentFontName; + commentFontSize = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/comment_font_point"] + applyConstraint:pos_round] ? : commentFontSize; + alpha = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/alpha"] + applyConstraint:clamp_uni] ? : alpha; + translucency = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/translucency"] + applyConstraint:clamp_uni] ? : translucency; + cornerRadius = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/corner_radius"] + applyConstraint:positive] ? : cornerRadius; + highlightedCornerRadius = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/hilited_corner_radius"] + applyConstraint:positive] ? : highlightedCornerRadius; + borderHeight = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/border_height"] + applyConstraint:pos_ceil] ? : borderHeight; + borderWidth = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/border_width"] + applyConstraint:pos_ceil] ? : borderWidth; + lineSpacing = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/line_spacing"] + applyConstraint:pos_round] ? : lineSpacing; + spacing = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/spacing"] + applyConstraint:pos_round] ? : spacing; + baseOffset = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/base_offset"]] ? : baseOffset; + lineLength = [config getOptionalDoubleForOption:[prefix stringByAppendingString:@"/line_length"]] ? : lineLength; } // TYPOGRAPHY refinement @@ -3602,8 +3830,8 @@ + (void)updateTheme:(SquirrelTheme *)theme preeditBackColor = preeditBackColor ? : isNative ? NSColor.windowBackgroundColor : nil; textColor = textColor ? : NSColor.textColor; candidateTextColor = candidateTextColor ? : NSColor.controlTextColor; - commentTextColor = commentTextColor ? : SquirrelTheme.secondaryTextColor; - candidateLabelColor = candidateLabelColor ? : isNative ? SquirrelTheme.accentColor : blendColors(candidateTextColor, backColor); + commentTextColor = commentTextColor ? : NSColor.secondaryTextColor; + candidateLabelColor = candidateLabelColor ? : isNative ? NSColor.accentColor : blendColors(candidateTextColor, backColor); highlightedBackColor = highlightedBackColor ? : isNative ? NSColor.selectedTextBackgroundColor : nil; highlightedTextColor = highlightedTextColor ? : NSColor.selectedTextColor; highlightedCandidateBackColor = highlightedCandidateBackColor ? : isNative ? NSColor.selectedContentBackgroundColor : nil; @@ -3630,15 +3858,15 @@ + (void)updateTheme:(SquirrelTheme *)theme [theme setCornerRadius:MIN(cornerRadius.doubleValue, lineHeight * 0.5) highlightedCornerRadius:MIN(highlightedCornerRadius.doubleValue, lineHeight * 0.5) separatorWidth:separatorWidth - borderInset:borderInset linespace:lineSpacing.doubleValue preeditLinespace:spacing.doubleValue alpha:alpha ? alpha.doubleValue : 1.0 translucency:translucency.doubleValue lineLength:lineLength.doubleValue > 0 ? MAX(ceil(lineLength.doubleValue), separatorWidth * 5) : 0.0 + borderInset:borderInset showPaging:showPaging.boolValue rememberSize:rememberSize.boolValue - tabled:tabled + tabular:tabular linear:linear vertical:vertical inlinePreedit:inlinePreedit.boolValue diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index a2454b3a8..1d82a21da 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -10,10 +10,13 @@ Revert previous modifications to see if the problem recurs."; "say_voice" = "Alex"; -"candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word."; +"candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word.\nPress and hold the ⌥option key to temporarily disable mouse interactions."; "delete" = "Click to ⌫Delete the input by character.\nSecondary click to ⎋Escape the composing."; "escape" = "Cannot delete any further.\nSecondary click to ⎋Escape the composing."; "page_up" = "Click to ⇞Page Up.\nSecondary click to jump to ↖Home."; "home" = "Cannot page up any further.\nSecondary click to jump to ↖Home."; "page_down" = "Click to ⇞Page Down.\nSecondary click to jump to ↘End."; "end" = "Cannot page down any further.\nSecondary click to jump to ↘End."; +"compress" = "Click to compress candidate window.\nSecondary click to lock this multiple-row view"; +"expand" = "Click to expand candidate window.\nSecondary click to lock this single-row view."; +"unlock" = "Click to unlock the view and allow it to be expanded or collapsed."; diff --git a/zh-HK.lproj/Localizable.strings b/zh-HK.lproj/Localizable.strings index 45818887d..81bd4f444 100644 --- a/zh-HK.lproj/Localizable.strings +++ b/zh-HK.lproj/Localizable.strings @@ -10,10 +10,13 @@ 請嘗試回退先前的改動,然後查看問題是否仍然存在。"; "say_voice" = "Sinji"; -"candidate" = "點按以⎆選取候選字。\n點按輔助按鈕以⎌清除所選的記憶字詞。"; +"candidate" = "點按以⎆選取候選字。\n點按輔助按鈕以⎌清除所選的記憶字詞。按住⌥Option鍵以暫時停用滑鼠與「鼠鬚筆」互動"; "delete" = "點按以逐字⌫刪除輸入。\n點按輔助按鈕以⎋取消輸入。"; "escape" = "無法再刪除。\n點按輔助按鈕以⎋取消輸入。"; "page_up" = "點按以⇞向上翻頁。\n點按輔助按鈕以跳至↖起點。"; "home" = "無法再向上翻頁。\n點按輔助按鈕以跳至↖起點。"; "page_down" = "點按以⇟向下翻頁。\n點按輔助按鈕以跳至↘結尾。"; "end" = "無法再向下翻頁。\n點按輔助按鈕以跳至↘結尾。"; +"compress" = "點按以收合候選字視窗。點按輔助按鈕以鎖定當前的多橫列顯示方式。"; +"expand" = "點按以展開候選字視窗。點按輔助按鈕以鎖定當前的單橫列顯示方式。"; +"unlock" = "點按以解鎖顯示方式,允許展開或收合候選字視窗。"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index cf655dc1b..e61410eed 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -10,10 +10,13 @@ 请尝试撤销之前的修改,然后查看问题是否仍旧存在。"; "say_voice" = "TingTing"; -"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。"; +"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。按住⌥Option键以暂时停用鼠标与“鼠须管”互动。"; "delete" = "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。"; "escape" = "不能再删除。\n辅助点按以⎋取消输入。"; "page_up" = "点按以⇞向上翻页。\n辅助点按以跳到↖开头。"; "home" = "不能再向上翻页。\n辅助点按以跳到↖开头。"; "page_down" = "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。"; "end" = "不能再向下翻页。\n辅助点按以跳到↘结尾。"; +"compress" = "点按以折叠候选字窗口。辅助点按以锁定当前的多行视图。"; +"expand" = "点按以展开候选字窗口。辅助点按以锁定当前的单行视图。"; +"unlock" = "点按以解锁视图,允许展开或折叠候选字窗口。"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings index 3124322d8..17ebacdaa 100644 --- a/zh-Hant.lproj/Localizable.strings +++ b/zh-Hant.lproj/Localizable.strings @@ -10,10 +10,13 @@ 請嘗試回退先前的修改,然後查看問題是否依然存在。"; "say_voice" = "MeiJia"; -"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。"; +"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。按住⌥Option鍵以暫時停用滑鼠與「鼠鬚管」互動。"; "delete" = "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。"; "escape" = "無法再刪除。\n點按輔助按鈕來⎋取消輸入。"; "page_up" = "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; "home" = "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; "page_down" = "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; "end" = "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; +"compress" = "點按來收合候選字視窗。點按輔助按鈕來鎖定當前的多橫列顯示方式。"; +"expand" = "點按來展開候選字視窗。點按輔助按鈕來鎖定當前的單橫列顯示方式。"; +"unlock" = "點按來解鎖顯示方式,允許展開或收合候選字視窗。";