Files

1906 lines
67 KiB
Objective-C

//
// Copyright 2006 ShortcutRecorder Contributors
// CC BY 4.0
//
#import <limits.h>
#import <objc/runtime.h>
#import <os/trace.h>
#import <os/activity.h>
#import "SRRecorderControl.h"
#import "SRShortcutAction.h"
#import "SRKeyCodeTransformer.h"
#import "SRModifierFlagsTransformer.h"
#ifndef SR_DEBUG_DRAWING
#define SR_DEBUG_DRAWING DEBUG && 0
#endif // SR_DEBUG_DRAWING
const NSLayoutPriority SRRecorderControlLabelWidthPriority = NSLayoutPriorityDefaultHigh + 1;
typedef NS_ENUM(NSUInteger, _SRRecorderControlButtonTag)
{
_SRRecorderControlInvalidButtonTag = -1,
_SRRecorderControlCancelButtonTag = 0,
_SRRecorderControlClearButtonTag = 1,
_SRRecorderControlMainButtonTag = 2
};
static void *_SRStyleUserInterfaceLayoutDirectionObservingContext = &_SRStyleUserInterfaceLayoutDirectionObservingContext;
static void *_SRStyleAppearanceObservingContext = &_SRStyleAppearanceObservingContext;
static void *_SRStyleGuideObservingContext = &_SRStyleGuideObservingContext;
#define _SRIfRespondsGet(obj, sel, default) [obj respondsToSelector:@selector(sel)] ? [obj sel] : (default)
#define _SRIfRespondsGetProp(obj, sel, prop, default) [obj respondsToSelector:@selector(sel)] ? [[obj sel] prop] : (default)
@implementation SRRecorderControl
{
BOOL _isCompatibilityModeEnabled;
SRRecorderControlStyle *_style;
NSInvocation *_notifyStyle;
NSTrackingArea *_mainButtonTrackingArea;
NSTrackingArea *_cancelButtonTrackingArea;
NSTrackingArea *_clearButtonTrackingArea;
_SRRecorderControlButtonTag _mouseTrackingButtonTag;
NSToolTipTag _cancelButtonToolTipTag;
NSToolTipTag _clearButtonToolTipTag;
SRShortcut *_objectValue;
// +NSEvent.modifierFlags may change across run loop calls
// Extra care is needed to ensure that all methods will see the same flags.
NSEventModifierFlags _lastSeenModifierFlags;
BOOL _isLazilyInitializingStyle;
// Controls intrinsic width of the label.
NSLayoutConstraint *_labelWidthConstraint;
}
- (instancetype)initWithFrame:(NSRect)aFrameRect
{
self = [super initWithFrame:aFrameRect];
if (self)
{
[self initInternalState];
}
return self;
}
- (void)initInternalState
{
self.enabled = YES;
_allowsEmptyModifierFlags = NO;
_drawsASCIIEquivalentOfShortcut = YES;
_allowsEscapeToCancelRecording = YES;
_allowsDeleteToClearShortcutAndEndRecording = YES;
_allowedModifierFlags = SRCocoaModifierFlagsMask;
_requiredModifierFlags = 0;
_mouseTrackingButtonTag = _SRRecorderControlInvalidButtonTag;
_cancelButtonToolTipTag = NSIntegerMax;
_clearButtonToolTipTag = NSIntegerMax;
_pausesGlobalShortcutMonitorWhileRecording = YES;
_notifyStyle = [NSInvocation invocationWithMethodSignature:[SRRecorderControlStyle instanceMethodSignatureForSelector:@selector(recorderControlAppearanceDidChange:)]];
_notifyStyle.selector = @selector(recorderControlAppearanceDidChange:);
[_notifyStyle retainArguments];
self.translatesAutoresizingMaskIntoConstraints = NO;
[self setContentHuggingPriority:NSLayoutPriorityDefaultLow
forOrientation:NSLayoutConstraintOrientationHorizontal];
[self setContentHuggingPriority:NSLayoutPriorityRequired
forOrientation:NSLayoutConstraintOrientationVertical];
[self setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow
forOrientation:NSLayoutConstraintOrientationHorizontal];
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired
forOrientation:NSLayoutConstraintOrientationVertical];
}
- (void)dealloc
{
[NSNotificationCenter.defaultCenter removeObserver:self];
[NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self];
[NSObject cancelPreviousPerformRequestsWithTarget:_notifyStyle];
if ([_style respondsToSelector:@selector(preferredComponents)])
{
[_style removeObserver:self
forKeyPath:@"preferredComponents.userInterfaceLayoutDirection"
context:_SRStyleUserInterfaceLayoutDirectionObservingContext];
[_style removeObserver:self
forKeyPath:@"preferredComponents.appearance"
context:_SRStyleAppearanceObservingContext];
}
[_style removeObserver:self forKeyPath:@"labelDrawingGuide.frame" context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(backgroundDrawingGuide)])
[_style removeObserver:self forKeyPath:@"backgroundDrawingGuide.frame" context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(cancelButtonDrawingGuide)])
[_style removeObserver:self forKeyPath:@"cancelButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(clearButtonDrawingGuide)])
[_style removeObserver:self forKeyPath:@"clearButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext];
}
#pragma mark Properties
@dynamic style;
+ (BOOL)automaticallyNotifiesObserversOfValue
{
return NO;
}
+ (BOOL)automaticallyNotifiesObserversOfObjectValue
{
return NO;
}
+ (BOOL)automaticallyNotifiesObserversOfStringValue
{
return NO;
}
+ (BOOL)automaticallyNotifiesObserversOfAttributedStringValue
{
return NO;
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDictionaryValue
{
return [NSSet setWithObject:@"objectValue"];
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingStringValue
{
return [NSSet setWithObjects:@"objectValue", @"userInterfaceLayoutDirection", @"stringValueRespectsUserInterfaceLayoutDirection", nil];
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAttributedStringValue
{
return [NSSet setWithObject:@"stringValue"];
}
+ (NSSet<NSString *> *)keyPathsForValuesAffectingUserInterfaceLayoutDirection
{
return [NSSet setWithObject:@"style"];
}
- (void)setAllowedModifierFlags:(NSEventModifierFlags)newAllowedModifierFlags
requiredModifierFlags:(NSEventModifierFlags)newRequiredModifierFlags
allowsEmptyModifierFlags:(BOOL)newAllowsEmptyModifierFlags
{
newAllowedModifierFlags &= SRCocoaModifierFlagsMask;
newRequiredModifierFlags &= SRCocoaModifierFlagsMask;
if ((newAllowedModifierFlags & newRequiredModifierFlags) != newRequiredModifierFlags)
{
[NSException raise:NSInvalidArgumentException
format:@"Required flags (%lu) MUST be allowed (%lu)", newAllowedModifierFlags, newRequiredModifierFlags];
}
if (newAllowsEmptyModifierFlags && newRequiredModifierFlags != 0)
{
[NSException raise:NSInvalidArgumentException
format:@"Empty modifier flags MUST be disallowed if required modifier flags are not empty."];
}
if (newAllowedModifierFlags == _allowedModifierFlags &&
newRequiredModifierFlags == _requiredModifierFlags &&
newAllowsEmptyModifierFlags == _allowsEmptyModifierFlags)
{
return;
}
[self endRecording];
[self willChangeValueForKey:@"allowedModifierFlags"];
[self willChangeValueForKey:@"requiredModifierFlags"];
[self willChangeValueForKey:@"allowsEmptyModifierFlags"];
_allowedModifierFlags = newAllowedModifierFlags;
_requiredModifierFlags = newRequiredModifierFlags;
_allowsEmptyModifierFlags = newAllowsEmptyModifierFlags;
[self didChangeValueForKey:@"allowedModifierFlags"];
[self didChangeValueForKey:@"requiredModifierFlags"];
[self didChangeValueForKey:@"allowsEmptyModifierFlags"];
}
- (SRShortcut *)objectValue
{
if (_isCompatibilityModeEnabled)
return (id)_objectValue.dictionaryRepresentation;
else
return _objectValue;
}
- (void)setObjectValue:(SRShortcut *)newObjectValue
{
if (newObjectValue == _objectValue || [newObjectValue isEqual:_objectValue])
return;
[self willChangeValueForKey:@"objectValue"];
// Cocoa KVO and KVC frequently uses NSNull as object substituation of nil.
// SRRecorderControl expects either nil or valid object value, it's convenient
// to handle NSNull here and convert it into nil.
if ((NSNull *)newObjectValue == NSNull.null)
newObjectValue = nil;
// Backward compatibility with Shortcut Recorder 2
else if ([newObjectValue isKindOfClass:NSDictionary.class] && _objectValue == nil)
{
NSLog(@"WARNING: Shortcut Recroder 2 compatibility mode enabled. Getters of objectValue and NSValueBinding will return an instance of NSDictionary.");
_isCompatibilityModeEnabled = YES;
newObjectValue = [SRShortcut shortcutWithDictionary:(NSDictionary *)newObjectValue];
}
_objectValue = [newObjectValue copy];
[self didChangeValueForKey:@"objectValue"];
if (_isCompatibilityModeEnabled)
[self propagateValue:_objectValue.dictionaryRepresentation forBinding:NSValueBinding];
else
[self propagateValue:_objectValue forBinding:NSValueBinding];
if (!self.isRecording)
{
NSAccessibilityPostNotification(self, NSAccessibilityTitleChangedNotification);
NSAccessibilityPostNotification(self, NSAccessibilityValueChangedNotification);
[self setNeedsDisplayInRect:self.style.labelDrawingGuide.frame];
[self updateLabelConstraints];
}
}
- (NSDictionary<SRShortcutKey, id> *)dictionaryValue
{
return _objectValue.dictionaryRepresentation;
}
- (void)setDictionaryValue:(NSDictionary<SRShortcutKey, id> *)newDictionaryValue
{
self.objectValue = [SRShortcut shortcutWithDictionary:newDictionaryValue];
}
- (id)value
{
return self.objectValue;
}
- (void)setValue:(id)newValue
{
if (NSIsControllerMarker(newValue))
[NSException raise:NSInternalInconsistencyException format:@"SRRecorderControl's NSValueBinding does not support controller value markers."];
self.objectValue = newValue;
}
- (SRRecorderControlStyle *)style
{
if (_style == nil)
{
_isLazilyInitializingStyle = YES;
[self _setStyle:[self makeDefaultStyle]];
_isLazilyInitializingStyle = NO;
}
return _style;
}
- (void)setStyle:(SRRecorderControlStyle *)newStyle
{
if (newStyle == nil)
newStyle = [self makeDefaultStyle];
else if ([newStyle isEqual:_style])
return;
else
newStyle = [newStyle copy];
[self _setStyle:newStyle];
}
- (void)_setStyle:(SRRecorderControlStyle *)newStyle
{
[NSObject cancelPreviousPerformRequestsWithTarget:_notifyStyle];
if ([_style respondsToSelector:@selector(prepareForRemoval)])
[_style prepareForRemoval];
if ([_style respondsToSelector:@selector(preferredComponents)])
{
[_style removeObserver:self
forKeyPath:@"preferredComponents.userInterfaceLayoutDirection"
context:_SRStyleUserInterfaceLayoutDirectionObservingContext];
[_style removeObserver:self
forKeyPath:@"preferredComponents.appearance"
context:_SRStyleAppearanceObservingContext];
}
[_style removeObserver:self forKeyPath:@"labelDrawingGuide.frame" context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(backgroundDrawingGuide)])
[_style removeObserver:self forKeyPath:@"backgroundDrawingGuide.frame" context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(cancelButtonDrawingGuide)])
[_style removeObserver:self forKeyPath:@"cancelButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(clearButtonDrawingGuide)])
[_style removeObserver:self forKeyPath:@"clearButtonDrawingGuide.frame" context:_SRStyleGuideObservingContext];
_style = newStyle;
if ([_style respondsToSelector:@selector(prepareForRecorderControl:)])
[_style prepareForRecorderControl:self];
_labelWidthConstraint = [_style.labelDrawingGuide.widthAnchor constraintEqualToConstant:0.0];
_labelWidthConstraint.priority = NSLayoutPriorityDefaultHigh + 1;
_labelWidthConstraint.active = YES;
if ([_style respondsToSelector:@selector(preferredComponents)])
{
[_style addObserver:self
forKeyPath:@"preferredComponents.userInterfaceLayoutDirection"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior
context:_SRStyleUserInterfaceLayoutDirectionObservingContext];
[_style addObserver:self
forKeyPath:@"preferredComponents.appearance"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:_SRStyleAppearanceObservingContext];
}
[_style addObserver:self
forKeyPath:@"labelDrawingGuide.frame"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:_SRStyleGuideObservingContext];
if ([_style respondsToSelector:@selector(backgroundDrawingGuide)])
{
[_style addObserver:self
forKeyPath:@"backgroundDrawingGuide.frame"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:_SRStyleGuideObservingContext];
}
if ([_style respondsToSelector:@selector(cancelButtonDrawingGuide)])
{
[_style addObserver:self
forKeyPath:@"cancelButtonDrawingGuide.frame"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:_SRStyleGuideObservingContext];
}
if ([_style respondsToSelector:@selector(clearButtonDrawingGuide)])
{
[_style addObserver:self
forKeyPath:@"clearButtonDrawingGuide.frame"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:_SRStyleGuideObservingContext];
}
if (self.isRecording)
self.toolTip = _SRIfRespondsGet(self.style, recordingTooltip, SRLoc(@"Type shortcut"));
else
self.toolTip = _SRIfRespondsGet(self.style, normalTooltip, SRLoc(@"Click to record shortcut"));
}
- (NSBezierPath *)focusRingShape
{
NSRect focusRingFrame = _SRIfRespondsGetProp(self.style, backgroundDrawingGuide, frame, self.bounds);
NSEdgeInsets alignmentInsets = self.alignmentRectInsets;
NSEdgeInsets focusRingInsets = _SRIfRespondsGet(self.style, focusRingInsets, NSEdgeInsetsZero);
NSSize cornerRadius = _SRIfRespondsGet(self.style, focusRingCornerRadius, NSZeroSize);
focusRingFrame.origin.x += alignmentInsets.left + focusRingInsets.left;
focusRingFrame.origin.y += alignmentInsets.top + focusRingInsets.top;
focusRingFrame.size.width = fdim(focusRingFrame.size.width,
alignmentInsets.left + alignmentInsets.right + focusRingInsets.left + focusRingInsets.right);
focusRingFrame.size.height = fdim(focusRingFrame.size.height,
alignmentInsets.top + alignmentInsets.bottom + focusRingInsets.top + focusRingInsets.bottom);
return [NSBezierPath bezierPathWithRoundedRect:focusRingFrame
xRadius:cornerRadius.width
yRadius:cornerRadius.height];
}
- (BOOL)isMainButtonHighlighted
{
if (_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag)
{
NSPoint locationInView = [self convertPoint:self.window.mouseLocationOutsideOfEventStream
fromView:nil];
return [self mouse:locationInView inRect:self.bounds];
}
else
return NO;
}
- (BOOL)isCancelButtonHighlighted
{
if (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag)
{
NSPoint locationInView = [self convertPoint:self.window.mouseLocationOutsideOfEventStream
fromView:nil];
NSRect cancelButtonFrame = _SRIfRespondsGetProp(self.style,
cancelButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
cancelButtonDrawingGuide,
frame,
NSZeroRect));
return [self mouse:locationInView inRect:cancelButtonFrame];
}
else
return NO;
}
- (BOOL)isClearButtonHighlighted
{
if (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag)
{
NSPoint locationInView = [self convertPoint:self.window.mouseLocationOutsideOfEventStream
fromView:nil];
NSRect clearButtonFrame = _SRIfRespondsGetProp(self.style,
clearButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
clearButtonDrawingGuide,
frame,
NSZeroRect));
return [self mouse:locationInView inRect:clearButtonFrame];
}
else
return NO;
}
- (NSString *)drawingLabel
{
NSString *label = nil;
if (self.isRecording)
{
if (_lastSeenModifierFlags)
{
__auto_type layoutDirection = self.stringValueRespectsUserInterfaceLayoutDirection ? self.userInterfaceLayoutDirection : NSUserInterfaceLayoutDirectionLeftToRight;
label = [SRSymbolicModifierFlagsTransformer.sharedTransformer transformedValue:@(_lastSeenModifierFlags)
layoutDirection:layoutDirection];
}
else
label = self.stringValue;
if (!label.length)
label = _SRIfRespondsGet(self.style, noValueRecordingLabel, SRLoc(@"Type shortcut"));
}
else
{
label = self.stringValue;
if (!label.length)
label = _SRIfRespondsGet(self.style, noValueNormalLabel, SRLoc(@"Record Shortcut"));
}
return label;
}
- (NSDictionary *)drawingLabelAttributes
{
if (self.enabled)
{
if (self.isRecording)
return _SRIfRespondsGet(self.style, recordingLabelAttributes, nil);
else
return _SRIfRespondsGet(self.style, normalLabelAttributes, nil);
}
else
return _SRIfRespondsGet(self.style, disabledLabelAttributes, nil);
}
#pragma mark Methods
- (SRRecorderControlStyle *)makeDefaultStyle
{
return [SRRecorderControlStyle new];
}
- (BOOL)beginRecording
{
__block BOOL result = NO;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
__auto_type DelegateShouldBeginRecording = ^{
if ([self.delegate respondsToSelector:@selector(recorderControlShouldBeginRecording:)])
return [self.delegate recorderControlShouldBeginRecording:self];
else if ([self.delegate respondsToSelector:@selector(shortcutRecorderShouldBeginRecording:)])
return [self.delegate shortcutRecorderShouldBeginRecording:self];
else
return YES;
};
#pragma clang diagnostic pop
os_activity_initiate("-[SRRecorderControl beginRecording]", OS_ACTIVITY_FLAG_DEFAULT, ^{
if (!self.enabled)
{
result = NO;
return;
}
if (self.isRecording)
{
result = YES;
return;
}
BOOL shouldBeginRecording = DelegateShouldBeginRecording();
if (!shouldBeginRecording)
{
[self playAlert];
result = NO;
return;
}
if (![self.window makeFirstResponder:self])
{
[self playAlert];
result = NO;
return;
}
self->_lastSeenModifierFlags = NSEvent.modifierFlags & self.allowedModifierFlags;
[self willChangeValueForKey:@"isRecording"];
self->_isRecording = YES;
[self didChangeValueForKey:@"isRecording"];
self.needsDisplay = YES;
[self updateActiveConstraints];
[self updateLabelConstraints];
[self updateTrackingAreas];
self.toolTip = _SRIfRespondsGet(self.style, recordingTooltip, SRLoc(@"Type shortcut"));
if (self.pausesGlobalShortcutMonitorWhileRecording)
[SRGlobalShortcutMonitor.sharedMonitor pause];
NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding];
if (bindingInfo)
{
id controller = bindingInfo[NSObservedObjectKey];
if ([controller respondsToSelector:@selector(objectDidBeginEditing:)])
[controller objectDidBeginEditing:(id<NSEditor>) self];
}
if ([self.delegate respondsToSelector:@selector(recorderControlDidBeginRecording:)])
[self.delegate recorderControlDidBeginRecording:self];
NSAccessibilityPostNotificationWithUserInfo(self,
NSAccessibilityLayoutChangedNotification,
@{NSAccessibilityUIElementsKey: @[self]});
NSAccessibilityPostNotification(self, NSAccessibilityTitleChangedNotification);
result = YES;
});
return result;
}
- (void)endRecording
{
if (!self.isRecording)
return;
os_activity_initiate("-[SRRecorderControl endRecording]", OS_ACTIVITY_FLAG_DEFAULT, ^{
[self endRecordingWithObjectValue:self->_objectValue];
});
}
- (void)clearAndEndRecording
{
if (!self.isRecording)
return;
os_activity_initiate("-[SRRecorderControl clearAndEndRecording]", OS_ACTIVITY_FLAG_DEFAULT, ^{
[self endRecordingWithObjectValue:nil];
});
}
- (void)endRecordingWithObjectValue:(SRShortcut *)anObjectValue
{
if (!self.isRecording)
return;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
__auto_type DelegateDidEndRecording = ^{
if ([self.delegate respondsToSelector:@selector(recorderControlDidEndRecording:)])
[self.delegate recorderControlDidEndRecording:self];
else if ([self.delegate respondsToSelector:@selector(shortcutRecorderDidEndRecording:)])
[self.delegate shortcutRecorderDidEndRecording:self];
};
#pragma clang diagnostic pop
os_activity_initiate("-[SRRecorderControl endRecordingWithObjectValue:]", OS_ACTIVITY_FLAG_IF_NONE_PRESENT, ^{
[self willChangeValueForKey:@"isRecording"];
self->_isRecording = NO;
[self didChangeValueForKey:@"isRecording"];
self.objectValue = anObjectValue;
self->_lastSeenModifierFlags = 0;
self.needsDisplay = YES;
[self updateActiveConstraints];
[self updateLabelConstraints];
[self updateTrackingAreas];
self.toolTip = _SRIfRespondsGet(self.style, normalTooltip, SRLoc(@"Click to record shortcut"));
if (self.pausesGlobalShortcutMonitorWhileRecording)
[SRGlobalShortcutMonitor.sharedMonitor resume];
NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding];
if (bindingInfo)
{
id controller = bindingInfo[NSObservedObjectKey];
if ([controller respondsToSelector:@selector(objectDidEndEditing:)])
[controller objectDidEndEditing:(id<NSEditor>)self];
}
if (self.window.firstResponder == self && !self.canBecomeKeyView)
[self.window makeFirstResponder:nil];
DelegateDidEndRecording();
[self sendAction:self.action to:self.target];
NSAccessibilityPostNotificationWithUserInfo(self,
NSAccessibilityLayoutChangedNotification,
@{NSAccessibilityUIElementsKey: @[self]});
NSAccessibilityPostNotification(self, NSAccessibilityTitleChangedNotification);
});
}
- (void)updateActiveConstraints
{
[NSLayoutConstraint activateConstraints:self.style.alwaysConstraints];
if (self.isRecording && _objectValue)
{
[NSLayoutConstraint deactivateConstraints:self.style.displayingConstraints];
[NSLayoutConstraint deactivateConstraints:self.style.recordingWithNoValueConstraints];
[NSLayoutConstraint activateConstraints:self.style.recordingWithValueConstraints];
}
else if (self.isRecording)
{
[NSLayoutConstraint deactivateConstraints:self.style.displayingConstraints];
[NSLayoutConstraint deactivateConstraints:self.style.recordingWithValueConstraints];
[NSLayoutConstraint activateConstraints:self.style.recordingWithNoValueConstraints];
}
else
{
[NSLayoutConstraint deactivateConstraints:self.style.recordingWithNoValueConstraints];
[NSLayoutConstraint deactivateConstraints:self.style.recordingWithValueConstraints];
[NSLayoutConstraint activateConstraints:self.style.displayingConstraints];
}
}
- (void)updateLabelConstraints
{
NSString *label = self.drawingLabel;
NSDictionary *labelAttributes = self.drawingLabelAttributes;
CGFloat labelWidth = NSWidth([label boundingRectWithSize:self.style.alignmentGuide.frame.size
options:0
attributes:labelAttributes
context:nil]);
// Extra 2 points to avoid clipping of smoothing pixels.
_labelWidthConstraint.constant = ceil(MAX(labelWidth,
[labelAttributes[SRMinimalDrawableWidthAttributeName] doubleValue]) + 2.0);
}
- (void)drawBackground:(NSRect)aDirtyRect
{
NSRect backgroundFrame = [self centerScanRect:_SRIfRespondsGetProp(self.style, backgroundDrawingGuide, frame, self.bounds)];
if (NSIsEmptyRect(backgroundFrame) || ![self needsToDrawRect:backgroundFrame])
return;
NSImage *left = nil;
NSImage *center = nil;
NSImage *right = nil;
[NSGraphicsContext saveGraphicsState];
if (self.isRecording)
{
left = _SRIfRespondsGet(self.style, bezelRecordingLeft, nil);
center = _SRIfRespondsGet(self.style, bezelRecordingCenter, nil);
right = _SRIfRespondsGet(self.style, bezelRecordingRight, nil);
}
else
{
if (self.isMainButtonHighlighted)
{
left = _SRIfRespondsGet(self.style, bezelPressedLeft, nil);
center = _SRIfRespondsGet(self.style, bezelPressedCenter, nil);
right = _SRIfRespondsGet(self.style, bezelPressedRight, nil);
}
else if (self.enabled)
{
left = _SRIfRespondsGet(self.style, bezelNormalLeft, nil);
center = _SRIfRespondsGet(self.style, bezelNormalCenter, nil);
right = _SRIfRespondsGet(self.style, bezelNormalRight, nil);
}
else
{
left = _SRIfRespondsGet(self.style, bezelDisabledLeft, nil);
center = _SRIfRespondsGet(self.style, bezelDisabledCenter, nil);
right = _SRIfRespondsGet(self.style, bezelDisabledRight, nil);
}
}
if (left && center && right)
{
os_trace_debug("#Developer drawing background using images");
NSDrawThreePartImage(backgroundFrame, left, center, right, NO, NSCompositeSourceOver, 1.0, self.isFlipped);
}
else
{
os_trace_debug("#Developer drawing background using color");
if (self.isOpaque)
[NSColor.windowBackgroundColor setFill];
else
[NSColor.clearColor setFill];
NSRectFill(NSIntersectionRect(backgroundFrame, aDirtyRect));
}
[NSGraphicsContext restoreGraphicsState];
}
- (void)drawInterior:(NSRect)aDirtyRect
{
[self drawLabel:aDirtyRect];
if (self.isRecording)
{
[self drawCancelButton:aDirtyRect];
if (_objectValue)
[self drawClearButton:aDirtyRect];
}
}
- (void)drawLabel:(NSRect)aDirtyRect
{
NSRect labelFrame = self.style.labelDrawingGuide.frame;
#if SR_DEBUG_DRAWING
[NSColor.systemRedColor set];
NSRectFill(labelFrame);
#endif
if (NSIsEmptyRect(labelFrame) || ![self needsToDrawRect:labelFrame])
return;
NSString *label = self.drawingLabel;
NSDictionary *labelAttributes = self.drawingLabelAttributes;
CGFloat baselineOffset = _SRIfRespondsGet(self.style, baselineLayoutOffsetFromBottom, self.style.baselineDrawingOffsetFromBottom);
labelFrame.origin.y = NSMaxY(labelFrame) - baselineOffset;
labelFrame = [self backingAlignedRect:labelFrame options:NSAlignRectFlipped |
NSAlignMinXOutward |
NSAlignMinYOutward |
NSAlignMaxXInward |
NSAlignMaxYInward];
[NSGraphicsContext saveGraphicsState];
#if SR_DEBUG_DRAWING
[[NSColor.systemRedColor highlightWithLevel:0.5] set];
NSRectFill(labelFrame);
#endif
CGFloat minWidth = [labelAttributes[SRMinimalDrawableWidthAttributeName] doubleValue];
if (labelFrame.size.width >= minWidth)
{
if (!self.isOpaque && self.style.isLabelDrawingFrameOpaque)
CGContextSetShouldSmoothFonts(NSGraphicsContext.currentContext.CGContext, true);
[label drawWithRect:labelFrame options:0 attributes:labelAttributes context:nil];
}
[NSGraphicsContext restoreGraphicsState];
}
- (void)drawCancelButton:(NSRect)aDirtyRect
{
NSRect cancelButtonFrame = [self centerScanRect:_SRIfRespondsGetProp(self.style, cancelButtonDrawingGuide, frame, NSZeroRect)];
#if SR_DEBUG_DRAWING
[NSColor.systemBlueColor set];
NSRectFill([self centerScanRect:_SRIfRespondsGetProp(self.style, cancelButtonLayoutGuide, frame, NSZeroRect)]);
[[NSColor.systemBlueColor highlightWithLevel:0.5] set];
NSRectFill(cancelButtonFrame);
#endif
if (NSIsEmptyRect(cancelButtonFrame) || ![self needsToDrawRect:cancelButtonFrame])
return;
NSImage *image = self.isCancelButtonHighlighted ? _SRIfRespondsGet(self.style, cancelButtonPressed, nil) : _SRIfRespondsGet(self.style, cancelButton, nil);
if (!image)
return;
[NSGraphicsContext saveGraphicsState];
[image drawInRect:cancelButtonFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil];
[NSGraphicsContext restoreGraphicsState];
}
- (void)drawClearButton:(NSRect)aDirtyRect
{
NSRect clearButtonFrame = [self centerScanRect:_SRIfRespondsGetProp(self.style, clearButtonDrawingGuide, frame, NSZeroRect)];
#if SR_DEBUG_DRAWING
[NSColor.systemGreenColor set];
NSRectFill([self centerScanRect:_SRIfRespondsGetProp(self.style, clearButtonLayoutGuide, frame, NSZeroRect)]);
[[NSColor.systemGreenColor highlightWithLevel:0.5] set];
NSRectFill(clearButtonFrame);
#endif
if (NSIsEmptyRect(clearButtonFrame) || ![self needsToDrawRect:clearButtonFrame])
return;
NSImage *image = self.isClearButtonHighlighted ? _SRIfRespondsGet(self.style, clearButtonPressed, nil) : _SRIfRespondsGet(self.style, clearButton, nil);
if (!image)
return;
[NSGraphicsContext saveGraphicsState];
[image drawInRect:clearButtonFrame fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil];
[NSGraphicsContext restoreGraphicsState];
}
- (BOOL)areModifierFlagsValid:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode
{
aModifierFlags &= SRCocoaModifierFlagsMask;
__block BOOL allowModifierFlags = YES;
os_activity_initiate("-[SRRecorderControl areModifierFlagsValid:forKeyCode:]", OS_ACTIVITY_FLAG_DEFAULT, ^{
if ((aModifierFlags & self.requiredModifierFlags) != self.requiredModifierFlags)
allowModifierFlags = NO;
else
allowModifierFlags = [self areModifierFlagsAllowed:aModifierFlags forKeyCode:aKeyCode];
});
return allowModifierFlags;
}
- (BOOL)areModifierFlagsAllowed:(NSEventModifierFlags)aModifierFlags forKeyCode:(SRKeyCode)aKeyCode
{
aModifierFlags &= SRCocoaModifierFlagsMask;
__block BOOL allowModifierFlags = YES;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
__auto_type DelegateShouldUnconditionallyAllowModifierFlags = ^{
if ([self.delegate respondsToSelector:@selector(recorderControl:shouldUnconditionallyAllowModifierFlags:forKeyCode:)])
{
return [self.delegate recorderControl:self shouldUnconditionallyAllowModifierFlags:aModifierFlags forKeyCode:aKeyCode];
}
else if ([self.delegate respondsToSelector:@selector(shortcutRecorder:shouldUnconditionallyAllowModifierFlags:forKeyCode:)])
{
return [self.delegate shortcutRecorder:self shouldUnconditionallyAllowModifierFlags:aModifierFlags forKeyCode:aKeyCode];
}
else
return NO;
};
#pragma clang diagnostic pop
os_activity_initiate("-[SRRecorderControl areModifierFlagsAllowed:forKeyCode:]", OS_ACTIVITY_FLAG_IF_NONE_PRESENT, ^{
if ((aModifierFlags == 0 && !self.allowsEmptyModifierFlags) ||
((aModifierFlags & self.allowedModifierFlags) != aModifierFlags))
{
allowModifierFlags = DelegateShouldUnconditionallyAllowModifierFlags();
}
});
return allowModifierFlags;
}
- (void)playAlert
{
NSBeep();
}
- (void)propagateValue:(id)aValue forBinding:(NSString *)aBinding
{
NSParameterAssert(aBinding != nil);
NSDictionary* bindingInfo = [self infoForBinding:aBinding];
if(!bindingInfo || (id)bindingInfo == NSNull.null)
return;
NSObject *boundObject = bindingInfo[NSObservedObjectKey];
if(!boundObject || (id)boundObject == NSNull.null)
[NSException raise:NSInternalInconsistencyException format:@"NSObservedObjectKey MUST NOT be nil for binding \"%@\"", aBinding];
NSString* boundKeyPath = bindingInfo[NSObservedKeyPathKey];
if(!boundKeyPath || (id)boundKeyPath == NSNull.null)
[NSException raise:NSInternalInconsistencyException format:@"NSObservedKeyPathKey MUST NOT be nil for binding \"%@\"", aBinding];
NSDictionary* bindingOptions = bindingInfo[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])
aValue = [transformer reverseTransformedValue:aValue];
else
NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", aBinding, __PRETTY_FUNCTION__);
}
}
if (!_isCompatibilityModeEnabled &&
([boundObject isKindOfClass:NSUserDefaults.class] || [boundObject isKindOfClass:NSUserDefaultsController.class]) &&
[aValue isKindOfClass:SRShortcut.class])
{
os_trace_error("#Error The control is bound to NSUserDefaults but is not transformed into an allowed CFPreferences value");
NSLog(@"WARNING: Shortcut Recroder 2 compatibility mode enabled. Getters of objectValue and NSValueBinding will return an instance of NSDictionary.");
_isCompatibilityModeEnabled = YES;
aValue = [aValue dictionaryRepresentation];
}
[boundObject setValue:aValue forKeyPath:boundKeyPath];
}
- (void)controlTintDidChange:(NSNotification *)aNotification
{
[self scheduleControlViewAppearanceDidChange:aNotification];
}
- (void)accessibilityDisplayOptionsDidChange:(NSNotification *)aNotification
{
[self scheduleControlViewAppearanceDidChange:aNotification];
}
- (void)scheduleControlViewAppearanceDidChange:(nullable id)aReason
{
if (_notifyStyle == nil || ![_style respondsToSelector:@selector(recorderControlAppearanceDidChange:)])
// recorderControlAppearanceDidChange: is called whenever _style is created.
return;
[NSObject cancelPreviousPerformRequestsWithTarget:_notifyStyle];
[_notifyStyle setArgument:&aReason atIndex:2];
[_notifyStyle performSelector:@selector(invokeWithTarget:) withObject:_style afterDelay:0.0 inModes:@[NSRunLoopCommonModes]];
}
- (BOOL)canCaptureKeyEvent
{
if (!self.enabled)
{
os_trace_debug("The control is disabled");
return NO;
}
else if (self.window.firstResponder != self)
{
os_trace_debug("The control is not the first responder");
return NO;
}
else if (self->_mouseTrackingButtonTag != _SRRecorderControlInvalidButtonTag)
{
os_trace_debug("The control is tracking %lu", self->_mouseTrackingButtonTag);
return NO;
}
else
return YES;
}
- (BOOL)canEndRecordingWithObjectValue:(nullable SRShortcut *)aShortcut
{
__block BOOL result = NO;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
__auto_type DelegateCanRecordShortcut = ^(SRShortcut *aShortcut){
if ([self.delegate respondsToSelector:@selector(recorderControl:canRecordShortcut:)])
return [self.delegate recorderControl:self canRecordShortcut:aShortcut];
else if ([self.delegate respondsToSelector:@selector(shortcutRecorder:canRecordShortcut:)])
return [self.delegate shortcutRecorder:self canRecordShortcut:aShortcut.dictionaryRepresentation];
else if ([self.delegate respondsToSelector:@selector(control:isValidObject:)])
return [self.delegate control:self isValidObject:aShortcut];
else
return YES;
};
#pragma clang diagnostic pop
os_activity_initiate("-[SRRecorderControl canEndRecordingWithObjectValue:]", OS_ACTIVITY_FLAG_DEFAULT, ^{
if ([self areModifierFlagsValid:aShortcut.modifierFlags forKeyCode:aShortcut.keyCode])
{
if (DelegateCanRecordShortcut(aShortcut))
{
os_trace_debug("Valid and accepted shortcut");
result = YES;
}
else
{
os_trace_debug("Delegate rejected");
result = NO;
}
}
else
{
os_trace_debug("Modifier flags %lu rejected", aShortcut.modifierFlags);
result = NO;
}
});
return result;
}
#pragma mark NSAccessibility
- (BOOL)isAccessibilityElement
{
return YES;
}
- (BOOL)isAccessibilityEnabled
{
return self.isEnabled;
}
- (NSString *)accessibilityLabel
{
if (self.isRecording)
{
return [SRLiteralModifierFlagsTransformer.sharedTransformer transformedValue:@(_lastSeenModifierFlags)
layoutDirection:NSUserInterfaceLayoutDirectionLeftToRight];
}
else
return super.accessibilityLabel;
}
- (id)accessibilityValue
{
if (self.isRecording)
return super.accessibilityValue;
else
{
if (!_objectValue)
return SRLoc(@"Empty");
NSString *flags = [SRLiteralModifierFlagsTransformer.sharedTransformer transformedValue:@(_objectValue.modifierFlags)
layoutDirection:NSUserInterfaceLayoutDirectionLeftToRight];
SRKeyCodeTransformer *transformer = nil;
if (self.drawsASCIIEquivalentOfShortcut)
transformer = SRASCIILiteralKeyCodeTransformer.sharedTransformer;
else
transformer = SRLiteralKeyCodeTransformer.sharedTransformer;
NSString *code = [transformer transformedValue:@(_objectValue.keyCode)];
if (!code)
code = [NSString stringWithFormat:@"%hu", _objectValue.keyCode];
if (flags.length > 0)
return [NSString stringWithFormat:@"%@-%@", flags, code];
else
return [NSString stringWithFormat:@"%@", code];
}
}
- (NSString *)accessibilityHelp
{
return nil;
}
- (NSAccessibilityRole)accessibilityRole
{
return NSAccessibilityButtonRole;
}
- (NSString *)accessibilityRoleDescription
{
if (self.isRecording)
return SRLoc(@"Type shortcut").localizedLowercaseString;
else
return SRLoc(@"Shortcut").localizedLowercaseString;
}
- (id)accessibilityHitTest:(NSPoint)aPoint
{
// NSControl's implementation relies on its cell which is nil for SRRecorderControl.
return self;
}
- (BOOL)accessibilityPerformPress
{
return [self beginRecording];
}
- (BOOL)accessibilityPerformCancel
{
if (self.isRecording)
{
[self endRecording];
return YES;
}
else
return NO;
}
- (BOOL)accessibilityPerformDelete
{
if (self.isRecording && _objectValue)
{
[self clearAndEndRecording];
return YES;
}
else
return NO;
}
#pragma mark NSEditor
- (BOOL)commitEditing
{
// Shortcuts recording is atomic (either all or nothing) and there are no pending changes.
[self discardEditing];
return YES;
}
- (void)commitEditingWithDelegate:(id)aDelegate didCommitSelector:(SEL)aDidCommitSelector contextInfo:(void *)aContextInfo
{
BOOL isEditingCommited = [self commitEditing];
// See AppKit's __NSSendCommitEditingSelector
NSInvocation *i = [NSInvocation invocationWithMethodSignature:[aDelegate methodSignatureForSelector:aDidCommitSelector]];
[i setSelector:aDidCommitSelector];
[i setArgument:(void*)&self atIndex:2];
[i setArgument:&isEditingCommited atIndex:3];
[i setArgument:&aContextInfo atIndex:4];
[i retainArguments];
[i performSelector:@selector(invokeWithTarget:) withObject:aDelegate afterDelay:0 inModes:@[NSRunLoopCommonModes]];
}
- (BOOL)commitEditingAndReturnError:(NSError * __autoreleasing *)outError
{
return [self commitEditing];
}
- (void)discardEditing
{
[self endRecording];
}
#pragma mark NSNibLoading
- (void)prepareForInterfaceBuilder
{
[super prepareForInterfaceBuilder];
self.objectValue = [SRShortcut shortcutWithCode:0
modifierFlags:NSEventModifierFlagControl | NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand
characters:@""
charactersIgnoringModifiers:@"a"];
}
#pragma mark NSViewToolTipOwner
- (NSString *)view:(NSView *)aView stringForToolTip:(NSToolTipTag)aTag point:(NSPoint)aPoint userData:(void *)aData
{
if (aTag == _cancelButtonToolTipTag)
return _SRIfRespondsGet(self.style, cancelButtonTooltip, _objectValue != nil ? SRLoc(@"Use old shortcut") : SRLoc(@"Cancel recording"));
else if (aTag == _clearButtonToolTipTag)
return _SRIfRespondsGet(self.style, clearButtonTooltip, SRLoc(@"Delete current shortcut"));
else
return [super view:aView stringForToolTip:aTag point:aPoint userData:aData];
}
#pragma mark NSCoding
- (instancetype)initWithCoder:(NSCoder *)aCoder
{
// Since Xcode 6.x, user can configure xib to Prefer Coder.
// In that case view will be instantiated with initWithCoder.
//
// awakeFromNib cannot be used to set up defaults for IBDesignable,
// because at the time it's called, it's impossible to know whether properties
// were set by a user in xib or they are compilation-time defaults.
self = [super initWithCoder:aCoder];
if (self)
{
[self initInternalState];
}
return self;
}
#pragma mark NSControl
@dynamic enabled;
@synthesize refusesFirstResponder = _refusesFirstResponder;
@synthesize tag = _tag;
+ (Class)cellClass
{
return nil;
}
- (NSAttributedString *)attributedStringValue
{
return [[NSAttributedString alloc] initWithString:self.stringValue];
}
- (void)setAttributedStringValue:(NSAttributedString *)newAttributedStringValue
{
[self setObjectValue:[SRShortcut shortcutWithKeyEquivalent:newAttributedStringValue.string]];
}
- (NSString *)stringValue
{
if (!_objectValue)
return SRLoc(@"");
__auto_type layoutDirection = self.stringValueRespectsUserInterfaceLayoutDirection ? self.userInterfaceLayoutDirection : NSUserInterfaceLayoutDirectionLeftToRight;
NSString *flags = [SRSymbolicModifierFlagsTransformer.sharedTransformer transformedValue:@(_objectValue.modifierFlags)
layoutDirection:layoutDirection];
SRKeyCodeTransformer *transformer = nil;
if (self.drawsASCIIEquivalentOfShortcut)
transformer = SRASCIILiteralKeyCodeTransformer.sharedTransformer;
else
transformer = SRLiteralKeyCodeTransformer.sharedTransformer;
NSString *code = [transformer transformedValue:@(_objectValue.keyCode)
withImplicitModifierFlags:nil
explicitModifierFlags:@(_objectValue.modifierFlags)
layoutDirection:layoutDirection];
if (!code)
code = [NSString stringWithFormat:@"<%hu>", _objectValue.keyCode];
if (layoutDirection == NSUserInterfaceLayoutDirectionRightToLeft)
return [NSString stringWithFormat:@"%@%@", code, flags];
else
return [NSString stringWithFormat:@"%@%@", flags, code];
}
- (void)setStringValue:(NSString *)newStringValue
{
[self setObjectValue:[SRShortcut shortcutWithKeyEquivalent:newStringValue]];
}
- (BOOL)isHighlighted
{
return self.isMainButtonHighlighted;
}
- (BOOL)abortEditing
{
[self endRecording];
return NO;
}
#pragma mark NSView
+ (BOOL)requiresConstraintBasedLayout
{
return YES;
}
- (BOOL)isOpaque
{
return self.style.isOpaque;
}
- (BOOL)allowsVibrancy
{
return self.style.allowsVibrancy;
}
- (BOOL)isFlipped
{
return YES;
}
- (void)setUserInterfaceLayoutDirection:(NSUserInterfaceLayoutDirection)newUserInterfaceLayoutDirection
{
NSNumber *currentValue = objc_getAssociatedObject(self, @selector(userInterfaceLayoutDirection));
if (currentValue && currentValue.integerValue == newUserInterfaceLayoutDirection)
return;
objc_setAssociatedObject(self,
@selector(userInterfaceLayoutDirection),
@(newUserInterfaceLayoutDirection),
OBJC_ASSOCIATION_RETAIN);
[self scheduleControlViewAppearanceDidChange:nil];
}
- (NSUserInterfaceLayoutDirection)userInterfaceLayoutDirection
{
// NSView uses associated objects to track whether default value was overridden.
// Here the lookup order is altered in the following way
// 1. View's own value
// 2. Style's value
// 3. View's default value that falls back to NSWindow and then NSApp
NSNumber *currentValue = objc_getAssociatedObject(self, @selector(userInterfaceLayoutDirection));
if (currentValue)
return currentValue.integerValue;
// If there is no style yet, return macOS's default value.
if (!_isLazilyInitializingStyle && [self.style respondsToSelector:@selector(preferredComponents)])
{
__auto_type layoutDirection = self.style.preferredComponents.layoutDirection;
if (layoutDirection != SRRecorderControlStyleComponentsLayoutDirectionUnspecified)
return SRRecorderControlStyleComponentsLayoutDirectionToSystem(layoutDirection);
}
return super.userInterfaceLayoutDirection;
}
- (void)drawRect:(NSRect)aDirtyRect
{
[self drawBackground:aDirtyRect];
[self drawInterior:aDirtyRect];
}
- (void)drawFocusRingMask
{
if (self.enabled && self.window.firstResponder == self)
[self.focusRingShape fill];
}
- (NSRect)focusRingMaskBounds
{
if (self.enabled && self.window.firstResponder == self)
return self.focusRingShape.bounds;
else
return NSZeroRect;
}
- (NSEdgeInsets)alignmentRectInsets
{
return self.style.alignmentRectInsets;
}
- (NSSize)intrinsicContentSize
{
return self.style.intrinsicContentSize;
}
- (CGFloat)baselineOffsetFromBottom
{
return _SRIfRespondsGet(self.style, baselineLayoutOffsetFromBottom, self.style.baselineDrawingOffsetFromBottom);
}
- (CGFloat)firstBaselineOffsetFromTop
{
return self.style.alignmentGuide.frame.size.height - self.baselineOffsetFromBottom;
}
- (void)updateTrackingAreas
{
static const NSTrackingAreaOptions TrackingOptions = NSTrackingMouseEnteredAndExited | NSTrackingActiveWhenFirstResponder | NSTrackingEnabledDuringMouseDrag;
NSRect cancelButtonFrame = _SRIfRespondsGetProp(self.style,
cancelButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
cancelButtonDrawingGuide,
frame,
NSZeroRect));
NSRect clearButtonFrame = _SRIfRespondsGetProp(self.style,
clearButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
clearButtonDrawingGuide,
frame,
NSZeroRect));
if (_mainButtonTrackingArea)
[self removeTrackingArea:_mainButtonTrackingArea];
_mainButtonTrackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
options:TrackingOptions
owner:self
userInfo:nil];
[self addTrackingArea:_mainButtonTrackingArea];
if (_cancelButtonTrackingArea)
{
[self removeTrackingArea:_cancelButtonTrackingArea];
_cancelButtonTrackingArea = nil;
}
if (_clearButtonTrackingArea)
{
[self removeTrackingArea:_clearButtonTrackingArea];
_clearButtonTrackingArea = nil;
}
if (_cancelButtonToolTipTag != NSIntegerMax)
{
[self removeToolTip:_cancelButtonToolTipTag];
_cancelButtonToolTipTag = NSIntegerMax;
}
if (_clearButtonToolTipTag != NSIntegerMax)
{
[self removeToolTip:_clearButtonToolTipTag];
_clearButtonToolTipTag = NSIntegerMax;
}
if (self.isRecording)
{
if (!NSIsEmptyRect(cancelButtonFrame))
{
_cancelButtonTrackingArea = [[NSTrackingArea alloc] initWithRect:cancelButtonFrame
options:TrackingOptions
owner:self
userInfo:nil];
[self addTrackingArea:_cancelButtonTrackingArea];
// Since this method is used to set up tracking rects of aux buttons, the rest of the code is aware
// it should be called whenever geometry or apperance changes. Therefore it's a good place to set up tooltip rects.
_cancelButtonToolTipTag = [self addToolTipRect:_cancelButtonTrackingArea.rect owner:self userData:NULL];
}
if (_objectValue && !NSIsEmptyRect(clearButtonFrame))
{
_clearButtonTrackingArea = [[NSTrackingArea alloc] initWithRect:clearButtonFrame
options:TrackingOptions
owner:self
userInfo:nil];
[self addTrackingArea:_clearButtonTrackingArea];
_clearButtonToolTipTag = [self addToolTipRect:_clearButtonTrackingArea.rect owner:self userData:NULL];
}
}
[super updateTrackingAreas];
}
- (void)updateConstraints
{
[self updateActiveConstraints];
[self updateLabelConstraints];
[super updateConstraints];
}
- (void)prepareForReuse
{
[self endRecording];
[super prepareForReuse];
}
- (void)viewWillMoveToWindow:(NSWindow *)aWindow
{
if (self.window)
{
[NSNotificationCenter.defaultCenter removeObserver:self
name:NSWindowDidResignKeyNotification
object:self.window];
[NSNotificationCenter.defaultCenter removeObserver:self
name:NSControlTintDidChangeNotification
object:NSApp];
[NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self
name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
object:nil];
}
if (aWindow)
{
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(endRecording)
name:NSWindowDidResignKeyNotification
object:aWindow];
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(controlTintDidChange:)
name:NSControlTintDidChangeNotification
object:NSApp];
[NSWorkspace.sharedWorkspace.notificationCenter addObserver:self
selector:@selector(accessibilityDisplayOptionsDidChange:) name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification
object:nil];
}
[super viewWillMoveToWindow:aWindow];
}
- (void)viewDidChangeBackingProperties
{
[super viewDidChangeBackingProperties];
[self scheduleControlViewAppearanceDidChange:nil];
}
- (void)viewDidChangeEffectiveAppearance
{
[super viewDidChangeEffectiveAppearance];
[self scheduleControlViewAppearanceDidChange:nil];
}
#pragma mark NSResponder
- (BOOL)acceptsFirstResponder
{
return self.enabled && !self.refusesFirstResponder;
}
- (BOOL)resignFirstResponder
{
[self endRecording];
_mouseTrackingButtonTag = _SRRecorderControlInvalidButtonTag;
return [super resignFirstResponder];
}
- (BOOL)acceptsFirstMouse:(NSEvent *)anEvent
{
return YES;
}
- (BOOL)canBecomeKeyView
{
// SRRecorderControl uses the button metaphor, but buttons cannot become key unless
// Full Keyboard Access is enabled. Respect this.
return super.canBecomeKeyView && NSApp.fullKeyboardAccessEnabled;
}
- (BOOL)needsPanelToBecomeKey
{
return YES;
}
- (void)mouseDown:(NSEvent *)anEvent
{
if (!self.enabled)
{
[super mouseDown:anEvent];
return;
}
NSPoint locationInView = [self convertPoint:anEvent.locationInWindow fromView:nil];
NSRect cancelButtonFrame = _SRIfRespondsGetProp(self.style,
cancelButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
cancelButtonDrawingGuide,
frame,
NSZeroRect));
NSRect clearButtonFrame = _SRIfRespondsGetProp(self.style,
clearButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
clearButtonDrawingGuide,
frame,
NSZeroRect));
if (self.isRecording)
{
if ([self mouse:locationInView inRect:cancelButtonFrame])
{
_mouseTrackingButtonTag = _SRRecorderControlCancelButtonTag;
[self setNeedsDisplayInRect:_SRIfRespondsGetProp(self.style, cancelButtonDrawingGuide, frame, NSZeroRect)];
}
else if ([self mouse:locationInView inRect:clearButtonFrame])
{
_mouseTrackingButtonTag = _SRRecorderControlClearButtonTag;
[self setNeedsDisplayInRect:_SRIfRespondsGetProp(self.style, clearButtonLayoutGuide, frame, NSZeroRect)];
}
else
[super mouseDown:anEvent];
}
else if ([self mouse:locationInView inRect:self.bounds])
{
_mouseTrackingButtonTag = _SRRecorderControlMainButtonTag;
self.needsDisplay = YES;
}
else
[super mouseDown:anEvent];
}
- (void)mouseUp:(NSEvent *)anEvent
{
if (!self.enabled)
{
[super mouseUp:anEvent];
return;
}
NSRect cancelButtonFrame = _SRIfRespondsGetProp(self.style,
cancelButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
cancelButtonDrawingGuide,
frame,
NSZeroRect));
NSRect clearButtonFrame = _SRIfRespondsGetProp(self.style,
clearButtonLayoutGuide,
frame,
_SRIfRespondsGetProp(self.style,
clearButtonDrawingGuide,
frame,
NSZeroRect));
if (_mouseTrackingButtonTag != _SRRecorderControlInvalidButtonTag)
{
if (!self.window.isKeyWindow)
{
// It's possible to receive this event after window resigned its key status
// e.g. when shortcut brings new window and makes it key.
self.needsDisplay = YES;
}
else
{
NSPoint locationInView = [self convertPoint:anEvent.locationInWindow fromView:nil];
if (_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag &&
[self mouse:locationInView inRect:self.bounds])
{
[self beginRecording];
}
else if (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag &&
[self mouse:locationInView inRect:cancelButtonFrame])
{
[self endRecording];
}
else if (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag &&
[self mouse:locationInView inRect:clearButtonFrame])
{
[self clearAndEndRecording];
}
}
_mouseTrackingButtonTag = _SRRecorderControlInvalidButtonTag;
}
else
[super mouseUp:anEvent];
}
- (void)mouseEntered:(NSEvent *)anEvent
{
if (self.enabled)
{
if (_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag && anEvent.trackingArea == _mainButtonTrackingArea)
[self setNeedsDisplayInRect:self.style.backgroundDrawingGuide.frame];
else if (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag && anEvent.trackingArea == _cancelButtonTrackingArea)
[self setNeedsDisplayInRect:self.style.cancelButtonDrawingGuide.frame];
else if (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag && anEvent.trackingArea == _clearButtonTrackingArea)
[self setNeedsDisplayInRect:self.style.clearButtonDrawingGuide.frame];
}
[super mouseEntered:anEvent];
}
- (void)mouseExited:(NSEvent *)anEvent
{
if (self.enabled)
{
if (_mouseTrackingButtonTag == _SRRecorderControlMainButtonTag && anEvent.trackingArea == _mainButtonTrackingArea)
[self setNeedsDisplayInRect:self.style.backgroundDrawingGuide.frame];
else if (_mouseTrackingButtonTag == _SRRecorderControlCancelButtonTag && anEvent.trackingArea == _cancelButtonTrackingArea)
[self setNeedsDisplayInRect:self.style.cancelButtonDrawingGuide.frame];
else if (_mouseTrackingButtonTag == _SRRecorderControlClearButtonTag && anEvent.trackingArea == _clearButtonTrackingArea)
[self setNeedsDisplayInRect:self.style.clearButtonDrawingGuide.frame];
}
[super mouseExited:anEvent];
}
- (void)keyDown:(NSEvent *)anEvent
{
if (![self performKeyEquivalent:anEvent])
[super keyDown:anEvent];
}
- (BOOL)performKeyEquivalent:(NSEvent *)anEvent
{
__block BOOL result = NO;
os_activity_initiate("-[SRRecorderControl performKeyEquivalent:]", OS_ACTIVITY_FLAG_DEFAULT, ^{
if (![self canCaptureKeyEvent])
return;
if (self.isRecording)
{
if (anEvent.keyCode == USHRT_MAX)
{
// This shouldn't really happen ever, but was rarely observed.
// See https://github.com/Kentzo/ShortcutRecorder/issues/40
os_trace_debug("Invalid key code");
result = NO;
}
else if (self.allowsEscapeToCancelRecording &&
anEvent.keyCode == SRKeyCodeEscape &&
(anEvent.modifierFlags & SRCocoaModifierFlagsMask) == 0)
{
os_trace_debug("Cancel via Esc");
[self endRecording];
result = YES;
}
else if (self.allowsDeleteToClearShortcutAndEndRecording &&
(anEvent.keyCode == SRKeyCodeDelete || anEvent.keyCode == SRKeyCodeForwardDelete) &&
(anEvent.modifierFlags & SRCocoaModifierFlagsMask) == 0)
{
os_trace_debug("Clear via Delete");
[self clearAndEndRecording];
result = YES;
}
else
{
SRShortcut *newObjectValue = [SRShortcut shortcutWithEvent:anEvent];
if ([self canEndRecordingWithObjectValue:newObjectValue])
[self endRecordingWithObjectValue:newObjectValue];
else
{
// Do not end editing and allow the client to make another attempt.
[self playAlert];
}
result = YES;
}
}
else if (anEvent.keyCode == SRKeyCodeSpace)
{
os_trace_debug("Begin recording via Space");
result = [self beginRecording];
}
else
result = NO;
});
return result;
}
- (void)flagsChanged:(NSEvent *)anEvent
{
if (self.isRecording && [self canCaptureKeyEvent])
{
__auto_type modifierFlags = anEvent.modifierFlags & SRCocoaModifierFlagsMask;
if (self.allowsModifierFlagsOnlyShortcut)
{
__auto_type keyCode = anEvent.keyCode;
__auto_type nextModifierFlags = _lastSeenModifierFlags;
// Only XOR when flag is added.
if ((modifierFlags & NSEventModifierFlagCommand) && (keyCode == kVK_Command || keyCode == kVK_RightCommand))
nextModifierFlags ^= NSEventModifierFlagCommand;
else if ((modifierFlags & NSEventModifierFlagOption) && (keyCode == kVK_Option || keyCode == kVK_RightOption))
nextModifierFlags ^= NSEventModifierFlagOption;
else if ((modifierFlags & NSEventModifierFlagShift) && (keyCode == kVK_Shift || keyCode == kVK_RightShift))
nextModifierFlags ^= NSEventModifierFlagShift;
else if ((modifierFlags & NSEventModifierFlagControl) && (keyCode == kVK_Control || keyCode == kVK_RightControl))
nextModifierFlags ^= NSEventModifierFlagControl;
else if (modifierFlags == 0 && _lastSeenModifierFlags != 0)
{
SRShortcut *newObjectValue = [SRShortcut shortcutWithCode:SRKeyCodeNone
modifierFlags:_lastSeenModifierFlags
characters:nil
charactersIgnoringModifiers:nil];
if ([self canEndRecordingWithObjectValue:newObjectValue])
[self endRecordingWithObjectValue:newObjectValue];
}
if (nextModifierFlags != _lastSeenModifierFlags && ![self areModifierFlagsAllowed:nextModifierFlags forKeyCode:SRKeyCodeNone])
[self playAlert];
else
_lastSeenModifierFlags = nextModifierFlags;
}
else
{
if (![self areModifierFlagsAllowed:modifierFlags forKeyCode:SRKeyCodeNone])
[self playAlert];
else
_lastSeenModifierFlags = modifierFlags;
}
[self setNeedsDisplayInRect:self.style.labelDrawingGuide.frame];
[self updateLabelConstraints];
}
[super flagsChanged:anEvent];
}
#pragma mark NSObject
+ (void)initialize
{
if (self == [SRRecorderControl class])
{
[self exposeBinding:NSValueBinding];
}
}
- (Class)valueClassForBinding:(NSBindingName)aBinding
{
if ([aBinding isEqualToString:NSValueBinding])
return SRShortcut.class;
else
return [super valueClassForBinding:aBinding];
}
- (NSArray<NSAttributeDescription *> *)optionDescriptionsForBinding:(NSBindingName)aBinding
{
if ([aBinding isEqualToString:NSValueBinding])
{
NSAttributeDescription *valueTransformer = [NSAttributeDescription new];
valueTransformer.name = NSValueTransformerBindingOption;
valueTransformer.attributeType = NSStringAttributeType;
valueTransformer.defaultValue = @"";
NSAttributeDescription *valueTransformerName = [NSAttributeDescription new];
valueTransformerName.name = NSValueTransformerNameBindingOption;
valueTransformerName.attributeType = NSStringAttributeType;
valueTransformerName.defaultValue = @"";
return @[valueTransformer, valueTransformerName];
}
else
return [super optionDescriptionsForBinding:aBinding];
}
- (void)observeValueForKeyPath:(NSString *)aKeyPath
ofObject:(id)anObject
change:(NSDictionary<NSKeyValueChangeKey, id> *)aChange
context:(void *)aContext
{
if (aContext == _SRStyleUserInterfaceLayoutDirectionObservingContext)
{
if ([aChange objectForKey:NSKeyValueChangeNotificationIsPriorKey])
[self willChangeValueForKey:@"userInterfaceLayoutDirection"];
else
[self didChangeValueForKey:@"userInterfaceLayoutDirection"];
}
else if (aContext == _SRStyleAppearanceObservingContext)
{
__auto_type appearance = [aChange[NSKeyValueChangeNewKey] unsignedIntegerValue];
if (appearance != SRRecorderControlStyleComponentsAppearanceUnspecified)
self.appearance = [NSAppearance appearanceNamed:SRRecorderControlStyleComponentsAppearanceToSystem(appearance)];
else
self.appearance = nil;
}
else if (aContext == _SRStyleGuideObservingContext)
{
[self setNeedsDisplayInRect:[aChange[NSKeyValueChangeOldKey] rectValue]];
[self setNeedsDisplayInRect:[aChange[NSKeyValueChangeNewKey] rectValue]];
}
else
[super observeValueForKeyPath:aKeyPath ofObject:anObject change:aChange context:aContext];
}
@end
#undef _SRIfRespondsGet
#undef _SRIfRespondsGetProp