macoscocoakeyboardkeyboard-eventscgeventtap

Stop Intercepting Keyboard Input While App Running - CGEventTap


What is the correct way to stop watching keyboard event taps using CGEventTap?

I am building a simple background app that converts the output of specific keys. Thanks to this excellent post on CGEventTap, I've been able to enable the key conversion. Unfortunately, I do not seem to be able to stop it short of killing the app.

The following method is called when the user toggles a checkbox to turn the functionality ON or OFF. Toggle ON happens immediately. Toggle OFF can take a minute or more before it takes affect. I see via log that the "Disabled. Stop converting taps." is detected. But the key conversion just keeps on going. I don't understand why.

- (void)watchEventTap
{    
        @autoreleasepool
        {
            CFRunLoopSourceRef runLoopSource = NULL;
            CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(NX_SYSDEFINED), myCGEventCallback, NULL);
            runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);

            if (!eventTap)
            {
                NSLog(@"Couldn't create event tap!");
                exit(1);
            }

            if (self.shortcutEnabled) // User default toggled ON
            {
                NSLog(@"Enabled. Convert taps.");
                CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
                CGEventTapEnable(eventTap, true);
                // CFRunLoopRun(); // This blocks rest of app from executing
            }
            else // User default toggled OFF
            {
                NSLog(@"Disabled. Stop converting taps.");
                CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
                CGEventTapEnable(eventTap, false);
                // Clean up the event tap and source after ourselves.
                CFMachPortInvalidate(eventTap);
                CFRunLoopSourceInvalidate(runLoopSource);
                CFRelease(eventTap);
                CFRelease(runLoopSource);
                eventTap = NULL;
                runLoopSource = NULL;
            }
        }
//        exit(0);  // This blocks rest of app from executing
}

Thanks for any suggestions. I'm new building Mac OS X apps, so please forgive me if I'm doing something ignorant.


Solution

  • Thanks to an experienced Mac developer, I got my issue resolved. I was creating a new runLoopsSource every time the method was called.

    Now I've created instance variables for the tapEvent and runLoop. Only one line was needed to stop the eventTap. Modified method below:

    - (void)watchEventTap
    {
    
        @autoreleasepool
        {
    
            if ( [[NSUserDefaults standardUserDefaults] isEnabledNumLockDV] == YES ) // User default toggled ON
            {
                _runLoopSource = NULL;
                 _eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(NX_SYSDEFINED), myCGEventCallback, NULL);
                _runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, _eventTap, 0);
    
                if (!_eventTap)
                {
                    NSLog(@"Couldn't create event tap!");
                    exit(1);
                }
    
                NSLog(@"Enabled. Convert taps.");
                CFRunLoopAddSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes);
                CGEventTapEnable(_eventTap, true);
            }
            else if ( [[NSUserDefaults standardUserDefaults] isEnabledNumLockDV] == NO ) // User default toggled OFF
            {
                NSLog(@"Disabled. Stop converting taps.");
                CGEventTapEnable(_eventTap, false);
            }
    
        }
    }