Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I Monitor Mouse Movement Events in All Windows (Not Just One) on X11

I'm trying write an X11 program to monitor all mouse movements on the desktop. The program should be able to receive a notification whenever the mouse is moved by the human user, or moved programmatically via XWarpPointer() by a robotic application. I know it should be possible by setting a PointerMotionMask via XSelectInput() and monitor MotionNotify, but I'm having troubles receiving mouse events from all windows, not just one.

Initially, I just tried to receive pointer motion events from the root window, in the following demo.

#include <stdio.h>
#include <X11/Xlib.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;
    XEvent event;

    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0); 
    XSelectInput(display, root_window, PointerMotionMask);

    while (1) {
        XNextEvent(display, &event);
        switch(event.type) { 
            case MotionNotify:
                printf("x: %d y: %d\n", event.xmotion.x, event.xmotion.y );
                break;
        }
    }   
    return 0;
}

But it doesn't receive any events, unless the mouse pointer is on an empty desktop background. It's clear that merely receiving events from the root window won't work. Then I tried a workaround: first, set SubstructureNotifyMask on the root window to monitor all CreateNotify events to catch all newly created windows, then call XSelectInput() to enable the PointerMotionMask on these windows.

#include <stdio.h>
#include <X11/Xlib.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;
    XEvent event;

    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0); 
    XSelectInput(display, root_window, SubstructureNotifyMask);

    while (1) {
        XNextEvent(display, &event);
        switch(event.type) { 
            case CreateNotify:
                XSelectInput(display, event.xcreatewindow.window, PointerMotionMask);
                break;
            case MotionNotify:
                printf("x: %d y: %d\n", event.xmotion.x, event.xmotion.y);
                break;
        }
    }   
    return 0;
}

This approach is more successful, I started to receive some mouse events from new windows. Unfortunately, it still doesn't work in all portions inside a window - for example, it cannot receive mouse events from the console area in terminal emulators, but can receive events when the mouse is located around the title bar. It appears that a window can create more subwindows, so the mouse events won't be recorded.

Then I tried another workaround - set both SubstructureNotifyMask and PointerMotionMask in CreateNotify, so when a window creates a child window, SubstructureNotifyMask ensures more CreateNotify events will be received in a recursive manner, so all child windows will get PointerMotionMask as well.

#include <stdio.h>
#include <X11/Xlib.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;
    XEvent event;

    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0); 
    XSelectInput(display, root_window, SubstructureNotifyMask);

    while (1) {
        XNextEvent(display, &event);
        switch(event.type) { 
            case CreateNotify:
                XSelectInput(display, event.xcreatewindow.window, SubstructureNotifyMask | PointerMotionMask);
                break;
            case MotionNotify:
                printf("x: %d y: %d\n", event.xmotion.x, event.xmotion.y);
                break;
        }
    }   
    return 0;
}

It works a bit better than the second example, but it's not reliable:

  • X is fully asynchronous, is it possible that the child window got created before we had a chance to XSelectInput()?

  • Sometimes it just reports a BadWindow error and crashes.

  • X event handling becomes messy - if the program already handles a lot of different X events, enabling SubstructureNotifyMask recursively will make many unrelated events delivered to other handlers, and it's a pain to add extra code to discriminate between wanted and unwanted events.


So, how do I monitor mouse movement events in all windows on X11?

like image 384
比尔盖子 Avatar asked Jun 18 '20 10:06

比尔盖子


1 Answers

After doing some research, especially reading the source code of Xeyes (I always fell the demo is stupid, but it helps a lot here!), I found:

  • Calling XSelectInput() on all the windows and subwindows is a futile attempt, you have to set a mask on every single window and child window ever created, it's not a robust solution, and not recommended.

  • Instead, it's better to just continuously pulling the mouse pointer from the X server explicitly via XQueryPointer(), rather than asking the X server to push MotionEvent to us.

One naive solution is simply setting up a timer by XtAppAddTimeOut() and calling XQueryPointer() periodically, it works, and indeed, it was what Xeyes did in the past! But it unnecessarily wastes CPU time. Nowadays, the best practice is to take advantage of XInputExtention 2.0. The workflow is:

  1. Initialize XInput v2.0

  2. Enable various masks via XISetMask() and XIEventMask() to receive XI_RawMotion events (or XI_Motion, see notes below) from XIAllMasterDevices (or XIAllDevices).

  3. When a XI_RawMotion (or XI_Motion) event has been received, call XQueryPointer().

  4. XQueryPointer() returns:

    • Mouse coordinates with respect to the root window.
    • The active window under the mouse cursor, if any.
  5. Perform a XTranslateCoordinates() if we want relative coordinates with respect to the active window under the mouse cursor.

