aboutsummaryrefslogtreecommitdiffstats
path: root/Framework/MASShortcutView.m
diff options
context:
space:
mode:
authorTomáš Znamenáček2014-08-05 11:13:13 +0200
committerTomáš Znamenáček2015-01-07 15:05:11 +0100
commit377b44220f2a4a8b7ffc3eda9e93cf073e8a74da (patch)
treeb83239fd741773451bd9a75480ebe5a276c7d885 /Framework/MASShortcutView.m
parenta3a459b4e4e47bf18dccd5dc7f315389346e3d6c (diff)
downloadMASShortcut-377b44220f2a4a8b7ffc3eda9e93cf073e8a74da.tar.bz2
Repackaged the code as a framework and included the demo.
Packaging the code as a framework is mostly just a formality. It doesn’t really change much, it just turns the code into a regular component. What it does change is that the code now has its own Xcode settings, which could make compatibility easier in the long run. Including the demo in the main repository makes it easier to hack on the library, since you can try the changes immediately. It also shows how to bundle the framework into an app that uses it.
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