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 }