Demo

Here's a demo (save as mouse.c, compile with gcc mouse.c -o mouse -lX11 -lXi). However, it cannot detect XWarpPointer(), see notes below.

#include <stdio.h>
#include <assert.h>
#include <X11/Xlib.h>
#include <X11/extensions/XInput2.h>

int main(int argc, char **argv)
{
    Display *display;
    Window root_window;

    /* Initialize (FIXME: no error checking). */
    display = XOpenDisplay(0);
    root_window = XRootWindow(display, 0);

    /* check XInput */
    int xi_opcode, event, error;
    if (!XQueryExtension(display, "XInputExtension", &xi_opcode, &event, &error)) {
        fprintf(stderr, "Error: XInput extension is not supported!\n");
        return 1;
    }

    /* Check XInput 2.0 */
    int major = 2;
    int minor = 0;
    int retval = XIQueryVersion(display, &major, &minor);
    if (retval != Success) {
        fprintf(stderr, "Error: XInput 2.0 is not supported (ancient X11?)\n");
        return 1;
    }

    /*
     * Set mask to receive XI_RawMotion events. Because it's raw,
     * XWarpPointer() events are not included, you can use XI_Motion
     * instead.
     */
    unsigned char mask_bytes[(XI_LASTEVENT + 7) / 8] = {0};  /* must be zeroed! */
    XISetMask(mask_bytes, XI_RawMotion);

    /* Set mask to receive events from all master devices */
    XIEventMask evmasks[1];
    /* You can use XIAllDevices for XWarpPointer() */
    evmasks[0].deviceid = XIAllMasterDevices;
    evmasks[0].mask_len = sizeof(mask_bytes);
    evmasks[0].mask = mask_bytes;
    XISelectEvents(display, root_window, evmasks, 1);

    XEvent xevent;
    while (1) {
        XNextEvent(display, &xevent);

        if (xevent.xcookie.type != GenericEvent || xevent.xcookie.extension != xi_opcode) {
            /* not an XInput event */
            continue;
        }
        XGetEventData(display, &xevent.xcookie);
        if (xevent.xcookie.evtype != XI_RawMotion) {
            /*
             * Not an XI_RawMotion event (you may want to detect
             * XI_Motion as well, see comments above).
             */
            XFreeEventData(display, &xevent.xcookie);
            continue;
        }
        XFreeEventData(display, &xevent.xcookie);

        Window root_return, child_return;
        int root_x_return, root_y_return;
        int win_x_return, win_y_return;
        unsigned int mask_return;
        /*
         * We need:
         *     child_return - the active window under the cursor
         *     win_{x,y}_return - pointer coordinate with respect to root window
         */
        int retval = XQueryPointer(display, root_window, &root_return, &child_return,
                                   &root_x_return, &root_y_return,
                                   &win_x_return, &win_y_return,
                                   &mask_return);
        if (!retval) {
            /* pointer is not in the same screen, ignore */
            continue;
        }

        /* We used root window as its reference, so both should be the same */
        assert(root_x_return == win_x_return);
        assert(root_y_return == win_y_return);

        printf("root: x %d y %d\n", root_x_return, root_y_return);

        if (child_return) {
            int local_x, local_y;
            XTranslateCoordinates(display, root_window, child_return,
                                  root_x_return, root_y_return,
                                  &local_x, &local_y, &child_return);
            printf("local: x %d y %d\n\n", local_x, local_y);
        }
    }

    XCloseDisplay(display);

    return 0;
}

Sample Output

root: x 631 y 334
local: x 140 y 251

root: x 628 y 338
local: x 137 y 255

root: x 619 y 343
local: x 128 y 260

XWarpPointer() Troubles

The demo above doesn't work if the pointer is moved via XWarpPointer() by a robotic application on newer systems after X.Org 1.10.4. This is intentional, see Bug 30068 on FreeDesktop.

In order to receive mouse events triggered by all mouse movements, including XWarpPointer(), change XI_RawMotion to XI_Motion, and change XIAllMasterDevices to XIAllDevices.

References

This demo lacks error checking and may contain bugs. If in doubts, please check the following authoritative references.

  • Tracking Cursor Position by Keith Packard, a real X expert, has been heavily involved in the development of X since the late 1980s, and responsible for many X extensions and technical papers.

  • Xeyes source code from X.Org.

like image 131
比尔盖子 Avatar answered Nov 18 '22 04:11

比尔盖子