1 #include <QTimer>
2
3 #include "birdtrayapp.h"
4 #include "windowtools_x11.h"
5 #include "utils.h"
6 #include "log.h"
7
8 /*
9 * This code is mostly taken from xlibutil.cpp KDocker project, licensed under GPLv2 or higher.
10 * The original code is copyrighted as following:
11 * Copyright (C) 2009, 2012, 2015 John Schember <john@nachtimwald.com>
12 * Copyright (C) 2004 Girish Ramakrishnan All Rights Reserved.
13 *
14 * THIS IS MODIFIED VERSION by George Yunaev, the modifications mostly excluded unused code,
15 * and adapted it for KWin on Plasma 5.
16 */
17
18 /*
19 * Assert validity of the window id. Get window attributes for the heck of it
20 * and see if the request went through.
21 */
isValidWindowId(Display * display,Window w)22 static bool isValidWindowId(Display *display, Window w) {
23 XWindowAttributes attrib;
24 return (XGetWindowAttributes(display, w, &attrib) != 0);
25 }
26
27 /*
28 * Checks if this window is a normal window (i.e)
29 * - Has a WM_STATE
30 * - Not modal window
31 * - Not a purely transient window (with no window type set)
32 * - Not a special window (desktop/menu/util) as indicated in the window type
33 */
isNormalWindow(Display * display,Window w)34 static bool isNormalWindow(Display *display, Window w) {
35 Atom type;
36 int format;
37 unsigned long left;
38 Atom *data = NULL;
39 unsigned long nitems;
40 Window transient_for = None;
41
42 static Atom wmState = XInternAtom(display, "WM_STATE", false);
43 static Atom windowState = XInternAtom(display, "_NET_WM_STATE", false);
44 static Atom modalWindow = XInternAtom(display, "_NET_WM_STATE_MODAL", false);
45 static Atom windowType = XInternAtom(display, "_NET_WM_WINDOW_TYPE", false);
46 static Atom normalWindow = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NORMAL", false);
47 static Atom dialogWindow = XInternAtom(display, "_NET_WM_WINDOW_TYPE_DIALOG", false);
48
49 int ret = XGetWindowProperty(display, w, wmState, 0, 10, false, AnyPropertyType, &type, &format, &nitems, &left, (unsigned char **) & data);
50
51 if (ret != Success || data == NULL) {
52 if (data != NULL)
53 XFree(data);
54 return false;
55 }
56 if (data) {
57 XFree(data);
58 }
59
60 ret = XGetWindowProperty(display, w, windowState, 0, 10, false, AnyPropertyType, &type, &format, &nitems, &left, (unsigned char **) & data);
61 if (ret == Success) {
62 unsigned int i;
63 for (i = 0; i < nitems; i++) {
64 if (data[i] == modalWindow) {
65 break;
66 }
67 }
68 XFree(data);
69 if (i < nitems) {
70 return false;
71 }
72 }
73
74 XGetTransientForHint(display, w, &transient_for);
75
76 ret = XGetWindowProperty(display, w, windowType, 0, 10, false, AnyPropertyType, &type, &format, &nitems, &left, (unsigned char **) & data);
77
78 if ((ret == Success) && data) {
79 unsigned int i;
80 for (i = 0; i < nitems; i++) {
81 if (data[i] != normalWindow && data[i] != dialogWindow) {
82 break;
83 }
84 }
85 XFree(data);
86 return (i == nitems);
87 } else {
88 return (transient_for == None);
89 }
90 }
91
92 /*
93 Window XLibUtil::pidToWid(Display *display, Window window, bool checkNormality, pid_t epid, QList<Window> dockedWindows) {
94 Window w = None;
95 Window root;
96 Window parent;
97 Window *child;
98 unsigned int num_child;
99
100 if (XQueryTree(display, window, &root, &parent, &child, &num_child) != 0) {
101 for (unsigned int i = 0; i < num_child; i++) {
102 if (epid == pid(display, child[i])) {
103 if (!dockedWindows.contains(child[i])) {
104 if (checkNormality) {
105 if (isNormalWindow(display, child[i])) {
106 return child[i];
107 }
108 } else {
109 return child[i];
110 }
111 }
112 }
113 w = pidToWid(display, child[i], checkNormality, epid);
114 if (w != None) {
115 break;
116 }
117 }
118 }
119
120 return w;
121 }
122 */
123
getWindowName(Display * display,Window w)124 static QString getWindowName( Display *display, Window w )
125 {
126 // Credits: https://stackoverflow.com/questions/8925377/why-is-xgetwindowproperty-returning-null
127 Atom nameAtom = XInternAtom( display, "_NET_WM_NAME", false );
128 Atom utf8Atom = XInternAtom( display, "UTF8_STRING", false );
129 Atom type;
130 int format;
131 unsigned long nitems, after;
132 unsigned char *data = 0;
133 QString out;
134
135 if ( Success == XGetWindowProperty( display, w, nameAtom, 0, 65536, false, utf8Atom, &type, &format, &nitems, &after, &data))
136 {
137 out = QString::fromUtf8( (const char*) data );
138 XFree(data);
139 }
140
141 return out;
142 }
143
144 /*
145 * The Grand Window Analyzer. Checks if window w has a expected pid of epid
146 * or a expected name of ename.
147 */
analyzeWindow(Display * display,Window w,const QString & ename)148 static bool analyzeWindow(Display *display, Window w, const QString &ename )
149 {
150 XClassHint ch;
151
152 bool this_is_our_man = false;
153
154 // Find the window name
155
156
157 // lets try the program name
158 if (XGetClassHint(display, w, &ch))
159 {
160 if (QString(ch.res_name).endsWith(ename)) {
161 this_is_our_man = true;
162 } else if (QString(ch.res_class).endsWith(ename)) {
163 this_is_our_man = true;
164 } else {
165 // sheer desperation
166 if ( getWindowName( display, w ).endsWith(ename) ) {
167 this_is_our_man = true;
168 }
169 }
170
171 if (ch.res_class) {
172 XFree(ch.res_class);
173 }
174 if (ch.res_name) {
175 XFree(ch.res_name);
176 }
177 }
178
179 // it's probably a good idea to check (obsolete) WM_COMMAND here
180 return this_is_our_man;
181 }
182
183 /*
184 * Given a starting window look though all children and try to find a window
185 * that matches the ename.
186 */
findWindow(Display * display,Window window,bool checkNormality,const QString & ename,QList<Window> dockedWindows=QList<Window> ())187 static Window findWindow(Display *display, Window window, bool checkNormality, const QString &ename, QList<Window> dockedWindows = QList<Window>() )
188 {
189 Window targetWindow = None;
190 Window root;
191 Window parent;
192 Window *children;
193 unsigned int num_child;
194
195 if (XQueryTree(display, window, &root, &parent, &children, &num_child) != 0) {
196 for (unsigned int i = 0; i < num_child; i++) {
197 if (analyzeWindow(display, children[i], ename) && !dockedWindows.contains(children[i])
198 && (!checkNormality || isNormalWindow(display, children[i]))) {
199 targetWindow = children[i];
200 break;
201 }
202 targetWindow = findWindow(display, children[i], checkNormality, ename);
203 if (targetWindow != None) {
204 break;
205 }
206 }
207 XFree(children);
208 }
209 return targetWindow;
210 }
211
212 /*
213 * Sends ClientMessage to a window.
214 */
sendMessage(Display * display,Window to,Window w,const char * type,int format,long mask,void * data,int size)215 static void sendMessage(Display* display, Window to, Window w, const char *type, int format, long mask, void* data, int size) {
216 XEvent ev;
217 memset(&ev, 0, sizeof (ev));
218 ev.xclient.type = ClientMessage;
219 ev.xclient.window = w;
220 ev.xclient.message_type = XInternAtom(display, type, true);
221 ev.xclient.format = format;
222 memcpy((char *) & ev.xclient.data, (const char *) data, size);
223 XSendEvent(display, to, false, mask, &ev);
224 XSync(display, false);
225 }
226
227 /*
228 * Returns the id of the currently active window.
229 */
activeWindow(Display * display)230 static Window activeWindow(Display * display) {
231 Atom active_window_atom = XInternAtom(display, "_NET_ACTIVE_WINDOW", true);
232 Atom type = None;
233 int format;
234 unsigned long nitems, after;
235 unsigned char *data = NULL;
236 int screen = DefaultScreen(display);
237 Window root = RootWindow(display, screen);
238
239 int r = XGetWindowProperty(display, root, active_window_atom, 0, 1, false, AnyPropertyType, &type, &format, &nitems, &after, &data);
240
241 Window w = None;
242 if ((r == Success) && data && (*reinterpret_cast<Window *> (data) != None)) {
243 w = *(Window *) data;
244 } else {
245 int revert;
246 XGetInputFocus(display, &w, &revert);
247 }
248 if (r == Success) {
249 XFree(data);
250 }
251 return w;
252 }
253
254 /*
255 GY: Unfortunately this doesn't work at least on KWin - the state changes, but close button is not disabled.
256 static bool disableCloseButton( Display * display, Window w )
257 {
258 // see https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#idm140130317577760
259 static Atom windowState = XInternAtom( display, "_NET_WM_ALLOWED_ACTIONS", false );
260 static Atom atomClose = XInternAtom( display, "_NET_WM_ACTION_CLOSE", false );
261 Atom type = None;
262 int format;
263 unsigned long nitems, after;
264 Atom *data = NULL;
265 QVector<Atom> newdata;
266
267 int r = XGetWindowProperty(display, w, windowState, 0, 10, false, AnyPropertyType, &type, &format, &nitems, &after, (unsigned char**) &data);
268
269 if ( r != Success)
270 return false;
271
272 for (unsigned int i = 0; i < nitems; i++)
273 if ( data[i] != atomClose )
274 newdata.push_back( data[i] );
275
276 XFree(data);
277
278 XChangeProperty( display, w, windowState, type, format, PropModeReplace, (unsigned char *) newdata.data(), newdata.size() );
279 XSync(display, False);
280 return true;
281 }
282 */
283
checkWindowState(Display * display,Window w,const char * state)284 static bool checkWindowState( Display * display, Window w, const char * state )
285 {
286 static Atom windowState = XInternAtom( display, "_NET_WM_STATE", false );
287 static Atom atomstate = XInternAtom( display, state, false );
288 Atom type = None;
289 int format;
290 unsigned long nitems, after;
291 Atom *data = NULL;
292
293 int r = XGetWindowProperty(display, w, windowState, 0, 10, false, AnyPropertyType, &type, &format, &nitems, &after, (unsigned char**) &data);
294
295 if (r == Success)
296 {
297 unsigned int i;
298
299 for (i = 0; i < nitems; i++)
300 if ( data[i] == atomstate )
301 break;
302
303 XFree(data);
304
305 if (i < nitems)
306 return true;
307 }
308
309 return false;
310 }
311
312 #if 0
313 /*
314 * Have events associated with mask for the window set in the X11 Event loop
315 * to the application.
316 */
317 static void subscribe(Display *display, Window w, long mask) {
318 Window root = RootWindow(display, DefaultScreen(display));
319 XWindowAttributes attr;
320
321 XGetWindowAttributes(display, w == None ? root : w, &attr);
322
323 XSelectInput(display, w == None ? root : w, attr.your_event_mask | mask);
324 XSync(display, false);
325 }
326
327 static void unSubscribe(Display *display, Window w) {
328 XSelectInput(display, w, NoEventMask);
329 XSync(display, false);
330 }
331
332 /*
333 * Sets data to the value of the requested window property.
334 */
335 static bool getCardinalProperty(Display *display, Window w, Atom prop, long *data) {
336 Atom type;
337 int format;
338 unsigned long nitems, bytes;
339 unsigned char *d = NULL;
340
341 if (XGetWindowProperty(display, w, prop, 0, 1, false, XA_CARDINAL, &type, &format, &nitems, &bytes, &d) == Success && d) {
342 if (data) {
343 *data = *reinterpret_cast<long *> (d);
344 }
345 XFree(d);
346 return true;
347 }
348 return false;
349 }
350 #endif
351
352
WindowTools_X11()353 WindowTools_X11::WindowTools_X11()
354 : WindowTools()
355 {
356 mWinId = None;
357 mHiddenStateCounter = 0;
358
359 connect( &mWindowStateTimer, &QTimer::timeout, this, &WindowTools_X11::timerWindowState );
360 mWindowStateTimer.setInterval( 250 );
361 mWindowStateTimer.start();
362 }
363
~WindowTools_X11()364 WindowTools_X11::~WindowTools_X11()
365 {
366 }
367
lookup()368 bool WindowTools_X11::lookup()
369 {
370 if ( isValid() )
371 return mWinId;
372
373 mWinId = findWindow(QX11Info::display(), QX11Info::appRootWindow(), true,
374 BirdtrayApp::get()->getSettings()->mThunderbirdWindowMatch);
375
376 Log::debug("Window ID found: %lX", mWinId );
377
378 return mWinId != None;
379 }
380
show()381 bool WindowTools_X11::show()
382 {
383 if ( !checkWindow() )
384 return false;
385
386 Display *display = QX11Info::display();
387 Window root = QX11Info::appRootWindow();
388
389 // We are still minimizing
390 if ( mHiddenStateCounter == 1 )
391 return false;
392
393 if ( mHiddenStateCounter == 2 )
394 {
395 XMapWindow( display, mWinId );
396 mSizeHint.flags = USPosition;
397 XSetWMNormalHints(display, mWinId, &mSizeHint );
398 }
399
400 XMapRaised( display, mWinId );
401 XFlush( display );
402
403 // Make it the active window
404 // 1 == request sent from application. 2 == from pager.
405 // We use 2 because KWin doesn't always give the window focus with 1.
406 long l_active[2] = {2, CurrentTime};
407 sendMessage( display, root, mWinId, "_NET_ACTIVE_WINDOW", 32, SubstructureNotifyMask | SubstructureRedirectMask, l_active, sizeof (l_active) );
408 XSetInputFocus(display, mWinId, RevertToParent, CurrentTime);
409
410 mHiddenStateCounter = 0;
411 return true;
412 }
413
hide()414 bool WindowTools_X11::hide()
415 {
416 if ( !checkWindow() )
417 return false;
418
419 if ( mHiddenStateCounter != 0 )
420 {
421 Log::debug("Warning: trying to hide already hidden window (counter %d), ignored", mHiddenStateCounter );
422 return false;
423 }
424
425 // Get screen number
426 Display *display = QX11Info::display();
427 long dummy;
428
429 XGetWMNormalHints( display, mWinId, &mSizeHint, &dummy );
430
431 // We call doHide() twice - at first call kWin only minimizes it,
432 // and only the second call actually hides the window from the taskbar.
433 QTimer::singleShot( 0, this, &WindowTools_X11::doHide );
434 QTimer::singleShot( 0, this, &WindowTools_X11::doHide );
435 return true;
436 }
437
isHidden()438 bool WindowTools_X11::isHidden()
439 {
440 return mHiddenStateCounter == 2 && mWinId != activeWindow( QX11Info::display() );
441 }
442
closeWindow()443 bool WindowTools_X11::closeWindow()
444 {
445 if ( !checkWindow() )
446 return false;
447
448 show();
449
450 // send _NET_CLOSE_WINDOW
451 long l[5] = {0, 0, 0, 0, 0};
452 sendMessage( QX11Info::display(), QX11Info::appRootWindow(), mWinId, "_NET_CLOSE_WINDOW", 32, SubstructureNotifyMask | SubstructureRedirectMask, l, sizeof (l));
453 return true;
454 }
455
isValid()456 bool WindowTools_X11::isValid()
457 {
458 return mWinId != None && isValidWindowId( QX11Info::display(), mWinId );
459 }
460
doHide()461 void WindowTools_X11::doHide()
462 {
463 // This function may end up being called more than two times because isHidden() not only checks the counter,
464 // but also checks the active window. Depending on window manager, the counter may get to 2 much faster than
465 // window manager removes the window from an active window. This would result in multiple calls to doHide().
466 if ( mHiddenStateCounter == 2 )
467 {
468 Log::debug("Window already should be removed from taskbar");
469 return;
470 }
471
472 Display *display = QX11Info::display();
473 long screen = DefaultScreen(display);
474
475 /*
476 * A simple call to XWithdrawWindow wont do. Here is what we do:
477 * 1. Iconify. This will make the application hide all its other windows. For
478 * example, xmms would take off the playlist and equalizer window.
479 * 2. Withdraw the window to remove it from the taskbar.
480 */
481 XIconifyWindow(display, mWinId, screen ); // good for effects too
482 XSync(display, False);
483 XWithdrawWindow(display, mWinId, screen );
484
485 // Increase the counter but do not exceed 2
486 mHiddenStateCounter++;
487
488 if ( mHiddenStateCounter == 2 )
489 Log::debug("Window removed from taskbar");
490 }
491
timerWindowState()492 void WindowTools_X11::timerWindowState()
493 {
494 if (mWinId == None || !BirdtrayApp::get()->getSettings()->mHideWhenMinimized) {
495 return;
496 }
497
498 // _NET_WM_STATE_HIDDEN is set for minimized windows, so if we see it, this means it was minimized by the user
499 if ( checkWindowState( QX11Info::display(), mWinId, "_NET_WM_STATE_HIDDEN" ) && mHiddenStateCounter == 0 )
500 {
501 mHiddenStateCounter = 1;
502 QTimer::singleShot( 0, this, &WindowTools_X11::doHide );
503 }
504 }
505
checkWindow()506 bool WindowTools_X11::checkWindow()
507 {
508 if ( mWinId == None || !isValidWindowId( QX11Info::display(), mWinId ) )
509 return lookup();
510
511 return true;
512 }
513