1 #include "xmainloop.h"
2
3 #include <X11/X.h>
4 #include <X11/Xatom.h>
5 #include <X11/Xlib.h>
6 #include <iostream>
7 #include <memory>
8
9 #include "client.h"
10 #include "clientmanager.h"
11 #include "decoration.h"
12 #include "desktopwindow.h"
13 #include "ewmh.h"
14 #include "framedecoration.h"
15 #include "frametree.h"
16 #include "hlwmcommon.h"
17 #include "ipc-server.h"
18 #include "keymanager.h"
19 #include "layout.h"
20 #include "monitor.h"
21 #include "monitormanager.h"
22 #include "mousemanager.h"
23 #include "panelmanager.h"
24 #include "root.h"
25 #include "rules.h"
26 #include "settings.h"
27 #include "tag.h"
28 #include "tagmanager.h"
29 #include "utils.h"
30 #include "watchers.h"
31 #include "xconnection.h"
32
33 using std::function;
34 using std::shared_ptr;
35
36 /** A custom event handler casting function.
37 *
38 * It ensures that the pointer that is casted to the EventHandler
39 * type is indeed a member function that accepts one pointer.
40 *
41 * Note that this is as (much or less) hacky as a member access to the XEvent
42 * union type itself.
43 */
44 template<typename T>
EH(void (XMainLoop::* handler)(T *))45 inline XMainLoop::EventHandler EH(void (XMainLoop::*handler)(T*)) {
46 return (XMainLoop::EventHandler) handler;
47 }
48
XMainLoop(XConnection & X,Root * root)49 XMainLoop::XMainLoop(XConnection& X, Root* root)
50 : X_(X)
51 , root_(root)
52 , aboutToQuit_(false)
53 , handlerTable_()
54 {
55 handlerTable_[ ButtonPress ] = EH(&XMainLoop::buttonpress);
56 handlerTable_[ ButtonRelease ] = EH(&XMainLoop::buttonrelease);
57 handlerTable_[ ClientMessage ] = EH(&XMainLoop::clientmessage);
58 handlerTable_[ ConfigureNotify ] = EH(&XMainLoop::configurenotify);
59 handlerTable_[ ConfigureRequest ] = EH(&XMainLoop::configurerequest);
60 handlerTable_[ CreateNotify ] = EH(&XMainLoop::createnotify);
61 handlerTable_[ DestroyNotify ] = EH(&XMainLoop::destroynotify);
62 handlerTable_[ EnterNotify ] = EH(&XMainLoop::enternotify);
63 handlerTable_[ Expose ] = EH(&XMainLoop::expose);
64 handlerTable_[ FocusIn ] = EH(&XMainLoop::focusin);
65 handlerTable_[ KeyPress ] = EH(&XMainLoop::keypress);
66 handlerTable_[ MapNotify ] = EH(&XMainLoop::mapnotify);
67 handlerTable_[ MapRequest ] = EH(&XMainLoop::maprequest);
68 handlerTable_[ MappingNotify ] = EH(&XMainLoop::mappingnotify);
69 handlerTable_[ MotionNotify ] = EH(&XMainLoop::motionnotify);
70 handlerTable_[ PropertyNotify ] = EH(&XMainLoop::propertynotify);
71 handlerTable_[ UnmapNotify ] = EH(&XMainLoop::unmapnotify);
72
73 root_->monitors->dropEnterNotifyEvents
74 .connect(this, &XMainLoop::dropEnterNotifyEvents);
75 }
76
77 //! scan for windows and add them to the list of managed clients
78 // from dwm.c
scanExistingClients()79 void XMainLoop::scanExistingClients() {
80 XWindowAttributes wa;
81 auto clientmanager = root_->clients();
82 auto& initialEwmhState = root_->ewmh->initialState();
83 auto& originalClients = initialEwmhState.original_client_list_;
84 auto isInOriginalClients = [&originalClients] (Window win) {
85 return originalClients.end()
86 != std::find(originalClients.begin(), originalClients.end(), win);
87 };
88 auto findTagForWindow = [this](Window win) -> function<void(ClientChanges&)> {
89 if (!root_->globals.importTagsFromEwmh) {
90 // do nothing, if import is disabled
91 return [] (ClientChanges&) {};
92 }
93 return [this,win] (ClientChanges& changes) {
94 long idx = this->root_->ewmh->windowGetInitialDesktop(win);
95 if (idx < 0) {
96 return;
97 }
98 HSTag* tag = root_->tags->byIdx((size_t)idx);
99 if (tag) {
100 changes.tag_name = tag->name();
101 }
102 };
103 };
104 for (auto win : X_.queryTree(X_.root())) {
105 if (!XGetWindowAttributes(X_.display(), win, &wa) || wa.override_redirect)
106 {
107 continue;
108 }
109 // only manage mapped windows.. no strange wins like:
110 // luakit/dbus/(ncurses-)vim
111 // but manage it if it was in the ewmh property _NET_CLIENT_LIST by
112 // the previous window manager
113 // TODO: what would dwm do?
114 if (root_->ewmh->isOwnWindow(win)) {
115 continue;
116 }
117 if (root_->ewmh->getWindowType(win) == NetWmWindowTypeDesktop)
118 {
119 DesktopWindow::registerDesktop(win);
120 DesktopWindow::lowerDesktopWindows();
121 XMapWindow(X_.display(), win);
122 }
123 else if (root_->ewmh->getWindowType(win) == NetWmWindowTypeDock)
124 {
125 root_->panels->registerPanel(win);
126 XSelectInput(X_.display(), win, PropertyChangeMask);
127 XMapWindow(X_.display(), win);
128 }
129 else if (wa.map_state == IsViewable
130 || isInOriginalClients(win)) {
131 Client* c = clientmanager->manage_client(win, true, false, findTagForWindow(win));
132 if (root_->monitors->byTag(c->tag())) {
133 XMapWindow(X_.display(), win);
134 }
135 }
136 }
137 // ensure every original client is managed again
138 for (auto win : originalClients) {
139 if (clientmanager->client(win)) {
140 continue;
141 }
142 if (!XGetWindowAttributes(X_.display(), win, &wa)
143 || wa.override_redirect)
144 {
145 continue;
146 }
147 XReparentWindow(X_.display(), win, X_.root(), 0,0);
148 clientmanager->manage_client(win, true, false, findTagForWindow(win));
149 }
150 }
151
152
run()153 void XMainLoop::run() {
154 XEvent event;
155 int x11_fd;
156 fd_set in_fds;
157 x11_fd = ConnectionNumber(X_.display());
158 while (!aboutToQuit_) {
159 FD_ZERO(&in_fds);
160 FD_SET(x11_fd, &in_fds);
161 // wait for an event or a signal
162 select(x11_fd + 1, &in_fds, nullptr, nullptr, nullptr);
163 if (aboutToQuit_) {
164 break;
165 }
166 XSync(X_.display(), False);
167 while (XQLength(X_.display())) {
168 XNextEvent(X_.display(), &event);
169 EventHandler handler = handlerTable_[event.type];
170 if (handler != nullptr) {
171 (this ->* handler)(&event);
172 }
173 root_->watchers->scanForChanges();
174 XSync(X_.display(), False);
175 }
176 }
177 }
178
quit()179 void XMainLoop::quit() {
180 aboutToQuit_ = true;
181 }
182
dropEnterNotifyEvents()183 void XMainLoop::dropEnterNotifyEvents()
184 {
185 if (duringEnterNotify_) {
186 // during a enternotify(), no artificial enter notify events
187 // can be created. Moreover, on quick mouse movements, an enter notify
188 // might be followed by further enter notify events, which
189 // must not be dropped.
190 return;
191 }
192 XEvent ev;
193 XSync(X_.display(), False);
194 while (XCheckMaskEvent(X_.display(), EnterWindowMask, &ev)) {
195 }
196 }
197
198 /* ----------------------------- */
199 /* event handler implementations */
200 /* ----------------------------- */
201
buttonpress(XButtonEvent * be)202 void XMainLoop::buttonpress(XButtonEvent* be) {
203 MouseManager* mm = root_->mouse();
204 HSDebug("name is: ButtonPress on sub 0x%lx, win 0x%lx\n", be->subwindow, be->window);
205 if (!mm->mouse_handle_event(be->state, be->button, be->window)) {
206 // if the event was not handled by the mouse manager, pass it to the client:
207 Client* client = root_->clients->client(be->window);
208 if (!client) {
209 client = Decoration::toClient(be->window);
210 }
211 if (client) {
212 bool raise = root_->settings->raise_on_click();
213 focus_client(client, false, true, raise);
214 if (be->window == client->decorationWindow()) {
215 if (client->dec->positionTriggersResize({be->x, be->y})) {
216 mm->mouse_initiate_resize(client, {});
217 } else {
218 mm->mouse_initiate_move(client, {});
219 }
220 }
221 }
222 }
223 FrameDecoration* frameDec = FrameDecoration::withWindow(be->window);
224 if (frameDec) {
225 auto frame = frameDec->frame();
226 if (frame) {
227 root_->focusFrame(frame);
228 }
229 }
230 XAllowEvents(X_.display(), ReplayPointer, be->time);
231 }
232
buttonrelease(XButtonEvent *)233 void XMainLoop::buttonrelease(XButtonEvent*) {
234 HSDebug("name is: ButtonRelease\n");
235 root_->mouse->mouse_stop_drag();
236 }
237
createnotify(XCreateWindowEvent * event)238 void XMainLoop::createnotify(XCreateWindowEvent* event) {
239 // printf("name is: CreateNotify\n");
240 if (root_->ipcServer_.isConnectable(event->window)) {
241 root_->ipcServer_.addConnection(event->window);
242 root_->ipcServer_.handleConnection(event->window,
243 HlwmCommon::callCommand);
244 }
245 }
246
configurerequest(XConfigureRequestEvent * cre)247 void XMainLoop::configurerequest(XConfigureRequestEvent* cre) {
248 HSDebug("name is: ConfigureRequest for 0x%lx\n", cre->window);
249 Client* client = root_->clients->client(cre->window);
250 if (client) {
251 bool changes = false;
252 auto newRect = client->float_size_;
253 if (client->sizehints_floating_ &&
254 (client->is_client_floated() || client->pseudotile_))
255 {
256 bool width_requested = 0 != (cre->value_mask & CWWidth);
257 bool height_requested = 0 != (cre->value_mask & CWHeight);
258 bool x_requested = 0 != (cre->value_mask & CWX);
259 bool y_requested = 0 != (cre->value_mask & CWY);
260 if (width_requested && newRect.width != cre->width) {
261 changes = true;
262 }
263 if (height_requested && newRect.height != cre->height) {
264 changes = true;
265 }
266 if (x_requested || y_requested) {
267 changes = true;
268 }
269 if (x_requested || y_requested) {
270 // if only one of the two dimensions is requested, then just
271 // set the other to some reasonable value.
272 if (!x_requested) {
273 cre->x = client->last_size_.x;
274 }
275 if (!y_requested) {
276 cre->y = client->last_size_.y;
277 }
278 // interpret the x and y coordinate relative to the monitor they are currently on
279 Monitor* m = root_->monitors->byTag(client->tag());
280 if (!m) {
281 // if the client is not visible at the moment, take the monitor that is
282 // most appropriate according to the requested cooridnates:
283 m = root_->monitors->byCoordinate({cre->x, cre->y});
284 }
285 if (!m) {
286 // if we have not found a suitable monitor, take the current
287 m = root_->monitors->focus();
288 }
289 // the requested coordinates are relative to the root window.
290 // convert them to coordinates relative to the monitor.
291 cre->x -= m->rect->x + *m->pad_left;
292 cre->y -= m->rect->y + *m->pad_up;
293 newRect.x = cre->x;
294 newRect.y = cre->y;
295 }
296 if (width_requested) {
297 newRect.width = cre->width;
298 }
299 if (height_requested) {
300 newRect.height = cre->height;
301 }
302 }
303 if (changes && client->is_client_floated()) {
304 client->float_size_ = newRect;
305 client->resize_floating(find_monitor_with_tag(client->tag()), client == get_current_client());
306 } else if (changes && client->pseudotile_) {
307 client->float_size_ = newRect;
308 Monitor* m = find_monitor_with_tag(client->tag());
309 if (m) {
310 m->applyLayout();
311 }
312 } else {
313 // FIXME: why send event and not XConfigureWindow or XMoveResizeWindow??
314 client->send_configure();
315 }
316 } else {
317 // if client not known.. then allow configure.
318 // its probably a nice conky or dzen2 bar :)
319 XWindowChanges wc;
320 wc.x = cre->x;
321 wc.y = cre->y;
322 wc.width = cre->width;
323 wc.height = cre->height;
324 wc.border_width = cre->border_width;
325 wc.sibling = cre->above;
326 wc.stack_mode = cre->detail;
327 XConfigureWindow(X_.display(), cre->window, cre->value_mask, &wc);
328 }
329 }
330
clientmessage(XClientMessageEvent * event)331 void XMainLoop::clientmessage(XClientMessageEvent* event) {
332 root_->ewmh->handleClientMessage(event);
333 }
334
configurenotify(XConfigureEvent * event)335 void XMainLoop::configurenotify(XConfigureEvent* event) {
336 if (event->window == g_root) {
337 root_->panels->rootWindowChanged(event->width, event->height);
338 if (root_->settings->auto_detect_monitors()) {
339 Input input = Input("detect_monitors");
340 std::ostringstream void_output;
341 root_->monitors->detectMonitorsCommand(input, void_output);
342 }
343 } else {
344 Rectangle geometry = { event->x, event->y, event->width, event->height };
345 root_->panels->geometryChanged(event->window, geometry);
346 }
347 // HSDebug("name is: ConfigureNotify\n");
348 }
349
destroynotify(XUnmapEvent * event)350 void XMainLoop::destroynotify(XUnmapEvent* event) {
351 // try to unmanage it
352 //HSDebug("name is: DestroyNotify for %lx\n", event->xdestroywindow.window);
353 auto cm = root_->clients();
354 auto client = cm->client(event->window);
355 if (client) {
356 cm->force_unmanage(client);
357 } else {
358 DesktopWindow::unregisterDesktop(event->window);
359 root_->panels->unregisterPanel(event->window);
360 }
361 }
362
enternotify(XCrossingEvent * ce)363 void XMainLoop::enternotify(XCrossingEvent* ce) {
364 HSDebug("name is: EnterNotify, focus = %d, window = 0x%lx\n", ce->focus, ce->window);
365 if (ce->mode != NotifyNormal || ce->detail == NotifyInferior) {
366 // ignore an event if it is caused by (un-)grabbing the mouse or
367 // if the pointer moves from a window to its decoration.
368 // for 'ce->detail' see:
369 // https://tronche.com/gui/x/xlib/events/window-entry-exit/normal.html
370 return;
371 }
372 // Warning: we have to set this to false again!
373 duringEnterNotify_ = true;
374 if (!root_->mouse->mouse_is_dragging()
375 && root_->settings()->focus_follows_mouse()
376 && ce->focus == false) {
377 Client* c = root_->clients->client(ce->window);
378 if (!c) {
379 c = Decoration::toClient(ce->window);
380 }
381 shared_ptr<FrameLeaf> target;
382 if (c && c->tag()->floating == false
383 && (target = c->tag()->frame->root_->frameWithClient(c))
384 && target->getLayout() == LayoutAlgorithm::max
385 && target->focusedClient() != c) {
386 // don't allow focus_follows_mouse if another window would be
387 // hidden during that focus change (which only occurs in max layout)
388 } else if (c) {
389 focus_client(c, false, true, false);
390 }
391 if (!c) {
392 // if it's not a client window, it's maybe a frame
393 FrameDecoration* frameDec = FrameDecoration::withWindow(ce->window);
394 if (frameDec) {
395 auto frame = frameDec->frame();
396 HSWeakAssert(frame);
397 if (frame) {
398 root_->focusFrame(frame);
399 }
400 }
401 }
402 }
403 duringEnterNotify_ = false;
404 }
405
expose(XEvent * event)406 void XMainLoop::expose(XEvent* event) {
407 //if (event->xexpose.count > 0) return;
408 //Window ewin = event->xexpose.window;
409 //HSDebug("name is: Expose for window %lx\n", ewin);
410 }
411
focusin(XEvent * event)412 void XMainLoop::focusin(XEvent* event) {
413 //HSDebug("name is: FocusIn\n");
414 }
415
keypress(XKeyEvent * event)416 void XMainLoop::keypress(XKeyEvent* event) {
417 //HSDebug("name is: KeyPress\n");
418 root_->keys()->handleKeyPress(event);
419 }
420
mappingnotify(XMappingEvent * ev)421 void XMainLoop::mappingnotify(XMappingEvent* ev) {
422 // regrab when keyboard map changes
423 XRefreshKeyboardMapping(ev);
424 if(ev->request == MappingKeyboard) {
425 root_->keys()->regrabAll();
426 //TODO: mouse_regrab_all();
427 }
428 }
429
motionnotify(XMotionEvent * event)430 void XMainLoop::motionnotify(XMotionEvent* event) {
431 // get newest motion notification
432 while (XCheckMaskEvent(X_.display(), ButtonMotionMask, (XEvent *)event)) {
433 ;
434 }
435 Point2D newCursorPos = { event->x_root, event->y_root };
436 root_->mouse->handle_motion_event(newCursorPos);
437 }
438
mapnotify(XMapEvent * event)439 void XMainLoop::mapnotify(XMapEvent* event) {
440 //HSDebug("name is: MapNotify\n");
441 Client* c = root_->clients()->client(event->window);
442 if (c != nullptr) {
443 // reset focus. so a new window gets the focus if it shall have the
444 // input focus
445 if (c == root_->clients->focus()) {
446 XSetInputFocus(X_.display(), c->window_, RevertToPointerRoot, CurrentTime);
447 }
448 // also update the window title - just to be sure
449 c->update_title();
450 } else if (!root_->ewmh->isOwnWindow(event->window)
451 && !is_herbstluft_window(X_.display(), event->window)) {
452 // the window is not managed.
453 HSDebug("MapNotify: briefly managing 0x%lx to apply rules\n", event->window);
454 root_->clients()->manage_client(event->window, true, true);
455 }
456 }
457
maprequest(XMapRequestEvent * mapreq)458 void XMainLoop::maprequest(XMapRequestEvent* mapreq) {
459 HSDebug("name is: MapRequest for 0x%lx\n", mapreq->window);
460 Window window = mapreq->window;
461 Client* c = root_->clients()->client(window);
462 if (root_->ewmh->isOwnWindow(window)
463 || is_herbstluft_window(X_.display(), window))
464 {
465 // just map the window if it wants that
466 XWindowAttributes wa;
467 if (!XGetWindowAttributes(X_.display(), window, &wa)) {
468 return;
469 }
470 XMapWindow(X_.display(), window);
471 } else if (c != nullptr) {
472 // a maprequest of a managed window means that
473 // the window wants to be un-minimized according to
474 // the item "Iconic -> Normal" in
475 // ICCCM 4.1.4 https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.3.1
476 c->minimized_ = false;
477 } else {
478 // c = nullptr, so the window is not yet managed.
479 if (root_->ewmh->getWindowType(window) == NetWmWindowTypeDesktop)
480 {
481 DesktopWindow::registerDesktop(window);
482 DesktopWindow::lowerDesktopWindows();
483 XMapWindow(X_.display(), window);
484 }
485 else if (root_->ewmh->getWindowType(window) == NetWmWindowTypeDock)
486 {
487 root_->panels->registerPanel(window);
488 XSelectInput(X_.display(), window, PropertyChangeMask);
489 XMapWindow(X_.display(), window);
490 } else {
491 // client should be managed (is not ignored)
492 // but is not managed yet
493 auto clientmanager = root_->clients();
494 auto client = clientmanager->manage_client(window, false, false);
495 if (client && find_monitor_with_tag(client->tag())) {
496 XMapWindow(X_.display(), window);
497 }
498 }
499 }
500 }
501
propertynotify(XPropertyEvent * ev)502 void XMainLoop::propertynotify(XPropertyEvent* ev) {
503 // printf("name is: PropertyNotify\n");
504 Client* client = root_->clients->client(ev->window);
505 if (ev->state == PropertyNewValue) {
506 if (root_->ipcServer_.isConnectable(ev->window)) {
507 root_->ipcServer_.handleConnection(ev->window,
508 HlwmCommon::callCommand);
509 } else if (client != nullptr) {
510 //char* atomname = XGetAtomName(X_.display(), ev->atom);
511 //HSDebug("Property notify for client %s: atom %d \"%s\"\n",
512 // client->window_id_str().c_str(),
513 // ev->atom,
514 // atomname);
515 if (ev->atom == XA_WM_HINTS) {
516 client->update_wm_hints();
517 } else if (ev->atom == XA_WM_NORMAL_HINTS) {
518 client->updatesizehints();
519 Monitor* m = find_monitor_with_tag(client->tag());
520 if (m) {
521 m->applyLayout();
522 }
523 } else if (ev->atom == XA_WM_NAME ||
524 ev->atom == root_->ewmh->netatom(NetWmName)) {
525 client->update_title();
526 } else if (ev->atom == XA_WM_CLASS && client) {
527 // according to the ICCCM specification, the WM_CLASS property may only
528 // be changed in the withdrawn state:
529 // https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html#wm_class_property
530 // If a client violates this, then the window rules like class=... etc are not applied.
531 // As a workaround, we do it now:
532 root_->clients()->applyRules(client, std::cerr);
533 }
534 } else {
535 root_->panels->propertyChanged(ev->window, ev->atom);
536 }
537 }
538 }
539
unmapnotify(XUnmapEvent * event)540 void XMainLoop::unmapnotify(XUnmapEvent* event) {
541 HSDebug("name is: UnmapNotify for %lx\n", event->window);
542 root_->clients()->unmap_notify(event->window);
543 if (event->send_event) {
544 // if the event was synthetic, then we need to understand it as a kind request
545 // by the window to be unmanaged. I don't understand fully how this is implied
546 // by the ICCCM documentation:
547 // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.4
548 //
549 // Anyway, we need to do the following because when running
550 // "telegram-desktop -startintray", a window flashes and only
551 // sends a synthetic UnmapNotify. So we unmanage the window here
552 // to forcefully make the window dissappear.
553 XUnmapWindow(X_.display(), event->window);
554 }
555 // drop all enternotify events
556 XSync(X_.display(), False);
557 XEvent ev;
558 while (XCheckMaskEvent(X_.display(), EnterWindowMask, &ev)) {
559 ;
560 }
561 }
562
563