objective-cmacoskeyboard-hooknsevent

Modify NSEvent to send a different key than the one that was pressed


I'm trying to create an OS X keyboard hook for assistive technology purposes (i.e. don't worry, not a keylogger).

When a user presses a key, I want to prevent the real keypress and send a fake keypress (character of my choosing) instead.

I have the following code:

- (void) hookTheKeyboard {
    CGEventMask keyboardMask = CGEventMaskBit(kCGEventKeyDown);
    id eventHandler = [NSEvent addGlobalMonitorForEventsMatchingMask:keyboardMask handler:^(NSEvent *keyboardEvent) {
        NSLog(@"keyDown: %c", [[keyboardEvent characters] characterAtIndex:0]);
        //Want to: Stop the keyboard input
        //Want to: Send another key input instead
    }];
}

Any help accomplishing either of those goals? Basically modifying the NSEvent "keyboardEvent" to send a different character. Thanks.


Solution

  • You can't do this with the NSEvent API, but you can do this with a CGEventTap. You can create an active event tap and register a callback that receives a CGEventRef and can modify it (if necessary) and return it to modify the actual event stream.


    EDIT

    Here's a simple program that, while running, replaces every "b" keystroke with a "v":

    #import <Cocoa/Cocoa.h>
    
    CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
      //0x0b is the virtual keycode for "b"
      //0x09 is the virtual keycode for "v"
      if (CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == 0x0B) {
        CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, 0x09);
      }
    
      return event;
    }
    
    int main(int argc, char *argv[]) {
      NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
      CFRunLoopSourceRef runLoopSource;
    
      CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, kCGEventMaskForAllEvents, myCGEventCallback, NULL);
    
      if (!eventTap) {
        NSLog(@"Couldn't create event tap!");
        exit(1);
      }
    
      runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
    
      CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
    
      CGEventTapEnable(eventTap, true);
    
      CFRunLoopRun();
    
      CFRelease(eventTap);
      CFRelease(runLoopSource);
      [pool release];
    
      exit(0);
    }
    

    (Funny story: as I was editing this post, I kept on trying to write "replaces every 'b' keystroke", but it kept on coming out as "replaces every 'v' keystroke". I was confused. Then I remembered that I hadn't stopped the app yet.)