1 #include "ewmh.h"
2
3 #include <X11/Xatom.h>
4 #include <X11/Xlib.h>
5 #include <algorithm>
6 #include <cstdio>
7
8 #include "client.h"
9 #include "globals.h"
10 #include "hlwmcommon.h"
11 #include "layout.h"
12 #include "monitor.h"
13 #include "monitormanager.h"
14 #include "mousemanager.h"
15 #include "root.h"
16 #include "settings.h"
17 #include "stack.h"
18 #include "tagmanager.h"
19 #include "utils.h"
20 #include "xconnection.h"
21
22 using std::pair;
23 using std::string;
24 using std::vector;
25
26 /* list of names of all _NET-atoms */
27 const std::array<const char*,NetCOUNT> Ewmh::netatomNames_ =
28 ArrayInitializer<const char*,NetCOUNT>({
29 { NetSupported , "_NET_SUPPORTED" },
30 { NetClientList , "_NET_CLIENT_LIST" },
31 { NetClientListStacking , "_NET_CLIENT_LIST_STACKING" },
32 { NetCloseWindow , "_NET_CLOSE_WINDOW" },
33 { NetNumberOfDesktops , "_NET_NUMBER_OF_DESKTOPS" },
34 { NetCurrentDesktop , "_NET_CURRENT_DESKTOP" },
35 { NetDesktopNames , "_NET_DESKTOP_NAMES" },
36 { NetWmDesktop , "_NET_WM_DESKTOP" },
37 { NetDesktopViewport , "_NET_DESKTOP_VIEWPORT" },
38 { NetActiveWindow , "_NET_ACTIVE_WINDOW" },
39 { NetWmName , "_NET_WM_NAME" },
40 { NetSupportingWmCheck , "_NET_SUPPORTING_WM_CHECK" },
41 { NetWmWindowType , "_NET_WM_WINDOW_TYPE" },
42 { NetWmState , "_NET_WM_STATE" },
43 { NetWmWindowOpacity , "_NET_WM_WINDOW_OPACITY" },
44 { NetMoveresizeWindow , "_NET_MOVERESIZE_WINDOW" },
45 { NetWmMoveresize , "_NET_WM_MOVERESIZE" },
46 { NetFrameExtents , "_NET_FRAME_EXTENTS" },
47 /* window states */
48 { NetWmStateFullscreen , "_NET_WM_STATE_FULLSCREEN" },
49 { NetWmStateHidden , "_NET_WM_STATE_HIDDEN" },
50 { NetWmStateDemandsAttention , "_NET_WM_STATE_DEMANDS_ATTENTION" },
51 /* window types */
52 { NetWmWindowTypeDesktop , "_NET_WM_WINDOW_TYPE_DESKTOP" },
53 { NetWmWindowTypeDock , "_NET_WM_WINDOW_TYPE_DOCK" },
54 { NetWmWindowTypeToolbar , "_NET_WM_WINDOW_TYPE_TOOLBAR" },
55 { NetWmWindowTypeMenu , "_NET_WM_WINDOW_TYPE_MENU" },
56 { NetWmWindowTypeUtility , "_NET_WM_WINDOW_TYPE_UTILITY" },
57 { NetWmWindowTypeSplash , "_NET_WM_WINDOW_TYPE_SPLASH" },
58 { NetWmWindowTypeDialog , "_NET_WM_WINDOW_TYPE_DIALOG" },
59 { NetWmWindowTypeDropdownMenu , "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU" },
60 { NetWmWindowTypePopupMenu , "_NET_WM_WINDOW_TYPE_POPUP_MENU" },
61 { NetWmWindowTypeTooltip , "_NET_WM_WINDOW_TYPE_TOOLTIP" },
62 { NetWmWindowTypeNotification , "_NET_WM_WINDOW_TYPE_NOTIFICATION" },
63 { NetWmWindowTypeCombo , "_NET_WM_WINDOW_TYPE_COMBO" },
64 { NetWmWindowTypeDnd , "_NET_WM_WINDOW_TYPE_DND" },
65 { NetWmWindowTypeNormal , "_NET_WM_WINDOW_TYPE_NORMAL" },
66 }).a;
67
Ewmh(XConnection & xconnection)68 Ewmh::Ewmh(XConnection& xconnection)
69 : X_(xconnection)
70 {
71 /* init ewmh net atoms */
72 for (int i = 0; i < NetCOUNT; i++) {
73 if (!netatomNames_[i]) {
74 HSWarning("no name specified in g_netatom_names "
75 "for atom number %d\n", i);
76 continue;
77 }
78 netatom_[i] = XInternAtom(X_.display(), netatomNames_[i], False);
79 }
80
81 vector<pair<WM,const char*>> wm2name = {
82 { WM::Name, "WM_NAME" },
83 { WM::Protocols, "WM_PROTOCOLS" },
84 { WM::Delete, "WM_DELETE_WINDOW" },
85 { WM::State, "WM_STATE" },
86 { WM::ChangeState, "WM_CHANGE_STATE" },
87 { WM::TakeFocus, "WM_TAKE_FOCUS" },
88 };
89 for (const auto& init : wm2name) {
90 auto atom = XInternAtom(X_.display(), init.second, False);
91 wmatom_[static_cast<size_t>(init.first)] = atom;
92 }
93
94 /* tell which ewmh atoms are supported */
95 XChangeProperty(X_.display(), X_.root(), netatom_[NetSupported], XA_ATOM, 32,
96 PropModeReplace, (unsigned char *) netatom_, NetCOUNT);
97
98 readInitialEwmhState();
99
100 /* init for the supporting wm check */
101 windowManagerWindow_ = XCreateSimpleWindow(X_.display(), X_.root(),
102 -100, -100, 1, 1, 0, 0, CWOverrideRedirect | CWEventMask);
103 X_.setPropertyWindow(X_.root(), netatom_[NetSupportingWmCheck], { windowManagerWindow_ });
104 X_.setPropertyWindow(windowManagerWindow_, netatom_[NetSupportingWmCheck], { windowManagerWindow_ });
105 XMapWindow(X_.display(), windowManagerWindow_);
106
107 /* init atoms that never change */
108 X_.setPropertyCardinal(X_.root(), netatom_[NetDesktopViewport], {0, 0});
109 }
110
111 //! read the current ewmh properties from the root window
readInitialEwmhState()112 void Ewmh::readInitialEwmhState()
113 {
114 // list of desktops
115 auto number = X_.getWindowPropertyCardinal(X_.root(), netatom_[NetNumberOfDesktops]);
116 if (number.has_value() && !number.value().empty()) {
117 auto val = number.value()[0];
118 initialState_.numberOfDesktops = (val >= 0) ? ((size_t)(val)) : 0;
119 }
120 auto maybe_names = X_.getWindowPropertyTextList(X_.root(), netatom_[NetDesktopNames]);
121 if (maybe_names.has_value()) {
122 initialState_.desktopNames = maybe_names.value();
123 }
124 // list of managed clients
125 auto maybe_clients =
126 X_.getWindowPropertyWindow(X_.root(), netatom_[NetClientList]);
127 initialState_.original_client_list_ =
128 maybe_clients.has_value() ? maybe_clients.value() : vector<Window>();
129 if (g_verbose) {
130 initialState_.print(stderr);
131 }
132 }
133
windowGetInitialDesktop(Window win)134 long Ewmh::windowGetInitialDesktop(Window win)
135 {
136 auto maybe_idx = X_.getWindowPropertyCardinal(win, netatom_[NetWmDesktop]);
137 if (maybe_idx.has_value() && !maybe_idx.value().empty()) {
138 return maybe_idx.value()[0];
139 }
140 return -1;
141 }
142
print(FILE * file)143 void Ewmh::InitialState::print(FILE *file)
144 {
145 fprintf(file, "EWMH: %zu desktops:", numberOfDesktops);
146 for (const auto& n : desktopNames) {
147 fprintf(file, " \'%s\'", n.c_str());
148 }
149 fprintf(file, "\n");
150 fprintf(file, "%zu managed clients: ", original_client_list_.size());
151 for (auto win : original_client_list_) {
152 fprintf(file, " window 0x%lx", win);
153 }
154 fprintf(file, "\n");
155 }
156
157
injectDependencies(Root * root)158 void Ewmh::injectDependencies(Root* root) {
159 root_ = root;
160 tags_ = root->tags();
161 }
162
updateAll()163 void Ewmh::updateAll() {
164 /* init many properties */
165 updateWmName();
166 updateClientList();
167 updateClientListStacking();
168 updateDesktops();
169 updateCurrentDesktop();
170 updateDesktopNames();
171 }
172
~Ewmh()173 Ewmh::~Ewmh() {
174 XDeleteProperty(X_.display(), X_.root(), netatom_[NetSupportingWmCheck]);
175 XDestroyWindow(X_.display(), windowManagerWindow_);
176 }
177
updateWmName()178 void Ewmh::updateWmName() {
179 string name = root_->settings->wmname();
180 X_.setPropertyString(windowManagerWindow_, netatom_[NetWmName], name);
181 X_.setPropertyString(X_.root(), netatom_[NetWmName], name);
182 }
183
updateClientList()184 void Ewmh::updateClientList() {
185 X_.setPropertyWindow(X_.root(), netatom_[NetClientList], netClientList_);
186 }
187
initialState()188 const Ewmh::InitialState &Ewmh::initialState()
189 {
190 return initialState_;
191 }
192
updateClientListStacking()193 void Ewmh::updateClientListStacking() {
194 // First: get the windows currently visible
195 vector<Window> buf;
196 auto addToVector = [&buf](Window w) { buf.push_back(w); };
197 g_monitors->extractWindowStack(true, addToVector);
198
199 // Then add all the invisible windows at the end
200 for (auto tag : *tags_) {
201 if (find_monitor_with_tag(tag)) {
202 // do not add tags because they are already added
203 continue;
204 }
205 tag->stack->extractWindows(true, addToVector);
206 }
207
208 // reverse stacking order, because ewmh requires bottom to top order
209 std::reverse(buf.begin(), buf.end());
210
211 X_.setPropertyWindow(X_.root(), netatom_[NetClientListStacking], buf);
212 }
213
addClient(Window win)214 void Ewmh::addClient(Window win) {
215 netClientList_.push_back(win);
216 updateClientList();
217 updateClientListStacking();
218 }
219
removeClient(Window win)220 void Ewmh::removeClient(Window win) {
221 netClientList_.erase(std::remove(netClientList_.begin(), netClientList_.end(), win), netClientList_.end());
222 updateClientList();
223 updateClientListStacking();
224 }
225
updateDesktops()226 void Ewmh::updateDesktops() {
227 X_.setPropertyCardinal(X_.root(), netatom_[NetNumberOfDesktops],
228 { (long) root_->tags->size() });
229 }
230
updateDesktopNames()231 void Ewmh::updateDesktopNames() {
232 vector<string> names;
233 for (auto tag : *tags_) {
234 names.push_back(tag->name);
235 }
236 X_.setPropertyString(X_.root(), netatom_[NetDesktopNames], names);
237 }
238
updateCurrentDesktop()239 void Ewmh::updateCurrentDesktop() {
240 HSTag* tag = get_current_monitor()->tag;
241 int index = tags_->index_of(tag);
242 if (index < 0) {
243 HSWarning("tag %s not found in internal list\n", tag->name->c_str());
244 return;
245 }
246 X_.setPropertyCardinal(X_.root(), netatom_[NetCurrentDesktop], { index });
247 }
248
windowUpdateTag(Window win,HSTag * tag)249 void Ewmh::windowUpdateTag(Window win, HSTag* tag) {
250 if (!tag) {
251 return;
252 }
253 int index = tag->index();
254 X_.setPropertyCardinal(win, netatom_[NetWmDesktop], { index });
255 }
256
updateActiveWindow(Window win)257 void Ewmh::updateActiveWindow(Window win) {
258 X_.setPropertyWindow(X_.root(), netatom_[NetActiveWindow], { win });
259 }
260
focusStealingAllowed(long source)261 bool Ewmh::focusStealingAllowed(long source) {
262 if (root_->settings->focus_stealing_prevention()) {
263 /* only allow it to pagers/taskbars */
264 return (source == 2);
265 } else {
266 /* no prevention */
267 return true;
268 }
269 }
270
handleClientMessage(XClientMessageEvent * me)271 void Ewmh::handleClientMessage(XClientMessageEvent* me) {
272 HSDebug("Received event: ClientMessage: \"%s\" for %lx\n",
273 X_.atomName(me->message_type).c_str(),
274 me->window);
275 if (me->message_type == wmatom(WM::ChangeState)) {
276 if (me->data.l[0] == static_cast<long>(WmState::WSIconicState)) {
277 Client* client = Root::common().client(me->window);
278 if (client) {
279 client->minimized_ = true;
280 }
281 }
282 return;
283 }
284 int index;
285 for (index = 0; index < NetCOUNT; index++) {
286 if (me->message_type == netatom_[index]) {
287 break;
288 }
289 }
290 if (index >= NetCOUNT) {
291 HSDebug("received unknown client message\n");
292 return;
293 }
294
295 int desktop_index;
296 switch (index) {
297 case NetActiveWindow: {
298 // only steal focus if allowed to the current source
299 // (i.e. me->data.l[0] in this case as specified by EWMH)
300 if (focusStealingAllowed(me->data.l[0])) {
301 auto client = Root::common().client(me->window);
302 if (client) {
303 focus_client(client, true, true, true);
304 }
305 } else {
306 // Focus stealing is not allowed, at least mark the client urgent
307 auto client = Root::common().client(me->window);
308 if (client) {
309 client->set_urgent(true);
310 }
311 }
312 break;
313 }
314
315 case NetCurrentDesktop: {
316 desktop_index = me->data.l[0];
317 if (desktop_index < 0 || desktop_index >= tag_get_count()) {
318 HSDebug("_NET_CURRENT_DESKTOP: invalid index \"%d\"\n",
319 desktop_index);
320 break;
321 }
322 HSTag* tag = get_tag_by_index(desktop_index);
323 monitor_set_tag(get_current_monitor(), tag);
324 break;
325 }
326
327 case NetWmDesktop: {
328 desktop_index = me->data.l[0];
329 if (!focusStealingAllowed(me->data.l[1])) {
330 break;
331 }
332 HSTag* target = get_tag_by_index(desktop_index);
333 auto client = Root::common().client(me->window);
334 if (client && target) {
335 tags_->moveClient(client, target);
336 }
337 break;
338 }
339
340 case NetWmState: {
341 auto client = Root::common().client(me->window);
342 /* ignore requests for unmanaged windows */
343 if (!client || !client->ewmhrequests_) {
344 break;
345 }
346
347 /* mapping between EWMH atoms and client struct members */
348 struct {
349 int atom_index;
350 bool enabled;
351 void (*callback)(Client*, bool);
352 } client_atoms[] = {
353 { NetWmStateFullscreen,
354 client->fullscreen_, [](Client* c, bool state){ c->fullscreen_ = state; } },
355 { NetWmStateDemandsAttention,
356 client->urgent_, [](Client* c, bool state){ c->set_urgent(state); } },
357 };
358
359 /* me->data.l[1] and [2] describe the properties to alter */
360 for (int prop = 1; prop <= 2; prop++) {
361 if (me->data.l[prop] == 0) {
362 /* skip if no property is specified */
363 continue;
364 }
365 /* check if we support the property data[prop] */
366 size_t i;
367 for (i = 0; i < LENGTH(client_atoms); i++) {
368 if (netatom_[client_atoms[i].atom_index]
369 == static_cast<unsigned int>(me->data.l[prop])) {
370 break;
371 }
372 }
373 if (i >= LENGTH(client_atoms)) {
374 /* property will not be handled */
375 continue;
376 }
377 auto new_value = ArrayInitializer<bool,3>({
378 { _NET_WM_STATE_REMOVE , false },
379 { _NET_WM_STATE_ADD , true },
380 { _NET_WM_STATE_TOGGLE , !client_atoms[i].enabled },
381 }).a;
382 int action = me->data.l[0];
383 if (action >= static_cast<int>(new_value.size())) {
384 HSDebug("_NET_WM_STATE: invalid action %d\n", action);
385 }
386 /* change the value */
387 client_atoms[i].callback(client, new_value[action]);
388 }
389 break;
390 }
391
392 case NetWmMoveresize: {
393 auto client = Root::common().client(me->window);
394 if (!client) {
395 break;
396 }
397 int direction = me->data.l[2];
398 if (direction == _NET_WM_MOVERESIZE_MOVE
399 || direction == _NET_WM_MOVERESIZE_MOVE_KEYBOARD) {
400 root_->mouse->mouse_initiate_move(client, {});
401 } else if (direction == _NET_WM_MOVERESIZE_CANCEL) {
402 if (root_->mouse->mouse_is_dragging()) {
403 root_->mouse->mouse_stop_drag();
404 }
405 } else {
406 // anything else is a resize
407 root_->mouse->mouse_initiate_resize(client, {});
408 }
409 break;
410 }
411
412 case NetCloseWindow: {
413 windowClose(me->window);
414 break;
415 }
416
417 default:
418 HSDebug("no handler for the client message \"%s\"\n",
419 netatomNames_[index]);
420 break;
421 }
422 }
423
updateWindowState(Client * client)424 void Ewmh::updateWindowState(Client* client) {
425 /* mapping between EWMH atoms and client struct members */
426 struct {
427 int atom_index;
428 bool enabled;
429 } client_atoms[] = {
430 { NetWmStateFullscreen, client->ewmhfullscreen_ },
431 { NetWmStateDemandsAttention, client->urgent_ },
432 { NetWmStateHidden, client->minimized_ },
433 };
434
435 /* find out which flags are set */
436 Atom window_state[LENGTH(client_atoms)];
437 size_t count_enabled = 0;
438 for (size_t i = 0; i < LENGTH(client_atoms); i++) {
439 if (client_atoms[i].enabled) {
440 window_state[count_enabled] = netatom_[client_atoms[i].atom_index];
441 count_enabled++;
442 }
443 }
444
445 /* write it to the window */
446 XChangeProperty(X_.display(), client->window_, netatom_[NetWmState], XA_ATOM,
447 32, PropModeReplace, (unsigned char *) window_state, count_enabled);
448 }
449
clearClientProperties(Window win)450 void Ewmh::clearClientProperties(Window win) {
451 // delete ewmh-properties and ICCCM-Properties such that the client knows
452 // that he has been unmanaged and now the client is allowed to be mapped
453 // again (e.g. if it is some dialog)
454 XDeleteProperty(X_.display(), win, netatom_[NetWmState]);
455 XDeleteProperty(X_.display(), win, wmatom(WM::State));
456 }
457
isWindowStateSet(Window win,Atom hint)458 bool Ewmh::isWindowStateSet(Window win, Atom hint) {
459 auto res = X_.getWindowPropertyAtom(win, netatom_[NetWmState]);
460 if (!res.has_value()) {
461 return false;
462 }
463 for (auto& h : res.value()) {
464 if (hint == h) {
465 return true;
466 }
467 }
468 return false;
469 }
470
isFullscreenSet(Window win)471 bool Ewmh::isFullscreenSet(Window win) {
472 return isWindowStateSet(win, netatom_[NetWmStateFullscreen]);
473 }
474
setWindowOpacity(Window win,double opacity)475 void Ewmh::setWindowOpacity(Window win, double opacity) {
476 /* Based on the EWMH proposal
477 * https://mail.gnome.org/archives/wm-spec-list/2003-December/msg00035.html
478 */
479 long long_opacity = 0xffffffff * CLAMP(opacity, 0, 1);
480 X_.setPropertyCardinal(win, netatom_[NetWmWindowOpacity], { long_opacity });
481 }
482
updateFrameExtents(Window win,int left,int right,int top,int bottom)483 void Ewmh::updateFrameExtents(Window win, int left, int right, int top, int bottom) {
484 X_.setPropertyCardinal(win, netatom_[NetFrameExtents],
485 { left, right, top, bottom });
486 }
487
windowUpdateWmState(Window win,WmState state)488 void Ewmh::windowUpdateWmState(Window win, WmState state) {
489 /* set full WM_STATE according to
490 * http://www.x.org/releases/X11R7.7/doc/xorg-docs/icccm/icccm.html#WM_STATE_Property
491 *
492 * It's crucial that the property has the type WM_STATE!
493 */
494 long wmstate[] = { static_cast<long>(state), None };
495 XChangeProperty(X_.display(), win, wmatom(WM::State), wmatom(WM::State),
496 32, PropModeReplace,
497 reinterpret_cast<unsigned char*>(wmstate), LENGTH(wmstate));
498 }
499
isOwnWindow(Window win)500 bool Ewmh::isOwnWindow(Window win) {
501 return windowManagerWindow_ == win;
502 }
503
clearInputFocus()504 void Ewmh::clearInputFocus() {
505 XSetInputFocus(X_.display(), windowManagerWindow_, RevertToPointerRoot, CurrentTime);
506 }
507
get()508 Ewmh& Ewmh::get() {
509 return *(Root::get()->ewmh);
510 }
511
512 /** send the given proto atom to the given window via XSendEvent(). If
513 * checkProtocols = true, this is done only if proto is present in the window's
514 * WM protocols. The return value tells whether the event was actually sent.
515 */
sendEvent(Window window,Ewmh::WM proto,bool checkProtocols)516 bool Ewmh::sendEvent(Window window, Ewmh::WM proto, bool checkProtocols) {
517 bool exists = false;
518 Atom protoAtom = wmatom(proto);
519 if (!checkProtocols) {
520 exists = true;
521 } else {
522 int n;
523 Atom *protocols;
524
525 if (XGetWMProtocols(X_.display(), window, &protocols, &n)) {
526 while (!exists && n--) {
527 exists = protocols[n] == protoAtom;
528 }
529 XFree(protocols);
530 }
531 }
532 if (exists) {
533 XEvent ev;
534 ev.type = ClientMessage;
535 ev.xclient.window = window;
536 ev.xclient.message_type = wmatom(WM::Protocols);
537 ev.xclient.format = 32;
538 ev.xclient.data.l[0] = protoAtom;
539 ev.xclient.data.l[1] = CurrentTime;
540 XSendEvent(X_.display(), window, False, NoEventMask, &ev);
541 }
542 return exists;
543 }
544
windowClose(Window window)545 void Ewmh::windowClose(Window window) {
546 sendEvent(window, WM::Delete, false);
547 }
548
netatom(int netatomEnum)549 Atom Ewmh::netatom(int netatomEnum)
550 {
551 return netatom_[netatomEnum];
552 }
553
netatomName(int netatomEnum)554 const char* Ewmh::netatomName(int netatomEnum)
555 {
556 return netatomNames_[static_cast<unsigned long>(netatomEnum)];
557 }
558
559 //! convenience wrapper around wmatom_
wmatom(WM proto)560 Atom Ewmh::wmatom(WM proto) {
561 return wmatom_[(int)proto];
562 }
563
getWindowTitle(Window win)564 string Ewmh::getWindowTitle(Window win) {
565 auto newName = X_.getWindowProperty(win, netatom_[NetWmName]);
566 if (newName.has_value()) {
567 return newName.value();
568 }
569 newName = X_.getWindowProperty(win, wmatom(WM::Name));
570 if (newName.has_value()) {
571 return newName.value();
572 }
573 return "";
574 }
575
576 /** Return the window type of the given window. If there are multiple entries, then
577 * only the first window type entry is returned. The return value is an enum value between
578 * NetWmWindowTypeFIRST and NetWmWindowTypeLAST (inclusive). Any other window
579 * type is not recognized and leads to -1 being returned.
580 */
getWindowType(Window win)581 int Ewmh::getWindowType(Window win) {
582 auto atoms = X_.getWindowPropertyAtom(win, netatom_[NetWmWindowType]);
583 if (!atoms.has_value() || atoms.value().empty()) {
584 return -1;
585 }
586 Atom windowtype = atoms.value()[0];
587 for (int i = NetWmWindowTypeFIRST; i <= NetWmWindowTypeLAST; i++) {
588 // try to find the window type
589 if (windowtype == netatom_[i]) {
590 return i;
591 }
592 }
593 return -1;
594 }
595