1 | /******************************************************************** |
2 | KWin - the KDE window manager |
3 | This file is part of the KDE project. |
4 | |
5 | Copyright (C) 1999, 2000 Matthias Ettrich <ettrich@kde.org> |
6 | Copyright (C) 2003 Lubos Lunak <l.lunak@kde.org> |
7 | |
8 | This program is free software; you can redistribute it and/or modify |
9 | it under the terms of the GNU General Public License as published by |
10 | the Free Software Foundation; either version 2 of the License, or |
11 | (at your option) any later version. |
12 | |
13 | This program is distributed in the hope that it will be useful, |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | GNU General Public License for more details. |
17 | |
18 | You should have received a copy of the GNU General Public License |
19 | along with this program. If not, see <http://www.gnu.org/licenses/>. |
20 | *********************************************************************/ |
21 | |
22 | /* |
23 | |
24 | This file contains things relevant to window grouping. |
25 | |
26 | */ |
27 | |
28 | //#define QT_CLEAN_NAMESPACE |
29 | |
30 | #include "group.h" |
31 | #include <QTextStream> |
32 | #include "workspace.h" |
33 | #include "client.h" |
34 | #include "effects.h" |
35 | |
36 | #include <assert.h> |
37 | #include <kstartupinfo.h> |
38 | #include <QX11Info> |
39 | |
40 | |
41 | /* |
42 | TODO |
43 | Rename as many uses of 'transient' as possible (hasTransient->hasSubwindow,etc.), |
44 | or I'll get it backwards in half of the cases again. |
45 | */ |
46 | |
47 | namespace KWin |
48 | { |
49 | |
50 | /* |
51 | Consistency checks for window relations. Since transients are determined |
52 | using Client::transiency_list and main windows are determined using Client::transientFor() |
53 | or the group for group transients, these have to match both ways. |
54 | */ |
55 | //#define ENABLE_TRANSIENCY_CHECK |
56 | |
57 | #ifdef NDEBUG |
58 | #undef ENABLE_TRANSIENCY_CHECK |
59 | #endif |
60 | |
61 | #ifdef ENABLE_TRANSIENCY_CHECK |
62 | static bool transiencyCheckNonExistent = false; |
63 | |
64 | bool performTransiencyCheck() |
65 | { |
66 | bool ret = true; |
67 | ClientList clients = Workspace::self()->clients; |
68 | for (ClientList::ConstIterator it1 = clients.constBegin(); |
69 | it1 != clients.constEnd(); |
70 | ++it1) { |
71 | if ((*it1)->deleting) |
72 | continue; |
73 | if ((*it1)->in_group == NULL) { |
74 | kDebug(1212) << "TC: " << *it1 << " in not in a group" << endl; |
75 | ret = false; |
76 | } else if (!(*it1)->in_group->members().contains(*it1)) { |
77 | kDebug(1212) << "TC: " << *it1 << " has a group " << (*it1)->in_group << " but group does not contain it" << endl; |
78 | ret = false; |
79 | } |
80 | if (!(*it1)->isTransient()) { |
81 | if (!(*it1)->mainClients().isEmpty()) { |
82 | kDebug(1212) << "TC: " << *it1 << " is not transient, has main clients:" << (*it1)->mainClients() << endl; |
83 | ret = false; |
84 | } |
85 | } else { |
86 | ClientList mains = (*it1)->mainClients(); |
87 | for (ClientList::ConstIterator it2 = mains.constBegin(); |
88 | it2 != mains.constEnd(); |
89 | ++it2) { |
90 | if (transiencyCheckNonExistent |
91 | && !Workspace::self()->clients.contains(*it2) |
92 | && !Workspace::self()->desktops.contains(*it2)) { |
93 | kDebug(1212) << "TC:" << *it1 << " has non-existent main client " ; |
94 | kDebug(1212) << "TC2:" << *it2; // this may crash |
95 | ret = false; |
96 | continue; |
97 | } |
98 | if (!(*it2)->transients_list.contains(*it1)) { |
99 | kdDebug(1212) << "TC:" << *it1 << " has main client " << *it2 << " but main client does not have it as a transient" << endl; |
100 | ret = false; |
101 | } |
102 | } |
103 | } |
104 | ClientList trans = (*it1)->transients_list; |
105 | for (ClientList::ConstIterator it2 = trans.constBegin(); |
106 | it2 != trans.constEnd(); |
107 | ++it2) { |
108 | if (transiencyCheckNonExistent |
109 | && !Workspace::self()->clients.contains(*it2) |
110 | && !Workspace::self()->desktops.contains(*it2)) { |
111 | kDebug(1212) << "TC:" << *it1 << " has non-existent transient " ; |
112 | kDebug(1212) << "TC2:" << *it2; // this may crash |
113 | ret = false; |
114 | continue; |
115 | } |
116 | if (!(*it2)->mainClients().contains(*it1)) { |
117 | kdDebug(1212) << "TC:" << *it1 << " has transient " << *it2 << " but transient does not have it as a main client" << endl; |
118 | ret = false; |
119 | } |
120 | } |
121 | } |
122 | GroupList groups = Workspace::self()->groups; |
123 | for (GroupList::ConstIterator it1 = groups.constBegin(); |
124 | it1 != groups.constEnd(); |
125 | ++it1) { |
126 | ClientList members = (*it1)->members(); |
127 | for (ClientList::ConstIterator it2 = members.constBegin(); |
128 | it2 != members.constEnd(); |
129 | ++it2) { |
130 | if ((*it2)->in_group != *it1) { |
131 | kDebug(1212) << "TC: Group " << *it1 << " contains client " << *it2 << " but client is not in that group" << endl; |
132 | ret = false; |
133 | } |
134 | } |
135 | } |
136 | return ret; |
137 | } |
138 | |
139 | static QString transiencyCheckStartBt; |
140 | static const Client* transiencyCheckClient; |
141 | static int transiencyCheck = 0; |
142 | |
143 | static void startTransiencyCheck(const QString& bt, const Client* c, bool ne) |
144 | { |
145 | if (++transiencyCheck == 1) { |
146 | transiencyCheckStartBt = bt; |
147 | transiencyCheckClient = c; |
148 | } |
149 | if (ne) |
150 | transiencyCheckNonExistent = true; |
151 | } |
152 | static void checkTransiency() |
153 | { |
154 | if (--transiencyCheck == 0) { |
155 | if (!performTransiencyCheck()) { |
156 | kDebug(1212) << "BT:" << transiencyCheckStartBt << endl; |
157 | kDebug(1212) << "CLIENT:" << transiencyCheckClient << endl; |
158 | abort(); |
159 | } |
160 | transiencyCheckNonExistent = false; |
161 | } |
162 | } |
163 | class TransiencyChecker |
164 | { |
165 | public: |
166 | TransiencyChecker(const QString& bt, const Client*c) { |
167 | startTransiencyCheck(bt, c, false); |
168 | } |
169 | ~TransiencyChecker() { |
170 | checkTransiency(); |
171 | } |
172 | }; |
173 | |
174 | void checkNonExistentClients() |
175 | { |
176 | startTransiencyCheck(kdBacktrace(), NULL, true); |
177 | checkTransiency(); |
178 | } |
179 | |
180 | #define TRANSIENCY_CHECK( c ) TransiencyChecker transiency_checker( kdBacktrace(), c ) |
181 | |
182 | #else |
183 | |
184 | #define TRANSIENCY_CHECK( c ) |
185 | |
186 | void checkNonExistentClients() |
187 | { |
188 | } |
189 | |
190 | #endif |
191 | |
192 | //******************************************** |
193 | // Group |
194 | //******************************************** |
195 | |
196 | Group::Group(Window leader_P) |
197 | : leader_client(NULL), |
198 | leader_wid(leader_P), |
199 | leader_info(NULL), |
200 | user_time(-1U), |
201 | refcount(0) |
202 | { |
203 | if (leader_P != None) { |
204 | leader_client = workspace()->findClient(WindowMatchPredicate(leader_P)); |
205 | unsigned long properties[ 2 ] = { 0, NET::WM2StartupId }; |
206 | leader_info = new NETWinInfo2(display(), leader_P, rootWindow(), |
207 | properties, 2); |
208 | } |
209 | effect_group = new EffectWindowGroupImpl(this); |
210 | workspace()->addGroup(this); |
211 | } |
212 | |
213 | Group::~Group() |
214 | { |
215 | delete leader_info; |
216 | delete effect_group; |
217 | } |
218 | |
219 | QPixmap Group::icon() const |
220 | { |
221 | if (leader_client != NULL) |
222 | return leader_client->icon(); |
223 | else if (leader_wid != None) { |
224 | QPixmap ic; |
225 | Client::readIcons(leader_wid, &ic, NULL, NULL, NULL); |
226 | return ic; |
227 | } |
228 | return QPixmap(); |
229 | } |
230 | |
231 | QPixmap Group::miniIcon() const |
232 | { |
233 | if (leader_client != NULL) |
234 | return leader_client->miniIcon(); |
235 | else if (leader_wid != None) { |
236 | QPixmap ic; |
237 | Client::readIcons(leader_wid, NULL, &ic, NULL, NULL); |
238 | return ic; |
239 | } |
240 | return QPixmap(); |
241 | } |
242 | |
243 | QPixmap Group::bigIcon() const |
244 | { |
245 | if (leader_client != NULL) |
246 | return leader_client->bigIcon(); |
247 | else if (leader_wid != None) { |
248 | QPixmap ic; |
249 | Client::readIcons(leader_wid, NULL, NULL, &ic, NULL); |
250 | return ic; |
251 | } |
252 | return QPixmap(); |
253 | } |
254 | |
255 | QPixmap Group::hugeIcon() const |
256 | { |
257 | if (leader_client != NULL) |
258 | return leader_client->hugeIcon(); |
259 | else if (leader_wid != None) { |
260 | QPixmap ic; |
261 | Client::readIcons(leader_wid, NULL, NULL, NULL, &ic); |
262 | return ic; |
263 | } |
264 | return QPixmap(); |
265 | } |
266 | |
267 | void Group::addMember(Client* member_P) |
268 | { |
269 | TRANSIENCY_CHECK(member_P); |
270 | _members.append(member_P); |
271 | // kDebug(1212) << "GROUPADD:" << this << ":" << member_P; |
272 | // kDebug(1212) << kBacktrace(); |
273 | } |
274 | |
275 | void Group::removeMember(Client* member_P) |
276 | { |
277 | TRANSIENCY_CHECK(member_P); |
278 | // kDebug(1212) << "GROUPREMOVE:" << this << ":" << member_P; |
279 | // kDebug(1212) << kBacktrace(); |
280 | Q_ASSERT(_members.contains(member_P)); |
281 | _members.removeAll(member_P); |
282 | // there are cases when automatic deleting of groups must be delayed, |
283 | // e.g. when removing a member and doing some operation on the possibly |
284 | // other members of the group (which would be however deleted already |
285 | // if there were no other members) |
286 | if (refcount == 0 && _members.isEmpty()) { |
287 | workspace()->removeGroup(this); |
288 | delete this; |
289 | } |
290 | } |
291 | |
292 | void Group::ref() |
293 | { |
294 | ++refcount; |
295 | } |
296 | |
297 | void Group::deref() |
298 | { |
299 | if (--refcount == 0 && _members.isEmpty()) { |
300 | workspace()->removeGroup(this); |
301 | delete this; |
302 | } |
303 | } |
304 | |
305 | void Group::gotLeader(Client* leader_P) |
306 | { |
307 | assert(leader_P->window() == leader_wid); |
308 | leader_client = leader_P; |
309 | } |
310 | |
311 | void Group::lostLeader() |
312 | { |
313 | assert(!_members.contains(leader_client)); |
314 | leader_client = NULL; |
315 | if (_members.isEmpty()) { |
316 | workspace()->removeGroup(this); |
317 | delete this; |
318 | } |
319 | } |
320 | |
321 | //*************************************** |
322 | // Workspace |
323 | //*************************************** |
324 | |
325 | Group* Workspace::findGroup(xcb_window_t leader) const |
326 | { |
327 | assert(leader != None); |
328 | for (GroupList::ConstIterator it = groups.constBegin(); |
329 | it != groups.constEnd(); |
330 | ++it) |
331 | if ((*it)->leader() == leader) |
332 | return *it; |
333 | return NULL; |
334 | } |
335 | |
336 | // Client is group transient, but has no group set. Try to find |
337 | // group with windows with the same client leader. |
338 | Group* Workspace::findClientLeaderGroup(const Client* c) const |
339 | { |
340 | TRANSIENCY_CHECK(c); |
341 | Group* ret = NULL; |
342 | for (ClientList::ConstIterator it = clients.constBegin(); |
343 | it != clients.constEnd(); |
344 | ++it) { |
345 | if (*it == c) |
346 | continue; |
347 | if ((*it)->wmClientLeader() == c->wmClientLeader()) { |
348 | if (ret == NULL || ret == (*it)->group()) |
349 | ret = (*it)->group(); |
350 | else { |
351 | // There are already two groups with the same client leader. |
352 | // This most probably means the app uses group transients without |
353 | // setting group for its windows. Merging the two groups is a bad |
354 | // hack, but there's no really good solution for this case. |
355 | ClientList old_group = (*it)->group()->members(); |
356 | // old_group autodeletes when being empty |
357 | for (int pos = 0; |
358 | pos < old_group.count(); |
359 | ++pos) { |
360 | Client* tmp = old_group[ pos ]; |
361 | if (tmp != c) |
362 | tmp->changeClientLeaderGroup(ret); |
363 | } |
364 | } |
365 | } |
366 | } |
367 | return ret; |
368 | } |
369 | |
370 | void Workspace::updateMinimizedOfTransients(Client* c) |
371 | { |
372 | // if mainwindow is minimized or shaded, minimize transients too |
373 | if (c->isMinimized()) { |
374 | for (ClientList::ConstIterator it = c->transients().constBegin(); |
375 | it != c->transients().constEnd(); |
376 | ++it) { |
377 | if ((*it)->isModal()) |
378 | continue; // there's no reason to hide modal dialogs with the main client |
379 | // but to keep them to eg. watch progress or whatever |
380 | if (!(*it)->isMinimized()) { |
381 | (*it)->minimize(); |
382 | updateMinimizedOfTransients((*it)); |
383 | } |
384 | } |
385 | if (c->isModal()) { // if a modal dialog is minimized, minimize its mainwindow too |
386 | foreach (Client * c2, c->mainClients()) |
387 | c2->minimize(); |
388 | } |
389 | } else { |
390 | // else unmiminize the transients |
391 | for (ClientList::ConstIterator it = c->transients().constBegin(); |
392 | it != c->transients().constEnd(); |
393 | ++it) { |
394 | if ((*it)->isMinimized()) { |
395 | (*it)->unminimize(); |
396 | updateMinimizedOfTransients((*it)); |
397 | } |
398 | } |
399 | if (c->isModal()) { |
400 | foreach (Client * c2, c->mainClients()) |
401 | c2->unminimize(); |
402 | } |
403 | } |
404 | } |
405 | |
406 | |
407 | /*! |
408 | Sets the client \a c's transient windows' on_all_desktops property to \a on_all_desktops. |
409 | */ |
410 | void Workspace::updateOnAllDesktopsOfTransients(Client* c) |
411 | { |
412 | for (ClientList::ConstIterator it = c->transients().constBegin(); |
413 | it != c->transients().constEnd(); |
414 | ++it) { |
415 | if ((*it)->isOnAllDesktops() != c->isOnAllDesktops()) |
416 | (*it)->setOnAllDesktops(c->isOnAllDesktops()); |
417 | } |
418 | } |
419 | |
420 | // A new window has been mapped. Check if it's not a mainwindow for some already existing transient window. |
421 | void Workspace::checkTransients(xcb_window_t w) |
422 | { |
423 | TRANSIENCY_CHECK(NULL); |
424 | for (ClientList::ConstIterator it = clients.constBegin(); |
425 | it != clients.constEnd(); |
426 | ++it) |
427 | (*it)->checkTransient(w); |
428 | } |
429 | |
430 | |
431 | //**************************************** |
432 | // Toplevel |
433 | //**************************************** |
434 | |
435 | // hacks for broken apps here |
436 | // all resource classes are forced to be lowercase |
437 | bool Toplevel::resourceMatch(const Toplevel* c1, const Toplevel* c2) |
438 | { |
439 | // xv has "xv" as resource name, and different strings starting with "XV" as resource class |
440 | if (qstrncmp(c1->resourceClass(), "xv" , 2) == 0 && c1->resourceName() == "xv" ) |
441 | return qstrncmp(c2->resourceClass(), "xv" , 2) == 0 && c2->resourceName() == "xv" ; |
442 | // Mozilla has "Mozilla" as resource name, and different strings as resource class |
443 | if (c1->resourceName() == "mozilla" ) |
444 | return c2->resourceName() == "mozilla" ; |
445 | return c1->resourceClass() == c2->resourceClass(); |
446 | } |
447 | |
448 | |
449 | //**************************************** |
450 | // Client |
451 | //**************************************** |
452 | |
453 | bool Client::belongToSameApplication(const Client* c1, const Client* c2, bool active_hack) |
454 | { |
455 | bool same_app = false; |
456 | |
457 | // tests that definitely mean they belong together |
458 | if (c1 == c2) |
459 | same_app = true; |
460 | else if (c1->isTransient() && c2->hasTransient(c1, true)) |
461 | same_app = true; // c1 has c2 as mainwindow |
462 | else if (c2->isTransient() && c1->hasTransient(c2, true)) |
463 | same_app = true; // c2 has c1 as mainwindow |
464 | else if (c1->group() == c2->group()) |
465 | same_app = true; // same group |
466 | else if (c1->wmClientLeader() == c2->wmClientLeader() |
467 | && c1->wmClientLeader() != c1->window() // if WM_CLIENT_LEADER is not set, it returns window(), |
468 | && c2->wmClientLeader() != c2->window()) // don't use in this test then |
469 | same_app = true; // same client leader |
470 | |
471 | // tests that mean they most probably don't belong together |
472 | else if (c1->pid() != c2->pid() |
473 | || c1->wmClientMachine(false) != c2->wmClientMachine(false)) |
474 | ; // different processes |
475 | else if (c1->wmClientLeader() != c2->wmClientLeader() |
476 | && c1->wmClientLeader() != c1->window() // if WM_CLIENT_LEADER is not set, it returns window(), |
477 | && c2->wmClientLeader() != c2->window()) // don't use in this test then |
478 | ; // different client leader |
479 | else if (!resourceMatch(c1, c2)) |
480 | ; // different apps |
481 | else if (!sameAppWindowRoleMatch(c1, c2, active_hack)) |
482 | ; // "different" apps |
483 | else if (c1->pid() == 0 || c2->pid() == 0) |
484 | ; // old apps that don't have _NET_WM_PID, consider them different |
485 | // if they weren't found to match above |
486 | else |
487 | same_app = true; // looks like it's the same app |
488 | |
489 | return same_app; |
490 | } |
491 | |
492 | // Non-transient windows with window role containing '#' are always |
493 | // considered belonging to different applications (unless |
494 | // the window role is exactly the same). KMainWindow sets |
495 | // window role this way by default, and different KMainWindow |
496 | // usually "are" different application from user's point of view. |
497 | // This help with no-focus-stealing for e.g. konqy reusing. |
498 | // On the other hand, if one of the windows is active, they are |
499 | // considered belonging to the same application. This is for |
500 | // the cases when opening new mainwindow directly from the application, |
501 | // e.g. 'Open New Window' in konqy ( active_hack == true ). |
502 | bool Client::sameAppWindowRoleMatch(const Client* c1, const Client* c2, bool active_hack) |
503 | { |
504 | if (c1->isTransient()) { |
505 | while (c1->transientFor() != NULL) |
506 | c1 = c1->transientFor(); |
507 | if (c1->groupTransient()) |
508 | return c1->group() == c2->group(); |
509 | #if 0 |
510 | // if a group transient is in its own group, it didn't possibly have a group, |
511 | // and therefore should be considered belonging to the same app like |
512 | // all other windows from the same app |
513 | || c1->group()->leaderClient() == c1 || c2->group()->leaderClient() == c2; |
514 | #endif |
515 | } |
516 | if (c2->isTransient()) { |
517 | while (c2->transientFor() != NULL) |
518 | c2 = c2->transientFor(); |
519 | if (c2->groupTransient()) |
520 | return c1->group() == c2->group(); |
521 | #if 0 |
522 | || c1->group()->leaderClient() == c1 || c2->group()->leaderClient() == c2; |
523 | #endif |
524 | } |
525 | int pos1 = c1->windowRole().indexOf('#'); |
526 | int pos2 = c2->windowRole().indexOf('#'); |
527 | if ((pos1 >= 0 && pos2 >= 0) |
528 | || |
529 | // hacks here |
530 | // Mozilla has resourceName() and resourceClass() swapped |
531 | (c1->resourceName() == "mozilla" && c2->resourceName() == "mozilla" )) { |
532 | if (!active_hack) // without the active hack for focus stealing prevention, |
533 | return c1 == c2; // different mainwindows are always different apps |
534 | if (!c1->isActive() && !c2->isActive()) |
535 | return c1 == c2; |
536 | else |
537 | return true; |
538 | } |
539 | return true; |
540 | } |
541 | |
542 | /* |
543 | |
544 | Transiency stuff: ICCCM 4.1.2.6, NETWM 7.3 |
545 | |
546 | WM_TRANSIENT_FOR is basically means "this is my mainwindow". |
547 | For NET::Unknown windows, transient windows are considered to be NET::Dialog |
548 | windows, for compatibility with non-NETWM clients. KWin may adjust the value |
549 | of this property in some cases (window pointing to itself or creating a loop, |
550 | keeping NET::Splash windows above other windows from the same app, etc.). |
551 | |
552 | Client::transient_for_id is the value of the WM_TRANSIENT_FOR property, after |
553 | possibly being adjusted by KWin. Client::transient_for points to the Client |
554 | this Client is transient for, or is NULL. If Client::transient_for_id is |
555 | poiting to the root window, the window is considered to be transient |
556 | for the whole window group, as suggested in NETWM 7.3. |
557 | |
558 | In the case of group transient window, Client::transient_for is NULL, |
559 | and Client::groupTransient() returns true. Such window is treated as |
560 | if it were transient for every window in its window group that has been |
561 | mapped _before_ it (or, to be exact, was added to the same group before it). |
562 | Otherwise two group transients can create loops, which can lead very very |
563 | nasty things (bug #67914 and all its dupes). |
564 | |
565 | Client::original_transient_for_id is the value of the property, which |
566 | may be different if Client::transient_for_id if e.g. forcing NET::Splash |
567 | to be kept on top of its window group, or when the mainwindow is not mapped |
568 | yet, in which case the window is temporarily made group transient, |
569 | and when the mainwindow is mapped, transiency is re-evaluated. |
570 | |
571 | This can get a bit complicated with with e.g. two Konqueror windows created |
572 | by the same process. They should ideally appear like two independent applications |
573 | to the user. This should be accomplished by all windows in the same process |
574 | having the same window group (needs to be changed in Qt at the moment), and |
575 | using non-group transients poiting to their relevant mainwindow for toolwindows |
576 | etc. KWin should handle both group and non-group transient dialogs well. |
577 | |
578 | In other words: |
579 | - non-transient windows : isTransient() == false |
580 | - normal transients : transientFor() != NULL |
581 | - group transients : groupTransient() == true |
582 | |
583 | - list of mainwindows : mainClients() (call once and loop over the result) |
584 | - list of transients : transients() |
585 | - every window in the group : group()->members() |
586 | */ |
587 | |
588 | void Client::readTransient() |
589 | { |
590 | TRANSIENCY_CHECK(this); |
591 | Xcb::TransientFor transientFor(window()); |
592 | xcb_window_t new_transient_for_id = XCB_WINDOW_NONE; |
593 | if (transientFor.getTransientFor(&new_transient_for_id)) { |
594 | m_originalTransientForId = new_transient_for_id; |
595 | new_transient_for_id = verifyTransientFor(new_transient_for_id, true); |
596 | } else { |
597 | m_originalTransientForId = XCB_WINDOW_NONE; |
598 | new_transient_for_id = verifyTransientFor(XCB_WINDOW_NONE, false); |
599 | } |
600 | setTransient(new_transient_for_id); |
601 | } |
602 | |
603 | void Client::setTransient(xcb_window_t new_transient_for_id) |
604 | { |
605 | TRANSIENCY_CHECK(this); |
606 | if (new_transient_for_id != m_transientForId) { |
607 | removeFromMainClients(); |
608 | transient_for = NULL; |
609 | m_transientForId = new_transient_for_id; |
610 | if (m_transientForId != XCB_WINDOW_NONE && !groupTransient()) { |
611 | transient_for = workspace()->findClient(WindowMatchPredicate(m_transientForId)); |
612 | assert(transient_for != NULL); // verifyTransient() had to check this |
613 | transient_for->addTransient(this); |
614 | } // checkGroup() will check 'check_active_modal' |
615 | checkGroup(NULL, true); // force, because transiency has changed |
616 | workspace()->updateClientLayer(this); |
617 | workspace()->resetUpdateToolWindowsTimer(); |
618 | emit transientChanged(); |
619 | } |
620 | } |
621 | |
622 | void Client::removeFromMainClients() |
623 | { |
624 | TRANSIENCY_CHECK(this); |
625 | if (transientFor() != NULL) |
626 | transientFor()->removeTransient(this); |
627 | if (groupTransient()) { |
628 | for (ClientList::ConstIterator it = group()->members().constBegin(); |
629 | it != group()->members().constEnd(); |
630 | ++it) |
631 | (*it)->removeTransient(this); |
632 | } |
633 | } |
634 | |
635 | // *sigh* this transiency handling is madness :( |
636 | // This one is called when destroying/releasing a window. |
637 | // It makes sure this client is removed from all grouping |
638 | // related lists. |
639 | void Client::cleanGrouping() |
640 | { |
641 | TRANSIENCY_CHECK(this); |
642 | // kDebug(1212) << "CLEANGROUPING:" << this; |
643 | // for ( ClientList::ConstIterator it = group()->members().begin(); |
644 | // it != group()->members().end(); |
645 | // ++it ) |
646 | // kDebug(1212) << "CL:" << *it; |
647 | // ClientList mains; |
648 | // mains = mainClients(); |
649 | // for ( ClientList::ConstIterator it = mains.begin(); |
650 | // it != mains.end(); |
651 | // ++it ) |
652 | // kDebug(1212) << "MN:" << *it; |
653 | removeFromMainClients(); |
654 | // kDebug(1212) << "CLEANGROUPING2:" << this; |
655 | // for ( ClientList::ConstIterator it = group()->members().begin(); |
656 | // it != group()->members().end(); |
657 | // ++it ) |
658 | // kDebug(1212) << "CL2:" << *it; |
659 | // mains = mainClients(); |
660 | // for ( ClientList::ConstIterator it = mains.begin(); |
661 | // it != mains.end(); |
662 | // ++it ) |
663 | // kDebug(1212) << "MN2:" << *it; |
664 | for (ClientList::ConstIterator it = transients_list.constBegin(); |
665 | it != transients_list.constEnd(); |
666 | ) { |
667 | if ((*it)->transientFor() == this) { |
668 | removeTransient(*it); |
669 | it = transients_list.constBegin(); // restart, just in case something more has changed with the list |
670 | } else |
671 | ++it; |
672 | } |
673 | // kDebug(1212) << "CLEANGROUPING3:" << this; |
674 | // for ( ClientList::ConstIterator it = group()->members().begin(); |
675 | // it != group()->members().end(); |
676 | // ++it ) |
677 | // kDebug(1212) << "CL3:" << *it; |
678 | // mains = mainClients(); |
679 | // for ( ClientList::ConstIterator it = mains.begin(); |
680 | // it != mains.end(); |
681 | // ++it ) |
682 | // kDebug(1212) << "MN3:" << *it; |
683 | // HACK |
684 | // removeFromMainClients() did remove 'this' from transient |
685 | // lists of all group members, but then made windows that |
686 | // were transient for 'this' group transient, which again |
687 | // added 'this' to those transient lists :( |
688 | ClientList group_members = group()->members(); |
689 | group()->removeMember(this); |
690 | in_group = NULL; |
691 | for (ClientList::ConstIterator it = group_members.constBegin(); |
692 | it != group_members.constEnd(); |
693 | ++it) |
694 | (*it)->removeTransient(this); |
695 | // kDebug(1212) << "CLEANGROUPING4:" << this; |
696 | // for ( ClientList::ConstIterator it = group_members.begin(); |
697 | // it != group_members.end(); |
698 | // ++it ) |
699 | // kDebug(1212) << "CL4:" << *it; |
700 | } |
701 | |
702 | // Make sure that no group transient is considered transient |
703 | // for a window that is (directly or indirectly) transient for it |
704 | // (including another group transients). |
705 | // Non-group transients not causing loops are checked in verifyTransientFor(). |
706 | void Client::checkGroupTransients() |
707 | { |
708 | TRANSIENCY_CHECK(this); |
709 | for (ClientList::ConstIterator it1 = group()->members().constBegin(); |
710 | it1 != group()->members().constEnd(); |
711 | ++it1) { |
712 | if (!(*it1)->groupTransient()) // check all group transients in the group |
713 | continue; // TODO optimize to check only the changed ones? |
714 | for (ClientList::ConstIterator it2 = group()->members().constBegin(); |
715 | it2 != group()->members().constEnd(); |
716 | ++it2) { // group transients can be transient only for others in the group, |
717 | // so don't make them transient for the ones that are transient for it |
718 | if (*it1 == *it2) |
719 | continue; |
720 | for (Client* cl = (*it2)->transientFor(); |
721 | cl != NULL; |
722 | cl = cl->transientFor()) { |
723 | if (cl == *it1) { |
724 | // don't use removeTransient(), that would modify *it2 too |
725 | (*it2)->transients_list.removeAll(*it1); |
726 | continue; |
727 | } |
728 | } |
729 | // if *it1 and *it2 are both group transients, and are transient for each other, |
730 | // make only *it2 transient for *it1 (i.e. subwindow), as *it2 came later, |
731 | // and should be therefore on top of *it1 |
732 | // TODO This could possibly be optimized, it also requires hasTransient() to check for loops. |
733 | if ((*it2)->groupTransient() && (*it1)->hasTransient(*it2, true) && (*it2)->hasTransient(*it1, true)) |
734 | (*it2)->transients_list.removeAll(*it1); |
735 | // if there are already windows W1 and W2, W2 being transient for W1, and group transient W3 |
736 | // is added, make it transient only for W2, not for W1, because it's already indirectly |
737 | // transient for it - the indirect transiency actually shouldn't break anything, |
738 | // but it can lead to exponentially expensive operations (#95231) |
739 | // TODO this is pretty slow as well |
740 | for (ClientList::ConstIterator it3 = group()->members().constBegin(); |
741 | it3 != group()->members().constEnd(); |
742 | ++it3) { |
743 | if (*it1 == *it2 || *it2 == *it3 || *it1 == *it3) |
744 | continue; |
745 | if ((*it2)->hasTransient(*it1, false) && (*it3)->hasTransient(*it1, false)) { |
746 | if ((*it2)->hasTransient(*it3, true)) |
747 | (*it2)->transients_list.removeAll(*it1); |
748 | if ((*it3)->hasTransient(*it2, true)) |
749 | (*it3)->transients_list.removeAll(*it1); |
750 | } |
751 | } |
752 | } |
753 | } |
754 | } |
755 | |
756 | /*! |
757 | Check that the window is not transient for itself, and similar nonsense. |
758 | */ |
759 | xcb_window_t Client::verifyTransientFor(xcb_window_t new_transient_for, bool set) |
760 | { |
761 | xcb_window_t new_property_value = new_transient_for; |
762 | // make sure splashscreens are shown above all their app's windows, even though |
763 | // they're in Normal layer |
764 | if (isSplash() && new_transient_for == XCB_WINDOW_NONE) |
765 | new_transient_for = rootWindow(); |
766 | if (new_transient_for == XCB_WINDOW_NONE) { |
767 | if (set) // sometimes WM_TRANSIENT_FOR is set to None, instead of root window |
768 | new_property_value = new_transient_for = rootWindow(); |
769 | else |
770 | return XCB_WINDOW_NONE; |
771 | } |
772 | if (new_transient_for == window()) { // pointing to self |
773 | // also fix the property itself |
774 | kWarning(1216) << "Client " << this << " has WM_TRANSIENT_FOR poiting to itself." ; |
775 | new_property_value = new_transient_for = rootWindow(); |
776 | } |
777 | // The transient_for window may be embedded in another application, |
778 | // so kwin cannot see it. Try to find the managed client for the |
779 | // window and fix the transient_for property if possible. |
780 | xcb_window_t before_search = new_transient_for; |
781 | while (new_transient_for != XCB_WINDOW_NONE |
782 | && new_transient_for != rootWindow() |
783 | && !workspace()->findClient(WindowMatchPredicate(new_transient_for))) { |
784 | Xcb::Tree tree(new_transient_for); |
785 | if (tree.isNull()) { |
786 | break; |
787 | } |
788 | new_transient_for = tree->parent; |
789 | } |
790 | if (Client* new_transient_for_client = workspace()->findClient(WindowMatchPredicate(new_transient_for))) { |
791 | if (new_transient_for != before_search) { |
792 | kDebug(1212) << "Client " << this << " has WM_TRANSIENT_FOR poiting to non-toplevel window " |
793 | << before_search << ", child of " << new_transient_for_client << ", adjusting." << endl; |
794 | new_property_value = new_transient_for; // also fix the property |
795 | } |
796 | } else |
797 | new_transient_for = before_search; // nice try |
798 | // loop detection |
799 | // group transients cannot cause loops, because they're considered transient only for non-transient |
800 | // windows in the group |
801 | int count = 20; |
802 | xcb_window_t loop_pos = new_transient_for; |
803 | while (loop_pos != XCB_WINDOW_NONE && loop_pos != rootWindow()) { |
804 | Client* pos = workspace()->findClient(WindowMatchPredicate(loop_pos)); |
805 | if (pos == NULL) |
806 | break; |
807 | loop_pos = pos->m_transientForId; |
808 | if (--count == 0 || pos == this) { |
809 | kWarning(1216) << "Client " << this << " caused WM_TRANSIENT_FOR loop." ; |
810 | new_transient_for = rootWindow(); |
811 | } |
812 | } |
813 | if (new_transient_for != rootWindow() |
814 | && workspace()->findClient(WindowMatchPredicate(new_transient_for)) == NULL) { |
815 | // it's transient for a specific window, but that window is not mapped |
816 | new_transient_for = rootWindow(); |
817 | } |
818 | if (new_property_value != m_originalTransientForId) |
819 | Xcb::setTransientFor(window(), new_property_value); |
820 | return new_transient_for; |
821 | } |
822 | |
823 | void Client::addTransient(Client* cl) |
824 | { |
825 | TRANSIENCY_CHECK(this); |
826 | assert(!transients_list.contains(cl)); |
827 | // assert( !cl->hasTransient( this, true )); will be fixed in checkGroupTransients() |
828 | assert(cl != this); |
829 | transients_list.append(cl); |
830 | if (workspace()->mostRecentlyActivatedClient() == this && cl->isModal()) |
831 | check_active_modal = true; |
832 | // kDebug(1212) << "ADDTRANS:" << this << ":" << cl; |
833 | // kDebug(1212) << kBacktrace(); |
834 | // for ( ClientList::ConstIterator it = transients_list.begin(); |
835 | // it != transients_list.end(); |
836 | // ++it ) |
837 | // kDebug(1212) << "AT:" << (*it); |
838 | } |
839 | |
840 | void Client::removeTransient(Client* cl) |
841 | { |
842 | TRANSIENCY_CHECK(this); |
843 | // kDebug(1212) << "REMOVETRANS:" << this << ":" << cl; |
844 | // kDebug(1212) << kBacktrace(); |
845 | transients_list.removeAll(cl); |
846 | // cl is transient for this, but this is going away |
847 | // make cl group transient |
848 | if (cl->transientFor() == this) { |
849 | cl->m_transientForId = XCB_WINDOW_NONE; |
850 | cl->transient_for = NULL; // SELI |
851 | // SELI cl->setTransient( rootWindow()); |
852 | cl->setTransient(XCB_WINDOW_NONE); |
853 | } |
854 | } |
855 | |
856 | // A new window has been mapped. Check if it's not a mainwindow for this already existing window. |
857 | void Client::checkTransient(xcb_window_t w) |
858 | { |
859 | TRANSIENCY_CHECK(this); |
860 | if (m_originalTransientForId != w) |
861 | return; |
862 | w = verifyTransientFor(w, true); |
863 | setTransient(w); |
864 | } |
865 | |
866 | // returns true if cl is the transient_for window for this client, |
867 | // or recursively the transient_for window |
868 | bool Client::hasTransient(const Client* cl, bool indirect) const |
869 | { |
870 | // checkGroupTransients() uses this to break loops, so hasTransient() must detect them |
871 | ConstClientList set; |
872 | return hasTransientInternal(cl, indirect, set); |
873 | } |
874 | |
875 | bool Client::hasTransientInternal(const Client* cl, bool indirect, ConstClientList& set) const |
876 | { |
877 | if (cl->transientFor() != NULL) { |
878 | if (cl->transientFor() == this) |
879 | return true; |
880 | if (!indirect) |
881 | return false; |
882 | if (set.contains(cl)) |
883 | return false; |
884 | set.append(cl); |
885 | return hasTransientInternal(cl->transientFor(), indirect, set); |
886 | } |
887 | if (!cl->isTransient()) |
888 | return false; |
889 | if (group() != cl->group()) |
890 | return false; |
891 | // cl is group transient, search from top |
892 | if (transients().contains(const_cast< Client* >(cl))) |
893 | return true; |
894 | if (!indirect) |
895 | return false; |
896 | if (set.contains(this)) |
897 | return false; |
898 | set.append(this); |
899 | for (ClientList::ConstIterator it = transients().constBegin(); |
900 | it != transients().constEnd(); |
901 | ++it) |
902 | if ((*it)->hasTransientInternal(cl, indirect, set)) |
903 | return true; |
904 | return false; |
905 | } |
906 | |
907 | ClientList Client::mainClients() const |
908 | { |
909 | if (!isTransient()) |
910 | return ClientList(); |
911 | if (transientFor() != NULL) |
912 | return ClientList() << const_cast< Client* >(transientFor()); |
913 | ClientList result; |
914 | for (ClientList::ConstIterator it = group()->members().constBegin(); |
915 | it != group()->members().constEnd(); |
916 | ++it) |
917 | if ((*it)->hasTransient(this, false)) |
918 | result.append(*it); |
919 | return result; |
920 | } |
921 | |
922 | ClientList Client::allMainClients() const |
923 | { |
924 | ClientList result = mainClients(); |
925 | foreach (const Client * cl, result) |
926 | result += cl->allMainClients(); |
927 | return result; |
928 | } |
929 | |
930 | Client* Client::findModal(bool allow_itself) |
931 | { |
932 | for (ClientList::ConstIterator it = transients().constBegin(); |
933 | it != transients().constEnd(); |
934 | ++it) |
935 | if (Client* ret = (*it)->findModal(true)) |
936 | return ret; |
937 | if (isModal() && allow_itself) |
938 | return this; |
939 | return NULL; |
940 | } |
941 | |
942 | // Client::window_group only holds the contents of the hint, |
943 | // but it should be used only to find the group, not for anything else |
944 | // Argument is only when some specific group needs to be set. |
945 | void Client::checkGroup(Group* set_group, bool force) |
946 | { |
947 | TRANSIENCY_CHECK(this); |
948 | Group* old_group = in_group; |
949 | if (old_group != NULL) |
950 | old_group->ref(); // turn off automatic deleting |
951 | if (set_group != NULL) { |
952 | if (set_group != in_group) { |
953 | if (in_group != NULL) |
954 | in_group->removeMember(this); |
955 | in_group = set_group; |
956 | in_group->addMember(this); |
957 | } |
958 | } else if (m_windowGroup != XCB_WINDOW_NONE) { |
959 | Group* new_group = workspace()->findGroup(m_windowGroup); |
960 | if (transientFor() != NULL && transientFor()->group() != new_group) { |
961 | // move the window to the right group (e.g. a dialog provided |
962 | // by different app, but transient for this one, so make it part of that group) |
963 | new_group = transientFor()->group(); |
964 | } |
965 | if (new_group == NULL) // doesn't exist yet |
966 | new_group = new Group(m_windowGroup); |
967 | if (new_group != in_group) { |
968 | if (in_group != NULL) |
969 | in_group->removeMember(this); |
970 | in_group = new_group; |
971 | in_group->addMember(this); |
972 | } |
973 | } else { |
974 | if (transientFor() != NULL) { |
975 | // doesn't have window group set, but is transient for something |
976 | // so make it part of that group |
977 | Group* new_group = transientFor()->group(); |
978 | if (new_group != in_group) { |
979 | if (in_group != NULL) |
980 | in_group->removeMember(this); |
981 | in_group = transientFor()->group(); |
982 | in_group->addMember(this); |
983 | } |
984 | } else if (groupTransient()) { |
985 | // group transient which actually doesn't have a group :( |
986 | // try creating group with other windows with the same client leader |
987 | Group* new_group = workspace()->findClientLeaderGroup(this); |
988 | if (new_group == NULL) |
989 | new_group = new Group(None); |
990 | if (new_group != in_group) { |
991 | if (in_group != NULL) |
992 | in_group->removeMember(this); |
993 | in_group = new_group; |
994 | in_group->addMember(this); |
995 | } |
996 | } else { // Not transient without a group, put it in its client leader group. |
997 | // This might be stupid if grouping was used for e.g. taskbar grouping |
998 | // or minimizing together the whole group, but as long as it is used |
999 | // only for dialogs it's better to keep windows from one app in one group. |
1000 | Group* new_group = workspace()->findClientLeaderGroup(this); |
1001 | if (in_group != NULL && in_group != new_group) { |
1002 | in_group->removeMember(this); |
1003 | in_group = NULL; |
1004 | } |
1005 | if (new_group == NULL) |
1006 | new_group = new Group(None); |
1007 | if (in_group != new_group) { |
1008 | in_group = new_group; |
1009 | in_group->addMember(this); |
1010 | } |
1011 | } |
1012 | } |
1013 | if (in_group != old_group || force) { |
1014 | for (ClientList::Iterator it = transients_list.begin(); |
1015 | it != transients_list.end(); |
1016 | ) { |
1017 | // group transients in the old group are no longer transient for it |
1018 | if ((*it)->groupTransient() && (*it)->group() != group()) |
1019 | it = transients_list.erase(it); |
1020 | else |
1021 | ++it; |
1022 | } |
1023 | if (groupTransient()) { |
1024 | // no longer transient for ones in the old group |
1025 | if (old_group != NULL) { |
1026 | for (ClientList::ConstIterator it = old_group->members().constBegin(); |
1027 | it != old_group->members().constEnd(); |
1028 | ++it) |
1029 | (*it)->removeTransient(this); |
1030 | } |
1031 | // and make transient for all in the new group |
1032 | for (ClientList::ConstIterator it = group()->members().constBegin(); |
1033 | it != group()->members().constEnd(); |
1034 | ++it) { |
1035 | if (*it == this) |
1036 | break; // this means the window is only transient for windows mapped before it |
1037 | (*it)->addTransient(this); |
1038 | } |
1039 | } |
1040 | // group transient splashscreens should be transient even for windows |
1041 | // in group mapped later |
1042 | for (ClientList::ConstIterator it = group()->members().constBegin(); |
1043 | it != group()->members().constEnd(); |
1044 | ++it) { |
1045 | if (!(*it)->isSplash()) |
1046 | continue; |
1047 | if (!(*it)->groupTransient()) |
1048 | continue; |
1049 | if (*it == this || hasTransient(*it, true)) // TODO indirect? |
1050 | continue; |
1051 | addTransient(*it); |
1052 | } |
1053 | } |
1054 | if (old_group != NULL) |
1055 | old_group->deref(); // can be now deleted if empty |
1056 | checkGroupTransients(); |
1057 | checkActiveModal(); |
1058 | workspace()->updateClientLayer(this); |
1059 | } |
1060 | |
1061 | // used by Workspace::findClientLeaderGroup() |
1062 | void Client::changeClientLeaderGroup(Group* gr) |
1063 | { |
1064 | // transientFor() != NULL are in the group of their mainwindow, so keep them there |
1065 | if (transientFor() != NULL) |
1066 | return; |
1067 | // also don't change the group for window which have group set |
1068 | if (m_windowGroup) |
1069 | return; |
1070 | checkGroup(gr); // change group |
1071 | } |
1072 | |
1073 | bool Client::check_active_modal = false; |
1074 | |
1075 | void Client::checkActiveModal() |
1076 | { |
1077 | // if the active window got new modal transient, activate it. |
1078 | // cannot be done in AddTransient(), because there may temporarily |
1079 | // exist loops, breaking findModal |
1080 | Client* check_modal = workspace()->mostRecentlyActivatedClient(); |
1081 | if (check_modal != NULL && check_modal->check_active_modal) { |
1082 | Client* new_modal = check_modal->findModal(); |
1083 | if (new_modal != NULL && new_modal != check_modal) { |
1084 | if (!new_modal->isManaged()) |
1085 | return; // postpone check until end of manage() |
1086 | workspace()->activateClient(new_modal); |
1087 | } |
1088 | check_modal->check_active_modal = false; |
1089 | } |
1090 | } |
1091 | |
1092 | } // namespace |
1093 | |