tAdd monitoring of events on root window - xmenu - drop-down menu for X11
 (HTM) git clone git://git.z3bra.org/xmenu.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
 (DIR) commit b02590ceeb90f07ab4ffe23bfa71ae1c8d096e9a
 (DIR) parent 61c19bc23c3509c5b1e32afe95f335516fd39d32
 (HTM) Author: Willy Goiffon <dev@z3bra.org>
       Date:   Wed, 20 Nov 2019 18:49:06 +0100
       
       Add monitoring of events on root window
       
       This will let the menu run as a daemon watching click events on the root
       window and pop the menu on such events.
       
       This saves a click compared to firing the application from an external
       programming that's listenning for events.
       
       Diffstat:
         M xmenu.c                             |     150 ++++++++++++++++++++++++-------
       
       1 file changed, 117 insertions(+), 33 deletions(-)
       ---
 (DIR) diff --git a/xmenu.c b/xmenu.c
       t@@ -15,6 +15,10 @@
        #define MAX(a,b) (((a)>(b))?(a):(b))
        #define MIN(a,b) (((a)<(b))?(a):(b))
        
       +struct geom {
       +        int x, y, w, h;
       +};
       +
        int verbose = 0;
        int current = -1;
        xcb_connection_t *dpy;
       t@@ -24,13 +28,12 @@ xcb_window_t wid;
        size_t nent = 0;
        size_t maxwidth = 0;
        size_t maxheight = 0;
       -int width = 0;
       -int height = 0;
       +struct geom menu = { 0, 0, 0, 0 };
        
        void
        usage(FILE *fd, char *name)
        {
       -        fprintf(fd, "usage: %s [-h]\n", name);
       +        fprintf(fd, "usage: %s [-hd]\n", name);
        }
        
        int
       t@@ -63,8 +66,8 @@ drawentries()
                size_t i, w, h;
        
                for (i = 0; i < nent; i++) {
       -                w = (width - xft_txtw(entries[i])) / 2;
       -                h = (height/nent) * (i + 0.5) - maxheight/2 + (maxheight - xft_txth(entries[i]));
       +                w = (menu.w - xft_txtw(entries[i])) / 2;
       +                h = (menu.h/nent) * (i + 0.5) - maxheight/2 + (maxheight - xft_txth(entries[i]));
                        xft_drawtext(dpy, wid, w, h, foreground, entries[i]);
                }
        
       t@@ -72,7 +75,7 @@ drawentries()
        }
        
        int
       -popwindow(int w, int h)
       +popwindow(int x, int y, int w, int h)
        {
                int mask, val[4];
        
       t@@ -84,14 +87,13 @@ popwindow(int w, int h)
                        | XCB_EVENT_MASK_KEY_PRESS
                        | XCB_EVENT_MASK_ENTER_WINDOW
                        | XCB_EVENT_MASK_LEAVE_WINDOW
       -                | XCB_EVENT_MASK_BUTTON_PRESS
                        | XCB_EVENT_MASK_BUTTON_RELEASE
                        | XCB_EVENT_MASK_POINTER_MOTION
                        | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
        
                wid = xcb_generate_id(dpy);
                xcb_create_window(dpy, screen->root_depth, wid, screen->root,
       -                0, 0, w, h, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
       +                x, y, w, h, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
                        screen->root_visual, mask, val);
        
                xcb_map_window(dpy, wid);
       t@@ -103,10 +105,10 @@ popwindow(int w, int h)
        int
        eventloop()
        {
       -        int loop = 1;
                xcb_generic_event_t *ev;
       +        xcb_motion_notify_event_t *e;
        
       -        while(loop) {
       +        for (;;) {
                        int last = current;
        
                        ev = xcb_wait_for_event(dpy);
       t@@ -114,18 +116,48 @@ eventloop()
                                break;
        
                        switch(ev->response_type & ~0x80) {
       +
       +                /*
       +                 * pressing escape (keycode 9) clears the selection and
       +                 * closes the menu
       +                 */
                        case XCB_KEY_PRESS:
       -                        if (((xcb_key_press_event_t *)ev)->detail == 9)
       -                                return -1;
       +                        if (((xcb_key_press_event_t *)ev)->detail == 9) {
       +                                current = -1;
       +                                return 0;
       +                        }
                                break;
       -                case XCB_BUTTON_RELEASE:
       -                        if (((xcb_button_release_event_t *)ev)->event == wid)
       -                                return current;
       +
       +                /*
       +                 * button press events MUST be caught on root window
       +                 * only to avoid popup loops
       +                 */
       +                case XCB_BUTTON_PRESS:
       +                        menu.x = ((xcb_button_press_event_t *)ev)->root_x;
       +                        menu.y = ((xcb_button_press_event_t *)ev)->root_y;
       +                        popwindow(menu.x, menu.y, menu.w, menu.h);
                                break;
       +
       +                /*
       +                 * releasing the mouse button closes the menu while
       +                 * keeping the current selection active. In other words,
       +                 * it validates the current selection (if any)
       +                 */
       +                case XCB_BUTTON_RELEASE:
       +                        return 0;
       +                        break; /* NOTREACHED */
       +
       +                /*
       +                 * make sure to keep track of window geometry changes
       +                 * to calculate cursor position correctly
       +                 */
                        case XCB_CONFIGURE_NOTIFY:
       -                        width =  ((xcb_configure_notify_event_t *)ev)->width;
       -                        height = ((xcb_configure_notify_event_t *)ev)->height;
       +                        menu.x =  ((xcb_configure_notify_event_t *)ev)->x;
       +                        menu.y = ((xcb_configure_notify_event_t *)ev)->y;
       +                        menu.w =  ((xcb_configure_notify_event_t *)ev)->width;
       +                        menu.h = ((xcb_configure_notify_event_t *)ev)->height;
                                break;
       +
                        case XCB_EXPOSE:
                                /*
                                 * TODO: clean after someone mess up current entry
       t@@ -139,17 +171,30 @@ eventloop()
                                 *
                                 * Damn punks.
                                 */
       -                        if (last >= 0)
       -                                hilight(wid, 0, (height/nent) * last, width, height/nent);
       +                        if (last >= 0 && last != current)
       +                                hilight(wid, 0, (menu.h/nent) * last, menu.w, menu.h/nent);
                                drawentries();
                                if (current >= 0)
       -                                hilight(wid, 0, (height/nent) * current, width, height/nent);
       +                                hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
                                break;
       +
       +                /*
       +                 * moving the cursor outside the menu clears the current
       +                 * selection, so that you can "cancel" the menu by releasing
       +                 * the mouse outside the menu
       +                 */
                        case XCB_LEAVE_NOTIFY:
       -                        hilight(wid, 0, (height/nent) * current, width, height/nent);
       +                        if (current >= 0)
       +                                hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
                                current = -1;
                                break;
       +
       +                /*
       +                 * calculate current item based on cursor position
       +                 * relative to the root window and current window dimensions
       +                 */
                        case XCB_MOTION_NOTIFY:
       +                        e = (xcb_motion_notify_event_t *)ev;
                                /*
                                 * in some special cases, MOTION events can be
                                 * reported in the window when the pointer is
       t@@ -160,13 +205,21 @@ eventloop()
                                 * cases, so we must check first that the pointer
                                 * is actually inside the window
                                 */
       -                        if (((xcb_motion_notify_event_t *)ev)->event_x > width
       -                                || ((xcb_motion_notify_event_t *)ev)->event_y > height)
       +                        if (e->root_x > menu.x + menu.w
       +                         || e->root_y > menu.y + menu.h
       +                         || e->root_x < menu.x
       +                         || e->root_y < menu.y)
                                        break;
        
       -                        current = nent * (((xcb_motion_notify_event_t *)ev)->event_y * 1.0 / height);
       -                        hilight(wid, 0, (height/nent) * last, width, height/nent);
       -                        hilight(wid, 0, (height/nent) * current, width, height/nent);
       +                        current = nent * ((e->root_y - menu.y) * 1.0 / menu.h);
       +
       +                        /* don't bother redrawing selection that didn't change */
       +                        if (last == current)
       +                                break;
       +                        if (last >= 0)
       +                                hilight(wid, 0, (menu.h/nent) * last, menu.w, menu.h/nent);
       +                        if (current >= 0)
       +                                hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
                                break;
                        }
        
       t@@ -180,11 +233,14 @@ eventloop()
        int
        main(int argc, char *argv[])
        {
       -        int selected = 0;
       +        int dflag = 0;
                long dpi;
                char *argv0;
        
                ARGBEGIN {
       +        case 'd':
       +                dflag = 1;
       +                break;
                case 'h':
                        usage(stdout, argv0);
                        return 0;
       t@@ -216,15 +272,43 @@ main(int argc, char *argv[])
                        maxheight = MAX(xft_txth(entries[nent]), maxheight);
                }
        
       -        width = maxwidth * 1.2;
       -        height = maxheight * nent * 2.5;
       +        menu.w = maxwidth * 1.5;
       +        menu.h = maxheight * nent * 2;
       +
       +        /*
       +         * daemon mode will catch mouse clicks on the root window and
       +         * pop the menu everytime it is clicked.
       +         * selection happens instantly and is validated upon releasing
       +         * the mouse button while hovering an item
       +         */
       +        if (dflag) {
       +                int val[] = {
       +                        XCB_EVENT_MASK_BUTTON_PRESS
       +                        | XCB_EVENT_MASK_BUTTON_RELEASE
       +                        | XCB_EVENT_MASK_POINTER_MOTION
       +                };
       +
       +                xcb_change_window_attributes(dpy, screen->root, XCB_CW_EVENT_MASK, val);
       +                xcb_flush(dpy);
       +
       +                for (;;) {
       +                        current = -1;
       +                        eventloop();
       +                        if (current >= 0) {
       +                                printf("%s\n", entries[current]);
       +                                fflush(stdout);
       +                        }
        
       -        popwindow(width, height);
       -        selected = eventloop();
       +                        xcb_destroy_window(dpy, wid);
       +                }
       +        }
        
       -        if (selected >= 0)
       -                printf("%s\n", entries[selected]);
       +        popwindow(menu.x, menu.y, menu.w, menu.h);
       +        eventloop();
       +        if (current >= 0)
       +                printf("%s\n", entries[current]);
        
       +        xcb_destroy_window(dpy, wid);
                xft_unload();
                xcb_disconnect(dpy);
                return 0;