aboutsummaryrefslogtreecommitdiffstats
path: root/Framework/MASShortcutView.m
diff options
context:
space:
mode:
Diffstat (limited to 'Framework/MASShortcutView.m')
-rw-r--r--Framework/MASShortcutView.m463
1 files changed, 463 insertions, 0 deletions
diff --git a/Framework/MASShortcutView.m b/Framework/MASShortcutView.m
new file mode 100644
index 0000000..dd75729
--- /dev/null
+++ b/Framework/MASShortcutView.m
@@ -0,0 +1,463 @@
+#import "MASShortcutView.h"
+#import "MASShortcut.h"
+
+#define HINT_BUTTON_WIDTH 23.0
+#define BUTTON_FONT_SIZE 11.0
+#define SEGMENT_CHROME_WIDTH 6.0
+
+#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;
+}
+
+@synthesize enabled = _enabled;
+@synthesize hinting = _hinting;
+@synthesize shortcutValue = _shortcutValue;
+@synthesize shortcutPlaceholder = _shortcutPlaceholder;
+@synthesize shortcutValueChange = _shortcutValueChange;
+@synthesize recording = _recording;
+@synthesize appearance = _appearance;
+
+#pragma mark -
+
++ (Class)shortcutCellClass
+{
+ return [NSButtonCell class];
+}
+
+- (id)initWithFrame:(CGRect)frameRect
+{
+ self = [super initWithFrame:frameRect];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)coder
+{
+ self = [super initWithCoder:coder];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (void)commonInit
+{
+ _shortcutCell = [[[self.class shortcutCellClass] alloc] init];
+ _shortcutCell.buttonType = NSPushOnPushOffButton;
+ _shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
+ _enabled = YES;
+ [self resetShortcutCellStyle];
+}
+
+- (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)setAppearance:(MASShortcutViewAppearance)appearance
+{
+ if (_appearance != appearance) {
+ _appearance = appearance;
+ [self resetShortcutCellStyle];
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (void)resetShortcutCellStyle
+{
+ switch (_appearance) {
+ case MASShortcutViewAppearanceDefault: {
+ _shortcutCell.bezelStyle = NSRoundRectBezelStyle;
+ break;
+ }
+ case MASShortcutViewAppearanceTexturedRect: {
+ _shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle;
+ break;
+ }
+ case MASShortcutViewAppearanceRounded: {
+ _shortcutCell.bezelStyle = NSRoundedBezelStyle;
+ break;
+ }
+ case MASShortcutViewAppearanceFlat: {
+ self.wantsLayer = YES;
+ _shortcutCell.backgroundColor = [NSColor clearColor];
+ _shortcutCell.bordered = NO;
+ break;
+ }
+ }
+}
+
+- (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 = flag ? self : nil;
+ }
+
+ // 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];
+
+ if (self.shortcutValueChange) {
+ self.shortcutValueChange(self);
+ }
+}
+
+- (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;
+
+ switch (_appearance) {
+ case MASShortcutViewAppearanceDefault: {
+ [_shortcutCell drawWithFrame:frame inView:self];
+ break;
+ }
+ case MASShortcutViewAppearanceTexturedRect: {
+ [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
+ break;
+ }
+ case MASShortcutViewAppearanceRounded: {
+ [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
+ break;
+ }
+ case MASShortcutViewAppearanceFlat: {
+ [_shortcutCell drawWithFrame:frame inView:self];
+ break;
+ }
+ }
+}
+
+- (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 Shortcut", @"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(@"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(@"Record Shortcut", @"Empty shortcut button in normal state")
+ alignment:NSCenterTextAlignment state:NSOffState];
+ }
+ }
+}
+
+#pragma mark - Mouse handling
+
+- (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef
+{
+ CGRect shortcutRect, hintRect;
+ CGFloat hintButtonWidth = HINT_BUTTON_WIDTH;
+ switch (self.appearance) {
+ case MASShortcutViewAppearanceTexturedRect: hintButtonWidth += 2.0; break;
+ case MASShortcutViewAppearanceRounded: hintButtonWidth += 3.0; break;
+ case MASShortcutViewAppearanceFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - BUTTON_FONT_SIZE); break;
+ default: break;
+ }
+ CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, 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) {
+ __weak MASShortcutView *weakSelf = self;
+ NSEventMask eventMask = (NSKeyDownMask | NSFlagsChangedMask);
+ eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
+
+ MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event];
+ if ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete)) {
+ // Delete shortcut
+ weakSelf.shortcutValue = nil;
+ weakSelf.recording = NO;
+ event = nil;
+ }
+ else if (shortcut.keyCode == kVK_Escape && !shortcut.modifierFlags) {
+ // Cancel recording
+ weakSelf.recording = NO;
+ event = nil;
+ }
+ else if (shortcut.shouldBypass) {
+ // Command + W, Command + Q, ESC should deactivate recorder
+ weakSelf.recording = NO;
+ }
+ else {
+ // Verify possible shortcut
+ if (shortcut.keyCodeString.length > 0) {
+ if (shortcut.valid) {
+ // Verify that shortcut is not used
+ NSError *error = nil;
+ if ([shortcut isTakenError:&error]) {
+ // Prevent cancel of recording when Alert window is key
+ [weakSelf activateResignObserver:NO];
+ [weakSelf activateEventMonitoring:NO];
+ NSString *format = NSLocalizedString(@"The key combination %@ cannot be used",
+ @"Title for alert when shortcut is already used");
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.alertStyle = NSCriticalAlertStyle;
+ alert.informativeText = [NSString stringWithFormat:format, shortcut];
+ alert.messageText = error.localizedDescription;
+ [alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")];
+
+ [alert runModal];
+ weakSelf.shortcutPlaceholder = nil;
+ [weakSelf activateResignObserver:YES];
+ [weakSelf activateEventMonitoring:YES];
+ }
+ else {
+ weakSelf.shortcutValue = shortcut;
+ weakSelf.recording = NO;
+ }
+ }
+ else {
+ // Key press with or without SHIFT is not valid input
+ NSBeep();
+ }
+ }
+ else {
+ // User is playing with modifier keys
+ weakSelf.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