diff options
Diffstat (limited to 'MASShortcutView.m')
| -rw-r--r-- | MASShortcutView.m | 404 | 
1 files changed, 404 insertions, 0 deletions
| diff --git a/MASShortcutView.m b/MASShortcutView.m new file mode 100644 index 0000000..b31ceb0 --- /dev/null +++ b/MASShortcutView.m @@ -0,0 +1,404 @@ +#import "MASShortcutView.h" +#import "MASShortcut.h" + +#define HINT_BUTTON_WIDTH 23.0f +#define BUTTON_FONT_SIZE 11.0f + +#pragma mark - + +@interface MASShortcutCell : NSButtonCell @end + +#pragma mark - + +@interface MASShortcutView () // Private accessors + +@property (nonatomic, getter = isHinting) BOOL hinting; +@property (nonatomic, copy) NSString *shortcutPlaceholder; + +@end + +#pragma mark - + +@implementation MASShortcutView { +    NSButtonCell *_shortcutCell; + +    NSInteger _shortcutToolTipTag; +    NSInteger _hintToolTipTag; +    NSTrackingArea *_hintArea; +} + +- (id)initWithFrame:(CGRect)frameRect +{ +    self = [super initWithFrame:frameRect]; +    if (self) { +        _shortcutCell = [[MASShortcutCell alloc] init]; +        [_shortcutCell setFont:[[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE]]; +        _enabled = YES; +    } +    return self; +} + +- (void)dealloc +{ +    [self activateEventMonitoring:NO]; +    [self activateResignObserver:NO]; +} + +#pragma mark - Public accessors + +- (void)setEnabled:(BOOL)flag +{ +    if (_enabled != flag) { +        _enabled = flag; +        [self updateTrackingAreas]; +        self.recording = NO; +        [self setNeedsDisplay:YES]; +    } +} + +- (void)setRecording:(BOOL)flag +{ +    // Only one recorder can be active at the moment +    static MASShortcutView *currentRecorder = nil; +    if (flag && (currentRecorder != self)) { +        currentRecorder.recording = NO; +        currentRecorder = self; +    } +     +    // Only enabled view supports recording +    if (flag && !self.enabled) return; +     +    if (_recording != flag) { +        _recording = flag; +        self.shortcutPlaceholder = nil; +        [self resetToolTips]; +        [self activateEventMonitoring:_recording]; +        [self activateResignObserver:_recording]; +        [self setNeedsDisplay:YES]; +    } +} + +- (void)setShortcutValue:(MASShortcut *)shortcutValue +{ +    _shortcutValue = shortcutValue; +    [self resetToolTips]; +    [self setNeedsDisplay:YES]; +} + +- (void)setShortcutPlaceholder:(NSString *)shortcutPlaceholder +{ +    _shortcutPlaceholder = shortcutPlaceholder.copy; +    [self setNeedsDisplay:YES]; +} + +#pragma mark - Drawing + +- (BOOL)isFlipped +{ +    return YES; +} + +- (void)drawInRect:(CGRect)frame withTitle:(NSString *)title alignment:(NSTextAlignment)alignment state:(NSInteger)state +{ +    _shortcutCell.title = title; +    _shortcutCell.alignment = alignment; +    _shortcutCell.state = state; +    _shortcutCell.enabled = self.enabled; +    [_shortcutCell drawWithFrame:frame inView:self]; +} + +- (void)drawRect:(CGRect)dirtyRect +{ +    if (self.shortcutValue) { +        [self drawInRect:self.bounds withTitle:MASShortcutChar(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphDeleteLeft) +               alignment:NSRightTextAlignment state:NSOffState]; +         +        CGRect shortcutRect; +        [self getShortcutRect:&shortcutRect hintRect:NULL]; +        NSString *title = (self.recording +                           ? (_hinting +                              ? NSLocalizedString(@"Use old shortuct", @"Cancel action button for non-empty shortcut in recording state") +                              : (self.shortcutPlaceholder.length > 0 +                                 ? self.shortcutPlaceholder +                                 : NSLocalizedString(@"Type new shortcut", @"Non-empty shortcut button in recording state"))) +                           : _shortcutValue ? _shortcutValue.description : @""); +        [self drawInRect:shortcutRect withTitle:title alignment:NSCenterTextAlignment state:self.isRecording ? NSOnState : NSOffState]; +    } +    else { +        if (self.recording) +        { +            [self drawInRect:self.bounds withTitle:MASShortcutChar(kMASShortcutGlyphEscape) alignment:NSRightTextAlignment state:NSOffState]; +             +            CGRect shortcutRect; +            [self getShortcutRect:&shortcutRect hintRect:NULL]; +            NSString *title = (_hinting +                               ? NSLocalizedString(@"Click to cancel", @"Cancel action button in recording state") +                               : (self.shortcutPlaceholder.length > 0 +                                  ? self.shortcutPlaceholder +                                  : NSLocalizedString(@"Type shortcut", @"Empty shortcut button in recording state"))); +            [self drawInRect:shortcutRect withTitle:title alignment:NSCenterTextAlignment state:NSOnState]; +        } +        else +        { +            [self drawInRect:self.bounds withTitle:NSLocalizedString(@"Click to record", @"Empty shortcut button in normal state") +                   alignment:NSCenterTextAlignment state:NSOffState]; +        } +    } +} + +#pragma mark - Mouse handling + +- (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef +{ +    CGRect shortcutRect, hintRect; +    CGRectDivide(self.bounds, &hintRect, &shortcutRect, HINT_BUTTON_WIDTH, CGRectMaxXEdge); +    if (shortcutRectRef)  *shortcutRectRef = shortcutRect; +    if (hintRectRef) *hintRectRef = hintRect; +} + +- (BOOL)locationInShortcutRect:(CGPoint)location +{ +    CGRect shortcutRect; +    [self getShortcutRect:&shortcutRect hintRect:NULL]; +    return CGRectContainsPoint(shortcutRect, [self convertPoint:location fromView:nil]); +} + +- (BOOL)locationInHintRect:(CGPoint)location +{ +    CGRect hintRect; +    [self getShortcutRect:NULL hintRect:&hintRect]; +    return CGRectContainsPoint(hintRect, [self convertPoint:location fromView:nil]); +} + +- (void)mouseDown:(NSEvent *)event +{ +    if (self.enabled) { +        if (self.shortcutValue) { +            if (self.recording) { +                if ([self locationInHintRect:event.locationInWindow]) { +                    self.recording = NO; +                } +            } +            else { +                if ([self locationInShortcutRect:event.locationInWindow]) { +                    self.recording = YES; +                } +                else { +                    self.shortcutValue = nil; +                } +            } +        } +        else { +            if (self.recording) { +                if ([self locationInHintRect:[event locationInWindow]]) { +                    self.recording = NO; +                } +            } +            else { +                self.recording = YES; +            } +        } +    } +    else { +        [super mouseDown:event]; +    } +} + +#pragma mark - Handling mouse over + +- (void)updateTrackingAreas +{ +    [super updateTrackingAreas]; +     +    if (_hintArea) { +        [self removeTrackingArea:_hintArea]; +        _hintArea = nil; +    } +     +    // Forbid hinting if view is disabled +    if (self.enabled) return; +     +    CGRect hintRect; +    [self getShortcutRect:NULL hintRect:&hintRect]; +    NSTrackingAreaOptions options = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingAssumeInside); +    _hintArea = [[NSTrackingArea alloc] initWithRect:hintRect options:options owner:self userInfo:nil]; +    [self addTrackingArea:_hintArea]; +} + +- (void)setHinting:(BOOL)flag +{ +    if (_hinting != flag) { +        _hinting = flag; +        [self setNeedsDisplay:YES]; +    } +} + +- (void)mouseEntered:(NSEvent *)event +{ +    self.hinting = YES; +} + +- (void)mouseExited:(NSEvent *)event +{ +    self.hinting = NO; +} + +void *kUserDataShortcut = &kUserDataShortcut; +void *kUserDataHint = &kUserDataHint; + +- (void)resetToolTips +{ +    if (_shortcutToolTipTag) { +        [self removeToolTip:_shortcutToolTipTag], _shortcutToolTipTag = 0; +    } +    if (_hintToolTipTag) { +        [self removeToolTip:_hintToolTipTag], _hintToolTipTag = 0; +    } +     +    if ((self.shortcutValue == nil) || self.recording || !self.enabled) return; +     +    CGRect shortcutRect, hintRect; +    [self getShortcutRect:&shortcutRect hintRect:&hintRect]; +    _shortcutToolTipTag = [self addToolTipRect:shortcutRect owner:self userData:kUserDataShortcut]; +    _hintToolTipTag = [self addToolTipRect:hintRect owner:self userData:kUserDataHint]; +} + +- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(CGPoint)point userData:(void *)data +{ +    if (data == kUserDataShortcut) { +        return NSLocalizedString(@"Click to record new shortcut", @"Tooltip for non-empty shortcut button"); +    } +    else if (data == kUserDataHint) { +        return NSLocalizedString(@"Delete shortcut", @"Tooltip for hint button near the non-empty shortcut"); +    } +    return nil; +} + +#pragma mark - Event monitoring + +- (void)activateEventMonitoring:(BOOL)shouldActivate +{ +    static BOOL isActive = NO; +    if (isActive == shouldActivate) return; +    isActive = shouldActivate; +     +    static id eventMonitor = nil; +    if (shouldActivate) { +        eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^(NSEvent *event) { + +            MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event]; +            if ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete)) { +                // Delete shortcut +                self.shortcutValue = nil; +                self.recording = NO; +                event = nil; +            } +            else if (shortcut.keyCode == kVK_Escape) { +                // Cancel recording +                self.recording = NO; +                event = nil; +            } +            else if (shortcut.shouldBypass) { +                // Command + W, Command + Q, ESC should deactivate recorder +                self.recording = NO; +            } +            else { +                // Verify possible shortcut +                if (shortcut.keyCodeString.length > 0) { +                    if (shortcut.hasRequiredModifierFlags) { +                        // Verify that shortcut is not used +                        NSError *error = nil; +                        if ([shortcut isTakenError:&error]) { +                            // Prevent cancel of recording when Alert window is key +                            [self activateResignObserver:NO]; +                            [self activateEventMonitoring:NO]; +                            NSString *format = NSLocalizedString(@"The key combination %@ cannot be used", +                                                                 @"Title for alert when shortcut is already used"); +                            NSRunCriticalAlertPanel([NSString stringWithFormat:format, shortcut], error.localizedDescription, +                                                    NSLocalizedString(@"OK", @"Alert button when shortcut is already used"), +                                                    nil, nil); +                            self.shortcutPlaceholder = nil; +                            [self activateResignObserver:YES]; +                            [self activateEventMonitoring:YES]; +                        } +                        else { +                            self.shortcutValue = shortcut; +                            self.recording = NO; +                        } +                    } +                    else { +                        // Key press with or without SHIFT is not valid input +                        NSBeep(); +                    } +                } +                else { +                    // User is playing with modifier keys +                    self.shortcutPlaceholder = shortcut.modifierFlagsString; +                } +                event = nil; +            } +            return event; +        }]; +    } +    else { +        [NSEvent removeMonitor:eventMonitor]; +    } +} + +- (void)activateResignObserver:(BOOL)shouldActivate +{ +    static BOOL isActive = NO; +    if (isActive == shouldActivate) return; +    isActive = shouldActivate; +     +    static id observer = nil; +    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; +    if (shouldActivate) { +        __weak MASShortcutView *weakSelf = self; +        observer = [notificationCenter addObserverForName:NSWindowDidResignKeyNotification object:self.window +                                                queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { +                                                    weakSelf.recording = NO; +                                                }]; +    } +    else { +        [notificationCenter removeObserver:observer]; +    } +} + +@end + +#pragma mark - + +@implementation MASShortcutCell + +- (id)init +{ +    self = [super init]; +    if (self) { +        self.buttonType = NSPushOnPushOffButton; +        self.bezelStyle = NSRoundRectBezelStyle; +    } +    return self; +} + +- (void)drawBezelWithFrame:(CGRect)frame inView:(NSView *)controlView +{ +    [super drawBezelWithFrame:frame inView:controlView]; +    if ([self state] == NSOnState) { +        NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 1.0f, 1.0f) xRadius:NSHeight(frame) / 2.0f yRadius:NSHeight(frame) / 2.0f]; +        [[[self class] fillGradient] drawInBezierPath:path angle:90.0f]; +    } +} + ++ (NSGradient *)fillGradient +{ +    static NSGradient *shared = nil; +    static dispatch_once_t onceToken; +    dispatch_once(&onceToken, ^{ +        shared = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithDeviceRed:0.88f green:0.94f blue:1.00f alpha:0.35f] +                                               endingColor:[NSColor colorWithDeviceRed:0.55f green:0.60f blue:0.65f alpha:0.65f]]; +    }); +    return shared; +} + +@end | 
