progman.exe^H^H^H^H
1/*
2 * Copyright 2020 joshua stein <jcs@jcs.org>
3 * Copyright 1998-2007 Decklin Foster <decklin@red-bean.com>.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to
7 * deal in the Software without restriction, including without limitation the
8 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9 * sell copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 */
22
23#include <stdlib.h>
24#include <stdio.h>
25#include <poll.h>
26#include <X11/Xatom.h>
27#include <X11/extensions/shape.h>
28#include "progman.h"
29#include "atom.h"
30
31#ifndef INFTIM
32#define INFTIM (-1)
33#endif
34
35static void handle_button_press(XButtonEvent *);
36static void handle_button_release(XButtonEvent *);
37static void handle_configure_request(XConfigureRequestEvent *);
38static void handle_circulate_request(XCirculateRequestEvent *);
39static void handle_map_request(XMapRequestEvent *);
40static void handle_destroy_event(XDestroyWindowEvent *);
41static void handle_client_message(XClientMessageEvent *);
42static void handle_property_change(XPropertyEvent *);
43static void handle_enter_event(XCrossingEvent *);
44static void handle_cmap_change(XColormapEvent *);
45static void handle_expose_event(XExposeEvent *);
46static void handle_shape_change(XShapeEvent *);
47
48static XEvent ev;
49
50void
51event_loop(void)
52{
53 struct pollfd pfd[2];
54
55 memset(&pfd, 0, sizeof(pfd));
56 pfd[0].fd = ConnectionNumber(dpy);
57 pfd[0].events = POLLIN;
58 pfd[1].fd = exitmsg[0];
59 pfd[1].events = POLLIN;
60
61 for (;;) {
62 if (!XPending(dpy)) {
63 poll(pfd, 2, INFTIM);
64 if (pfd[1].revents)
65 /* exitmsg */
66 break;
67
68 if (!XPending(dpy))
69 continue;
70 }
71
72 XNextEvent(dpy, &ev);
73#ifdef DEBUG
74 show_event(ev);
75#endif
76 switch (ev.type) {
77 case ButtonPress:
78 handle_button_press(&ev.xbutton);
79 break;
80 case ButtonRelease:
81 handle_button_release(&ev.xbutton);
82 break;
83 case ConfigureRequest:
84 handle_configure_request(&ev.xconfigurerequest);
85 break;
86 case CirculateRequest:
87 handle_circulate_request(&ev.xcirculaterequest);
88 break;
89 case MapRequest:
90 handle_map_request(&ev.xmaprequest);
91 break;
92 case UnmapNotify:
93 handle_unmap_event(&ev.xunmap);
94 break;
95 case DestroyNotify:
96 handle_destroy_event(&ev.xdestroywindow);
97 break;
98 case ClientMessage:
99 handle_client_message(&ev.xclient);
100 break;
101 case ColormapNotify:
102 handle_cmap_change(&ev.xcolormap);
103 break;
104 case PropertyNotify:
105 handle_property_change(&ev.xproperty);
106 break;
107 case EnterNotify:
108 handle_enter_event(&ev.xcrossing);
109 break;
110 case Expose:
111 handle_expose_event(&ev.xexpose);
112 break;
113 case KeyPress:
114 case KeyRelease:
115 handle_key_event(&ev.xkey);
116 break;
117 default:
118 if (shape_support && ev.type == shape_event)
119 handle_shape_change((XShapeEvent *)&ev);
120 }
121 }
122}
123
124/*
125 * Someone clicked a button. If they clicked on a window, we want the button
126 * press, but if they clicked on the root, we're only interested in the button
127 * release. Thus, two functions.
128 *
129 * If it was on the root, we get the click by default. If it's on a window
130 * frame, we get it as well.
131 *
132 * If it's on a client window, it may still fall through to us if the client
133 * doesn't select for mouse-click events. The upshot of this is that you should
134 * be able to click on the blank part of a GTK window with Button2 to move
135 * it.
136 */
137static void
138handle_button_press(XButtonEvent *e)
139{
140 client_t *c = find_client(e->window, MATCH_ANY);
141 int i;
142
143 if (e->window == root) {
144 client_t *fc;
145 /*
146 * Clicking inside transparent icons may fall through to the
147 * root, so check for an iconified client here
148 */
149 if ((fc = find_client_at_coords(e->window, e->x, e->y)) &&
150 (fc->state & STATE_ICONIFIED)) {
151 c = fc;
152 e->window = c->icon;
153 }
154 }
155
156 if (c && (c->state & STATE_DOCK)) {
157 /* pass button event through */
158 XAllowEvents(dpy, ReplayPointer, CurrentTime);
159 } else if (c) {
160 if (opt_drag_button && e->button == opt_drag_button &&
161 (e->state & opt_drag_mod) &&
162 !(c->state & (STATE_FULLSCREEN | STATE_ZOOMED |
163 STATE_ICONIFIED))) {
164 /* alt+click, begin moving */
165 focus_client(c, FOCUS_NORMAL);
166 move_client(c);
167 } else if (find_client(e->window, MATCH_FRAME)) {
168 /* raising our frame will also raise the window */
169 focus_client(c, FOCUS_NORMAL);
170 user_action(c, e->window, e->x, e->y, e->button, 1);
171 } else {
172 if (e->button == 1)
173 focus_client(c, FOCUS_NORMAL);
174
175 /* pass button event through */
176 XAllowEvents(dpy, ReplayPointer, CurrentTime);
177 }
178 } else if (e->window == root) {
179 for (i = 0; i < nkey_actions; i++) {
180 if (key_actions[i].type == BINDING_TYPE_DESKTOP &&
181 key_actions[i].mod == e->state &&
182 key_actions[i].button == e->button) {
183 take_action(&key_actions[i]);
184 break;
185 }
186 }
187 }
188}
189
190static void
191handle_button_release(XButtonEvent *e)
192{
193 client_t *c = find_client(e->window, MATCH_ANY);
194
195 if (e->window == root) {
196 client_t *fc;
197 if ((fc = find_client_at_coords(e->window, e->x, e->y)) &&
198 (fc->state & STATE_ICONIFIED)) {
199 c = fc;
200 e->window = c->icon;
201 }
202 }
203
204 if (c) {
205 if (find_client(e->window, MATCH_FRAME))
206 user_action(c, e->window, e->x, e->y, e->button, 0);
207
208 XAllowEvents(dpy, ReplayPointer, CurrentTime);
209 }
210}
211
212/*
213 * Because we are redirecting the root window, we get ConfigureRequest events
214 * from both clients we're handling and ones that we aren't. For clients we
215 * manage, we need to adjust the frame and the client window, and for unmanaged
216 * windows we have to pass along everything unchanged.
217 *
218 * Most of the assignments here are going to be garbage, but only the ones that
219 * are masked in by e->value_mask will be looked at by the X server.
220 */
221static void
222handle_configure_request(XConfigureRequestEvent *e)
223{
224 client_t *c = NULL;
225 XWindowChanges wc;
226
227 if ((c = find_client(e->window, MATCH_WINDOW))) {
228 recalc_frame(c);
229
230 if (e->value_mask & CWX)
231 c->geom.x = e->x + (c->geom.x - c->frame_geom.x);
232 if (e->value_mask & CWY)
233 c->geom.y = e->y + (c->geom.y - c->frame_geom.y);
234 if (e->value_mask & CWWidth)
235 c->geom.w = e->width;
236 if (e->value_mask & CWHeight)
237 c->geom.h = e->height;
238
239 constrain_frame(c);
240
241 wc.x = c->frame_geom.x;
242 wc.y = c->frame_geom.y;
243 wc.width = c->frame_geom.w;
244 wc.height = c->frame_geom.h;
245 wc.border_width = 0;
246 wc.sibling = e->above;
247 wc.stack_mode = e->detail;
248#ifdef DEBUG
249 dump_geom(c, c->frame_geom, "moving frame to");
250#endif
251 XConfigureWindow(dpy, c->frame, e->value_mask, &wc);
252 if (e->value_mask & (CWWidth | CWHeight))
253 set_shape(c);
254 if ((c->state & STATE_ZOOMED) &&
255 (e->value_mask & (CWX | CWY | CWWidth | CWHeight))) {
256#ifdef DEBUG
257 dump_name(c, __func__, NULL,
258 "unzooming from XConfigureRequest");
259#endif
260 unzoom_client(c);
261 } else {
262 redraw_frame(c, None);
263 send_config(c);
264 }
265 }
266
267 if (c) {
268 wc.x = c->geom.x - c->frame_geom.x;
269 wc.y = c->geom.y - c->frame_geom.y;
270 wc.width = c->geom.w;
271 wc.height = c->geom.h;
272 } else {
273 wc.x = e->x;
274 wc.y = e->y;
275 wc.width = e->width;
276 wc.height = e->height;
277 }
278 wc.sibling = e->above;
279 wc.stack_mode = e->detail;
280 XConfigureWindow(dpy, e->window, e->value_mask, &wc);
281
282 /* top client may not be the focused one now */
283 if ((c = top_client()) && IS_ON_CUR_DESK(c))
284 focus_client(c, FOCUS_FORCE);
285}
286
287/*
288 * The only window that we will circulate children for is the root (because
289 * nothing else would make sense). After a client requests that the root's
290 * children be circulated, the server will determine which window needs to be
291 * raised or lowered, and so all we have to do is make it so.
292 */
293static void
294handle_circulate_request(XCirculateRequestEvent *e)
295{
296 client_t *c;
297
298 if (e->parent == root) {
299 c = find_client(e->window, MATCH_ANY);
300
301 if (e->place == PlaceOnBottom) {
302 if (c) {
303 adjust_client_order(c, ORDER_BOTTOM);
304 if (focused)
305 focus_client(focused, FOCUS_FORCE);
306 } else
307 XLowerWindow(dpy, e->window);
308 } else {
309 if (c && IS_ON_CUR_DESK(c))
310 focus_client(c, FOCUS_FORCE);
311 else if (c)
312 adjust_client_order(c, ORDER_TOP);
313 else
314 XRaiseWindow(dpy, e->window);
315 }
316 }
317}
318
319/*
320 * Two possibilities if a client is asking to be mapped. One is that it's a new
321 * window, so we handle that if it isn't in our clients list anywhere. The
322 * other is that it already exists and wants to de-iconify, which is simple to
323 * take care of. Since we iconify all of a window's transients when iconifying
324 * that window, de-iconify them here.
325 */
326static void
327handle_map_request(XMapRequestEvent *e)
328{
329 client_t *c, *p;
330
331 if ((c = find_client(e->window, MATCH_WINDOW))) {
332 uniconify_client(c);
333 for (p = focused; p; p = p->next)
334 if (p->trans == c->win)
335 uniconify_client(p);
336 } else {
337 c = new_client(e->window);
338 map_client(c);
339 }
340}
341
342/*
343 * We don't get to intercept Unmap events, so this is post mortem. If we caused
344 * the unmap ourselves earlier (explictly or by remapping), we will have
345 * incremented c->ignore_unmap. If not, time to destroy the client.
346 *
347 * Because most clients unmap and destroy themselves at once, they're gone
348 * before we even get the Unmap event, never mind the Destroy one. Therefore we
349 * must be extra careful in del_client.
350 */
351void
352handle_unmap_event(XUnmapEvent *e)
353{
354 client_t *c;
355
356 if ((c = find_client(e->window, MATCH_WINDOW))) {
357 if (c->ignore_unmap)
358 c->ignore_unmap--;
359 else
360 del_client(c, DEL_WITHDRAW);
361 }
362}
363
364/*
365 * But a window can also go away when it's not mapped, in which case there is
366 * no Unmap event.
367 */
368static void
369handle_destroy_event(XDestroyWindowEvent *e)
370{
371 client_t *c;
372
373 if ((c = find_client(e->window, MATCH_WINDOW)))
374 del_client(c, DEL_WITHDRAW);
375}
376
377static void
378handle_client_message(XClientMessageEvent *e)
379{
380 client_t *c;
381
382 if (e->window == root) {
383 if (e->message_type == net_cur_desk && e->format == 32)
384 goto_desk(e->data.l[0]);
385 else if (e->message_type == net_num_desks && e->format == 32) {
386 if (e->data.l[0] < ndesks)
387 /* TODO: move clients from deleted desks */
388 return;
389 ndesks = e->data.l[0];
390 }
391 return;
392 }
393
394 c = find_client(e->window, MATCH_WINDOW);
395 if (!c)
396 return;
397 if (e->format != 32)
398 return;
399
400 if (e->message_type == wm_change_state && e->data.l[0] == IconicState)
401 iconify_client(c);
402 else if (e->message_type == net_close_window)
403 send_wm_delete(c);
404 else if (e->message_type == net_active_window) {
405 c->desk = cur_desk;
406 map_if_desk(c);
407 if (c->state == STATE_ICONIFIED)
408 uniconify_client(c);
409 focus_client(c, FOCUS_NORMAL);
410 } else if (e->message_type == net_wm_state &&
411 e->data.l[1] == net_wm_state_fs) {
412 if (e->data.l[0] == net_wm_state_add ||
413 (e->data.l[0] == net_wm_state_toggle &&
414 c->state != STATE_FULLSCREEN))
415 fullscreen_client(c);
416 else
417 unfullscreen_client(c);
418 }
419}
420
421/*
422 * If we have something copied to a variable, or displayed on the screen, make
423 * sure it is up to date. If redrawing the name is necessary, clear the window
424 * because Xft uses alpha rendering.
425 */
426static void
427handle_property_change(XPropertyEvent *e)
428{
429 client_t *c;
430#ifdef DEBUG
431 char *atom;
432#endif
433
434 if (!(c = find_client(e->window, MATCH_WINDOW)))
435 return;
436
437#ifdef DEBUG
438 atom = XGetAtomName(dpy, e->atom);
439 dump_name(c, __func__, "", atom);
440 XFree(atom);
441#endif
442
443 if (e->atom == XA_WM_NAME || e->atom == net_wm_name) {
444 if (c->name)
445 XFree(c->name);
446 c->name = get_wm_name(c->win);
447 if (c->frame_style & FRAME_TITLEBAR)
448 redraw_frame(c, c->titlebar);
449 } else if (e->atom == XA_WM_ICON_NAME || e->atom == net_wm_icon_name) {
450 if (c->icon_name)
451 XFree(c->icon_name);
452 c->icon_name = get_wm_icon_name(c->win);
453 if (c->state & STATE_ICONIFIED)
454 redraw_icon(c, c->icon_label);
455 } else if (e->atom == XA_WM_NORMAL_HINTS) {
456 update_size_hints(c);
457 fix_size(c);
458 redraw_frame(c, None);
459 send_config(c);
460 } else if (e->atom == XA_WM_HINTS) {
461 if (c->wm_hints)
462 XFree(c->wm_hints);
463 c->wm_hints = XGetWMHints(dpy, c->win);
464 if (c->wm_hints &&
465 c->wm_hints->flags & (IconPixmapHint | IconMaskHint)) {
466 get_client_icon(c);
467 if (c->state & STATE_ICONIFIED)
468 redraw_icon(c, c->icon);
469 }
470 } else if (e->atom == net_wm_state || e->atom == wm_state) {
471 int was_state = c->state;
472 check_states(c);
473 if (was_state != c->state) {
474 if (c->state & STATE_ICONIFIED)
475 iconify_client(c);
476 else if (c->state & STATE_ZOOMED)
477 zoom_client(c);
478 else if (c->state & STATE_FULLSCREEN)
479 fullscreen_client(c);
480 else {
481 if (was_state & STATE_ZOOMED)
482 unzoom_client(c);
483 else if (was_state & STATE_ICONIFIED)
484 uniconify_client(c);
485 else if (was_state & STATE_FULLSCREEN)
486 unfullscreen_client(c);
487 }
488 }
489 } else if (e->atom == net_wm_desk) {
490 if (get_atoms(c->win, net_wm_desk, XA_CARDINAL, 0,
491 &c->desk, 1, NULL)) {
492 if (c->desk == -1)
493 c->desk = DESK_ALL; /* FIXME */
494 map_if_desk(c);
495 }
496 }
497#ifdef DEBUG
498 else {
499 printf("%s: unknown atom %ld (%s)\n", __func__, (long)e->atom,
500 XGetAtomName(dpy, e->atom));
501 }
502#endif
503}
504
505/* Support click-to-focus policy. */
506static void
507handle_enter_event(XCrossingEvent *e)
508{
509 client_t *c;
510
511 if ((c = find_client(e->window, MATCH_FRAME)))
512 XGrabButton(dpy, Button1, AnyModifier, c->win, False,
513 ButtonMask, GrabModeSync, GrabModeSync, None, None);
514}
515
516/*
517 * Colormap policy: when a client installs a new colormap on itself, set the
518 * display's colormap to that. We do this even if it's not focused.
519 */
520static void
521handle_cmap_change(XColormapEvent *e)
522{
523 client_t *c;
524
525 if ((c = find_client(e->window, MATCH_WINDOW)) && e->new) {
526 c->cmap = e->colormap;
527 XInstallColormap(dpy, c->cmap);
528 }
529}
530
531/*
532 * If we were covered by multiple windows, we will usually get multiple expose
533 * events, so ignore them unless e->count (the number of outstanding exposes)
534 * is zero.
535 */
536static void
537handle_expose_event(XExposeEvent *e)
538{
539 client_t *c;
540
541 if ((c = find_client(e->window, MATCH_FRAME)) && e->count == 0)
542 redraw_frame(c, e->window);
543}
544
545static void
546handle_shape_change(XShapeEvent *e)
547{
548 client_t *c;
549
550 if ((c = find_client(e->window, MATCH_WINDOW)))
551 set_shape(c);
552}
553
554#ifdef DEBUG
555void
556show_event(XEvent e)
557{
558 char ev_type[128];
559 Window w;
560 client_t *c;
561
562 switch (e.type) {
563 SHOW_EV(ButtonPress, xbutton)
564 SHOW_EV(ButtonRelease, xbutton)
565 SHOW_EV(ClientMessage, xclient)
566 SHOW_EV(ColormapNotify, xcolormap)
567 SHOW_EV(ConfigureNotify, xconfigure)
568 SHOW_EV(ConfigureRequest, xconfigurerequest)
569 SHOW_EV(CirculateRequest, xcirculaterequest)
570 SHOW_EV(CreateNotify, xcreatewindow)
571 SHOW_EV(DestroyNotify, xdestroywindow)
572 SHOW_EV(EnterNotify, xcrossing)
573 SHOW_EV(Expose, xexpose)
574 SHOW_EV(KeyPress, xkey)
575 SHOW_EV(KeyRelease, xkey)
576 SHOW_EV(NoExpose, xexpose)
577 SHOW_EV(MapNotify, xmap)
578 SHOW_EV(MapRequest, xmaprequest)
579 SHOW_EV(MappingNotify, xmapping)
580 SHOW_EV(PropertyNotify, xproperty)
581 SHOW_EV(ReparentNotify, xreparent)
582 SHOW_EV(ResizeRequest, xresizerequest)
583 SHOW_EV(UnmapNotify, xunmap)
584 SHOW_EV(MotionNotify, xmotion)
585 default:
586 if (shape_support && e.type == shape_event) {
587 snprintf(ev_type, sizeof(ev_type), "ShapeNotify");
588 w = ((XShapeEvent *) & e)->window;
589 break;
590 }
591 snprintf(ev_type, sizeof(ev_type), "unknown event %d", e.type);
592 w = None;
593 break;
594 }
595
596 if ((c = find_client(w, MATCH_WINDOW)))
597 dump_name(c, ev_type, "window", c->name);
598 else if ((c = find_client(w, MATCH_FRAME))) {
599 /*
600 * ConfigureNotify can only come from us (otherwise it'd be a
601 * ConfigureRequest) and NoExpose events are just not useful
602 */
603 if (e.type != ConfigureNotify && e.type != NoExpose)
604 dump_name(c, ev_type, frame_name(c, w), c->name);
605 } else if (w == root)
606 dump_name(NULL, ev_type, "root", "(root)");
607}
608#endif