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