diff options
Diffstat (limited to 'Framework/MASShortcutView.m')
| -rw-r--r-- | Framework/MASShortcutView.m | 511 |
1 files changed, 511 insertions, 0 deletions
diff --git a/Framework/MASShortcutView.m b/Framework/MASShortcutView.m new file mode 100644 index 0000000..aace67e --- /dev/null +++ b/Framework/MASShortcutView.m @@ -0,0 +1,511 @@ +#import "MASShortcutView.h" +#import "MASShortcutValidator.h" + +NSString *const MASShortcutBinding = @"shortcutValue"; + +#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; +} + +#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]; + _shortcutValidator = [MASShortcutValidator sharedValidator]; + _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)setStyle:(MASShortcutViewStyle)newStyle +{ + if (_style != newStyle) { + _style = newStyle; + [self resetShortcutCellStyle]; + [self setNeedsDisplay:YES]; + } +} + +- (void)resetShortcutCellStyle +{ + switch (_style) { + case MASShortcutViewStyleDefault: { + _shortcutCell.bezelStyle = NSRoundRectBezelStyle; + break; + } + case MASShortcutViewStyleTexturedRect: { + _shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle; + break; + } + case MASShortcutViewStyleRounded: { + _shortcutCell.bezelStyle = NSRoundedBezelStyle; + break; + } + case MASShortcutViewStyleFlat: { + 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]; + [self propagateValue:shortcutValue forBinding:@"shortcutValue"]; + + 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 (_style) { + case MASShortcutViewStyleDefault: { + [_shortcutCell drawWithFrame:frame inView:self]; + break; + } + case MASShortcutViewStyleTexturedRect: { + [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self]; + break; + } + case MASShortcutViewStyleRounded: { + [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self]; + break; + } + case MASShortcutViewStyleFlat: { + [_shortcutCell drawWithFrame:frame inView:self]; + break; + } + } +} + +- (void)drawRect:(CGRect)dirtyRect +{ + if (self.shortcutValue) { + [self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(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:NSStringFromMASKeyCode(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.style) { + case MASShortcutViewStyleTexturedRect: hintButtonWidth += 2.0; break; + case MASShortcutViewStyleRounded: hintButtonWidth += 3.0; break; + case MASShortcutViewStyleFlat: 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) { + + // Create a shortcut from the event + MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event]; + + // If the shortcut is a plain Delete or Backspace, clear the current shortcut and cancel recording + if (!shortcut.modifierFlags && ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete))) { + weakSelf.shortcutValue = nil; + weakSelf.recording = NO; + event = nil; + } + + // If the shortcut is a plain Esc, cancel recording + else if (!shortcut.modifierFlags && shortcut.keyCode == kVK_Escape) { + weakSelf.recording = NO; + event = nil; + } + + // If the shortcut is Cmd-W or Cmd-Q, cancel recording and pass the event through + else if ((shortcut.modifierFlags == NSCommandKeyMask) && (shortcut.keyCode == kVK_ANSI_W || shortcut.keyCode == kVK_ANSI_Q)) { + weakSelf.recording = NO; + } + + else { + // Verify possible shortcut + if (shortcut.keyCodeString.length > 0) { + if ([_shortcutValidator isShortcutValid:shortcut]) { + // Verify that shortcut is not used + NSString *explanation = nil; + if ([_shortcutValidator isShortcutAlreadyTakenBySystem:shortcut explanation:&explanation]) { + // 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 = explanation; + alert.messageText = [NSString stringWithFormat:format, shortcut]; + [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]; + } +} + +#pragma mark Bindings + +// http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/ +-(void) propagateValue:(id)value forBinding:(NSString*)binding; +{ + NSParameterAssert(binding != nil); + + //WARNING: bindingInfo contains NSNull, so it must be accounted for + NSDictionary* bindingInfo = [self infoForBinding:binding]; + if(!bindingInfo) + return; //there is no binding + + //apply the value transformer, if one has been set + NSDictionary* bindingOptions = [bindingInfo objectForKey:NSOptionsKey]; + if(bindingOptions){ + NSValueTransformer* transformer = [bindingOptions valueForKey:NSValueTransformerBindingOption]; + if(!transformer || (id)transformer == [NSNull null]){ + NSString* transformerName = [bindingOptions valueForKey:NSValueTransformerNameBindingOption]; + if(transformerName && (id)transformerName != [NSNull null]){ + transformer = [NSValueTransformer valueTransformerForName:transformerName]; + } + } + + if(transformer && (id)transformer != [NSNull null]){ + if([[transformer class] allowsReverseTransformation]){ + value = [transformer reverseTransformedValue:value]; + } else { + NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", binding, __PRETTY_FUNCTION__); + } + } + } + + id boundObject = [bindingInfo objectForKey:NSObservedObjectKey]; + if(!boundObject || boundObject == [NSNull null]){ + NSLog(@"ERROR: NSObservedObjectKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__); + return; + } + + NSString* boundKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey]; + if(!boundKeyPath || (id)boundKeyPath == [NSNull null]){ + NSLog(@"ERROR: NSObservedKeyPathKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__); + return; + } + + [boundObject setValue:value forKeyPath:boundKeyPath]; +} + +@end |
