diff --git a/Sources/ShortcutRecorder/SRCommon.m b/Sources/ShortcutRecorder/SRCommon.m index 27996012..c138426f 100644 --- a/Sources/ShortcutRecorder/SRCommon.m +++ b/Sources/ShortcutRecorder/SRCommon.m @@ -34,6 +34,7 @@ SRModifierFlagString const SRModifierFlagStringOption = @"⌥"; SRModifierFlagString const SRModifierFlagStringShift = @"⇧"; SRModifierFlagString const SRModifierFlagStringControl = @"⌃"; +SRModifierFlagString const SRModifierFlagStringFunction = @"fn"; NSBundle *SRBundle() diff --git a/Sources/ShortcutRecorder/SRKeyBindingTransformer.m b/Sources/ShortcutRecorder/SRKeyBindingTransformer.m index 9ca91dc3..0e676451 100644 --- a/Sources/ShortcutRecorder/SRKeyBindingTransformer.m +++ b/Sources/ShortcutRecorder/SRKeyBindingTransformer.m @@ -73,6 +73,9 @@ - (SRShortcut *)transformedValue:(NSString *)aValue if ([modifierFlagsString containsString:@"@"]) modifierFlags |= NSEventModifierFlagCommand; + + if ([modifierFlagsString containsString:@"_"]) + modifierFlags |= NSEventModifierFlagFunction; keyCodeString = keyCodeString.lowercaseString; NSNumber *keyCode = [SRASCIISymbolicKeyCodeTransformer.sharedTransformer reverseTransformedValue:keyCodeString]; @@ -214,6 +217,9 @@ - (NSString *)reverseTransformedValue:(SRShortcut *)aValue if (modifierFlagsValue & NSEventModifierFlagCommand) [keyBinding appendString:@"@"]; + + if (modifierFlagsValue & NSEventModifierFlagFunction) + [keyBinding appendString:@"_"]; if (isNumPad) [keyBinding appendString:@"#"]; diff --git a/Sources/ShortcutRecorder/SRModifierFlagsTransformer.m b/Sources/ShortcutRecorder/SRModifierFlagsTransformer.m index 93841a89..332af7a2 100644 --- a/Sources/ShortcutRecorder/SRModifierFlagsTransformer.m +++ b/Sources/ShortcutRecorder/SRModifierFlagsTransformer.m @@ -107,6 +107,9 @@ - (NSString *)transformedValue:(NSNumber *)aValue layoutDirection:(NSUserInterfa if (flags & NSEventModifierFlagCommand) [flagsStringComponents addObject:SRLoc(@"Command")]; + + if (flags & NSEventModifierFlagFunction) + [flagsStringComponents addObject:SRLoc(@"Function")]; if (aDirection == NSUserInterfaceLayoutDirectionRightToLeft) return [[[flagsStringComponents reverseObjectEnumerator] allObjects] componentsJoinedByString:SRLoc(@"-")]; @@ -146,7 +149,10 @@ - (NSString *)transformedValue:(NSNumber *)aValue layoutDirection:(NSUserInterfa NSEventModifierFlags flags = aValue.unsignedIntegerValue; NSMutableArray *flagsStringFragments = NSMutableArray.array; - + + if (flags & NSEventModifierFlagFunction) + [flagsStringFragments addObject:SRModifierFlagStringFunction]; + if (flags & NSEventModifierFlagControl) [flagsStringFragments addObject:SRModifierFlagStringControl]; @@ -174,12 +180,25 @@ - (NSNumber *)reverseTransformedValue:(NSString *)aValue } __block NSEventModifierFlags flags = 0; - __block BOOL foundInvalidSubstring = NO; + + NSError *error = NULL; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: [NSString stringWithFormat: @"(?:%@|%@|%@|%@|%@)", SRModifierFlagStringFunction, SRModifierFlagStringControl, SRModifierFlagStringOption, SRModifierFlagStringShift, SRModifierFlagStringCommand] + options:NSRegularExpressionCaseInsensitive + error:&error]; + + if (error != NULL) { + NSLog(@"Got an error making a regex: %@", error); + panic("bad modifier transformer regex"); + } - [aValue enumerateSubstringsInRange:NSMakeRange(0, aValue.length) - options:NSStringEnumerationByComposedCharacterSequences - usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) - { + NSArray *matches = [regex matchesInString:aValue + options:0 + range:NSMakeRange(0, [aValue length])]; + + for (NSTextCheckingResult *match in matches) { + + NSString *substring = [aValue substringWithRange: [match rangeAtIndex:0]]; + if ([substring isEqualToString:SRModifierFlagStringControl] && (flags & NSEventModifierFlagControl) == 0) flags |= NSEventModifierFlagControl; else if ([substring isEqualToString:SRModifierFlagStringOption] && (flags & NSEventModifierFlagOption) == 0) @@ -188,17 +207,8 @@ - (NSNumber *)reverseTransformedValue:(NSString *)aValue flags |= NSEventModifierFlagShift; else if ([substring isEqualToString:SRModifierFlagStringCommand] && (flags & NSEventModifierFlagCommand) == 0) flags |= NSEventModifierFlagCommand; - else - { - foundInvalidSubstring = YES; - *stop = YES; - } - }]; - - if (foundInvalidSubstring) - { - os_trace_error("#Error Invalid value for reverse transformation"); - return nil; + else if ([substring isEqualToString:SRModifierFlagStringFunction] && (flags & NSEventModifierFlagFunction) == 0) + flags |= NSEventModifierFlagFunction; } return @(flags); diff --git a/Sources/ShortcutRecorder/SRRecorderControl.m b/Sources/ShortcutRecorder/SRRecorderControl.m index 87f350ed..2ec6bf30 100644 --- a/Sources/ShortcutRecorder/SRRecorderControl.m +++ b/Sources/ShortcutRecorder/SRRecorderControl.m @@ -573,7 +573,7 @@ - (BOOL)beginRecording if (self.pausesGlobalShortcutMonitorWhileRecording) { _didPauseGlobalShortcutMonitor = YES; - [SRGlobalShortcutMonitor.sharedMonitor pause]; + [SRAXGlobalShortcutMonitor.sharedMonitor pause]; } NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding]; @@ -650,7 +650,7 @@ - (void)endRecordingWithObjectValue:(SRShortcut *)anObjectValue if (_didPauseGlobalShortcutMonitor) { _didPauseGlobalShortcutMonitor = NO; - [SRGlobalShortcutMonitor.sharedMonitor resume]; + [SRAXGlobalShortcutMonitor.sharedMonitor resume]; } NSDictionary *bindingInfo = [self infoForBinding:NSValueBinding]; @@ -1803,6 +1803,8 @@ - (void)flagsChanged:(NSEvent *)anEvent nextModifierFlags ^= NSEventModifierFlagShift; else if ((modifierFlags & NSEventModifierFlagControl) && (keyCode == kVK_Control || keyCode == kVK_RightControl)) nextModifierFlags ^= NSEventModifierFlagControl; + else if ((modifierFlags & NSEventModifierFlagFunction) && (keyCode == kVK_Function)) + nextModifierFlags ^= NSEventModifierFlagFunction; else if (modifierFlags == 0 && _lastSeenModifierFlags != 0) { SRShortcut *newObjectValue = [SRShortcut shortcutWithCode:SRKeyCodeNone diff --git a/Sources/ShortcutRecorder/SRShortcut.m b/Sources/ShortcutRecorder/SRShortcut.m index 07707bfe..37dd6803 100644 --- a/Sources/ShortcutRecorder/SRShortcut.m +++ b/Sources/ShortcutRecorder/SRShortcut.m @@ -64,6 +64,8 @@ + (instancetype)shortcutWithEvent:(NSEvent *)aKeyboardEvent ignoringCharacters:( modifierFlags |= NSEventModifierFlagShift; else if (keyCode == kVK_Control || keyCode == kVK_RightControl) modifierFlags |= NSEventModifierFlagControl; + else if (keyCode == kVK_Function) + modifierFlags |= NSEventModifierFlagFunction; keyCode = SRKeyCodeNone; } @@ -134,23 +136,14 @@ + (instancetype)shortcutWithDictionary:(NSDictionary *)aDictionary + (instancetype)shortcutWithKeyEquivalent:(NSString *)aKeyEquivalent { - static NSCharacterSet *PossibleFlags = nil; - static dispatch_once_t OnceToken; - dispatch_once(&OnceToken, ^{ - PossibleFlags = [NSCharacterSet characterSetWithCharactersInString:[NSString stringWithFormat:@"%C%C%C%C", - SRModifierFlagGlyphCommand, - SRModifierFlagGlyphOption, - SRModifierFlagGlyphShift, - SRModifierFlagGlyphControl]]; - }); - - NSScanner *parser = [NSScanner scannerWithString:aKeyEquivalent]; - parser.caseSensitive = NO; - - NSString *modifierFlagsString = @""; - [parser scanCharactersFromSet:PossibleFlags intoString:&modifierFlagsString]; - NSString *keyCodeString = [aKeyEquivalent substringFromIndex:parser.scanLocation]; - + NSString *both = SRSplitKeycodeEquivalent(aKeyEquivalent); + + NSArray *splits = [both componentsSeparatedByString:SRSplitKeycodeSeparator]; + assert([splits count] == 2); + + NSString *modifierFlagsString = splits[0]; + NSString *keyCodeString = splits[1]; + if (!modifierFlagsString.length && !keyCodeString.length) return nil; @@ -241,6 +234,38 @@ - (NSString *)readableStringRepresentation:(BOOL)isASCII #pragma clang diagnostic pop } +- (BOOL) shouldFireForShortcut:(SRShortcut *)shortcut { + + if (self.keyCode != SRKeyCodeNone) { + return [self isEqualToShortcut:shortcut]; + } + + // for a modifier only shortcut, we fire if the incoming shortcut is + // 1. also a modifier only shortcut + // 2. the currently pressed modifiers &'s cleanly with self + // This way, if you have extranious modifiers pressed, we still fire when you hit the target ones. + + if (shortcut.keyCode == SRKeyCodeNone) { + if ((self.modifierFlags & shortcut.modifierFlags) == self.modifierFlags) { + return true; + } else { + return false; + } + } else { + return false; + } + +} + +// for non-modifiers, it's just if the keycode matches. +// for modifiers, it's the key is in the set, it's an up. +- (BOOL) keyBreaksShortcut:(SRKeyCode)keyCode { + if (self.keyCode != SRKeyCodeNone) { + return self.keyCode == keyCode; + } else { + return (SRKeyCodeToCocoaFlag(keyCode) & self.modifierFlags) != 0; + } +} #pragma mark Equality @@ -321,16 +346,30 @@ - (BOOL)isEqualToKeyEquivalent:(NSString *)aKeyEquivalent NSEventModifierFlagCommand, NSEventModifierFlagShift, NSEventModifierFlagOption, + NSEventModifierFlagFunction, NSEventModifierFlagControl | NSEventModifierFlagCommand, NSEventModifierFlagControl | NSEventModifierFlagShift, NSEventModifierFlagControl | NSEventModifierFlagOption, + NSEventModifierFlagControl | NSEventModifierFlagFunction, NSEventModifierFlagCommand | NSEventModifierFlagShift, NSEventModifierFlagCommand | NSEventModifierFlagOption, + NSEventModifierFlagCommand | NSEventModifierFlagFunction, NSEventModifierFlagShift | NSEventModifierFlagOption, + NSEventModifierFlagShift | NSEventModifierFlagFunction, + NSEventModifierFlagOption | NSEventModifierFlagFunction, NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagShift, NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagOption, + NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagFunction, NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagOption, - NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagOption + NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagFunction, + NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagFunction, + NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagOption, + NSEventModifierFlagFunction | NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagOption, + NSEventModifierFlagControl | NSEventModifierFlagFunction | NSEventModifierFlagShift | NSEventModifierFlagOption, + NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagFunction | NSEventModifierFlagOption, + NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagFunction, + NSEventModifierFlagControl | NSEventModifierFlagCommand | NSEventModifierFlagShift | NSEventModifierFlagOption, NSEventModifierFlagFunction + }; static const size_t PossibleFlagsSize = sizeof(PossibleFlags) / sizeof(NSEventModifierFlags); @@ -509,11 +548,12 @@ - (UInt32)carbonModifierFlags if (!c) c = [NSString stringWithFormat:@"<%hu>", aKeyCode]; - return [NSString stringWithFormat:@"%@%@%@%@%@", + return [NSString stringWithFormat:@"%@%@%@%@%@%@", (aModifierFlags & NSEventModifierFlagCommand ? SRLoc(@"Command-") : @""), (aModifierFlags & NSEventModifierFlagOption ? SRLoc(@"Option-") : @""), (aModifierFlags & NSEventModifierFlagControl ? SRLoc(@"Control-") : @""), (aModifierFlags & NSEventModifierFlagShift ? SRLoc(@"Shift-") : @""), + (aModifierFlags & NSEventModifierFlagFunction ? SRLoc(@"Fn-") : @""), c]; } @@ -526,11 +566,12 @@ - (UInt32)carbonModifierFlags if (!c) c = [NSString stringWithFormat:@"<%hu>", aKeyCode]; - return [NSString stringWithFormat:@"%@%@%@%@%@", + return [NSString stringWithFormat:@"%@%@%@%@%@%@", (aModifierFlags & NSEventModifierFlagCommand ? SRLoc(@"Command-") : @""), (aModifierFlags & NSEventModifierFlagOption ? SRLoc(@"Option-") : @""), (aModifierFlags & NSEventModifierFlagControl ? SRLoc(@"Control-") : @""), (aModifierFlags & NSEventModifierFlagShift ? SRLoc(@"Shift-") : @""), + (aModifierFlags & NSEventModifierFlagFunction ? SRLoc(@"Fn-") : @""), c]; } diff --git a/Sources/ShortcutRecorder/SRShortcutAction.m b/Sources/ShortcutRecorder/SRShortcutAction.m index 67dd603a..4e014ed6 100644 --- a/Sources/ShortcutRecorder/SRShortcutAction.m +++ b/Sources/ShortcutRecorder/SRShortcutAction.m @@ -373,6 +373,9 @@ + (SRKeyEventType)SR_keyEventTypeForEventType:(NSEventType)anEventType keyCode:(unsigned short)aKeyCode modifierFlags:(NSEventModifierFlags)aModifierFlags { + // picking the event type here, need to have key-up go right. + // if the ctrl key is down, we interpret other flag-ups as an up. + // if the ctrl key is up, we don't get shit. SRKeyEventType eventType = SRKeyEventTypeDown; switch (anEventType) @@ -391,11 +394,19 @@ + (SRKeyEventType)SR_keyEventTypeForEventType:(NSEventType)anEventType if (keyCode == kVK_Command || keyCode == kVK_RightCommand) eventType = modifierFlags & NSEventModifierFlagCommand ? SRKeyEventTypeDown : SRKeyEventTypeUp; else if (keyCode == kVK_Option || keyCode == kVK_RightOption) - eventType = modifierFlags & NSEventModifierFlagOption ? SRKeyEventTypeDown : SRKeyEventTypeUp; + { + eventType = modifierFlags & NSEventModifierFlagOption ? SRKeyEventTypeDown : SRKeyEventTypeUp; + } else if (keyCode == kVK_Shift || keyCode == kVK_RightShift) eventType = modifierFlags & NSEventModifierFlagShift ? SRKeyEventTypeDown : SRKeyEventTypeUp; else if (keyCode == kVK_Control || keyCode == kVK_RightControl) - eventType = modifierFlags & NSEventModifierFlagControl ? SRKeyEventTypeDown : SRKeyEventTypeUp; + { + eventType = modifierFlags & NSEventModifierFlagControl ? SRKeyEventTypeDown : SRKeyEventTypeUp; + } + else if (keyCode == kVK_Function) + { + eventType = modifierFlags & NSEventModifierFlagFunction ? SRKeyEventTypeDown : SRKeyEventTypeUp; + } else os_trace("#Error Unexpected key code %hu for the FlagsChanged event", keyCode); break; @@ -1049,6 +1060,7 @@ - (void)pause - (OSStatus)handleEvent:(EventRef)anEvent { + NSLog(@"HANDLING ENEV"); __block OSStatus error = eventNotHandledErr; os_activity_initiate("-[SRGlobalShortcutMonitor handleEvent:]", OS_ACTIVITY_FLAG_DETACHED, ^{ @@ -1187,6 +1199,7 @@ - (void)_registerHotKeyForShortcutIfNeeded:(SRShortcut *)aShortcut if (aShortcut.keyCode == SRKeyCodeNone) { + NSLog(@"Acutally aaborting//"); os_trace_error("#Error Shortcut without a key code cannot be registered as Carbon hot key"); return; } @@ -1273,6 +1286,18 @@ - (void)willRemoveShortcut:(SRShortcut *)aShortcut @implementation SRAXGlobalShortcutMonitor { BOOL _canActivelyFilterEvents; + NSInteger _disableCounter; + SRShortcut *_downShortcut; +} + ++ (SRAXGlobalShortcutMonitor *)sharedMonitor +{ + static SRAXGlobalShortcutMonitor *Shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Shared = [[SRAXGlobalShortcutMonitor alloc] initWithRunLoop:NSRunLoop.currentRunLoop tapOptions:kCGEventTapOptionDefault]; + }); + return Shared; } CGEventRef _Nullable _SRQuartzEventHandler(CGEventTapProxy aProxy, CGEventType aType, CGEventRef anEvent, void * _Nullable aUserInfo) @@ -1306,29 +1331,15 @@ - (instancetype)initWithRunLoop:(NSRunLoop *)aRunLoop - (instancetype)initWithRunLoop:(NSRunLoop *)aRunLoop tapOptions:(CGEventTapOptions)aTapOptions { - static const CGEventMask Mask = (CGEventMaskBit(kCGEventKeyDown) | - CGEventMaskBit(kCGEventKeyUp) | - CGEventMaskBit(kCGEventFlagsChanged)); - __auto_type eventTap = CGEventTapCreate(kCGSessionEventTap, - kCGHeadInsertEventTap, - aTapOptions, - Mask, - _SRQuartzEventHandler, - (__bridge void *)self); - if (!eventTap) - { - os_trace_error("#Critical Unable to create event tap: make sure Accessibility is enabled"); - return nil; - } self = [super init]; if (self) { - _eventTap = eventTap; - _eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); _canActivelyFilterEvents = (aTapOptions & kCGEventTapOptionListenOnly) == 0; - CFRunLoopAddSource(aRunLoop.getCFRunLoop, _eventTapSource, kCFRunLoopDefaultMode); + _eventTapRunLoop = aRunLoop; + _eventTapOptions = aTapOptions; + [self resetEventTap]; } return self; @@ -1345,6 +1356,64 @@ - (void)dealloc #pragma mark Methods +- (void)removeEventTap +{ + if (_eventTap) { + CFRunLoopRemoveSource(_eventTapRunLoop.getCFRunLoop, _eventTapSource, kCFRunLoopDefaultMode); + + CFRelease(_eventTap); + CFRelease(_eventTapSource); + + _eventTap = NULL; + _eventTapSource = NULL; + } +} + +- (void)resetEventTap +{ + + [self removeEventTap]; + + static const CGEventMask Mask = (CGEventMaskBit(kCGEventKeyDown) | + CGEventMaskBit(kCGEventKeyUp) | + CGEventMaskBit(kCGEventFlagsChanged)); + __auto_type eventTap = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + _eventTapOptions, + Mask, + _SRQuartzEventHandler, + (__bridge void *)self); + + _eventTap = eventTap; + if (!eventTap) + { + os_trace_error("#Critical Unable to create event tap: make sure Accessibility is enabled"); + return; + } + + _eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); + CFRunLoopAddSource(_eventTapRunLoop.getCFRunLoop, _eventTapSource, kCFRunLoopDefaultMode); + +} + +- (void)resume +{ + @synchronized (_actions) + { + os_trace_debug("Global Shortcut Monitor counter: %ld -> %ld", _disableCounter, _disableCounter - 1); + _disableCounter -= 1; + } +} + +- (void)pause +{ + @synchronized (_actions) + { + os_trace_debug("Global Shortcut Monitor counter: %ld -> %ld", _disableCounter, _disableCounter + 1); + _disableCounter += 1; + } +} + - (CGEventRef)handleEvent:(CGEventRef)anEvent { __block __auto_type result = anEvent; @@ -1354,6 +1423,17 @@ - (CGEventRef)handleEvent:(CGEventRef)anEvent __auto_type eventType = CGEventGetType(anEvent); __auto_type cocoaModifierFlags = SRCoreGraphicsToCocoaFlags(CGEventGetFlags(anEvent)); NSEventType cocoaEventType; + + if (_disableCounter != 0) { + return; + } + + __auto_type isRepeat = CGEventGetIntegerValueField(anEvent, kCGKeyboardEventAutorepeat); + + if (isRepeat) { + return; + } + switch (eventType) { case kCGEventKeyDown: @@ -1370,16 +1450,78 @@ - (CGEventRef)handleEvent:(CGEventRef)anEvent break; } + // Find the event type + __auto_type keyEventType = [NSEvent SR_keyEventTypeForEventType:cocoaEventType + keyCode:(unsigned short)eventKeyCode + modifierFlags:cocoaModifierFlags]; + + // This is a normalization problem. The only way a keycode is a flag is if we have flags changed. + // here's my fix for this problem. if it's an up, and modifiers, or the modifier back into the flags + if (eventType == kCGEventFlagsChanged && keyEventType == SRKeyEventTypeUp) { + NSEventModifierFlags keyFlag = SRKeyCodeToCocoaFlag(eventKeyCode); + cocoaModifierFlags |= keyFlag; + } + __auto_type shortcut = [SRShortcut shortcutWithCode:eventType != kCGEventFlagsChanged ? (SRKeyCode)eventKeyCode : SRKeyCodeNone modifierFlags:cocoaModifierFlags characters:nil charactersIgnoringModifiers:nil]; - __auto_type keyEventType = [NSEvent SR_keyEventTypeForEventType:cocoaEventType - keyCode:(unsigned short)eventKeyCode - modifierFlags:cocoaModifierFlags]; - __auto_type actions = [self enabledActionsForShortcut:shortcut keyEvent:keyEventType]; + + + // SO this is a down for this shortcut? + // is it ok if we limit it to one shortcut at a time? + // we can't get stuck . + + // "hold ctl, hold opt, hold k, release opt, release k" + // "hold ctrl, hold fn, release ctrl, release fn" + // do we up on any up after we've downed? + // for modifiers, up is if any of the modifiers is upped., extra modifiers ignored for up. + // might be the same for down, actually. + + // i think we should change the comparison strategy. instead of looking for it in a dict, we shoulf have a fn. + + + // IF shortcutCode != SRKEyCodeName + // IF Down SET IS DOWN (for key?) + // IF UP// check if down is set, then go. + + // So maybe there isn't a way to tell if a key is related to other keys? + // IF ShortcutCode == CODENONE + // IF DOwn ( // check the flargs. If any of them are the key? -- hard to get there. rn we can only ask for them by name. + // IF UP // check if down is set, then compare, if everything is still down, do nothing, if any have come up, then done. + // if up, we have a keycap, so we can just ask if that keycap is in the down flargs + + // what happens if you change the key combo? Does that update the actions? + // I'm still working out how the key combo is stored. + + NSArray *actionsToFire= ^NSArray*() { + + if (self->_downShortcut == nil) { + if (keyEventType == SRKeyEventTypeDown) { + __auto_type targetShortcuts = [self shortcuts]; + + for (SRShortcut *targetShortcut in targetShortcuts) { + if ([targetShortcut shouldFireForShortcut: shortcut]) { + self->_downShortcut = targetShortcut; + return [self enabledActionsForShortcut:targetShortcut keyEvent:keyEventType]; + } + } + } + } else { + if (keyEventType == SRKeyEventTypeUp) { + if ([self->_downShortcut keyBreaksShortcut:eventKeyCode]) { + NSArray *actions = [self enabledActionsForShortcut:self->_downShortcut keyEvent:keyEventType]; + self->_downShortcut = nil; + return actions; + } + } + } + + return @[]; + }(); + __block BOOL isHandled = NO; - [actions enumerateObjectsWithOptions:NSEnumerationReverse + [actionsToFire enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(SRShortcutAction *obj, NSUInteger idx, BOOL *stop) { *stop = isHandled = [obj performActionOnTarget:nil]; @@ -1398,13 +1540,13 @@ - (CGEventRef)handleEvent:(CGEventRef)anEvent - (void)didAddShortcut:(SRShortcut *)aShortcut { - if (_shortcuts.count) + if (_shortcuts.count && _eventTap) CGEventTapEnable(_eventTap, true); } - (void)willRemoveShortcut:(SRShortcut *)aShortcut { - if (_shortcuts.count == 1 && [_shortcuts countForObject:aShortcut] == 1) + if (_shortcuts.count == 1 && [_shortcuts countForObject:aShortcut] == 1 && _eventTap) CGEventTapEnable(_eventTap, false); } diff --git a/Sources/ShortcutRecorder/include/ShortcutRecorder/SRCommon.h b/Sources/ShortcutRecorder/include/ShortcutRecorder/SRCommon.h index 37e8ebdf..ee279310 100644 --- a/Sources/ShortcutRecorder/include/ShortcutRecorder/SRCommon.h +++ b/Sources/ShortcutRecorder/include/ShortcutRecorder/SRCommon.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN Mask representing subset of Cocoa modifier flags suitable for shortcuts. */ NS_SWIFT_NAME(CocoaModifierFlagsMask) -static const NSEventModifierFlags SRCocoaModifierFlagsMask = NSEventModifierFlagCommand | NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagControl; +static const NSEventModifierFlags SRCocoaModifierFlagsMask = NSEventModifierFlagCommand | NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagControl | NSEventModifierFlagFunction; /*! @@ -25,7 +25,7 @@ static const UInt32 SRCarbonModifierFlagsMask = cmdKey | optionKey | shiftKey | NS_SWIFT_NAME(CoreGraphicsModifierFlagsMask) -static const CGEventFlags SRCoreGraphicsModifierFlagsMask = kCGEventFlagMaskCommand | kCGEventFlagMaskAlternate | kCGEventFlagMaskShift | kCGEventFlagMaskControl; +static const CGEventFlags SRCoreGraphicsModifierFlagsMask = kCGEventFlagMaskCommand | kCGEventFlagMaskAlternate | kCGEventFlagMaskShift | kCGEventFlagMaskControl | kCGEventFlagMaskSecondaryFn; /*! Dawable unicode characters for key codes that do not have appropriate constants in Carbon and Cocoa. @@ -246,6 +246,7 @@ extern SRModifierFlagString const SRModifierFlagStringCommand; extern SRModifierFlagString const SRModifierFlagStringOption; extern SRModifierFlagString const SRModifierFlagStringShift; extern SRModifierFlagString const SRModifierFlagStringControl; +extern SRModifierFlagString const SRModifierFlagStringFunction; /*! @@ -300,6 +301,9 @@ NS_INLINE UInt32 SRCocoaToCarbonFlags(NSEventModifierFlags aCocoaFlags) if (aCocoaFlags & NSEventModifierFlagShift) carbonFlags |= shiftKey; + + if (aCocoaFlags & NSEventModifierFlagFunction) + NSLog(@"WELL WE TRIED TO CONVERT FN TO CARBON"); return carbonFlags; } @@ -320,10 +324,78 @@ NS_INLINE NSEventModifierFlags SRCoreGraphicsToCocoaFlags(CGEventFlags aCoreGrap if (aCoreGraphicsFlags & kCGEventFlagMaskShift) cocoaFlags |= NSEventModifierFlagShift; + + if (aCoreGraphicsFlags & kCGEventFlagMaskSecondaryFn) + cocoaFlags |= NSEventModifierFlagFunction; return cocoaFlags; } +NS_SWIFT_NAME(keyCodeToCocoaFlag(_:)) +NS_INLINE NSEventModifierFlags SRKeyCodeToCocoaFlag(SRKeyCode keyCode) +{ + if (keyCode == kVK_Command || keyCode == kVK_RightCommand) + return NSEventModifierFlagCommand; + else if (keyCode == kVK_Option || keyCode == kVK_RightOption) + return NSEventModifierFlagOption; + else if (keyCode == kVK_Shift || keyCode == kVK_RightShift) + return NSEventModifierFlagShift; + else if (keyCode == kVK_Control || keyCode == kVK_RightControl) + return NSEventModifierFlagControl; + else if (keyCode == kVK_Function) + return NSEventModifierFlagFunction; + else + return 0; +} + +NS_SWIFT_NAME(keyFatCodeToCocoaFlag(_:)) +NS_INLINE NSEventModifierFlags SRKeyFatCodeToCocoaFlag(SRKeyCode keyCode) +{ + if (keyCode == kVK_Command || keyCode == kVK_RightCommand) + return NSEventModifierFlagCommand; + else if (keyCode == kVK_Option || keyCode == kVK_RightOption) + return NSEventModifierFlagOption; + else if (keyCode == kVK_Shift || keyCode == kVK_RightShift) + return NSEventModifierFlagShift; + else if (keyCode == kVK_Control || keyCode == kVK_RightControl) + return NSEventModifierFlagControl; + else if (keyCode == kVK_Function) + return NSEventModifierFlagFunction; + else + return 10; +} + +static NSString* const SRSplitKeycodeSeparator = @"ΩΩ"; + +/*! + Return string represeeeeentation of a shortcut with modifier flags replaced with their + localized readable equivalents (e.g. ⌥ -> Option) and ASCII character with a key code. + + */ +NS_SWIFT_NAME(splitKeycodeEquivalent(_:)) +NS_INLINE NSString* SRSplitKeycodeEquivalent(NSString *keyEquiv) { + + NSError *error = NULL; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: [NSString stringWithFormat: @"^((?:%@|%@|%@|%@|%@)*)(.*)$", SRModifierFlagStringFunction, SRModifierFlagStringControl, SRModifierFlagStringOption, SRModifierFlagStringShift, SRModifierFlagStringCommand] + options:NSRegularExpressionCaseInsensitive + error:&error]; + + if (error != NULL) { + NSLog(@"Got an error making a regex: %@", error); + panic("bad modifier regex"); + } + + NSArray *matches = [regex matchesInString:keyEquiv + options:0 + range:NSMakeRange(0, [keyEquiv length])]; + + NSString *modifiers = [keyEquiv substringWithRange:[matches[0] rangeAtIndex:1]]; + NSString *keycode = [keyEquiv substringWithRange:[matches[0] rangeAtIndex:2]]; + + NSString *splits = [NSString stringWithFormat:@"%@%@%@", modifiers, SRSplitKeycodeSeparator, keycode]; + + return splits; +} /*! Return Bundle where resources can be found. diff --git a/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcut.h b/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcut.h index 001e085f..80976d13 100644 --- a/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcut.h +++ b/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcut.h @@ -186,6 +186,25 @@ NS_SWIFT_NAME(Shortcut) */ - (NSString *)readableStringRepresentation:(BOOL)isASCII NS_SWIFT_NAME(readableStringRepresentation(isASCII:)); + +/*! + Return true if the pressed shortcut matches self. + + For most shotrcuts, that will be a simple equality check. + For modifier only shortcuts, we fire on &. + + */ +- (BOOL) shouldFireForShortcut:(SRShortcut *)shortcut; + +/*! + Return true if this key going up breaks the shortcut. + + For most shotrcuts, that is if the keycode matches the shortcut. Modifiers are ignored for ending a press. + For modifier only shortcuts, it's if that key is included in any of the shortcut's modifiers. + + */ +- (BOOL) keyBreaksShortcut:(SRKeyCode)keyCode; + /*! Compare the shortcut to another shortcut. diff --git a/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcutAction.h b/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcutAction.h index 2884df85..4c67243f 100644 --- a/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcutAction.h +++ b/Sources/ShortcutRecorder/include/ShortcutRecorder/SRShortcutAction.h @@ -406,6 +406,8 @@ NS_SWIFT_NAME(GlobalShortcutMonitor) NS_SWIFT_NAME(AXGlobalShortcutMonitor) @interface SRAXGlobalShortcutMonitor : SRShortcutMonitor +@property (class, readonly) SRAXGlobalShortcutMonitor *sharedMonitor NS_SWIFT_NAME(shared); + /*! Mach port that corresponds to the event tap used under the hood. @@ -427,6 +429,11 @@ NS_SWIFT_NAME(AXGlobalShortcutMonitor) */ @property (readonly) NSRunLoop *eventTapRunLoop; +/*! + Options for the event tap + */ +@property (readonly) CGEventTapOptions eventTapOptions; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnullability" /*! @@ -471,6 +478,40 @@ NS_SWIFT_NAME(AXGlobalShortcutMonitor) */ - (nullable CGEventRef)handleEvent:(CGEventRef)anEvent; +/*! + Remove the event tap for the monitor. + + @discussion +If there are problems with the event tap, like because of AX not being enabled upon its creation, this method will reset it. + */ +- (void)removeEventTap; + +/*! + Reset the event tap for the monitor. + + @discussion +If there are problems with the event tap, like because of AX not being enabled upon its creation, this method will reset it. + */ +- (void)resetEventTap; + +/*! + Enable system-wide shortcut monitoring. + + @discussion + This method has an underlying counter, i.e. every pause must be matched with a resume. + The initial state is resumed. + */ +- (void)resume; + +/*! + Disable system-wide shortcut monitoring. + + @discussion + This method has an underlying counter, i.e. every resume must be matched with a pause. + The initial state is resumed. + */ +- (void)pause; + @end diff --git a/Tests/ShortcutRecorderTests/SRCommonTests.swift b/Tests/ShortcutRecorderTests/SRCommonTests.swift index 0f57aae3..fa42cdf9 100644 --- a/Tests/ShortcutRecorderTests/SRCommonTests.swift +++ b/Tests/ShortcutRecorderTests/SRCommonTests.swift @@ -12,7 +12,7 @@ class SRCommonTests: XCTestCase { func testCocoaModifierFlagsMask() { let allFlags = NSEvent.ModifierFlags.init(rawValue: UInt.max) let cocoaFlags = allFlags.intersection(CocoaModifierFlagsMask) - let expectedFlags: NSEvent.ModifierFlags = [.command, .control, .option, .shift] + let expectedFlags: NSEvent.ModifierFlags = [.command, .control, .option, .shift, .function] XCTAssertEqual(cocoaFlags, expectedFlags) } diff --git a/Tests/ShortcutRecorderTests/SRModifierFlagsTransformerTests.swift b/Tests/ShortcutRecorderTests/SRModifierFlagsTransformerTests.swift index afdee847..220a1426 100644 --- a/Tests/ShortcutRecorderTests/SRModifierFlagsTransformerTests.swift +++ b/Tests/ShortcutRecorderTests/SRModifierFlagsTransformerTests.swift @@ -9,7 +9,7 @@ import ShortcutRecorder class SRModifierFlagsTransformerTests: XCTestCase { func testSymbolicTransformerIsReversible() { - let flags: NSEvent.ModifierFlags = [.control, .option, .shift, .command] + let flags: NSEvent.ModifierFlags = [.control, .option, .shift, .command, .function] let transformer = SymbolicModifierFlagsTransformer.shared let string = transformer.transformedValue(flags.rawValue as NSNumber)! let restoredFlags = NSEvent.ModifierFlags(rawValue: transformer.reverseTransformedValue(string) as! UInt) diff --git a/Tests/ShortcutRecorderTests/SRShortcutTests.swift b/Tests/ShortcutRecorderTests/SRShortcutTests.swift index 0c17f29a..bfb568a3 100644 --- a/Tests/ShortcutRecorderTests/SRShortcutTests.swift +++ b/Tests/ShortcutRecorderTests/SRShortcutTests.swift @@ -492,4 +492,39 @@ class SRShortcutTests: XCTestCase { XCTAssertEqual(shift_cmd, Shortcut(event: shift_cmd_down_event)) XCTAssertEqual(shift_cmd, Shortcut(event: shift_cmd_up_event)) } + + func testSplittingKeyEquiv() { + + struct splitTestCase { + var keyEquiv: String + var expectedModifiers: String + var expectedKeyCode: String + } + + + let testCases = [ splitTestCase(keyEquiv: "⇧⌘K", expectedModifiers: "⇧⌘", expectedKeyCode: "K"), + splitTestCase(keyEquiv: "⇧⌘", expectedModifiers: "⇧⌘", expectedKeyCode: ""), + splitTestCase(keyEquiv: "fn⇧⌘K", expectedModifiers: "fn⇧⌘", expectedKeyCode: "K"), + splitTestCase(keyEquiv: "fn", expectedModifiers: "fn", expectedKeyCode: ""), + splitTestCase(keyEquiv: "fnf", expectedModifiers: "fn", expectedKeyCode: "f"), + splitTestCase(keyEquiv: "fnF", expectedModifiers: "fn", expectedKeyCode: "F"), + splitTestCase(keyEquiv: "⇧⌘KS", expectedModifiers: "⇧⌘", expectedKeyCode: "KS"), + ] + + for testCase in testCases { + + let both = splitKeycodeEquivalent(testCase.keyEquiv) + + let splits = both.components(separatedBy: SRSplitKeycodeSeparator) + + XCTAssertEqual(splits.count, 2) + + XCTAssertEqual(splits[0], testCase.expectedModifiers) + XCTAssertEqual(splits[1], testCase.expectedKeyCode) + + print(both) + + } + + } }