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