Kyle McDonald Kyle McDonald - 9 days ago 8
Objective-C Question

Constrain mouse movement to region in OS X without jitter

I would like to constrain mouse movement to a specific rectangular region of the screen in OS X 10.11. I modified some code from MouseTools (below) to do this but it's jittery when you hit the edge of the screen. How can I get rid of this jitter?

// gcc -Wall constrain.cpp -framework ApplicationServices -o constrain

#include <ApplicationServices/ApplicationServices.h>

int main (int argc, const char * argv[]) {
if(argc != 5) {
printf("Usage: constrain left top right bottom\n");
return 0;
}
int leftBound = strtol(argv[1], NULL, 10);
int topBound = strtol(argv[2], NULL, 10);
int rightBound = strtol(argv[3], NULL, 10);
int bottomBound = strtol(argv[4], NULL, 10);
CGEventTapLocation tapLocation = kCGHIDEventTap;
CGEventSourceRef sourceRef = CGEventSourceCreate(kCGEventSourceStatePrivate);
while(true) {
CGEventRef mouseEvent = CGEventCreate(NULL);
CGPoint mouseLoc = CGEventGetLocation(mouseEvent);
int x = mouseLoc.x, y = mouseLoc.y;
if(x < leftBound || x > rightBound || y < topBound || y > bottomBound) {
if(x < leftBound) x = leftBound;
if(x > rightBound) x = rightBound;
if(y < topBound) y = topBound;
if(y > bottomBound) y = bottomBound;
CGEventRef moveMouse = CGEventCreateMouseEvent(sourceRef, kCGEventMouseMoved, CGPointMake(x, y), 0);
CGEventPost(tapLocation, moveMouse);
CFRelease(moveMouse);
}
CFRelease(mouseEvent);
usleep(8*1000); // 8ms, ~120fps
}
CFRelease(sourceRef);
return 0;
}


Here are some other things I tried: Receiving, Filtering, and Modifying Mouse Events shows how to create a callback for mouse movements using
CGEventTapCreate
. In the code it says "We can change aspects of the mouse event. For example, we can use
CGEventSetLocation(event, newLocation)
." Unfortunately this doesn't actually change the mouse location (complete example here). The only ways I've been able to change the mouse location are by doing a
CGEventPost
or
CGWarpMouseCursorPosition
. I also tried using

kCGHIDEventTap
instead of
kCGSessionEventTap
, and I checked that I had super user permissions and accessibility enabled for the app. It seems like it should work because there is another example of modifying key presses which works correctly. Instead of using
CGEventSetLocation
I also tried using
CGEventSetIntegerValueField
(like in the keyboard example) to set the x and y deltas to 0, but this didn't change anything.

Finally, I also tried using
CGEventPost
and
CGWarpMouseCursorPosition
inside the
CGEventCallback
, the "invisible border" still it is more jittery than the above code.

Answer

I've implemented this for Wine in the Mac driver. It gets a bit complicated.

The approach is to basically disable movement of the mouse cursor by calling CGAssociateMouseAndMouseCursorPosition(false). This right here achieves what you want if the constraining rectangle is the 1x1 rect at the cursor's current location.

On top of that, I then use an event tap to observe the mouse move events. These don't change position but have the deltas of what the user attempted to do. I then use CGWarpMouseCursorPosition() to move the cursor according to those deltas and adjust the events before passing them along.

Note that the location information in the mouse events is not meaningful. It will be stale. Some of the events will have been queued before your last warp and so have an old location. You need to track where the cursor is yourself. It starts wherever it was when you disassociated it from the mouse and changes whenever you warp it. You add the deltas to that (not to the event location), clip that to the constraining rectangle, and use that as the new position for the event and for the warp. For the next event, you'll start from this new position. Etc.

There's an additional unfortunate wrinkle. CGWarpMouseCursorPosition() affects the delta values of subsequent mouse move events. Basically, the distance that you warped the cursor is added to the first mouse move event that's queued after the warp. Unchecked, this will effectively double the user's mouse movements with each event for a runaway feedback loop and the cursor will shoot off to one extreme or the other.

So, you have to keep a record of the warps you perform, when you performed them, and how much they changed the position. Then, as events come through the tap, you compare their time to the warp times. For any warps that are earlier than the current event, you subtract out their position change from the event's deltas and remove them from the list.