macoskeyboard-eventsiokitnsevent

In OSX, how to determine which keyboard generated an NSEvent?


I've been trying to determine (from within the event handler) which keyboard triggered the event. I've been using these two posts:

In the second article, the author successfully separate out his keyboards using a Carbon technique, but attempting the same trick using Cocoa fails.

On my own system both fail, maybe because my wireless keyboard is also apple manufactured, so maybe reports the same identifier as the inbuilt keyboard.

In the first article someone proposes a solution of watching keyboard events at a lower level (where it IS possible to distinguish the keyboards) and storing this data in a queue, and having the event handler retrieve the information.

This looks a bit hairy, so I'm just checking here to see if anyone has found something better.

Here is my code that demonstrates the failing of differentiating the keyboards in the event handler:

// compile and run from the commandline with:
//    clang -fobjc-arc -framework Cocoa -framework Carbon  ./tap_k.m  -o tap_k
//    sudo ./tap_k

#import <Foundation/Foundation.h>
#import <AppKit/NSEvent.h>
//#import <CarbonEventsCore.h>
#include <Carbon/Carbon.h>


typedef CFMachPortRef EventTap;

// - - - - - - - - - - - - - - - - - - - - -

@interface KeyChanger : NSObject
{
@private
    EventTap            _eventTap;
    CFRunLoopSourceRef  _runLoopSource;
    CGEventRef          _lastEvent;
}
@end

// - - - - - - - - - - - - - - - - - - - - -

CGEventRef _tapCallback(
                        CGEventTapProxy proxy,
                        CGEventType     type,
                        CGEventRef      event,
                        KeyChanger*     listener
                        );

// - - - - - - - - - - - - - - - - - - - - -

@implementation KeyChanger

- (BOOL)tapEvents
{
    if (!_eventTap) {
        NSLog(@"Initializing an event tap.");

        // kCGHeadInsertEventTap -- new event tap should be inserted before any pre-existing event taps at the same location,
        _eventTap = CGEventTapCreate( kCGHIDEventTap, // kCGSessionEventTap,
                                      kCGHeadInsertEventTap,
                                      kCGEventTapOptionDefault,
                                           CGEventMaskBit( kCGEventKeyDown )
                                         | CGEventMaskBit( kCGEventFlagsChanged )
                                         | CGEventMaskBit( NSSystemDefined )
                                         ,
                                      (CGEventTapCallBack)_tapCallback,
                                      (__bridge void *)(self));
        if (!_eventTap) {
            NSLog(@"unable to create event tap. must run as root or "
                    "add privlidges for assistive devices to this app.");
            return NO;
        }
    }
    CGEventTapEnable(_eventTap, TRUE);

    return [self isTapActive];
}

- (BOOL)isTapActive
{
    return CGEventTapIsEnabled(_eventTap);
}

- (void)listen
{
    if( ! _runLoopSource ) {
        if( _eventTap ) { // dont use [self tapActive]
            NSLog(@"Registering event tap as run loop source.");
            _runLoopSource = CFMachPortCreateRunLoopSource( kCFAllocatorDefault, _eventTap, 0 );

            // Add to the current run loop.
            CFRunLoopAddSource( CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes );

            CFRunLoopRun();
        }else{
            NSLog(@"No Event tap in place! You will need to call listen after tapEvents to get events.");
        }
    }
}

