Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit with my sources from CodeBox.

  • Loading branch information...
commit 6e0d714bea4940a63ca6dae7043d6b558f20760c 0 parents
@shpakovski authored
17 .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
52 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
259 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
9 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
404 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
15 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.
Please sign in to comment.
Something went wrong with that request. Please try again.