Fix ghost keystrokes after app exits

Instead of using a null EventTap to do a pre-flight check for permissions, using a dedicated EventTap for KeyDown events serves the same purpose and fixes the issue.
Also convert KCEventTap to ARC.

[fixes #72]
[fixes #311]
This commit is contained in:
Andrew Kitchen 2024-11-23 10:58:13 -08:00
parent cf95a5c58f
commit 1025e8fcb4
2 changed files with 75 additions and 80 deletions

View file

@ -1,5 +1,5 @@
// Copyright (c) 2009 Stephen Deken
// Copyright (c) 2014-2023 Andrew Kitchen
// Copyright (c) 2014-2024 Andrew Kitchen
//
// All rights reserved.
//
@ -26,6 +26,9 @@
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#if !__has_feature(objc_arc)
#error "ARC is required for this file -- enable with -fobjc-arc"
#endif
#import "KCEventTap.h"
#import "KCKeystroke.h"
@ -33,9 +36,10 @@
#import "KCMouseEvent.h"
@interface KCEventTap () {
CFMachPortRef eventTap;
CFRunLoopRef eventTapRunLoop;
CFRunLoopSourceRef eventTapEventSource;
CFMachPortRef keyEventTap;
CFMachPortRef mouseAndFlagsEventTap;
CFRunLoopSourceRef keyEventTapSource;
CFRunLoopSourceRef mouseAndFlagsEventTapSource;
}
- (void)_noteMouseEvent:(CGEventRef)eventRef;
@ -44,22 +48,33 @@
@end
CGEventRef nullEventTapCallback(
CGEventRef keyEventTapCallback(
CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void *vp)
void *context)
{
return NULL;
KCEventTap *eventTap = (__bridge KCEventTap *)context;
switch (type)
{
case kCGEventKeyDown:
[eventTap _noteKeyEvent:event];
break;
case kCGEventKeyUp:
break;
default:
break;
}
return event;
}
CGEventRef eventTapCallback(
CGEventRef mouseAndFlagsEventTapCallback(
CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void *vp)
void *context)
{
KCEventTap* keyTap = (KCEventTap*)vp;
KCEventTap *eventTap = (__bridge KCEventTap *)context;
switch (type)
{
case kCGEventLeftMouseDown:
@ -71,24 +86,19 @@ CGEventRef eventTapCallback(
case kCGEventOtherMouseDown:
case kCGEventOtherMouseUp:
case kCGEventOtherMouseDragged:
[keyTap _noteMouseEvent:event];
break;
case kCGEventKeyDown:
[keyTap _noteKeyEvent:event];
[eventTap _noteMouseEvent:event];
break;
case kCGEventFlagsChanged:
[keyTap _noteFlagsChanged:event];
[eventTap _noteFlagsChanged:event];
break;
default:
break;
}
return NULL;
return event;
}
@implementation KCEventTap
@synthesize delegate = _delegate;
-(id) init
{
if (!(self = [super init]))
@ -101,8 +111,6 @@ CGEventRef eventTapCallback(
if (_tapInstalled) {
[self removeTap];
}
[super dealloc];
}
-(NSError*) constructErrorWithDescription:(NSString*)description {
@ -121,69 +129,52 @@ CGEventRef eventTapCallback(
// We have to try to tap the keydown event independently because CGEventTapCreate will succeed if it can
// install the event tap for the flags changed event, which apparently doesn't require universal access
// to be enabled. Thus, the call would succeed but KeyCastr would be, um, useless.
CFMachPortRef tapKeyDown = CGEventTapCreate(
kCGSessionEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
CGEventMaskBit(kCGEventKeyDown),
nullEventTapCallback,
self
);
if (tapKeyDown == NULL) {
keyEventTap = CGEventTapCreate(kCGSessionEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
CGEventMaskBit(kCGEventKeyDown)
| CGEventMaskBit(kCGEventKeyUp),
keyEventTapCallback,
(__bridge void *)self
);
if (keyEventTap == NULL) {
if (error != NULL) {
*error = [self constructErrorWithDescription:@"Could not create keyDown event tap!"];
}
return NO;
}
CFRelease( tapKeyDown );
eventTap = CGEventTapCreate(
kCGSessionEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
CGEventMaskBit(kCGEventLeftMouseDown)
| CGEventMaskBit(kCGEventLeftMouseUp)
| CGEventMaskBit(kCGEventRightMouseDown)
| CGEventMaskBit(kCGEventRightMouseUp)
| CGEventMaskBit(kCGEventLeftMouseDragged)
| CGEventMaskBit(kCGEventRightMouseDragged)
| CGEventMaskBit(kCGEventKeyDown)
| CGEventMaskBit(kCGEventFlagsChanged)
| CGEventMaskBit(kCGEventOtherMouseDown)
| CGEventMaskBit(kCGEventOtherMouseUp)
| CGEventMaskBit(kCGEventOtherMouseDragged),
eventTapCallback,
self
);
if (eventTap == NULL) {
if (error != NULL) {
*error = [self constructErrorWithDescription:@"Could not create keyDown|flagsChanged event tap!"];
*error = [self constructErrorWithDescription:@"Could not create key event tap! Permissions needed..."];
}
return NO;
}
eventTapEventSource = CFMachPortCreateRunLoopSource(NULL, eventTap, 0);
if (eventTapEventSource == NULL) {
CFRelease(eventTap);
mouseAndFlagsEventTap = CGEventTapCreate(kCGSessionEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
CGEventMaskBit(kCGEventLeftMouseDown)
| CGEventMaskBit(kCGEventLeftMouseUp)
| CGEventMaskBit(kCGEventRightMouseDown)
| CGEventMaskBit(kCGEventRightMouseUp)
| CGEventMaskBit(kCGEventLeftMouseDragged)
| CGEventMaskBit(kCGEventRightMouseDragged)
| CGEventMaskBit(kCGEventFlagsChanged)
| CGEventMaskBit(kCGEventOtherMouseDown)
| CGEventMaskBit(kCGEventOtherMouseUp)
| CGEventMaskBit(kCGEventOtherMouseDragged),
mouseAndFlagsEventTapCallback,
(__bridge void *)self
);
if (mouseAndFlagsEventTap == NULL) {
if (error != NULL) {
*error = [self constructErrorWithDescription:@"Could not create run loop source!"];
*error = [self constructErrorWithDescription:@"Could not create mouse and modifiers event tap!"];
}
return NO;
}
eventTapRunLoop = CFRunLoopGetCurrent();
if (eventTapRunLoop == NULL) {
CFRelease(eventTapEventSource);
CFRelease(eventTap);
if (error != NULL) {
*error = [self constructErrorWithDescription:@"Could not get current run loop!"];
}
return NO;
}
CFRunLoopAddSource(eventTapRunLoop, eventTapEventSource, kCFRunLoopDefaultMode);
keyEventTapSource = CFMachPortCreateRunLoopSource(NULL, keyEventTap, 0);
mouseAndFlagsEventTapSource = CFMachPortCreateRunLoopSource(NULL, mouseAndFlagsEventTap, 0);
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, keyEventTapSource, kCFRunLoopDefaultMode);
CFRunLoopAddSource(runLoop, mouseAndFlagsEventTapSource, kCFRunLoopDefaultMode);
_tapInstalled = YES;
@ -194,10 +185,15 @@ CGEventRef eventTapCallback(
if (!_tapInstalled) {
return;
}
CFRunLoopRemoveSource(eventTapRunLoop, eventTapEventSource, kCFRunLoopDefaultMode);
CFRelease(eventTapEventSource);
CFRelease(eventTap);
CFRunLoopSourceInvalidate(keyEventTapSource);
CFRunLoopSourceInvalidate(mouseAndFlagsEventTapSource);
CFRelease(keyEventTapSource);
CFRelease(mouseAndFlagsEventTapSource);
CFRelease(keyEventTap);
CFRelease(mouseAndFlagsEventTap);
_tapInstalled = NO;
}

View file

@ -37,7 +37,7 @@
3D3F52A70F30BF1E001C7272 /* KeyCastr.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3D3F52A60F30BF1E001C7272 /* KeyCastr.icns */; };
3D3F52BA0F30C68B001C7272 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D3F52B90F30C68B001C7272 /* Quartz.framework */; };
3D3F52EB0F30CB13001C7272 /* KCPrefsWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D3F52EA0F30CB13001C7272 /* KCPrefsWindowController.m */; };
3D3F540C0F30E8E7001C7272 /* KCEventTap.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D3F540B0F30E8E7001C7272 /* KCEventTap.m */; };
3D3F540C0F30E8E7001C7272 /* KCEventTap.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D3F540B0F30E8E7001C7272 /* KCEventTap.m */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; };
3D3F548A0F30F37E001C7272 /* GeneralIcon.tif in Resources */ = {isa = PBXBuildFile; fileRef = 3D3F54870F30F37E001C7272 /* GeneralIcon.tif */; };
3D3F548B0F30F37E001C7272 /* DisplayIcon.tif in Resources */ = {isa = PBXBuildFile; fileRef = 3D3F54880F30F37E001C7272 /* DisplayIcon.tif */; };
3D3F548E0F30F3C7001C7272 /* UpdateIcon.tif in Resources */ = {isa = PBXBuildFile; fileRef = 3D3F548D0F30F3C7001C7272 /* UpdateIcon.tif */; };
@ -796,7 +796,6 @@
ProvisioningStyle = Automatic;
};
8D1107260486CEB800E47090 = {
DevelopmentTeam = 68TUCLNAPS;
ProvisioningStyle = Automatic;
};
AFC172182377CFF500292155 = {
@ -1521,7 +1520,7 @@
PRODUCT_BUNDLE_IDENTIFIER = io.github.keycastr;
PRODUCT_NAME = KeyCastr;
PROVISIONING_PROFILE_SPECIFIER = "KeyCastr Developer ID";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "KeyCastr Developer ID";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "KeyCastr Developer ID Distribution";
WRAPPER_EXTENSION = app;
};
name = Release;