- (CGEventRef)processEvent:(CGEventRef)cgEvent
{
    NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];


    //NSEventType type = [event type];

    EventRef ce = (EventRef)[event eventRef];

    if(ce)
    {
        unsigned kbt;
        GetEventParameter(
                            ce,
                            kEventParamKeyboardType,
                            typeUInt32, NULL,
                            sizeof kbt, NULL,
                            & kbt
                            );

        NSLog(@"CARBON Keyboard type: %d",kbt);
    }

    CGEventSourceRef evSrc = CGEventCreateSourceFromEvent( cgEvent );

    if(evSrc)
    {
        unsigned kbt = (NSUInteger) CGEventSourceGetKeyboardType( evSrc );
        CFRelease(evSrc);
        NSLog(@"COCOA: %d",kbt);
    }

    //[super sendEvent:anEvent];
    //}


    NSUInteger modifiers = [event modifierFlags] &
        (NSCommandKeyMask | NSAlternateKeyMask | NSShiftKeyMask | NSControlKeyMask);

    enum {
       kVK_ANSI_3 = 0x14,
    };


    switch( event.type ) {
        case NSFlagsChanged:
            NSLog(@"NSFlagsChanged: %d", event.keyCode);
            break;

        case NSSystemDefined:
            NSLog(@"NSSystemDefined: %lx", event.data1);
            return NULL;

        case kCGEventKeyDown:
            NSLog(@"KeyDown: %d", event.keyCode);
            break;

        default:
            NSLog(@"WTF");
    }


    // TODO: add other cases and do proper handling of case
    if (
        //[event.characters caseInsensitiveCompare:@"3"] == NSOrderedSame
        event.keyCode == kVK_ANSI_3
        && modifiers == NSShiftKeyMask
        ) 
    {
        NSLog(@"Got SHIFT+3");

        event = [NSEvent keyEventWithType: event.type
                                 location: NSZeroPoint
                            modifierFlags: event.modifierFlags & ! NSShiftKeyMask
                                timestamp: event.timestamp
                             windowNumber: event.windowNumber
                                  context: event.context
                               characters: @"#"
              charactersIgnoringModifiers: @"#"
                                isARepeat: event.isARepeat
                                  keyCode: event.keyCode];
    }
    _lastEvent = [event CGEvent];
    CFRetain(_lastEvent); // must retain the event. will be released by the system
    return _lastEvent;
}

- (void)dealloc
{
    if( _runLoopSource ) {
        CFRunLoopRemoveSource( CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes );
        CFRelease( _runLoopSource );
    }
    if( _eventTap ) {
        //kill the event tap
        CGEventTapEnable( _eventTap, FALSE );
        CFRelease( _eventTap );
    }
}

@end

// - - - - - - - - - - - - - - - - - - - - -

CGEventRef _tapCallback(
                        CGEventTapProxy proxy,
                        CGEventType     type,
                        CGEventRef      event,
                        KeyChanger*     listener
                        )
{
    //Do not make the NSEvent here.
    //NSEvent will throw an exception if we try to make an event from the tap timout type
    @autoreleasepool {
        if( type == kCGEventTapDisabledByTimeout ) {
            NSLog(@"event tap has timed out, re-enabling tap");
            [listener tapEvents];
            return nil;
        }
        if( type != kCGEventTapDisabledByUserInput ) {
            return [listener processEvent:event];
        }
    }
    return event;
}

// - - - - - - - - - - - - - - - - - - - - -

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        KeyChanger* keyChanger = [KeyChanger new];
        [keyChanger tapEvents];
        [keyChanger listen];//blocking call.
    }
    return 0;
}

Solution

  • Here's an outline of what you could do:

    You set up an IOHIDManager and set input matching dictionaries to match keyboards.

    The IOHIDManager will then provide you with references to all attached keyboards as IOHIDDevices.

    Then finally you can set up input callbacks for the IOHIDDevices. Now you can have a separate input callback for each device!

    This is a little cumbersome to set up and work with, and it doesn't let you filter/alter events like a CGEventTap would. But it's the only method I know for monitoring input such that you know which device caused which input.


    Here are some starting points:

    IOHIDManager docs

    IOHIDUsageTables.h and IOHIDDeviceKeys.h

    For keyboards you'll want to declare the match dict like this

    NSDictionary *matchDict = @{
        @(kIOHIDDeviceUsagePageKey): @(kHIDPage_GenericDesktop),
        @(kIOHIDDeviceUsageKey): @(kHIDUsage_GD_Keyboard),
    };
    

    (And then convert it to CFDictionaryRef via toll-free bridging) (Not sure if correct - none of this is tested)