diff --git a/SquirrelApplicationDelegate.h b/SquirrelApplicationDelegate.h index 30299f858..df13bd1a8 100644 --- a/SquirrelApplicationDelegate.h +++ b/SquirrelApplicationDelegate.h @@ -5,20 +5,20 @@ @class SquirrelPanel; @class SquirrelOptionSwitcher; -typedef enum { +// Note: the SquirrelApplicationDelegate is instantiated automatically as an outlet of NSApp's instance +@interface SquirrelApplicationDelegate : NSObject + +typedef NS_ENUM(NSUInteger, SquirrelNotificationPolicy) { kShowNotificationsNever = 0, kShowNotificationsWhenAppropriate = 1, kShowNotificationsAlways = 2 -} SquirrelNotificationPolicy; - -// Note: the SquirrelApplicationDelegate is instantiated automatically as an outlet of NSApp's instance -@interface SquirrelApplicationDelegate : NSObject +}; -@property(nonatomic, copy) IBOutlet NSMenu *menu; -@property(nonatomic, strong) IBOutlet SquirrelPanel *panel; -@property(nonatomic, strong) IBOutlet id updater; +@property(nonatomic, weak) IBOutlet NSMenu *menu; +@property(nonatomic, weak) IBOutlet SquirrelPanel *panel; +@property(nonatomic, weak) IBOutlet id updater; -@property(nonatomic, readonly, strong) SquirrelConfig *config; +@property(nonatomic, strong, readonly) SquirrelConfig *config; @property(nonatomic, readonly) SquirrelNotificationPolicy showNotifications; - (IBAction)deploy:(id)sender; @@ -35,13 +35,15 @@ typedef enum { @property(nonatomic, readonly) BOOL problematicLaunchDetected; -@end +@end // SquirrelApplicationDelegate + @interface NSApplication (SquirrelApp) -@property(nonatomic, readonly, strong) SquirrelApplicationDelegate *squirrelAppDelegate; +@property(nonatomic, strong, readonly) SquirrelApplicationDelegate *squirrelAppDelegate; + +@end // NSApplication (SquirrelApp) -@end // also used in main.m extern void show_notification(const char *msg_text); diff --git a/SquirrelApplicationDelegate.m b/SquirrelApplicationDelegate.m index 46f1d1844..7406d085c 100644 --- a/SquirrelApplicationDelegate.m +++ b/SquirrelApplicationDelegate.m @@ -109,7 +109,7 @@ static void notification_handler(void *context_object, RimeSessionId session_id, if ([app_delegate.panel.optionSwitcher containsOption:@(option_name)]) { if ([app_delegate.panel.optionSwitcher updateGroupState:@(message_value) ofOption:@(option_name)]) { - NSString *schemaId = [app_delegate panel].optionSwitcher.schemaId; + NSString *schemaId = app_delegate.panel.optionSwitcher.schemaId; [app_delegate loadSchemaSpecificLabels:schemaId]; [app_delegate loadSchemaSpecificSettings:schemaId withRimeSession:session_id]; diff --git a/SquirrelConfig.h b/SquirrelConfig.h index 4329c4b58..9304a8057 100644 --- a/SquirrelConfig.h +++ b/SquirrelConfig.h @@ -1,8 +1,5 @@ #import -typedef NSDictionary SquirrelAppOptions; -typedef NSMutableDictionary SquirrelMutableAppOptions; - @interface SquirrelOptionSwitcher : NSObject @property(nonatomic, strong, readonly) NSString *schemaId; @@ -32,6 +29,9 @@ typedef NSMutableDictionary SquirrelMutableAppOptions; @interface SquirrelConfig : NSObject +typedef NSDictionary SquirrelAppOptions; +typedef NSMutableDictionary SquirrelMutableAppOptions; + @property(nonatomic, readonly) BOOL isOpen; @property(nonatomic, strong) NSString *colorSpace; @property(nonatomic, strong, readonly) NSString *schemaId; diff --git a/SquirrelInputController.h b/SquirrelInputController.h index 9ae167778..92c7bd6f3 100644 --- a/SquirrelInputController.h +++ b/SquirrelInputController.h @@ -1,11 +1,13 @@ #import #import -typedef enum { +@interface SquirrelInputController : IMKInputController + +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) -} SquirrelAction; +}; typedef NS_ENUM(NSUInteger, SquirrelIndex) { // 0 ... 9 are ordinal digits, used as (int) index @@ -21,9 +23,7 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { kVoidSymbol = 0xffffff // XK_VoidSymbol }; -@interface SquirrelInputController : IMKInputController - -@property(nonatomic, class, readonly) SquirrelInputController *currentController; +@property(class, weak, readonly) SquirrelInputController *currentController; - (void)moveCursor:(NSUInteger)cursorPosition toPosition:(NSUInteger)targetPosition @@ -33,4 +33,4 @@ typedef NS_ENUM(NSUInteger, SquirrelIndex) { - (void)perform:(SquirrelAction)action onIndex:(SquirrelIndex)index; -@end +@end // SquirrelInputController diff --git a/SquirrelInputController.m b/SquirrelInputController.m index 1e61d90d1..c3855a306 100644 --- a/SquirrelInputController.m +++ b/SquirrelInputController.m @@ -6,17 +6,10 @@ #import "macos_keycode.h" #import #import +#import #import #import -@interface SquirrelInputController (Private) -- (void)createSession; -- (void)destroySession; -- (void)rimeConsumeCommittedText; -- (void)rimeUpdate; -- (void)updateAppOptions; -@end - const int N_KEY_ROLL_OVER = 50; static NSString *kFullWidthSpace = @" "; @@ -49,6 +42,7 @@ @implementation SquirrelInputController { + (void)setCurrentController:(SquirrelInputController *)controller { _currentController = controller; + NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; } + (SquirrelInputController *)currentController { @@ -59,14 +53,14 @@ + (void)setDeactivationTimeForController:(SquirrelInputController *)controller { [_controllerDeactivationTime setObject:[[NSDate alloc] init] forKey:controller]; } -+ (NSDate *)lastControllerDeactivationTime { - return [_controllerDeactivationTime objectForKey:_currentController]; -} - + (void)removeDeactivationTimeForController:(SquirrelInputController *)controller { [_controllerDeactivationTime removeObjectForKey:controller]; } ++ (NSDate *)lastDeactivationTime { + return [_controllerDeactivationTime objectForKey:_currentController]; +} + /*! @method @abstract Receive incoming event @@ -209,7 +203,8 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index client:(id)sender { *keepTracking = NO; @autoreleasepool { - if ((!_inlinePreedit && !_inlineCandidate) || _caretPos == index || + if ((!_inlinePreedit && !_inlineCandidate) || + _composedString.length == 0 || _caretPos == index || (flags & NSEventModifierFlagDeviceIndependentFlagsMask)) { return NO; } @@ -218,7 +213,7 @@ - (BOOL)mouseDownOnCharacterIndex:(NSUInteger)index lineHeightRectangle:NULL][@"IMKBaseline"] pointValue]; NSPoint tail = [[sender attributesForCharacterIndex:markedRange.length - 1 lineHeightRectangle:NULL][@"IMKBaseline"] pointValue]; - if (point.x > tail.x || index >= markedRange.length -1) { + if (point.x > tail.x || index >= markedRange.length) { if (_inlineCandidate && !_inlinePreedit) { return NO; } @@ -298,6 +293,7 @@ - (BOOL)processKey:(int)rime_keycode return handled; } + - (void)moveCursor:(NSUInteger)cursorPosition toPosition:(NSUInteger)targetPosition inlinePreedit:(BOOL)inlinePreedit @@ -306,8 +302,10 @@ - (void)moveCursor:(NSUInteger)cursorPosition NSString *composition = !inlinePreedit && !inlineCandidate ? _composedString : _preeditString.string; RIME_STRUCT(RimeContext, ctx); if (cursorPosition > targetPosition) { - NSString *targetPrefix = [composition substringToIndex:targetPosition]; - NSString *prefix = [composition substringToIndex:cursorPosition]; + NSString *targetPrefix = [[composition substringToIndex:targetPosition] + stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSString *prefix = [[composition substringToIndex:cursorPosition] + stringByReplacingOccurrencesOfString:@" " withString:@""]; while (targetPrefix.length < prefix.length) { rime_get_api()->process_key(_session, vertical ? XK_Up : XK_Left, kControlMask); rime_get_api()->get_context(_session, &ctx); @@ -315,23 +313,28 @@ - (void)moveCursor:(NSUInteger)cursorPosition size_t length = ctx.composition.cursor_pos < ctx.composition.sel_end ? (size_t)ctx.composition.cursor_pos : strlen(ctx.commit_text_preview) - (inlinePreedit ? 0 : (size_t)(ctx.composition.cursor_pos - ctx.composition.sel_end)); - prefix = [[NSString alloc] initWithBytes:ctx.commit_text_preview - length:(NSUInteger)length - encoding:NSUTF8StringEncoding]; + prefix = [[[NSString alloc] initWithBytes:ctx.commit_text_preview + length:(NSUInteger)length + encoding:NSUTF8StringEncoding] + stringByReplacingOccurrencesOfString:@" " withString:@""]; } else { - prefix = [[NSString alloc] initWithBytes:ctx.composition.preedit - length:(NSUInteger)ctx.composition.cursor_pos - encoding:NSUTF8StringEncoding]; + prefix = [[[NSString alloc] initWithBytes:ctx.composition.preedit + length:(NSUInteger)ctx.composition.cursor_pos + encoding:NSUTF8StringEncoding] + stringByReplacingOccurrencesOfString:@" " withString:@""]; } rime_get_api()->free_context(&ctx); } } else if (cursorPosition < targetPosition) { - NSString *targetSuffix = [composition substringFromIndex:targetPosition]; - NSString *suffix = [composition substringFromIndex:cursorPosition]; + NSString *targetSuffix = [[composition substringFromIndex:targetPosition] + stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSString *suffix = [[composition substringFromIndex:cursorPosition] + stringByReplacingOccurrencesOfString:@" " withString:@""]; while (targetSuffix.length < suffix.length) { rime_get_api()->process_key(_session, vertical ? XK_Down : XK_Right, kControlMask); rime_get_api()->get_context(_session, &ctx); - suffix = @(ctx.composition.preedit + ctx.composition.cursor_pos + (!inlinePreedit && !inlineCandidate ? 3 : 0)); + suffix = [@(ctx.composition.preedit + ctx.composition.cursor_pos + (!inlinePreedit && !inlineCandidate ? 3 : 0)) + stringByReplacingOccurrencesOfString:@" " withString:@""]; rime_get_api()->free_context(&ctx); } } @@ -340,7 +343,7 @@ - (void)moveCursor:(NSUInteger)cursorPosition - (void)perform:(SquirrelAction)action onIndex:(SquirrelIndex)index { - //NSLog(@"perform action: %u on index: %lu", action, (unsigned long)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); @@ -424,7 +427,7 @@ - (void)clearChord { - (NSUInteger)recognizedEvents:(id)sender { //NSLog(@"recognizedEvents:"); - return NSEventMaskKeyDown | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown; + return NSEventMaskKeyDown|NSEventMaskFlagsChanged|NSEventMaskLeftMouseDown; } NSString *getOptionLabel(RimeSessionId session, const char *option, Bool state) { @@ -462,6 +465,9 @@ - (void)showInitialStatus { NSString *foldedOptions = options.count == 0 ? schemaName : [NSString stringWithFormat:@"%@|%@", schemaName, [options componentsJoinedByString:@" "]]; [NSApp.squirrelAppDelegate.panel updateStatusLong:foldedOptions statusShort:schemaName]; + if (@available(macOS 14.0, *)) { + _lastModifiers |= NSEventModifierFlagHelp; + } [self rimeUpdate]; } } @@ -483,10 +489,10 @@ - (void)activateServer:(id)sender { [sender overrideKeyboardWithKeyboardNamed:keyboardLayout]; } + [SquirrelInputController removeDeactivationTimeForController:self]; if (NSApp.squirrelAppDelegate.showNotifications == kShowNotificationsAlways) { if (!SquirrelInputController.currentController || - (![SquirrelInputController.currentController isEqualTo:self] && - [SquirrelInputController.lastControllerDeactivationTime timeIntervalSinceNow] < -0.5)) { + SquirrelInputController.lastDeactivationTime.timeIntervalSinceNow < -1.0) { [self showInitialStatus]; } } @@ -510,9 +516,6 @@ - (instancetype)initWithServer:(IMKServer *)server delegate:delegate client:inputClient]) { [self createSession]; - _preeditString = [[NSMutableAttributedString alloc] init]; - _originalString = [[NSMutableString alloc] init]; - _composedString = [[NSMutableString alloc] init]; } return self; } @@ -539,27 +542,18 @@ - (void)deactivateServer:(id)sender { - (void)commitComposition:(id)sender { //NSLog(@"commitComposition:"); // commit raw input + [self commitString:[self composedString:sender]]; + [self hidePalettes]; if (_session) { - const char *raw_input = rime_get_api()->get_input(_session); - if (raw_input) { - [self commitString:@(raw_input)]; - rime_get_api()->clear_composition(_session); - } + rime_get_api()->clear_composition(_session); } } -- (void)inputControllerWillClose { - if (_session) { - [self destroySession]; - } - if ([SquirrelInputController.currentController isEqualTo:self]) { - [SquirrelInputController setCurrentController:nil]; - } - [SquirrelInputController removeDeactivationTimeForController:self]; +- (void)clearBuffer { + NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; _preeditString = nil; _originalString = nil; _composedString = nil; - [super inputControllerWillClose]; } // a piece of comment from SunPinyin's macos wrapper says: @@ -610,25 +604,26 @@ - (void)hidePalettes { } - (void)dealloc { + //NSLog(@"dealloc"); [self destroySession]; - _preeditString = nil; - _originalString = nil; - _composedString = nil; + [self clearBuffer]; } - (void)commitString:(id)string { //NSLog(@"commitString:"); - [self.client insertText:string - replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; - _preeditString = nil; - _preeditString = nil; - _originalString = nil; - _composedString = nil; + if (string) { + [self.client insertText:string + replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; + } + [self clearBuffer]; } - (void)cancelComposition { [self commitString:[self originalString:self.client]]; - rime_get_api()->clear_composition(_session); + [self hidePalettes]; + if (_session) { + rime_get_api()->clear_composition(_session); + } } - (void)showPreeditString:(NSString *)preedit @@ -662,19 +657,10 @@ - (void)showPreeditString:(NSString *)preedit replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; } -- (void)showPanelWithPreedit:(NSString *)preedit - selRange:(NSRange)selRange - caretPos:(NSUInteger)caretPos - candidates:(NSArray *)candidates - comments:(NSArray *)comments - highlightedIndex:(NSUInteger)highlightedIndex - pageNum:(NSUInteger)pageNum - lastPage:(BOOL)lastPage { - //NSLog(@"showPanelWithPreedit:...:"); - _candidates = candidates; - SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; - NSRect IbeamRect; - [self.client attributesForCharacterIndex:0 lineHeightRectangle:&IbeamRect]; +- (CGRect)getIbeamRect { + NSRect IbeamRect = NSZeroRect; + [self.client attributesForCharacterIndex:0 + lineHeightRectangle:&IbeamRect]; if (NSEqualRects(IbeamRect, NSZeroRect) && _preeditString.length == 0) { if (self.client.selectedRange.length == 0) { // activate inline session, in e.g. table cells, by fake inputs @@ -691,8 +677,13 @@ - (void)showPanelWithPreedit:(NSString *)preedit lineHeightRectangle:&IbeamRect]; } } + if (NSIsEmptyRect(IbeamRect)) { + return IbeamRect; + } if (@available(macOS 14.0, *)) { // avoid overlapping with cursor effects view - if (_goodOldCapsLock && (_lastModifiers & NSEventModifierFlagCapsLock)) { + if ((_goodOldCapsLock && (_lastModifiers & NSEventModifierFlagCapsLock)) || + (_lastModifiers & NSEventModifierFlagHelp)) { + _lastModifiers &= ~NSEventModifierFlagHelp; NSRect screenRect = NSScreen.mainScreen.frame; if (NSIntersectsRect(IbeamRect, screenRect)) { screenRect = NSScreen.mainScreen.visibleFrame; @@ -716,22 +707,36 @@ - (void)showPanelWithPreedit:(NSString *)preedit } } } - panel.IbeamRect = IbeamRect; - [panel showPreedit:preedit - selRange:selRange - caretPos:caretPos - candidates:candidates - comments:comments - highlightedIndex:highlightedIndex - pageNum:pageNum - lastPage:lastPage]; + return IbeamRect; } -@end // SquirrelController - +- (void)showPanelWithPreedit:(NSString *)preedit + selRange:(NSRange)selRange + caretPos:(NSUInteger)caretPos + candidates:(NSArray *)candidates + comments:(NSArray *)comments + highlightedIndex:(NSUInteger)highlightedIndex + pageNum:(NSUInteger)pageNum + lastPage:(BOOL)lastPage { + //NSLog(@"showPanelWithPreedit:...:"); + _candidates = candidates; + SquirrelPanel *panel = NSApp.squirrelAppDelegate.panel; + panel.IbeamRect = [self getIbeamRect]; + if (NSIsEmptyRect(panel.IbeamRect) && panel.statusMessage.length > 0) { + [panel updateStatusLong:nil statusShort:nil]; + } else { + [panel showPreedit:preedit + selRange:selRange + caretPos:caretPos + candidates:candidates + comments:comments + highlightedIndex:highlightedIndex + pageNum:pageNum + lastPage:lastPage]; + } +} -// implementation of private interface -@implementation SquirrelInputController (Private) +#pragma mark - Private methods - (void)createSession { NSString *app = [self.client bundleIdentifier]; @@ -763,6 +768,7 @@ - (void)updateAppOptions } _lastModifiers = 0; _lastEventType = 0; + NSApp.squirrelAppDelegate.panel.IbeamRect = NSZeroRect; [self rimeUpdate]; } } @@ -785,8 +791,8 @@ - (void)rimeConsumeCommittedText { } } -NSUInteger UTF8LengthToUTF16Length(const char* cstring, int length) { - return [[NSString alloc] initWithBytes:cstring +NSUInteger inline UTF8LengthToUTF16Length(const char* string, int length) { + return [[NSString alloc] initWithBytes:string length:(NSUInteger)length encoding:NSUTF8StringEncoding].length; } @@ -851,42 +857,42 @@ - (void)rimeUpdate { NSUInteger length = UTF8LengthToUTF16Length(preedit, ctx.composition.length); NSUInteger numCandidate = (NSUInteger)ctx.menu.num_candidates; - if (!_showingSwitcherMenu && !showingStatus) { - if (_inlineCandidate) { - const char *candidatePreview = ctx.commit_text_preview; - NSString *candidatePreviewText = candidatePreview ? @(candidatePreview) : @""; - if (_inlinePreedit) { - if (end <= caretPos && caretPos < length) { - candidatePreviewText = [candidatePreviewText stringByAppendingString: - [preeditText substringWithRange:NSMakeRange(caretPos, length - caretPos)]]; - } - [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(start, candidatePreviewText.length - (length - end) - start) - caretPos:caretPos < end ? caretPos : candidatePreviewText.length - (length - caretPos)]; - } else { - if (end < caretPos && caretPos <= length) { - candidatePreviewText = [candidatePreviewText substringToIndex:candidatePreviewText.length - (caretPos - end)]; - } else if (caretPos < end && end < length) { - candidatePreviewText = [candidatePreviewText substringToIndex:candidatePreviewText.length - (length - end)]; - } - [self showPreeditString:candidatePreviewText - selRange:NSMakeRange(start - (caretPos < end), - candidatePreviewText.length - start + (caretPos < end)) - caretPos:caretPos < end ? caretPos - 1 : candidatePreviewText.length]; + if (showingStatus) { + [self clearBuffer]; + } else if (_inlineCandidate && !_showingSwitcherMenu) { + const char *candidatePreview = ctx.commit_text_preview; + NSString *candidatePreviewText = candidatePreview ? @(candidatePreview) : @""; + if (_inlinePreedit) { + if (end <= caretPos && caretPos < length) { + candidatePreviewText = [candidatePreviewText stringByAppendingString: + [preeditText substringWithRange:NSMakeRange(caretPos, length - caretPos)]]; } + [self showPreeditString:candidatePreviewText + selRange:NSMakeRange(start, candidatePreviewText.length - (length - end) - start) + caretPos:caretPos < end ? caretPos : candidatePreviewText.length - (length - caretPos)]; } else { - if (_inlinePreedit) { - [self showPreeditString:preeditText - selRange:NSMakeRange(start, end - start) - caretPos:caretPos]; - } else { - // TRICKY: display a non-empty string to prevent iTerm2 from echoing each character in preedit. - // note this is a full-width space U+3000; using narrow characters like "..." will result in - // an unstable baseline when composing Chinese characters. - [self showPreeditString:preedit ? kFullWidthSpace : @"" - selRange:NSMakeRange(0, 0) - caretPos:0]; + if (end < caretPos && caretPos <= length) { + candidatePreviewText = [candidatePreviewText substringToIndex:candidatePreviewText.length - (caretPos - end)]; + } else if (caretPos < end && end < length) { + candidatePreviewText = [candidatePreviewText substringToIndex:candidatePreviewText.length - (length - end)]; } + [self showPreeditString:candidatePreviewText + selRange:NSMakeRange(start - (caretPos < end), + candidatePreviewText.length - start + (caretPos < end)) + caretPos:caretPos < end ? caretPos - 1 : candidatePreviewText.length]; + } + } else if (!_showingSwitcherMenu) { + if (_inlinePreedit) { + [self showPreeditString:preeditText + selRange:NSMakeRange(start, end - start) + caretPos:caretPos]; + } else { + // TRICKY: display a non-empty string to prevent iTerm2 from echoing each character in preedit. + // note this is a full-width space U+3000; using narrow characters like "..." will result in + // an unstable baseline when composing Chinese characters. + [self showPreeditString:preedit ? kFullWidthSpace : @"" + selRange:NSMakeRange(0, 0) + caretPos:0]; } } // update candidates @@ -907,9 +913,7 @@ - (void)rimeUpdate { rime_get_api()->free_context(&ctx); } else { [self hidePalettes]; - _preeditString = nil; - _composedString = nil; - _originalString = nil; + [self clearBuffer]; } } diff --git a/SquirrelPanel.h b/SquirrelPanel.h index 3cc6105c9..63a85a55a 100644 --- a/SquirrelPanel.h +++ b/SquirrelPanel.h @@ -3,13 +3,13 @@ @class SquirrelConfig; @class SquirrelOptionSwitcher; -typedef enum { +@interface SquirrelPanel : NSPanel + +typedef NS_ENUM(NSUInteger, SquirrelAppear) { defaultAppear = 0, lightAppear = 0, darkAppear = 1 -} SquirrelAppear; - -@interface SquirrelPanel : NSPanel +}; // Linear candidate list, as opposed to stacked candidate list. @property(nonatomic, readonly) BOOL linear; @@ -24,7 +24,7 @@ typedef enum { // Store switch options that change style (color theme) settings @property(nonatomic, strong) SquirrelOptionSwitcher *optionSwitcher; // Status message before pop-up is displayed; nil before normal panel is displayed -@property(nonatomic, readonly, strong) NSString *statusMessage; +@property(nonatomic, strong, readonly) NSString *statusMessage; // position of the text input I-beam cursor on screen. @property(nonatomic, assign) NSRect IbeamRect; @@ -48,4 +48,4 @@ typedef enum { - (void)loadLabelConfig:(SquirrelConfig *)config directUpdate:(BOOL)update; -@end +@end // SquirrelPanel diff --git a/SquirrelPanel.m b/SquirrelPanel.m index 024113aad..5994f2a91 100644 --- a/SquirrelPanel.m +++ b/SquirrelPanel.m @@ -24,7 +24,6 @@ - (CGPathRef)quartzPath { if (numElements > 0) { CGMutablePathRef path = CGPathCreateMutable(); NSPoint points[3]; - BOOL didClosePath = YES; for (NSInteger i = 0; i < numElements; i++) { switch ([self elementAtIndex:i associatedPoints:points]) { case NSBezierPathElementMoveTo: @@ -32,29 +31,21 @@ - (CGPathRef)quartzPath { break; case NSBezierPathElementLineTo: CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); - didClosePath = NO; break; case NSBezierPathElementCurveTo: CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y); - didClosePath = NO; break; case NSBezierPathElementQuadraticCurveTo: CGPathAddQuadCurveToPoint(path, NULL, points[0].x, points[0].y, points[1].x, points[1].y); - didClosePath = NO; break; case NSBezierPathElementClosePath: CGPathCloseSubpath(path); - didClosePath = YES; break; } } - // Be sure the path is closed or Quartz may not do valid hit detection. - if (!didClosePath) { - CGPathCloseSubpath(path); - } immutablePath = (CGPathRef)CFAutorelease(CGPathCreateCopy(path)); CGPathRelease(path); } @@ -314,11 +305,11 @@ - (NSColor *)invertLuminanceWithAdjustment:(NSInteger)sign { @interface SquirrelTheme : NSObject -typedef enum { +typedef NS_ENUM(NSUInteger, SquirrelStatusMessageType) { kStatusMessageTypeMixed = 0, kStatusMessageTypeShort = 1, kStatusMessageTypeLong = 2 -} SquirrelStatusMessageType; +}; @property(nonatomic, strong, readonly) NSColor *backColor; @property(nonatomic, strong, readonly) NSColor *highlightedCandidateBackColor; @@ -375,6 +366,8 @@ @interface SquirrelTheme : NSObject @property(nonatomic, strong, readonly) NSArray *candidateFormats; @property(nonatomic, strong, readonly) NSArray *candidateHighlightedFormats; +- (instancetype)init; + - (void) setBackColor:(NSColor *)backColor highlightedCandidateBackColor:(NSColor *)highlightedCandidateBackColor highlightedPreeditBackColor:(NSColor *)highlightedPreeditBackColor @@ -432,15 +425,86 @@ - (void)setAnnotationHeight:(CGFloat)height; @implementation SquirrelTheme +static inline NSColor *blendColors(NSColor *foregroundColor, NSColor *backgroundColor) { + return [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction + ofColor:backgroundColor ? : NSColor.lightGrayColor] + colorWithAlphaComponent:foregroundColor.alphaComponent]; +} + +static NSFontDescriptor *getFontDescriptor(NSString *fullname) { + if (fullname.length == 0) { + return nil; + } + NSArray *fontNames = [fullname componentsSeparatedByString:@","]; + NSMutableArray *validFontDescriptors = [[NSMutableArray alloc] + initWithCapacity:fontNames.count]; + for (NSString *fontName in fontNames) { + NSFont *font = [NSFont fontWithName:[fontName stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceAndNewlineCharacterSet] size:0.0]; + if (font != nil) { + // If the font name is not valid, NSFontDescriptor will still create something for us. + // However, when we draw the actual text, Squirrel will crash if there is any font descriptor + // with invalid font name. + NSFontDescriptor *fontDescriptor = font.fontDescriptor; + NSFontDescriptor *UIFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits: + NSFontDescriptorTraitUIOptimized]; + [validFontDescriptors addObject:[NSFont fontWithDescriptor:UIFontDescriptor size:0.0] != nil ? + UIFontDescriptor : fontDescriptor]; + } + } + if (validFontDescriptors.count == 0) { + return nil; + } + NSFontDescriptor *initialFontDescriptor = validFontDescriptors[0]; + NSFontDescriptor *emojiFontDescriptor = + [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0]; + NSArray *fallbackDescriptors = [[validFontDescriptors subarrayWithRange: + NSMakeRange(1, validFontDescriptors.count - 1)] + arrayByAddingObject:emojiFontDescriptor]; + return [initialFontDescriptor fontDescriptorByAddingAttributes: + @{NSFontCascadeListAttribute:fallbackDescriptors}]; +} + +static CGFloat getLineHeight(NSFont *font, BOOL vertical) { + if (vertical) { + font = font.verticalFont; + } + CGFloat lineHeight = ceil(font.ascender - font.descender); + NSArray *fallbackList = [font.fontDescriptor + objectForKey:NSFontCascadeListAttribute]; + for (NSFontDescriptor *fallback in fallbackList) { + NSFont *fallbackFont = [NSFont fontWithDescriptor:fallback + size:font.pointSize]; + if (vertical) { + fallbackFont = fallbackFont.verticalFont; + } + lineHeight = MAX(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); + } + return lineHeight; +} + +static NSFont *getTallestFont(NSArray *fonts, BOOL vertical) { + NSFont *tallestFont; + CGFloat maxHeight = 0.0; + for (NSFont *font in fonts) { + CGFloat fontHeight = getLineHeight(font, vertical); + if (fontHeight > maxHeight) { + tallestFont = font; + maxHeight = fontHeight; + } + } + return tallestFont; +} + static NSArray * formatLabels(NSAttributedString *format, NSArray *labels) { NSRange enumRange = NSMakeRange(0, 0); NSMutableArray *formatted = [[NSMutableArray alloc] initWithCapacity:labels.count]; NSCharacterSet *labelCharacters = [NSCharacterSet characterSetWithCharactersInString: - [labels componentsJoinedByString:@""]]; + [labels componentsJoinedByString:@""]]; if ([[NSCharacterSet characterSetWithRange:NSMakeRange(0xFF10, 10)] isSupersetOfSet:labelCharacters]) { // 01..9 - if ([format.string containsString:@"%c\u20E3"]) { // 1⃣..9⃣0⃣ + if ([format.string containsString:@"%c\u20E3"]) { // 1︎⃣..9︎⃣0︎⃣ enumRange = [format.string rangeOfString:@"%c\u20E3"]; for (NSString *label in labels) { const unichar chars[] = {[label characterAtIndex:0] - 0xFF10 + 0x0030, 0xFE0E, 0x20E3, 0x0}; @@ -453,7 +517,7 @@ @implementation SquirrelTheme enumRange = [format.string rangeOfString:@"%c\u20DD"]; for (NSString *label in labels) { const unichar chars[] = {[label characterAtIndex:0] == 0xFF10 ? 0x24EA : - [label characterAtIndex:0] - 0xFF11 + 0x2460, 0x0}; + [label characterAtIndex:0] - 0xFF11 + 0x2460, 0x0}; NSMutableAttributedString *newFormat = format.mutableCopy; [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithFormat:@"%S", chars]]; @@ -463,7 +527,7 @@ @implementation SquirrelTheme enumRange = [format.string rangeOfString:@"(%c)"]; for (NSString *label in labels) { const unichar chars[] = {[label characterAtIndex:0] == 0xFF10 ? 0x247D : - [label characterAtIndex:0] - 0xFF11 + 0x2474, 0x0}; + [label characterAtIndex:0] - 0xFF11 + 0x2474, 0x0}; NSMutableAttributedString *newFormat = format.mutableCopy; [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithFormat:@"%S", chars]]; @@ -473,8 +537,8 @@ @implementation SquirrelTheme enumRange = [format.string rangeOfString:@"%c."]; for (NSString *label in labels) { const unichar chars[] = {[label characterAtIndex:0] == 0xFF10 ? 0xD83C : - [label characterAtIndex:0] - 0xFF11 + 0x2488, - [label characterAtIndex:0] == 0xFF10 ? 0xDD00 : 0x0, 0x0}; + [label characterAtIndex:0] - 0xFF11 + 0x2488, + [label characterAtIndex:0] == 0xFF10 ? 0xDD00 : 0x0, 0x0}; NSMutableAttributedString *newFormat = format.mutableCopy; [newFormat replaceCharactersInRange:enumRange withString:[NSString stringWithFormat:@"%S", chars]]; @@ -532,6 +596,111 @@ @implementation SquirrelTheme 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]; + paragraphStyle.alignment = NSTextAlignmentLeft; + // Use left-to-right marks to declare the default writing direction and prevent strong right-to-left + // characters from setting the writing direction in case the label are direction-less symbols + paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; + + NSMutableParagraphStyle *preeditParagraphStyle = paragraphStyle.mutableCopy; + NSMutableParagraphStyle *pagingParagraphStyle = paragraphStyle.mutableCopy; + NSMutableParagraphStyle *statusParagraphStyle = paragraphStyle.mutableCopy; + + preeditParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; + statusParagraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; + + NSFont *userFont = [NSFont fontWithDescriptor: + getFontDescriptor([NSFont userFontOfSize:0.0].fontName) + size:kDefaultFontSize]; + NSFont *userMonoFont = [NSFont fontWithDescriptor: + getFontDescriptor([NSFont userFixedPitchFontOfSize:0.0].fontName) + size:kDefaultFontSize]; + NSFont *monoDigitFont = [NSFont monospacedDigitSystemFontOfSize:kDefaultFontSize + weight:NSFontWeightRegular]; + + NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init]; + attrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; + attrs[NSFontAttributeName] = userFont; + // Use left-to-right embedding to prevent right-to-left text from changing the layout of the candidate. + attrs[NSWritingDirectionAttributeName] = @[@(0)]; + + NSMutableDictionary *highlightedAttrs = attrs.mutableCopy; + highlightedAttrs[NSForegroundColorAttributeName] = NSColor.selectedMenuItemTextColor; + + NSMutableDictionary *labelAttrs = attrs.mutableCopy; + labelAttrs[NSForegroundColorAttributeName] = SquirrelTheme.accentColor; + labelAttrs[NSFontAttributeName] = userMonoFont; + + NSMutableDictionary *labelHighlightedAttrs = labelAttrs.mutableCopy; + labelHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.alternateSelectedControlTextColor; + + NSMutableDictionary *commentAttrs = [[NSMutableDictionary alloc] init]; + commentAttrs[NSForegroundColorAttributeName] = SquirrelTheme.secondaryTextColor; + commentAttrs[NSFontAttributeName] = userFont; + + NSMutableDictionary *commentHighlightedAttrs = commentAttrs.mutableCopy; + commentHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.alternateSelectedControlTextColor; + + NSMutableDictionary *preeditAttrs = [[NSMutableDictionary alloc] init]; + preeditAttrs[NSForegroundColorAttributeName] = NSColor.textColor; + preeditAttrs[NSFontAttributeName] = userFont; + preeditAttrs[NSLigatureAttributeName] = @(0); + preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; + + NSMutableDictionary *preeditHighlightedAttrs = preeditAttrs.mutableCopy; + preeditHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.selectedTextColor; + + NSMutableDictionary *pagingAttrs = [[NSMutableDictionary alloc] init]; + pagingAttrs[NSFontAttributeName] = monoDigitFont; + pagingAttrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; + + NSMutableDictionary *pagingHighlightedAttrs = pagingAttrs.mutableCopy; + pagingHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.selectedMenuItemTextColor; + + NSMutableDictionary *statusAttrs = commentAttrs.mutableCopy; + statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; + + [self setAttrs:attrs + highlightedAttrs:highlightedAttrs + labelAttrs:labelAttrs + labelHighlightedAttrs:labelHighlightedAttrs + commentAttrs:commentAttrs + commentHighlightedAttrs:commentHighlightedAttrs + preeditAttrs:preeditAttrs + preeditHighlightedAttrs:preeditHighlightedAttrs + pagingAttrs:pagingAttrs + pagingHighlightedAttrs:pagingHighlightedAttrs + statusAttrs:statusAttrs]; + + [self setParagraphStyle:paragraphStyle + preeditParagraphStyle:preeditParagraphStyle + pagingParagraphStyle:pagingParagraphStyle + statusParagraphStyle:statusParagraphStyle]; + + [self setSelectKeys:@"12345" labels:@[@"1", @"2", @"3", @"4", @"5"] directUpdate:NO]; + [self setCandidateFormat:kDefaultCandidateFormat]; + } + return self; +} + - (void) setBackColor:(NSColor *)backColor highlightedCandidateBackColor:(NSColor *)highlightedCandidateBackColor highlightedPreeditBackColor:(NSColor *)highlightedPreeditBackColor @@ -618,7 +787,6 @@ - (void) setAttrs:(NSDictionary *)attrs @"Symbols/chevron.%@.circle.fill", _linear ? @"up" : @"left"]]; NSMutableDictionary *attrsBackFill = pagingAttrs.mutableCopy; attrsBackFill[NSAttachmentAttributeName] = attmBackFill; - attrsBackFill[NSToolTipAttributeName] = NSLocalizedString(@"page_up", nil); _symbolBackFill = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsBackFill]; @@ -627,7 +795,6 @@ - (void) setAttrs:(NSDictionary *)attrs @"Symbols/chevron.%@.circle", _linear ? @"up" : @"left"]]; NSMutableDictionary *attrsBackStroke = pagingAttrs.mutableCopy; attrsBackStroke[NSAttachmentAttributeName] = attmBackStroke; - attrsBackStroke[NSToolTipAttributeName] = NSLocalizedString(@"home", nil); _symbolBackStroke = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsBackStroke]; @@ -636,7 +803,6 @@ - (void) setAttrs:(NSDictionary *)attrs @"Symbols/chevron.%@.circle.fill", _linear ? @"down" : @"right"]]; NSMutableDictionary *attrsForwardFill = pagingAttrs.mutableCopy; attrsForwardFill[NSAttachmentAttributeName] = attmForwardFill; - attrsForwardFill[NSToolTipAttributeName] = NSLocalizedString(@"page_down", nil); _symbolForwardFill = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsForwardFill]; @@ -645,7 +811,6 @@ - (void) setAttrs:(NSDictionary *)attrs @"Symbols/chevron.%@.circle", _linear ? @"down" : @"right"]]; NSMutableDictionary *attrsForwardStroke = pagingAttrs.mutableCopy; attrsForwardStroke[NSAttachmentAttributeName] = attmForwardStroke; - attrsForwardStroke[NSToolTipAttributeName] = NSLocalizedString(@"end", nil); _symbolForwardStroke = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsForwardStroke]; @@ -653,7 +818,6 @@ - (void) setAttrs:(NSDictionary *)attrs attmDeleteFill.image = [NSImage imageNamed:@"Symbols/delete.backward.fill"]; NSMutableDictionary *attrsDeleteFill = preeditAttrs.mutableCopy; attrsDeleteFill[NSAttachmentAttributeName] = attmDeleteFill; - attrsDeleteFill[NSToolTipAttributeName] = NSLocalizedString(@"delete", nil); attrsDeleteFill[NSVerticalGlyphFormAttributeName] = @(NO); _symbolDeleteFill = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsDeleteFill]; @@ -662,7 +826,6 @@ - (void) setAttrs:(NSDictionary *)attrs attmDeleteStroke.image = [NSImage imageNamed:@"Symbols/delete.backward"]; NSMutableDictionary *attrsDeleteStroke = preeditAttrs.mutableCopy; attrsDeleteStroke[NSAttachmentAttributeName] = attmDeleteStroke; - attrsDeleteStroke[NSToolTipAttributeName] = NSLocalizedString(@"escape", nil); attrsDeleteStroke[NSVerticalGlyphFormAttributeName] = @(NO); _symbolDeleteStroke = [[NSAttributedString alloc] initWithString:attmCharacter attributes:attrsDeleteStroke]; @@ -770,9 +933,7 @@ - (void)setAnnotationHeight:(CGFloat)height { #pragma mark - Typesetting extensions for TextKit 1 (Mac OSX 10.9 to MacOS 11) @interface SquirrelLayoutManager : NSLayoutManager -@end - -@implementation SquirrelLayoutManager +@end @implementation SquirrelLayoutManager - (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)origin { @@ -919,9 +1080,7 @@ - (NSRect) layoutManager:(NSLayoutManager *)layoutManager API_AVAILABLE(macos(12.0)) @interface SquirrelTextLayoutFragment : NSTextLayoutFragment -@end - -@implementation SquirrelTextLayoutFragment +@end @implementation SquirrelTextLayoutFragment - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)context { @@ -954,9 +1113,7 @@ - (void)drawAtPoint:(CGPoint)point API_AVAILABLE(macos(12.0)) @interface SquirrelTextLayoutManager : NSTextLayoutManager -@end - -@implementation SquirrelTextLayoutManager +@end @implementation SquirrelTextLayoutManager - (BOOL) textLayoutManager:(NSTextLayoutManager *)textLayoutManager shouldBreakLineBeforeLocation:(id)location @@ -983,25 +1140,25 @@ - (NSTextLayoutFragment *)textLayoutManager:(NSTextLayoutManager *)textLayoutMan @interface SquirrelView : NSView -@property(nonatomic, readonly) NSTextView *textView; -@property(nonatomic, readonly) NSTextStorage *textStorage; -@property(nonatomic, readonly, strong) SquirrelTheme *currentTheme; -@property(nonatomic, readonly) CAShapeLayer *shape; -@property(nonatomic, readonly) NSRect contentRect; -@property(nonatomic, readonly) NSRect preeditBlock; -@property(nonatomic, readonly) NSRect candidateBlock; -@property(nonatomic, readonly) NSRect pagingBlock; -@property(nonatomic, readonly) SquirrelAppear appear; -@property(nonatomic, readonly) NSEdgeInsets alignmentRectInsets; -@property(nonatomic, readonly) NSArray *candidateRanges; +@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 *deleteBackPath; +@property(nonatomic, strong, readonly) NSArray *candidateRanges; @property(nonatomic, readonly) NSRange preeditRange; @property(nonatomic, readonly) NSRange highlightedPreeditRange; @property(nonatomic, readonly) NSRange pagingRange; @property(nonatomic, readonly) NSUInteger highlightedIndex; @property(nonatomic, readonly) SquirrelIndex functionButton; -@property(nonatomic, readonly) NSBezierPath *deleteBackPath; -@property(nonatomic, readonly) NSMutableArray *candidatePaths; -@property(nonatomic, readonly) NSMutableArray *pagingPaths; +@property(nonatomic, readonly) NSRect contentRect; +@property(nonatomic, readonly) NSRect preeditBlock; +@property(nonatomic, readonly) NSRect candidateBlock; +@property(nonatomic, readonly) NSRect pagingBlock; +@property(nonatomic, readonly) NSEdgeInsets alignmentRectInsets; +@property(nonatomic, readonly) SquirrelAppear appear; - (NSTextRange *)getTextRangeFromCharRange:(NSRange)charRange API_AVAILABLE(macos(12.0)); @@ -1023,7 +1180,7 @@ - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets - (void)highlightFunctionButton:(SquirrelIndex)functionButton; -- (NSUInteger)getIndexFromClickSpot:(NSPoint)spot; +- (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot; @end @@ -1056,10 +1213,6 @@ - (SquirrelAppear)appear { return defaultAppear; } -- (BOOL)allowsVibrancy { - return YES; -} - - (SquirrelTheme *)selectTheme:(SquirrelAppear)appear { return appear == darkAppear ? _darkTheme : _defaultTheme; } @@ -1074,46 +1227,47 @@ - (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]; + textLayoutManager.usesFontLeading = NO; + textLayoutManager.usesHyphenation = NO; + textLayoutManager.delegate = textLayoutManager; + NSTextContainer *textContainer = [[NSTextContainer alloc] + initWithSize:NSZeroSize]; + textContainer.lineFragmentPadding = 0; + textLayoutManager.textContainer = textContainer; + NSTextContentStorage *contentStorage = [[NSTextContentStorage alloc] init]; + [contentStorage addTextLayoutManager:textLayoutManager]; + _textView = [[NSTextView alloc] initWithFrame:frameRect + textContainer:textContainer]; + _textStorage = _textView.textContentStorage.textStorage; + } else { + SquirrelLayoutManager *layoutManager = [[SquirrelLayoutManager alloc] init]; + layoutManager.backgroundLayoutEnabled = YES; + layoutManager.usesFontLeading = NO; + layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; + layoutManager.delegate = layoutManager; + NSTextContainer *textContainer = [[NSTextContainer alloc] + initWithContainerSize:NSZeroSize]; + textContainer.lineFragmentPadding = 0; + [layoutManager addTextContainer:textContainer]; + _textStorage = [[NSTextStorage alloc] init]; + [_textStorage addLayoutManager:layoutManager]; + _textView = [[NSTextView alloc] initWithFrame:frameRect + textContainer:textContainer]; + } + _textView.drawsBackground = NO; + _textView.editable = NO; + _textView.selectable = NO; + _textView.wantsLayer = NO; - if (@available(macOS 12.0, *)) { - SquirrelTextLayoutManager *textLayoutManager = [[SquirrelTextLayoutManager alloc] init]; - textLayoutManager.usesFontLeading = NO; - textLayoutManager.usesHyphenation = NO; - textLayoutManager.delegate = textLayoutManager; - NSTextContainer *textContainer = [[NSTextContainer alloc] - initWithSize:NSZeroSize]; - textContainer.lineFragmentPadding = 0; - textLayoutManager.textContainer = textContainer; - NSTextContentStorage *contentStorage = [[NSTextContentStorage alloc] init]; - [contentStorage addTextLayoutManager:textLayoutManager]; - _textView = [[NSTextView alloc] initWithFrame:frameRect - textContainer:textContainer]; - _textStorage = _textView.textContentStorage.textStorage; - } else { - SquirrelLayoutManager *layoutManager = [[SquirrelLayoutManager alloc] init]; - layoutManager.backgroundLayoutEnabled = YES; - layoutManager.usesFontLeading = NO; - layoutManager.typesetterBehavior = NSTypesetterLatestBehavior; - layoutManager.delegate = layoutManager; - NSTextContainer *textContainer = [[NSTextContainer alloc] - initWithContainerSize:NSZeroSize]; - textContainer.lineFragmentPadding = 0; - [layoutManager addTextContainer:textContainer]; - _textStorage = [[NSTextStorage alloc] init]; - [_textStorage addLayoutManager:layoutManager]; - _textView = [[NSTextView alloc] initWithFrame:frameRect - textContainer:textContainer]; - } - _textView.drawsBackground = NO; - _textView.editable = NO; - _textView.selectable = NO; - _textView.wantsLayer = NO; - - _shape = [[CAShapeLayer alloc] init]; - _defaultTheme = [[SquirrelTheme alloc] init]; - if (@available(macOS 10.14, *)) { - _darkTheme = [[SquirrelTheme alloc] init]; + _shape = [[CAShapeLayer alloc] init]; + _defaultTheme = [[SquirrelTheme alloc] init]; + if (@available(macOS 10.14, *)) { + _darkTheme = [[SquirrelTheme alloc] init]; + } } return self; } @@ -1342,7 +1496,6 @@ - (void)drawViewWithInsets:(NSEdgeInsets)alignmentRectInsets _candidatePaths = [[NSMutableArray alloc] initWithCapacity:candidateRanges.count]; _pagingPaths = [[NSMutableArray alloc] initWithCapacity:pagingRange.length > 0 ? 2 : 0]; _functionButton = kVoidSymbol; - [self removeAllToolTips]; // 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]; @@ -1528,10 +1681,13 @@ - (void)updateLayer { NSRect panelRect = self.bounds; NSRect backgroundRect = [self backingAlignedRect:NSInsetRect(panelRect, theme.borderInset.width, theme.borderInset.height) options:NSAlignAllEdgesNearest]; - NSBezierPath *panelPath = squirclePath(rectVertex(panelRect), - MIN(theme.cornerRadius, NSHeight(panelRect) * 0.5)); - NSBezierPath *backgroundPath = squirclePath(rectVertex(backgroundRect), - MIN(theme.highlightedCornerRadius, NSHeight(backgroundRect) * 0.5)); + CGFloat outerCornerRadius = MIN(theme.cornerRadius, NSHeight(panelRect) * 0.5); + CGFloat innerCornerRadius = MAX(MIN(theme.highlightedCornerRadius, NSHeight(backgroundRect) * 0.5), + outerCornerRadius - MIN(theme.borderInset.width, theme.borderInset.height)); + NSBezierPath *panelPath = squirclePath(rectVertex(panelRect), outerCornerRadius); + NSBezierPath *backgroundPath = squirclePath(rectVertex(backgroundRect), innerCornerRadius); + NSBezierPath *borderPath = [panelPath copy]; + [borderPath appendBezierPath:backgroundPath]; NSRange visibleRange; if (@available(macOS 12.0, *)) { @@ -1616,9 +1772,6 @@ - (void)updateLayer { deleteBackRect = [self backingAlignedRect:NSIntersectionRect(deleteBackRect, _preeditBlock) options:NSAlignAllEdgesNearest]; _deleteBackPath = squirclePath(rectVertex(deleteBackRect), cornerRadius); - [self addToolTipRect:deleteBackRect owner:[_textStorage attribute:NSToolTipAttributeName - atIndex:NSMaxRange(_preeditRange) - 1 - effectiveRange:NULL] userData:nil]; } // Draw candidate Rect @@ -1718,15 +1871,6 @@ - (void)updateLayer { options:NSAlignAllEdgesNearest]; } } - NSRange labelRange; - NSString *toolTip = [_textStorage attribute:NSToolTipAttributeName - atIndex:candidateRange.location - effectiveRange:&labelRange]; - NSRect labelRect = [self blockRectForRange:labelRange]; - labelRect.origin = NSIsEmptyRect(leadingRect) ? bodyRect.origin : leadingRect.origin; - labelRect.size.width += theme.separatorWidth; - labelRect.size.height += ceil(theme.linespace * 0.5); - [self addToolTipRect:labelRect owner:toolTip userData:nil]; NSBezierPath *candidatePath; // Handles the special case where containing boxes are separated @@ -1753,16 +1897,6 @@ - (void)updateLayer { candidateRect = [self backingAlignedRect:NSIntersectionRect(candidateRect, _candidateBlock) options:NSAlignAllEdgesNearest]; _candidatePaths[i] = squirclePath(rectVertex(candidateRect), cornerRadius); - - CGFloat indent = [[_textStorage attribute:NSParagraphStyleAttributeName - atIndex:candidateRange.location - effectiveRange:NULL] headIndent]; - NSString *toolTip = [_textStorage attribute:NSToolTipAttributeName - atIndex:candidateRange.location - effectiveRange:NULL]; - NSRect labelRect = {candidateRect.origin, - {indent + theme.separatorWidth * 0.5, candidateRect.size.height}}; - [self addToolTipRect:labelRect owner:toolTip userData:nil]; } } } @@ -1801,40 +1935,19 @@ - (void)updateLayer { MIN(NSWidth(pageDownRect), NSHeight(pageDownRect)) * 0.5); _pagingPaths[0] = squirclePath(rectVertex(pageUpRect), cornerRadius); _pagingPaths[1] = squirclePath(rectVertex(pageDownRect), cornerRadius); - [self addToolTipRect:pageUpRect owner:[_textStorage attribute:NSToolTipAttributeName - atIndex:pagingRange.location - effectiveRange:NULL] userData:nil]; - [self addToolTipRect:pageDownRect owner:[_textStorage attribute:NSToolTipAttributeName - atIndex:NSMaxRange(pagingRange) - 1 - effectiveRange:NULL] userData:nil]; - NSRect pageNumRect = NSMakeRect(NSMinX(pageUpRect), - NSMinY(pageUpRect), - NSMinX(pageDownRect) - NSMaxX(pageUpRect), - NSHeight(pageDownRect)); - [self addToolTipRect:pageNumRect owner:[_textStorage attribute:NSToolTipAttributeName - atIndex:pagingRange.location + 2 - effectiveRange:NULL] userData:nil]; } // Set layers - CIFilter *sourceOutFilter = [CIFilter filterWithName:@"CISourceOutCompositing"]; - [sourceOutFilter setDefaults]; _shape.path = panelPath.quartzPath; _shape.fillColor = NSColor.whiteColor.CGColor; self.layer.sublayers = nil; - CAShapeLayer *panelLayer = [[CAShapeLayer alloc] init]; - // border layer - BOOL drawBorders = !NSEqualSizes(theme.borderInset, NSZeroSize) && theme.borderColor; - panelLayer.path = panelPath.quartzPath; - panelLayer.fillColor = drawBorders ? theme.borderColor.CGColor : nil; + // layers of large background elements + CALayer *BackLayers = [[CALayer alloc] init]; if (@available(macOS 10.14, *)) { - panelLayer.opacity = 1.0f - (float)theme.translucency; + BackLayers.opacity = 1.0f - (float)theme.translucency; + BackLayers.allowsGroupOpacity = YES; } - [self.layer addSublayer:panelLayer]; - // background color layer - CAShapeLayer *backgroundLayer = [[CAShapeLayer alloc] init]; - backgroundLayer.path = backgroundPath.quartzPath; - backgroundLayer.fillColor = theme.backColor.CGColor; + [self.layer addSublayer:BackLayers]; // background image (pattern style) layer if (theme.backImage.valid) { CAShapeLayer *backImageLayer = [[CAShapeLayer alloc] init]; @@ -1846,73 +1959,103 @@ - (void)updateLayer { (backgroundPath.quartzPath, &transform)); backImageLayer.fillColor = [NSColor colorWithPatternImage:theme.backImage].CGColor; backImageLayer.affineTransform = CGAffineTransformInvert(transform); - backImageLayer.backgroundFilters = drawBorders ? @[sourceOutFilter] : nil; - [panelLayer addSublayer:backImageLayer]; - } else if (drawBorders) { - backgroundLayer.backgroundFilters = @[sourceOutFilter]; + [BackLayers addSublayer:backImageLayer]; } - [panelLayer addSublayer:backgroundLayer]; - if ((_preeditRange.length > 0 || (!theme.linear && _pagingRange.length > 0)) && + // background color layer + CAShapeLayer *backColorLayer = [[CAShapeLayer alloc] init]; + if ((!NSIsEmptyRect(_preeditBlock) || !NSIsEmptyRect(_pagingBlock)) && theme.preeditBackColor) { - backgroundLayer.fillColor = theme.preeditBackColor.CGColor; if (candidateBlockPath) { + NSBezierPath *nonCandidatePath = [backgroundPath copy]; + [nonCandidatePath appendBezierPath:candidateBlockPath]; + backColorLayer.path = nonCandidatePath.quartzPath; + backColorLayer.fillRule = kCAFillRuleEvenOdd; + backColorLayer.strokeColor = theme.preeditBackColor.CGColor; + backColorLayer.lineWidth = 0.5; + backColorLayer.fillColor = theme.preeditBackColor.CGColor; + [BackLayers addSublayer:backColorLayer]; + // candidate block's background color layer CAShapeLayer *candidateLayer = [[CAShapeLayer alloc] init]; candidateLayer.path = candidateBlockPath.quartzPath; candidateLayer.fillColor = theme.backColor.CGColor; - candidateLayer.backgroundFilters = @[sourceOutFilter]; - [backgroundLayer addSublayer:candidateLayer]; + [BackLayers addSublayer:candidateLayer]; + } else { + backColorLayer.path = backgroundPath.quartzPath; + backColorLayer.strokeColor = theme.preeditBackColor.CGColor; + backColorLayer.lineWidth = 0.5; + backColorLayer.fillColor = theme.preeditBackColor.CGColor; + [BackLayers addSublayer:backColorLayer]; } + } else { + backColorLayer.path = backgroundPath.quartzPath; + backColorLayer.strokeColor = theme.backColor.CGColor; + backColorLayer.lineWidth = 0.5; + 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; + borderLayer.fillRule = kCAFillRuleEvenOdd; + borderLayer.fillColor = (theme.borderColor ? : theme.backColor).CGColor; + [BackLayers addSublayer:borderLayer]; + // layers of small highlighting elements + CALayer *ForeLayers = [[CALayer alloc] init]; + CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; + maskLayer.path = backgroundPath.quartzPath; + maskLayer.fillColor = NSColor.whiteColor.CGColor; + ForeLayers.mask = maskLayer; + [self.layer addSublayer:ForeLayers]; // highlighted preedit layer if (highlightedPreeditPath && theme.highlightedPreeditBackColor) { CAShapeLayer *highlightedPreeditLayer = [[CAShapeLayer alloc] init]; highlightedPreeditLayer.path = highlightedPreeditPath.quartzPath; highlightedPreeditLayer.fillColor = theme.highlightedPreeditBackColor.CGColor; - [self.layer addSublayer:highlightedPreeditLayer]; + [ForeLayers addSublayer:highlightedPreeditLayer]; } // highlighted candidate layer if (_highlightedIndex < _candidatePaths.count && theme.highlightedCandidateBackColor) { CAShapeLayer *highlightedCandidateLayer = [[CAShapeLayer alloc] init]; highlightedCandidateLayer.path = _candidatePaths[_highlightedIndex].quartzPath; highlightedCandidateLayer.fillColor = theme.highlightedCandidateBackColor.CGColor; - [self.layer addSublayer:highlightedCandidateLayer]; + [ForeLayers addSublayer:highlightedCandidateLayer]; } // function buttons (page up, page down, backspace) layer if (_functionButton != kVoidSymbol) { CAShapeLayer *functionButtonLayer = [self getFunctionButtonLayer]; if (functionButtonLayer) { - [self.layer addSublayer:functionButtonLayer]; + [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]; - [panelLayer addSublayer:gridLayer]; - } // logo at the beginning for status message - if (preeditRange.length == 0 && candidateBlockRange.length == 0) { + if (NSIsEmptyRect(_preeditBlock) && NSIsEmptyRect(_candidateBlock)) { CALayer *logoLayer = [[CALayer alloc] init]; CGFloat height = [theme.statusAttrs[NSParagraphStyleAttributeName] minimumLineHeight]; - NSRect logoRect = NSMakeRect(backgroundRect.origin.x, - backgroundRect.origin.y, - height, height); + NSRect logoRect = NSMakeRect(backgroundRect.origin.x, backgroundRect.origin.y, height, height); logoLayer.frame = [self backingAlignedRect:NSInsetRect(logoRect, -0.1 * height, -0.1 * height) options:NSAlignAllEdgesNearest]; NSImage *logoImage = [NSImage imageNamed:NSImageNameApplicationIcon]; logoImage.size = logoRect.size; - CGFloat scaleFactor = [logoImage recommendedLayerContentsScale:[self.window backingScaleFactor]]; + CGFloat scaleFactor = [logoImage recommendedLayerContentsScale: + self.window.backingScaleFactor]; logoLayer.contents = logoImage; logoLayer.contentsScale = scaleFactor; - logoLayer.affineTransform = theme.vertical ? CGAffineTransformMakeRotation(-M_PI_2) : CGAffineTransformIdentity; - [self.layer addSublayer:logoLayer]; + logoLayer.affineTransform = theme.vertical ? CGAffineTransformMakeRotation(-M_PI_2) + : CGAffineTransformIdentity; + [ForeLayers addSublayer:logoLayer]; } } -- (NSUInteger)getIndexFromClickSpot:(NSPoint)spot { +- (NSUInteger)getIndexFromMouseSpot:(NSPoint)spot { NSPoint point = [self convertPoint:spot fromView:nil]; if (NSPointInRect(point, self.bounds)) { if (NSPointInRect(point, _preeditBlock)) { @@ -1937,12 +2080,107 @@ - (NSUInteger)getIndexFromClickSpot:(NSPoint)spot { @end // SquirrelView + +@interface SquirrelToolTip : NSWindow + +@property(nonatomic, weak, readonly) SquirrelPanel *panel; + +@end + +@implementation SquirrelToolTip { + NSVisualEffectView *_backView; + NSTextField *_textView; + NSTimer *_displayTimer; +} + +- (instancetype)initWithPanel:(SquirrelPanel *)panel { + self = [super initWithContentRect:NSZeroRect + styleMask:NSWindowStyleMaskNonactivatingPanel + backing:NSBackingStoreBuffered + defer:YES]; + if (self) { + _panel = panel; + self.level = panel.level + 1; + self.appearanceSource = panel; + self.backgroundColor = NSColor.clearColor; + self.opaque = YES; + self.hasShadow = YES; + NSView *contentView = [[NSView alloc] init]; + _backView = [[NSVisualEffectView alloc] init]; + _backView.material = NSVisualEffectMaterialToolTip; + [contentView addSubview:_backView]; + _textView = [[NSTextField alloc] init]; + _textView.bezeled = YES; + _textView.bezelStyle = NSTextFieldSquareBezel; + _textView.selectable = NO; + [contentView addSubview:_textView]; + self.contentView = contentView; + } + return self; +} + +- (void)showWithToolTip:(NSString *)toolTip { + if (toolTip.length == 0) { + [self hide]; + return; + } + + _textView.stringValue = toolTip; + _textView.font = [NSFont toolTipsFontOfSize:0]; + _textView.textColor = NSColor.windowFrameTextColor; + [_textView sizeToFit]; + NSSize contentSize = _textView.fittingSize; + + NSPoint spot = NSEvent.mouseLocation; + NSCursor *cursor = NSCursor.currentSystemCursor; + spot.x += cursor.image.size.width - cursor.hotSpot.x; + spot.y -= cursor.image.size.height - cursor.hotSpot.y; + NSRect windowRect = NSMakeRect(spot.x, spot.y - contentSize.height, contentSize.width, contentSize.height); + + NSRect screenRect = _panel.screen.visibleFrame; + if (NSMaxX(windowRect) > NSMaxX(screenRect)) { + windowRect.origin.x = NSMaxX(screenRect) - NSWidth(windowRect); + } + if (NSMinY(windowRect) < NSMinY(screenRect)) { + windowRect.origin.y = NSMinY(screenRect); + } + [self setFrame:[_panel.screen backingAlignedRect:windowRect + options:NSAlignAllEdgesNearest] + display:NO]; + _textView.frame = self.contentView.bounds; + _backView.frame = self.contentView.bounds; + + _displayTimer = [NSTimer scheduledTimerWithTimeInterval:kShowStatusDuration + target:self + selector:@selector(delayedDisplay:) + userInfo:nil + repeats:NO]; +} + +- (void)delayedDisplay:(NSTimer *)timer { + [self display]; + [self orderFrontRegardless]; +} + +- (void)hide { + if (_displayTimer) { + [_displayTimer invalidate]; + _displayTimer = nil; + } + if (self.visible) { + [self orderOut:nil]; + } +} + +@end // SquirrelToolTipView + #pragma mark - Panel window, dealing with text content and mouse interactions @implementation SquirrelPanel { SquirrelView *_view; NSVisualEffectView *_back; NSScreen *_screen; + SquirrelToolTip *_toolTip; NSSize _maxSize; CGFloat _textWidthLimit; @@ -1979,135 +2217,23 @@ - (BOOL)inlineCandidate { return _view.currentTheme.inlineCandidate; } -+ (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]; - } -} - - (SquirrelInputController *)inputController { return SquirrelInputController.currentController; } -- (void)initializeUIStyleForAppearance:(SquirrelAppear)appear { - SquirrelTheme *theme = [_view selectTheme:appear]; - - NSMutableParagraphStyle *preeditParagraphStyle = [[NSMutableParagraphStyle alloc] init]; - NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; - NSMutableParagraphStyle *pagingParagraphStyle = [[NSMutableParagraphStyle alloc] init]; - NSMutableParagraphStyle *statusParagraphStyle = [[NSMutableParagraphStyle alloc] init]; - - preeditParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; - statusParagraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; - - preeditParagraphStyle.alignment = NSTextAlignmentLeft; - paragraphStyle.alignment = NSTextAlignmentLeft; - pagingParagraphStyle.alignment = NSTextAlignmentLeft; - statusParagraphStyle.alignment = NSTextAlignmentLeft; - - // Use left-to-right marks to declare the default writing direction and prevent strong right-to-left - // characters from setting the writing direction in case the label are direction-less symbols - preeditParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; - paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; - pagingParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; - statusParagraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; - - NSColor *secondaryTextColor = SquirrelPanel.secondaryTextColor; - NSColor *accentColor = SquirrelPanel.accentColor; - NSFont *userFont = [NSFont fontWithDescriptor:getFontDescriptor([NSFont userFontOfSize:0.0].fontName) - size:kDefaultFontSize]; - NSFont *userMonoFont = [NSFont fontWithDescriptor:getFontDescriptor([NSFont userFixedPitchFontOfSize:0.0].fontName) - size:kDefaultFontSize]; - NSFont *monoDigitFont = [NSFont monospacedDigitSystemFontOfSize:kDefaultFontSize - weight:NSFontWeightRegular]; - - NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init]; - attrs[NSForegroundColorAttributeName] = NSColor.controlTextColor; - attrs[NSFontAttributeName] = userFont; - // Use left-to-right embedding to prevent right-to-left text from changing the layout of the candidate. - attrs[NSWritingDirectionAttributeName] = @[@(0)]; - - NSMutableDictionary *highlightedAttrs = attrs.mutableCopy; - highlightedAttrs[NSForegroundColorAttributeName] = NSColor.selectedMenuItemTextColor; - - NSMutableDictionary *labelAttrs = attrs.mutableCopy; - labelAttrs[NSForegroundColorAttributeName] = accentColor; - labelAttrs[NSFontAttributeName] = userMonoFont; - - NSMutableDictionary *labelHighlightedAttrs = labelAttrs.mutableCopy; - labelHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.alternateSelectedControlTextColor; - - NSMutableDictionary *commentAttrs = [[NSMutableDictionary alloc] init]; - commentAttrs[NSForegroundColorAttributeName] = secondaryTextColor; - commentAttrs[NSFontAttributeName] = userFont; - - NSMutableDictionary *commentHighlightedAttrs = commentAttrs.mutableCopy; - commentHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.alternateSelectedControlTextColor; - - NSMutableDictionary *preeditAttrs = [[NSMutableDictionary alloc] init]; - preeditAttrs[NSForegroundColorAttributeName] = NSColor.textColor; - preeditAttrs[NSFontAttributeName] = userFont; - preeditAttrs[NSLigatureAttributeName] = @(0); - preeditAttrs[NSParagraphStyleAttributeName] = preeditParagraphStyle; - - NSMutableDictionary *preeditHighlightedAttrs = preeditAttrs.mutableCopy; - preeditHighlightedAttrs[NSForegroundColorAttributeName] = NSColor.selectedTextColor; - - NSMutableDictionary *pagingAttrs = [[NSMutableDictionary alloc] init]; - pagingAttrs[NSFontAttributeName] = theme.linear ? userMonoFont : monoDigitFont; - pagingAttrs[NSForegroundColorAttributeName] = theme.linear ? accentColor : NSColor.controlTextColor; - - NSMutableDictionary *pagingHighlightedAttrs = pagingAttrs.mutableCopy; - pagingHighlightedAttrs[NSForegroundColorAttributeName] = theme.linear ? - NSColor.alternateSelectedControlTextColor : NSColor.selectedMenuItemTextColor; - - NSMutableDictionary *statusAttrs = commentAttrs.mutableCopy; - statusAttrs[NSParagraphStyleAttributeName] = statusParagraphStyle; - - [theme setAttrs:attrs - highlightedAttrs:highlightedAttrs - labelAttrs:labelAttrs - labelHighlightedAttrs:labelHighlightedAttrs - commentAttrs:commentAttrs - commentHighlightedAttrs:commentHighlightedAttrs - preeditAttrs:preeditAttrs - preeditHighlightedAttrs:preeditHighlightedAttrs - pagingAttrs:pagingAttrs - pagingHighlightedAttrs:pagingHighlightedAttrs - statusAttrs:statusAttrs]; - - [theme setParagraphStyle:paragraphStyle - preeditParagraphStyle:preeditParagraphStyle - pagingParagraphStyle:pagingParagraphStyle - statusParagraphStyle:statusParagraphStyle]; - - [theme setSelectKeys:@"12345" labels:@[@"1", @"2", @"3", @"4", @"5"] directUpdate:NO]; - [theme setCandidateFormat:kDefaultCandidateFormat]; -} - - (instancetype)init { self = [super initWithContentRect:_IbeamRect styleMask:NSWindowStyleMaskNonactivatingPanel|NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES]; if (self) { - self.level = CGWindowLevelForKey(kCGAssistiveTechHighWindowLevel) + 10; + self.level = CGWindowLevelForKey(kCGCursorWindowLevelKey) - 100; self.alphaValue = 1.0; self.hasShadow = NO; self.opaque = NO; self.backgroundColor = NSColor.clearColor; self.delegate = self; - self.allowsToolTipsWhenApplicationIsInactive = YES; + self.acceptsMouseMovedEvents = YES; NSView *contentView = [[NSView alloc] init]; _view = [[SquirrelView alloc] initWithFrame:self.contentView.bounds]; @@ -2126,10 +2252,7 @@ - (instancetype)init { self.contentView = contentView; [self updateDisplayParameters]; - [self initializeUIStyleForAppearance:defaultAppear]; - if (@available(macOS 10.14, *)) { - [self initializeUIStyleForAppearance:darkAppear]; - } + _toolTip = [[SquirrelToolTip alloc] initWithPanel:self]; } return self; } @@ -2166,14 +2289,14 @@ - (void)sendEvent:(NSEvent *)event { } break; case NSEventTypeLeftMouseUp: - cursorIndex = [_view getIndexFromClickSpot:self.mouseLocationOutsideOfEventStream]; + cursorIndex = [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; if (event.clickCount == 1 && cursorIndex != NSNotFound && (cursorIndex == _highlightedIndex || cursorIndex == _functionButton)) { [self.inputController perform:kSELECT onIndex:cursorIndex]; } break; case NSEventTypeRightMouseUp: - cursorIndex = [_view getIndexFromClickSpot:self.mouseLocationOutsideOfEventStream]; + cursorIndex = [_view getIndexFromMouseSpot:self.mouseLocationOutsideOfEventStream]; if (event.clickCount == 1 && cursorIndex != NSNotFound) { if (cursorIndex == _highlightedIndex) { [self.inputController perform:kDELETE onIndex:cursorIndex]; @@ -2193,9 +2316,13 @@ - (void)sendEvent:(NSEvent *)event { } break; case NSEventTypeMouseMoved: - cursorIndex = [_view getIndexFromClickSpot:self.mouseLocationOutsideOfEventStream]; + 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 || @@ -2213,6 +2340,7 @@ - (void)sendEvent:(NSEvent *)event { range:NSMakeRange(NSMaxRange(_view.preeditRange) - 1, 1)]; } cursorIndex = _firstPage ? kHome : kPageUp; + [_toolTip showWithToolTip:NSLocalizedString(_firstPage ? @"home" : @"page_up", nil)]; break; case kPageDown: [_view.textStorage addAttributes:theme.pagingAttrs @@ -2224,6 +2352,7 @@ - (void)sendEvent:(NSEvent *)event { 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 @@ -2235,6 +2364,7 @@ - (void)sendEvent:(NSEvent *)event { range:NSMakeRange(NSMaxRange(_view.pagingRange) - 1, 1)]; } cursorIndex = _caretAtHome ? kEscape : kBackSpace; + [_toolTip showWithToolTip:NSLocalizedString(_caretAtHome ? @"escape" : @"delete", nil)]; break; } [_view highlightFunctionButton:cursorIndex]; @@ -2503,7 +2633,6 @@ - (void)show { if (theme.translucency > 0) { [_back setBoundsOrigin:NSZeroPoint]; _back.frame = viewRect; - _back.appearance = self.effectiveAppearance; _back.hidden = NO; } else { _back.hidden = YES; @@ -2521,6 +2650,7 @@ - (void)hide { [_statusTimer invalidate]; _statusTimer = nil; } + [_toolTip hide]; [self orderOut:nil]; _maxSize = NSZeroSize; _initPosition = YES; @@ -2591,6 +2721,45 @@ - (BOOL)shouldUseTabInRange:(NSRange)range } } +- (NSMutableAttributedString *)getPageNumString:(NSUInteger)pageNum { + SquirrelTheme *theme = _view.currentTheme; + if (!theme.vertical) { + return [[NSMutableAttributedString alloc] + initWithString:[NSString stringWithFormat:@" %lu ", pageNum + 1] + attributes:theme.pagingAttrs]; + } + NSAttributedString *pageNumString =[[NSAttributedString alloc] + initWithString:[NSString stringWithFormat:@"%lu", pageNum + 1] + attributes:theme.pagingAttrs]; + NSMutableDictionary *pageNumAttrs = [theme.pagingAttrs mutableCopy]; + NSFont *font = pageNumAttrs[NSFontAttributeName]; + CGFloat lineHeight = (theme.linear ? theme.paragraphStyle : theme.pagingParagraphStyle).minimumLineHeight; + CGFloat width = MAX(lineHeight, pageNumString.size.width); + NSImage *pageNumImage = [NSImage imageWithSize:NSMakeSize(lineHeight, width) + flipped:YES + drawingHandler:^BOOL(NSRect dstRect) { + CGContextRef context = NSGraphicsContext.currentContext.CGContext; + CGContextSaveGState(context); + CGContextTranslateCTM(context, lineHeight * 0.5 + font.ascender * 0.5 + font.descender * 0.5, width); + CGContextRotateCTM(context, -M_PI_2); + [pageNumString drawAtPoint:NSMakePoint(width * 0.5 - pageNumString.size.width * 0.5, -font.ascender)]; + CGContextRestoreGState(context); + return YES; + }]; + pageNumImage.resizingMode = NSImageResizingModeStretch; + pageNumImage.size = NSMakeSize(lineHeight, lineHeight); + 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]; + [attmString addAttribute:NSAttachmentAttributeName + value:pageNumAttm + range:NSMakeRange(1, 1)]; + return attmString; +} + // Main function to add attributes to text output from librime - (void)showPreedit:(NSString *)preedit selRange:(NSRange)selRange @@ -2795,9 +2964,9 @@ - (void)showPreedit:(NSString *)preedit // paging indication if (theme.showPaging) { - NSMutableAttributedString *paging = [[NSMutableAttributedString alloc] initWithString: - [NSString stringWithFormat:@" %lu ", pageNum + 1] attributes:theme.pagingAttrs]; - [paging insertAttributedString:pageNum > 0 ? theme.symbolBackFill : theme.symbolBackStroke atIndex:0]; + NSMutableAttributedString *paging = [self getPageNumString:pageNum]; + [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; @@ -2942,78 +3111,6 @@ - (void)hideStatus:(NSTimer *)timer { [self hide]; } -static inline NSColor *blendColors(NSColor *foregroundColor, - NSColor *backgroundColor) { - return [[foregroundColor blendedColorWithFraction:kBlendedBackgroundColorFraction - ofColor:backgroundColor ? : NSColor.lightGrayColor] - colorWithAlphaComponent:foregroundColor.alphaComponent]; -} - -static NSFontDescriptor *getFontDescriptor(NSString *fullname) { - if (fullname.length == 0) { - return nil; - } - NSArray *fontNames = [fullname componentsSeparatedByString:@","]; - NSMutableArray *validFontDescriptors = [[NSMutableArray alloc] - initWithCapacity:fontNames.count]; - for (NSString *fontName in fontNames) { - NSFont *font = [NSFont fontWithName:[fontName stringByTrimmingCharactersInSet: - NSCharacterSet.whitespaceAndNewlineCharacterSet] size:0.0]; - if (font != nil) { - // If the font name is not valid, NSFontDescriptor will still create something for us. - // However, when we draw the actual text, Squirrel will crash if there is any font descriptor - // with invalid font name. - NSFontDescriptor *fontDescriptor = font.fontDescriptor; - NSFontDescriptor *UIFontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits: - NSFontDescriptorTraitUIOptimized]; - [validFontDescriptors addObject:[NSFont fontWithDescriptor:UIFontDescriptor size:0.0] != nil ? - UIFontDescriptor : fontDescriptor]; - } - } - if (validFontDescriptors.count == 0) { - return nil; - } - NSFontDescriptor *initialFontDescriptor = validFontDescriptors[0]; - NSFontDescriptor *emojiFontDescriptor = - [NSFontDescriptor fontDescriptorWithName:@"AppleColorEmoji" size:0.0]; - NSArray *fallbackDescriptors = [[validFontDescriptors subarrayWithRange: - NSMakeRange(1, validFontDescriptors.count - 1)] - arrayByAddingObject:emojiFontDescriptor]; - return [initialFontDescriptor fontDescriptorByAddingAttributes: - @{NSFontCascadeListAttribute:fallbackDescriptors}]; -} - -static CGFloat getLineHeight(NSFont *font, BOOL vertical) { - if (vertical) { - font = font.verticalFont; - } - CGFloat lineHeight = ceil(font.ascender - font.descender); - NSArray *fallbackList = [font.fontDescriptor - objectForKey:NSFontCascadeListAttribute]; - for (NSFontDescriptor *fallback in fallbackList) { - NSFont *fallbackFont = [NSFont fontWithDescriptor:fallback - size:font.pointSize]; - if (vertical) { - fallbackFont = fallbackFont.verticalFont; - } - lineHeight = MAX(lineHeight, ceil(fallbackFont.ascender - fallbackFont.descender)); - } - return lineHeight; -} - -static NSFont *getTallestFont(NSArray *fonts, BOOL vertical) { - NSFont *tallestFont; - CGFloat maxHeight = 0.0; - for (NSFont *font in fonts) { - CGFloat fontHeight = getLineHeight(font, vertical); - if (fontHeight > maxHeight) { - tallestFont = font; - maxHeight = fontHeight; - } - } - return tallestFont; -} - static void updateCandidateListLayout(BOOL *isLinear, BOOL *isTabled, SquirrelConfig *config, NSString *prefix) { NSString *candidateListLayout = [config getString: @@ -3442,12 +3539,7 @@ + (void)updateTheme:(SquirrelTheme *)theme pagingAttrs[NSVerticalGlyphFormAttributeName] = @(NO); pagingHighlightedAttrs[NSVerticalGlyphFormAttributeName] = @(NO); - labelAttrs[NSToolTipAttributeName] = NSLocalizedString(@"candidate", nil); - labelHighlightedAttrs[NSToolTipAttributeName] = NSLocalizedString(@"candidate", nil); - // CHROMATICS refinement - NSColor *secondaryTextColor = SquirrelPanel.secondaryTextColor; - NSColor *accentColor = SquirrelPanel.accentColor; translucency = translucency ? : @(0.0); if (@available(macOS 10.14, *)) { if (translucency.doubleValue > 0 && !isNative && backColor != nil && @@ -3474,8 +3566,8 @@ + (void)updateTheme:(SquirrelTheme *)theme preeditBackColor = preeditBackColor ? : isNative ? NSColor.windowBackgroundColor : nil; textColor = textColor ? : NSColor.textColor; candidateTextColor = candidateTextColor ? : NSColor.controlTextColor; - commentTextColor = commentTextColor ? : secondaryTextColor; - candidateLabelColor = candidateLabelColor ? : isNative ? accentColor : blendColors(candidateTextColor, backColor); + commentTextColor = commentTextColor ? : SquirrelTheme.secondaryTextColor; + candidateLabelColor = candidateLabelColor ? : isNative ? SquirrelTheme.accentColor : blendColors(candidateTextColor, backColor); highlightedBackColor = highlightedBackColor ? : isNative ? NSColor.selectedTextBackgroundColor : nil; highlightedTextColor = highlightedTextColor ? : NSColor.selectedTextColor; highlightedCandidateBackColor = highlightedCandidateBackColor ? : isNative ? NSColor.selectedContentBackgroundColor : nil; @@ -3545,3 +3637,4 @@ + (void)updateTheme:(SquirrelTheme *)theme } @end // SquirrelPanel + diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index dc7286b11..8d3f6e4f6 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -11,7 +11,7 @@ "say_voice" = "Alex"; "candidate" = "Click a candidate to ⎆select.\nSecondary click to ⎌forget selected word."; -"delete" = "Click to ⌫Delete the input.\nSecondary click to ⎋Escape the composing."; +"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."; diff --git a/input_source.m b/input_source.m index 05a7e5f02..97df4dd60 100644 --- a/input_source.m +++ b/input_source.m @@ -22,18 +22,19 @@ void RegisterInputSource(void) { } } -void ActivateInputSource(int modes) { +void ActivateInputSource(RimeInputMode modes) { CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty + (inputSource, kTISPropertyInputSourceID); //NSLog(@"Examining input source: %@", sourceID); if ((!CFStringCompare(sourceID, kHansInputModeID, 0) && (modes & HANS_INPUT_MODE)) || (!CFStringCompare(sourceID, kHantInputModeID, 0) && (modes & HANT_INPUT_MODE))) { TISEnableInputSource(inputSource); NSLog(@"Enabled input source: %@", sourceID); - CFBooleanRef isSelectable = (CFBooleanRef)TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsSelectCapable); + CFBooleanRef isSelectable = (CFBooleanRef)TISGetInputSourceProperty + (inputSource, kTISPropertyInputSourceIsSelectCapable); if (CFBooleanGetValue(isSelectable)) { TISSelectInputSource(inputSource); NSLog(@"Selected input source: %@", sourceID); @@ -47,12 +48,13 @@ void DeactivateInputSource(void) { CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); for (CFIndex i = CFArrayGetCount(sourceList); i > 0; --i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i - 1); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty + (inputSource, kTISPropertyInputSourceID); //NSLog(@"Examining input source: %@", sourceID); if (!CFStringCompare(sourceID, kHansInputModeID, 0) || !CFStringCompare(sourceID, kHantInputModeID, 0)) { - CFBooleanRef isEnabled = (CFBooleanRef)(TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsEnabled)); + CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty + (inputSource, kTISPropertyInputSourceIsEnabled); if (CFBooleanGetValue(isEnabled)) { TISDisableInputSource(inputSource); NSLog(@"Disabled input source: %@", sourceID); @@ -67,12 +69,13 @@ RimeInputMode GetEnabledInputModes(void) { CFArrayRef sourceList = TISCreateInputSourceList(NULL, true); for (CFIndex i = 0; i < CFArrayGetCount(sourceList); ++i) { TISInputSourceRef inputSource = (TISInputSourceRef)CFArrayGetValueAtIndex(sourceList, i); - CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID); + CFStringRef sourceID = (CFStringRef)TISGetInputSourceProperty + (inputSource, kTISPropertyInputSourceID); //NSLog(@"Examining input source: %@", sourceID); if (!CFStringCompare(sourceID, kHansInputModeID, 0) || !CFStringCompare(sourceID, kHantInputModeID, 0)) { - CFBooleanRef isEnabled = (CFBooleanRef)(TISGetInputSourceProperty( - inputSource, kTISPropertyInputSourceIsEnabled)); + CFBooleanRef isEnabled = (CFBooleanRef)TISGetInputSourceProperty + (inputSource, kTISPropertyInputSourceIsEnabled); if (CFBooleanGetValue(isEnabled)) { if (!CFStringCompare(sourceID, kHansInputModeID, 0)) { input_modes |= HANS_INPUT_MODE; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 97c328aa0..d7e14296a 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -17,10 +17,10 @@ 请尝试撤销之前的修改,然后查看问题是否仍旧存在。"; "say_voice" = "TingTing"; -"candidate" = "点按以⎆选择候选字。辅助点按以⎌删除所选的记忆字词。"; -"delete" = "点按以⌫删除输入。辅助点按以⎋取消输入。"; -"escape" = "不能再删除。辅助点按以⎋取消输入。"; -"page_up" = "点按以⇞向上翻页。辅助点按以跳到↖开头。"; -"home" = "不能再向上翻页。辅助点按以跳到↖开头。"; -"page_down" = "点按以⇟向下翻页。辅助点按以跳到↘结尾。"; -"end" = "不能再向下翻页。辅助点按以跳到↘结尾。"; +"candidate" = "点按以⎆选择候选字。\n辅助点按以⎌删除所选的记忆字词。"; +"delete" = "点按以逐字⌫删除输入。\n辅助点按以⎋取消输入。"; +"escape" = "不能再删除。\n辅助点按以⎋取消输入。"; +"page_up" = "点按以⇞向上翻页。\n辅助点按以跳到↖开头。"; +"home" = "不能再向上翻页。\n辅助点按以跳到↖开头。"; +"page_down" = "点按以⇟向下翻页。\n辅助点按以跳到↘结尾。"; +"end" = "不能再向下翻页。\n辅助点按以跳到↘结尾。"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings index 4777f0fe6..6834c42d1 100644 --- a/zh-Hant.lproj/Localizable.strings +++ b/zh-Hant.lproj/Localizable.strings @@ -17,10 +17,10 @@ 請嘗試回退先前的修改,然後查看問題是否依然存在。"; "say_voice" = "MeiJia"; -"candidate" = "點按來⎆選取候選字。點按輔助按鈕來⎌清除所選的記憶字詞。"; -"delete" = "點按來⌫刪除輸入。點按輔助按鈕來⎋取消輸入。"; -"escape" = "無法再刪除。點按輔助按鈕來⎋取消輸入。"; -"page_up" = "點按來⇞向上翻頁。點按輔助按鈕來跳至↖起始處。"; -"home" = "無法再向上翻頁。點按輔助按鈕來跳至↖起始處。"; -"page_down" = "點按來⇟向下翻頁。點按輔助按鈕來跳至↘結尾處。"; -"end" = "無法再向下翻頁。點按輔助按鈕來跳至↘結尾處。"; +"candidate" = "點按來⎆選取候選字。\n點按輔助按鈕來⎌清除所選的記憶字詞。"; +"delete" = "點按來逐字⌫刪除輸入。\n點按輔助按鈕來⎋取消輸入。"; +"escape" = "無法再刪除。\n點按輔助按鈕來⎋取消輸入。"; +"page_up" = "點按來⇞向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; +"home" = "無法再向上翻頁。\n點按輔助按鈕來跳至↖起始處。"; +"page_down" = "點按來⇟向下翻頁。\n點按輔助按鈕來跳至↘結尾處。"; +"end" = "無法再向下翻頁。\n點按輔助按鈕來跳至↘結尾處。";