diff options
| -rw-r--r-- | .gitignore | 17 | ||||
| -rw-r--r-- | MASShortcut.h | 52 | ||||
| -rw-r--r-- | MASShortcut.m | 259 | ||||
| -rw-r--r-- | MASShortcutView.h | 9 | ||||
| -rw-r--r-- | MASShortcutView.m | 404 | ||||
| -rw-r--r-- | README.md | 15 | 
6 files changed, 756 insertions, 0 deletions
| diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f72ba0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Xcode +build/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.xcworkspace +!default.xcworkspace +xcuserdata +profile +*.moved-aside +# Finder +.DS_Store
\ No newline at end of file diff --git a/MASShortcut.h b/MASShortcut.h new file mode 100644 index 0000000..173d994 --- /dev/null +++ b/MASShortcut.h @@ -0,0 +1,52 @@ +#import <Carbon/Carbon.h> + +#define MASShortcutChar(char) [NSString stringWithFormat:@"%C", (unsigned short)(char)] +#define MASShortcutClear(flags) (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask)) +#define MASShortcutCarbonFlags(cocoaFlags) (\ +    (cocoaFlags & NSCommandKeyMask ? cmdKey : 0) | \ +    (cocoaFlags & NSAlternateKeyMask ? optionKey : 0) | \ +    (cocoaFlags & NSControlKeyMask ? controlKey : 0) | \ +    (cocoaFlags & NSShiftKeyMask ? shiftKey : 0)) + +// These glyphs are missed in Carbon.h +enum { +    kMASShortcutGlyphEject = 0x23CF, +    kMASShortcutGlyphClear = 0x2715, +	kMASShortcutGlyphDeleteLeft = 0x232B, +	kMASShortcutGlyphDeleteRight = 0x2326, +    kMASShortcutGlyphLeftArrow = 0x2190, +	kMASShortcutGlyphRightArrow = 0x2192, +	kMASShortcutGlyphUpArrow = 0x2191, +	kMASShortcutGlyphDownArrow = 0x2193, +	kMASShortcutGlyphEscape = 0x238B, +	kMASShortcutGlyphHelp = 0x003F, +    kMASShortcutGlyphPageDown = 0x21DF, +	kMASShortcutGlyphPageUp = 0x21DE, +	kMASShortcutGlyphTabRight = 0x21E5, +	kMASShortcutGlyphReturn = 0x2305, +	kMASShortcutGlyphReturnR2L = 0x21A9,	 +	kMASShortcutGlyphPadClear = 0x2327, +	kMASShortcutGlyphNorthwestArrow = 0x2196, +	kMASShortcutGlyphSoutheastArrow = 0x2198, +} MASShortcutGlyph; + +@interface MASShortcut : NSObject <NSCoding> + +@property (nonatomic) NSUInteger keyCode; +@property (nonatomic) NSUInteger modifierFlags; +@property (nonatomic, readonly) NSUInteger carbonFlags; +@property (nonatomic, readonly) NSString *keyCodeString; +@property (nonatomic, readonly) NSString *modifierFlagsString; +@property (nonatomic, readonly) NSData *data; +@property (nonatomic, readonly) BOOL shouldBypass; +@property (nonatomic, readonly) BOOL hasRequiredModifierFlags; + +- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags; + ++ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags; ++ (MASShortcut *)shortcutWithEvent:(NSEvent *)anEvent; ++ (MASShortcut *)shortcutWithData:(NSData *)aData; + +- (BOOL)isTakenError:(NSError **)error; + +@end diff --git a/MASShortcut.m b/MASShortcut.m new file mode 100644 index 0000000..0720e9d --- /dev/null +++ b/MASShortcut.m @@ -0,0 +1,259 @@ +#import "MASShortcut.h" + +NSString *const kMASShortcutKeyCode = @"KeyCode"; +NSString *const kMASShortcutModifierFlags = @"ModifierFlags"; + +@implementation MASShortcut { +    NSUInteger _keyCode; // NSNotFound if empty +    NSUInteger _modifierFlags; // 0 if empty +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ +    [coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:kMASShortcutKeyCode]; +    [coder encodeInteger:(NSInteger)self.modifierFlags forKey:kMASShortcutModifierFlags]; +} + +- (id)initWithCoder:(NSCoder *)decoder +{ +    self = [super init]; +    if (self) { +        NSInteger code = [decoder decodeIntegerForKey:kMASShortcutKeyCode]; +        self.keyCode = (code < 0 ? NSNotFound : (NSUInteger)code); +        self.modifierFlags = [decoder decodeIntegerForKey:kMASShortcutModifierFlags]; +    } +    return self; +} + +- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags +{ +    self = [super init]; +    if (self) { +        _keyCode = code; +        _modifierFlags = MASShortcutClear(flags); +    } +    return self; +} + ++ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags +{ +    return [[self alloc] initWithKeyCode:code modifierFlags:flags]; +} + ++ (MASShortcut *)shortcutWithEvent:(NSEvent *)event +{ +    return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags]; +} + ++ (MASShortcut *)shortcutWithData:(NSData *)data +{ +    return (data ? (MASShortcut *)[NSKeyedUnarchiver unarchiveObjectWithData:data] : nil); +} + +#pragma mark - Shortcut accessors + +- (NSData *)data +{ +    return [NSKeyedArchiver archivedDataWithRootObject:self]; +} + +- (void)setModifierFlags:(NSUInteger)value +{ +    _modifierFlags = MASShortcutClear(value); +} + +- (NSUInteger)carbonFlags +{ +    return MASShortcutCarbonFlags(self.modifierFlags); +} + +- (NSString *)description +{ +    return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString]; +} + +- (NSString *)keyCodeString +{ +    // Some key codes don't have an equivalent +    switch (self.keyCode) { +        case NSNotFound: return @""; +        case kVK_F1: return @"F1"; +        case kVK_F2: return @"F2"; +        case kVK_F3: return @"F3"; +        case kVK_F4: return @"F4"; +        case kVK_F5: return @"F5"; +        case kVK_F6: return @"F6"; +        case kVK_F7: return @"F7"; +        case kVK_F8: return @"F8"; +        case kVK_F9: return @"F9"; +        case kVK_F10: return @"F10"; +        case kVK_F11: return @"F11"; +        case kVK_F12: return @"F12"; +        case kVK_F13: return @"F13"; +        case kVK_F14: return @"F14"; +        case kVK_F15: return @"F15"; +        case kVK_F16: return @"F16"; +        case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key"); +        case kVK_Escape: return MASShortcutChar(kMASShortcutGlyphEscape); +        case kVK_Delete: return MASShortcutChar(kMASShortcutGlyphDeleteLeft); +        case kVK_ForwardDelete: return MASShortcutChar(kMASShortcutGlyphDeleteRight); +        case kVK_LeftArrow: return MASShortcutChar(kMASShortcutGlyphLeftArrow); +        case kVK_RightArrow: return MASShortcutChar(kMASShortcutGlyphRightArrow); +        case kVK_UpArrow: return MASShortcutChar(kMASShortcutGlyphUpArrow); +        case kVK_DownArrow: return MASShortcutChar(kMASShortcutGlyphDownArrow); +        case kVK_Help: return MASShortcutChar(kMASShortcutGlyphHelp); +        case kVK_PageUp: return MASShortcutChar(kMASShortcutGlyphPageUp); +        case kVK_PageDown: return MASShortcutChar(kMASShortcutGlyphPageDown); +        case kVK_Tab: return MASShortcutChar(kMASShortcutGlyphTabRight); +        case kVK_Return: return MASShortcutChar(kMASShortcutGlyphReturnR2L); +             +        // Keypad +        case kVK_ANSI_Keypad0: return @"0"; +        case kVK_ANSI_Keypad1: return @"1"; +        case kVK_ANSI_Keypad2: return @"2"; +        case kVK_ANSI_Keypad3: return @"3"; +        case kVK_ANSI_Keypad4: return @"4"; +        case kVK_ANSI_Keypad5: return @"5"; +        case kVK_ANSI_Keypad6: return @"6"; +        case kVK_ANSI_Keypad7: return @"7"; +        case kVK_ANSI_Keypad8: return @"8"; +        case kVK_ANSI_Keypad9: return @"9"; +        case kVK_ANSI_KeypadDecimal: return @"."; +        case kVK_ANSI_KeypadMultiply: return @"*"; +        case kVK_ANSI_KeypadPlus: return @"+"; +        case kVK_ANSI_KeypadClear: return MASShortcutChar(kMASShortcutGlyphPadClear); +        case kVK_ANSI_KeypadDivide: return @"/"; +        case kVK_ANSI_KeypadEnter: return MASShortcutChar(kMASShortcutGlyphReturn); +        case kVK_ANSI_KeypadMinus: return @"–"; +        case kVK_ANSI_KeypadEquals: return @"="; +             +        // Hardcode +        case 119: return MASShortcutChar(kMASShortcutGlyphSoutheastArrow); +        case 115: return MASShortcutChar(kMASShortcutGlyphNorthwestArrow); +    } +     +    // Everything else should be printable so look it up in the current keyboard +    OSStatus error = noErr; +    NSString *keystroke = nil; +    TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource(); +    if (inputSource) { +        CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData); +        if (layoutDataRef) { +            UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef); +            UniCharCount length = 0; +            UniChar  chars[256] = { 0 }; +            UInt32 deadKeyState = 0; +            error = UCKeyTranslate(layoutData, self.keyCode, kUCKeyActionDisplay, 0, // No modifiers +                                   LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState, +                                   sizeof(chars) / sizeof(UniChar), &length, chars); +            keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @""); +        } +        CFRelease(inputSource); +    } +     +    // Validate keystroke +    if (keystroke.length) { +        static NSMutableCharacterSet *validChars = nil; +        if (validChars == nil) { +            validChars = [[NSMutableCharacterSet alloc] init]; +            [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]]; +            [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; +            [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]]; +        } +        for (NSUInteger i = 0, length = keystroke.length; i < length; i++) { +            if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) { +                keystroke = @""; +                break; +            } +        } +    } +     +    // Finally, we've got a shortcut! +    return keystroke.uppercaseString; +} + +- (NSString *)modifierFlagsString +{ +    unichar chars[4]; +    NSUInteger count = 0; +    // These are in the same order as the menu manager shows them +    if (self.modifierFlags & NSControlKeyMask) chars[count++] = kControlUnicode; +    if (self.modifierFlags & NSAlternateKeyMask) chars[count++] = kOptionUnicode; +    if (self.modifierFlags & NSShiftKeyMask) chars[count++] = kShiftUnicode; +    if (self.modifierFlags & NSCommandKeyMask) chars[count++] = kCommandUnicode; +    return (count ? [NSString stringWithCharacters:chars length:count] : @""); +} + +#pragma mark - Validation logic + +- (BOOL)shouldBypass +{ +    NSString *codeString = self.keyCodeString; +    return (self.modifierFlags == NSCommandKeyMask) && ([codeString isEqualToString:@"W"] || [codeString isEqualToString:@"Q"]); +} + +- (BOOL)hasRequiredModifierFlags +{ +    BOOL hasFlags = (_modifierFlags > 0); +    BOOL hasCommand = ((_modifierFlags & NSCommandKeyMask) > 0); +    BOOL hasControl = ((_modifierFlags & NSControlKeyMask) > 0); +    BOOL hasOption = ((_modifierFlags & NSAlternateKeyMask) > 0); +    BOOL isSpecial = ((_keyCode == kVK_Space) || (_keyCode == kVK_Escape) || (_keyCode == kVK_Return) || +                      (_keyCode == kVK_F1) || (_keyCode == kVK_F2) || (_keyCode == kVK_F3) || (_keyCode == kVK_F4) || +                      (_keyCode == kVK_F5) || (_keyCode == kVK_F6) || (_keyCode == kVK_F7) || (_keyCode == kVK_F8) || +                      (_keyCode == kVK_F9) || (_keyCode == kVK_F10) || (_keyCode == kVK_F11) || (_keyCode == kVK_F12) || +                      (_keyCode == kVK_F13) || (_keyCode == kVK_F14) || (_keyCode == kVK_F15) || (_keyCode == kVK_F16) || +                      (_keyCode == kVK_F17) || (_keyCode == kVK_F18) || (_keyCode == kVK_F19) || (_keyCode == kVK_F20)); +    return (hasFlags && (hasCommand || hasControl || (hasOption && isSpecial))); +} + +- (BOOL)isKeyEquivalent:(NSString *)keyEquivalent flags:(NSUInteger)flags takenInMenu:(NSMenu *)menu error:(NSError **)outError +{ +    for (NSMenuItem *menuItem in menu.itemArray) { +        if (menuItem.hasSubmenu && [self isKeyEquivalent:keyEquivalent flags:flags takenInMenu:menuItem.submenu error:outError]) return YES; +         +        BOOL equalFlags = (MASShortcutClear(menuItem.keyEquivalentModifierMask) == flags); +        BOOL equalHotkey = [menuItem.keyEquivalent.uppercaseString isEqualToString:keyEquivalent]; +        if (equalFlags && equalHotkey) { +            if (outError) { +                NSString *format = NSLocalizedString(@"This shortcut cannot be used used because it is already used by the menu item ‘%@’.", +                                                     @"Message for alert when shortcut is already used"); +                NSDictionary *info = @{ NSLocalizedDescriptionKey : [NSString stringWithFormat:format, menuItem.title] }; +                *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info]; +            } +            return YES; +        } +    } +    return NO; +} + +- (BOOL)isTakenError:(NSError **)outError +{ +	CFArrayRef globalHotKeys; +	if (CopySymbolicHotKeys(&globalHotKeys) == noErr) { + +        // Enumerate all global hotkeys and check if any of them matches current shortcut +        for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) { +            CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i); +            CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode); +            CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers); + +            if (([(__bridge NSNumber *)code unsignedIntegerValue] == self.keyCode) && +                ([(__bridge NSNumber *)flags unsignedIntegerValue] == self.carbonFlags)) { + +                if (outError) { +                    NSString *description = NSLocalizedString(@"This combination cannot be used used because it is already used by a system-wide " +                                                              @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts " +                                                              @"can be changed in the Keyboard & Mouse panel in System Preferences.", +                                                              @"Message for alert when shortcut is already used by the system"); +                    *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{ NSLocalizedDescriptionKey : description }]; +                } +                return YES; +            } +        } +        CFRelease(globalHotKeys); +    } +    return [self isKeyEquivalent:self.keyCodeString flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError]; +} + +@end diff --git a/MASShortcutView.h b/MASShortcutView.h new file mode 100644 index 0000000..0892c0b --- /dev/null +++ b/MASShortcutView.h @@ -0,0 +1,9 @@ +@class MASShortcut; + +@interface MASShortcutView : NSView + +@property (nonatomic, strong) MASShortcut *shortcutValue; +@property (nonatomic, getter = isRecording) BOOL recording; +@property (nonatomic, getter = isEnabled) BOOL enabled; + +@end 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b8ed6e --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# MASShortcut + +This set of classes is a modern interface for recording and storing global keyboard shortcuts. + +Prior to Xcode 4 we used the framework [ShortcutRecorder](http://wafflesoftware.net/shortcut/). However, it is incompatible with a new plugin architecture. + +This repository has ARC-enabled Objective-C code compatible with OS X 10.8 and its sandboxed environment. Enjoy! + +# How to use + +You can find a Demo project at [MASShortcutDemo](https://github.com/shpakovski/MASShortcutDemo). + +# License + +MASShortcut is licensed under the BSD license.
\ No newline at end of file | 
