aboutsummaryrefslogtreecommitdiffstats
path: root/MASShortcutView.m
diff options
context:
space:
mode:
authorVadim Shpakovski2012-07-06 13:45:06 +0300
committerVadim Shpakovski2012-07-06 13:45:06 +0300
commit6e0d714bea4940a63ca6dae7043d6b558f20760c (patch)
treea3bd606d6c28d7dcbbcbbaf3b8622578188903ae /MASShortcutView.m
downloadMASShortcut-6e0d714bea4940a63ca6dae7043d6b558f20760c.tar.bz2
Initial commit with my sources from CodeBox.
Diffstat (limited to 'MASShortcutView.m')
-rw-r--r--MASShortcutView.m404
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