views:

79

answers:

2

Hi gang,

I need to write something using Cocoa to surface raw mouse movement data. Optimally, the app would just be a little daemon that would run, passing the data to a socket server which another application could tap into to gain access to the events.

Can anyone point me in the right direction with regard to approach and tools? I am not even sure where to begin with this right now.

Thanks!

Chris

A: 

Write an EventTap. Documentation can be found here.

In MacOS X every event (e.g. every keyboard key pressed, every mouse key pressed or mouse movement) creates an event that travels the following path:

Driver (Kernel) -> Window Server (privileged) -> User (Login) Session -> Active Application

Everywhere where I wrote an arrow (->) an EventTap can be placed to either only look at the event (a listen only EventTap) or to either modify or drop the event (an event filtering EventTap). Please note that to catch an event between Driver and WindowServer, your daemon must run with root privileges.

Here is some sample code:

// Compile with:
// gcc -framework ApplicationServices -o MouseWatcher MouseWatcher.c
//
// Start with:
// ./MouseWatcher
//
// Terminate by hitting CTRL+C

#include <ApplicationServices/ApplicationServices.h>


static CGEventRef myEventTapCallback (
    CGEventTapProxy proxy,
    CGEventType type,
    CGEventRef event,
    void * refcon
) {
    CGPoint mouseLocation;

    // If we would get different kind of events, we can distinguish them
    // by the variable "type", but we know we only get mouse moved events

    mouseLocation = CGEventGetLocation(event);
    printf(
        "Mouse is at x/y: %u/%u\n",
        (unsigned int)mouseLocation.x,
        (unsigned int)mouseLocation.y
    );
    // Pass on the event, we must not modify it anyway, we are a listener
    return event;
}


int main (
    int argc,
    char ** argv
) {
    CGEventMask emask;
    CFMachPortRef myEventTap;
    CFRunLoopSourceRef eventTapRLSrc;

    // We only want one kind of event at the moment: The mouse has moved
    emask = CGEventMaskBit(kCGEventMouseMoved);

    // Create the Tap
    myEventTap = CGEventTapCreate (
        kCGSessionEventTap, // Catch all events for current user session
        kCGTailAppendEventTap, // Append to end of EventTap list
        kCGEventTapOptionListenOnly, // We only listen, we don't modify
        emask,
        &myEventTapCallback,
        NULL // We need no extra data in the callback
    );

    // Create a RunLoop Source for it
    eventTapRLSrc = CFMachPortCreateRunLoopSource(
        kCFAllocatorDefault,
        myEventTap,
        0
    );

    // Add the source to the current RunLoop
    CFRunLoopAddSource(
        CFRunLoopGetCurrent(),
        eventTapRLSrc,
        kCFRunLoopDefaultMode
    );

    // Keep the RunLoop running forever
    CFRunLoopRun();

    // Not reached (RunLoop above never stops running)
    return 0;
}

The answer from Dave is the nicer Cocoa way of doing pretty much the same thing; basically Cocoa does the same as I do in my sample above behind the scenes, just wrapped into a static method. The code of Dave works only on 10.6, though, the above works in 10.4, 10.5 and 10.6.

Mecki
Hi Mecki, thanks for this. I am using 10.6, and as such, the single line method Dave described is a more desirable solution. Appreciate your input!
Raconteur
A: 

The other simple way to do this is to add a global event monitor (10.6 only, however):

id eventHandler = [NSEvent addGlobalMonitorForEventsMatchingMask:NSMouseMovedMask handler:^(NSEvent * mouseEvent) {
  NSLog(@"Mouse moved: %@", NSStringFromPoint([mouseEvent locationInWindow]));
}];

Then when you're done tracking, you do:

[NSEvent removeMonitor:eventHandler];
Dave DeLong
But only works with 10.6, while my code even works with 10.4 and up :-P
Mecki
@Mecki yep, that's the only caveat. If the poster can target 10.6 exclusively, then a global monitor is definitely easier. Otherwise he'll need an event tap.
Dave DeLong
Thanks for the info. I tried adding Dave's code to my app, but I get a compilation error saying that NSEvent may not respond to mouseLocation and that NSStringFromPoint is being passed an invalid argument.What is the caret for before the (NSEvent *mouseEvent)?? Is that the problem? I am not familiar with that syntax...Given that I cannot try this, let me ask, will this give me the raw mouse data? Basically, even if the mouse is pegged to the upper left corner, I need to know that the user has continued to move it up and left, be that the case. Will this accomplish that?Thanks again!
Raconteur
@Raconteur whoops, I used the wrong method! Fixed. This will give you the location of the mouse with respect to the screen. (0,0) is the bottom left corner of your screen. The caret means that we're using a Block (ie a Lambda/Closure), which are new in 10.6. This will get invoked every time the mouse moves, regardless of which app is frontmost or if the movement is only by 1 pixel.
Dave DeLong
@Dave- Thanks. But now, if the cursor is "stuck" in the corner, will I just get 0,0 over and over if I keep moving in that diagonal direction? What I need is the underlying message that the mouse moved down and left by some amount. The screen coordinates notwithstanding. Does that make sense?
Raconteur
Yeah... damn... that is doing what I had done with[window setAcceptsMouseMovedEvents:YES];and[NSEvent mouseLocation]Do I need a kext to get at what I am after? If so, any ideas on if something like this exists?
Raconteur
@Raconteur yes, it will keep firing as long as the mouse is moving, even if the cursor isn't. You can use `[mouseEvent deltaX]` and `[mouseEvent deltaY]` to see how much *the mouse* (not the cursor) has moved since the previous callback.
Dave DeLong
@Dave: That did it! Thanks a MILLION! Was really worried about needing to do the kext thing...
Raconteur
@Dave: One last question on this... I need to do this in a daemon. How do I go about getting UI constructs like mouse info with a headless app??Basically, I just want to surface the mouse deltas through a socket server.
Raconteur
@Raconteur that's a lot more difficult, since NSEvent only exists in AppKit. If you need this to be a headless app that can't link against AppKit, then you'll have to use an event tap.
Dave DeLong
@Dave: Ok, thanks. Now I am battling with the loop that captures the mouse data, and figuring out how to integrate that into the run-loop for the stream that is spewed out via the socket. Got any magic for that? If it makes more sense I can post this a separate questions.
Raconteur
@Raconteur better as a new question
Dave DeLong
Thanks, Dave. Done.
Raconteur

related questions