txmenu.c - xmenu - drop-down menu for X11
 (HTM) git clone git://git.z3bra.org/xmenu.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
       txmenu.c (7291B)
       ---
            1 #include <stdio.h>
            2 #include <stdint.h>
            3 #include <stdlib.h>
            4 #include <unistd.h>
            5 
            6 #include <xcb/xcb.h>
            7 
            8 #include "arg.h"
            9 #include "config.h"
           10 #include "font.h"
           11 
           12 #define MAX(a,b) (((a)>(b))?(a):(b))
           13 #define MIN(a,b) (((a)<(b))?(a):(b))
           14 
           15 struct geom {
           16         int x, y, w, h;
           17 };
           18 
           19 int verbose = 0;
           20 int current = -1;
           21 xcb_connection_t *dpy;
           22 xcb_screen_t *screen;
           23 xcb_window_t wid;
           24 
           25 size_t nent = 0;
           26 size_t maxwidth = 0;
           27 size_t maxheight = 0;
           28 struct geom menu = { 0, 0, 0, 0 };
           29 char **entries;
           30 
           31 void
           32 usage(FILE *fd, char *name)
           33 {
           34         fprintf(fd, "usage: %s [-hd] item..\n", name);
           35 }
           36 
           37 int
           38 hilight(xcb_drawable_t xid, int x, int y, int w, int h)
           39 {
           40         int mask, val[3];
           41         xcb_gcontext_t gc;
           42         xcb_rectangle_t r;
           43 
           44         gc = xcb_generate_id(dpy);
           45 
           46         mask = XCB_GC_FUNCTION | XCB_GC_SUBWINDOW_MODE;
           47         val[0] = XCB_GX_INVERT;
           48         val[1] = XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS;
           49         xcb_create_gc(dpy, gc, xid, mask, val);
           50 
           51         /* draw inverted rectangle */
           52         r.x = x;
           53         r.y = y;
           54         r.width = w;
           55         r.height = h;
           56         xcb_poly_fill_rectangle(dpy, xid, gc, 1, &r);
           57 
           58         return 0;
           59 }
           60 
           61 int
           62 drawentries()
           63 {
           64         size_t i, w, h;
           65 
           66         for (i = 0; i < nent; i++) {
           67                 w = (menu.w - xft_txtw(entries[i])) / 2;
           68                 h = (menu.h/nent) * (i + 0.5) - maxheight/2 + (maxheight - xft_txth(entries[i]));
           69                 xft_drawtext(dpy, wid, w, h, foreground, entries[i]);
           70         }
           71 
           72         return 0;
           73 }
           74 
           75 int
           76 popwindow(int x, int y, int w, int h)
           77 {
           78         int mask, val[4];
           79 
           80         mask = XCB_CW_BACK_PIXEL
           81                 | XCB_CW_EVENT_MASK;
           82 
           83         val[0] = background;
           84         val[1] = XCB_EVENT_MASK_EXPOSURE
           85                 | XCB_EVENT_MASK_KEY_PRESS
           86                 | XCB_EVENT_MASK_ENTER_WINDOW
           87                 | XCB_EVENT_MASK_LEAVE_WINDOW
           88                 | XCB_EVENT_MASK_BUTTON_PRESS
           89                 | XCB_EVENT_MASK_BUTTON_RELEASE
           90                 | XCB_EVENT_MASK_POINTER_MOTION
           91                 | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
           92 
           93         wid = xcb_generate_id(dpy);
           94         xcb_create_window(dpy, screen->root_depth, wid, screen->root,
           95                 x, y, w, h, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
           96                 screen->root_visual, mask, val);
           97 
           98         xcb_map_window(dpy, wid);
           99         xcb_flush(dpy);
          100 
          101         xcb_grab_pointer(dpy, 1, wid, XCB_EVENT_MASK_NO_EVENT,
          102                 XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC,
          103                 wid, XCB_NONE, XCB_CURRENT_TIME);
          104 
          105         return 0;
          106 }
          107 
          108 int
          109 eventloop()
          110 {
          111         xcb_generic_event_t *ev;
          112         xcb_motion_notify_event_t *e;
          113 
          114         for (;;) {
          115                 int last = current;
          116 
          117                 xcb_flush(dpy);
          118 
          119                 ev = xcb_wait_for_event(dpy);
          120                 if (!ev)
          121                         break;
          122 
          123                 switch(ev->response_type & ~0x80) {
          124 
          125                 /*
          126                  * pressing escape (keycode 9) clears the selection and
          127                  * closes the menu
          128                  */
          129                 case XCB_KEY_PRESS:
          130                         if (((xcb_key_press_event_t *)ev)->detail == 9) {
          131                                 current = -1;
          132                                 return 0;
          133                         }
          134                         break;
          135 
          136                 /*
          137                  * button press events MUST be caught on root window
          138                  * only to avoid popup loops
          139                  */
          140                 case XCB_BUTTON_PRESS:
          141                         if (((xcb_button_press_event_t *)ev)->event != screen->root)
          142                                 break;
          143                         if (((xcb_button_press_event_t *)ev)->detail != mousebutton)
          144                                 break;
          145                         menu.x = ((xcb_button_press_event_t *)ev)->root_x;
          146                         menu.y = ((xcb_button_press_event_t *)ev)->root_y;
          147                         popwindow(menu.x, menu.y, menu.w, menu.h);
          148                         break;
          149 
          150                 /*
          151                  * releasing the mouse button closes the menu while
          152                  * keeping the current selection active. In other words,
          153                  * it validates the current selection (if any)
          154                  */
          155                 case XCB_BUTTON_RELEASE:
          156                         if (((xcb_button_press_event_t *)ev)->detail == mousebutton)
          157                                 return 0;
          158                         break;
          159 
          160                 /*
          161                  * make sure to keep track of window geometry changes
          162                  * to calculate cursor position correctly
          163                  */
          164                 case XCB_CONFIGURE_NOTIFY:
          165                         menu.x =  ((xcb_configure_notify_event_t *)ev)->x;
          166                         menu.y = ((xcb_configure_notify_event_t *)ev)->y;
          167                         menu.w =  ((xcb_configure_notify_event_t *)ev)->width;
          168                         menu.h = ((xcb_configure_notify_event_t *)ev)->height;
          169                         break;
          170 
          171                 case XCB_EXPOSE:
          172                         /*
          173                          * TODO: clean after someone mess up current entry
          174                          * As the current entry is represented using an
          175                          * XCB_GX_INVERT function, an external program
          176                          * could mess up the display by drawing over
          177                          * the selection using a non-inverting function.
          178                          * Inverting a partially cleared region would
          179                          * end up inverting the artifacts, so we must
          180                          * find a way to clean up this mess somehow.
          181                          *
          182                          * Damn punks.
          183                          */
          184                         if (last >= 0 && last != current)
          185                                 hilight(wid, 0, (menu.h/nent) * last, menu.w, menu.h/nent);
          186                         drawentries();
          187                         if (current >= 0)
          188                                 hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
          189                         break;
          190 
          191                 /*
          192                  * moving the cursor outside the menu clears the current
          193                  * selection, so that you can "cancel" the menu by releasing
          194                  * the mouse outside the menu
          195                  */
          196                 case XCB_LEAVE_NOTIFY:
          197                         if (current >= 0)
          198                                 hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
          199                         current = -1;
          200                         break;
          201 
          202                 /*
          203                  * calculate current item based on cursor position
          204                  * relative to the root window and current window dimensions
          205                  */
          206                 case XCB_MOTION_NOTIFY:
          207                         e = (xcb_motion_notify_event_t *)ev;
          208                         /*
          209                          * in some special cases, MOTION events can be
          210                          * reported in the window when the pointer is
          211                          * actually outside. For example when pressing
          212                          * mouse button inside the window and the moving
          213                          * out while holding it.
          214                          * When don't want to select an entry in such
          215                          * cases, so we must check first that the pointer
          216                          * is actually inside the window
          217                          */
          218                         if (e->root_x > menu.x + menu.w
          219                          || e->root_y > menu.y + menu.h
          220                          || e->root_x < menu.x
          221                          || e->root_y < menu.y) {
          222                                 if (current >= 0)
          223                                         hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
          224                                 current = -1;
          225                                 break;
          226                         }
          227 
          228                         current = nent * ((e->root_y - menu.y) * 1.0 / menu.h);
          229 
          230                         /* don't bother redrawing selection that didn't change */
          231                         if (last == current)
          232                                 break;
          233                         if (last >= 0)
          234                                 hilight(wid, 0, (menu.h/nent) * last, menu.w, menu.h/nent);
          235                         if (current >= 0)
          236                                 hilight(wid, 0, (menu.h/nent) * current, menu.w, menu.h/nent);
          237                         break;
          238                 }
          239 
          240                 free(ev);
          241         }
          242 
          243         return -1;
          244 }
          245 
          246 int
          247 main(int argc, char *argv[])
          248 {
          249         int dflag = 0;
          250         char *argv0;
          251 
          252         ARGBEGIN {
          253         case 'd':
          254                 dflag = 1;
          255                 break;
          256         case 'h':
          257                 usage(stdout, argv0);
          258                 return 0;
          259                 break;
          260         case 'v':
          261                 verbose++;
          262                 break;
          263         default:
          264                 usage(stderr, argv0);
          265                 return -1;
          266                 break;
          267         } ARGEND;
          268 
          269         if (!argc) {
          270                 usage(stderr, argv0);
          271                 return -1;
          272         }
          273 
          274         dpy = xcb_connect(NULL, NULL);
          275         if (xcb_connection_has_error(dpy))
          276                 return -1;
          277 
          278         screen = xcb_setup_roots_iterator(xcb_get_setup(dpy)).data;
          279         if (!screen)
          280                 return -1;
          281 
          282         xft_loadfont(dpy, screen, font);
          283 
          284         entries = argv;
          285         for (nent = 0; nent < (size_t)argc; nent++) {
          286                 maxwidth  = MAX(xft_txtw(entries[nent]), maxwidth);
          287                 maxheight = MAX(xft_txth(entries[nent]), maxheight);
          288         }
          289 
          290         menu.w = maxwidth * 1.5;
          291         menu.h = maxheight * nent * 2;
          292 
          293         /*
          294          * daemon mode will catch mouse clicks on the root window and
          295          * pop the menu everytime it is clicked.
          296          * selection happens instantly and is validated upon releasing
          297          * the mouse button while hovering an item
          298          */
          299         if (dflag) {
          300                 int val[] = {
          301                         XCB_EVENT_MASK_BUTTON_PRESS
          302                         | XCB_EVENT_MASK_BUTTON_RELEASE
          303                         | XCB_EVENT_MASK_POINTER_MOTION
          304                 };
          305 
          306                 xcb_change_window_attributes(dpy, screen->root, XCB_CW_EVENT_MASK, val);
          307                 xcb_flush(dpy);
          308         } else {
          309                 popwindow(menu.x, menu.y, menu.w, menu.h);
          310         }
          311 
          312         do {
          313                 current = -1;
          314                 eventloop();
          315                 if (current >= 0) {
          316                         printf("%s\n", entries[current]);
          317                         fflush(stdout);
          318                 }
          319 
          320                 xcb_destroy_window(dpy, wid);
          321                 xcb_flush(dpy);
          322         } while (dflag);
          323 
          324         xft_unload();
          325         xcb_disconnect(dpy);
          326         return 0;
          327 }