1 /*
2 SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
3 SPDX-FileCopyrightText: 2008 Aaron J. Seigo <aseigo@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7
8 #include "xwindowtasksmodel.h"
9 #include "tasktools.h"
10 #include "xwindowsystemeventbatcher.h"
11
12 #include <KDesktopFile>
13 #include <KDirWatch>
14 #include <KIconLoader>
15 #include <KService>
16 #include <KSharedConfig>
17 #include <KStartupInfo>
18 #include <KSycoca>
19 #include <KWindowInfo>
20 #include <KWindowSystem>
21
22 #include <QBuffer>
23 #include <QDir>
24 #include <QFile>
25 #include <QIcon>
26 #include <QSet>
27 #include <QTimer>
28 #include <QUrlQuery>
29 #include <QX11Info>
30
31 namespace TaskManager
32 {
33 static const NET::Properties windowInfoFlags =
34 NET::WMState | NET::XAWMState | NET::WMDesktop | NET::WMVisibleName | NET::WMGeometry | NET::WMFrameExtents | NET::WMWindowType | NET::WMPid;
35 static const NET::Properties2 windowInfoFlags2 =
36 NET::WM2DesktopFileName | NET::WM2Activities | NET::WM2WindowClass | NET::WM2AllowedActions | NET::WM2AppMenuObjectPath | NET::WM2AppMenuServiceName;
37
38 class Q_DECL_HIDDEN XWindowTasksModel::Private
39 {
40 public:
41 Private(XWindowTasksModel *q);
42 ~Private();
43
44 QVector<WId> windows;
45
46 // key=transient child, value=leader
47 QHash<WId, WId> transients;
48 // key=leader, values=transient children
49 QMultiHash<WId, WId> transientsDemandingAttention;
50
51 QHash<WId, KWindowInfo *> windowInfoCache;
52 QHash<WId, AppData> appDataCache;
53 QHash<WId, QRect> delegateGeometries;
54 QSet<WId> usingFallbackIcon;
55 QHash<WId, QTime> lastActivated;
56 QList<WId> cachedStackingOrder;
57 WId activeWindow = -1;
58 KSharedConfig::Ptr rulesConfig;
59 KDirWatch *configWatcher = nullptr;
60 QTimer sycocaChangeTimer;
61
62 void init();
63 void addWindow(WId window);
64 void removeWindow(WId window);
65 void windowChanged(WId window, NET::Properties properties, NET::Properties2 properties2);
66 void transientChanged(WId window, NET::Properties properties, NET::Properties2 properties2);
67 void dataChanged(WId window, const QVector<int> &roles);
68
69 KWindowInfo *windowInfo(WId window);
70 AppData appData(WId window);
71 QString appMenuServiceName(WId window);
72 QString appMenuObjectPath(WId window);
73
74 QIcon icon(WId window);
75 static QString mimeType();
76 static QString groupMimeType();
77 QUrl windowUrl(WId window);
78 QUrl launcherUrl(WId window, bool encodeFallbackIcon = true);
79 bool demandsAttention(WId window);
80
81 private:
82 XWindowTasksModel *q;
83 };
84
Private(XWindowTasksModel * q)85 XWindowTasksModel::Private::Private(XWindowTasksModel *q)
86 : q(q)
87 {
88 }
89
~Private()90 XWindowTasksModel::Private::~Private()
91 {
92 qDeleteAll(windowInfoCache);
93 windowInfoCache.clear();
94 }
95
init()96 void XWindowTasksModel::Private::init()
97 {
98 auto clearCacheAndRefresh = [this] {
99 if (!windows.count()) {
100 return;
101 }
102
103 appDataCache.clear();
104
105 // Emit changes of all roles satisfied from app data cache.
106 Q_EMIT q->dataChanged(q->index(0, 0),
107 q->index(windows.count() - 1, 0),
108 QVector<int>{Qt::DecorationRole,
109 AbstractTasksModel::AppId,
110 AbstractTasksModel::AppName,
111 AbstractTasksModel::GenericName,
112 AbstractTasksModel::LauncherUrl,
113 AbstractTasksModel::LauncherUrlWithoutIcon,
114 AbstractTasksModel::SkipTaskbar});
115 };
116
117 cachedStackingOrder = KWindowSystem::stackingOrder();
118
119 sycocaChangeTimer.setSingleShot(true);
120 sycocaChangeTimer.setInterval(100);
121
122 QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, clearCacheAndRefresh);
123
124 void (KSycoca::*myDatabaseChangeSignal)(const QStringList &) = &KSycoca::databaseChanged;
125 QObject::connect(KSycoca::self(), myDatabaseChangeSignal, q, [this](const QStringList &changedResources) {
126 if (changedResources.contains(QLatin1String("services")) || changedResources.contains(QLatin1String("apps"))
127 || changedResources.contains(QLatin1String("xdgdata-apps"))) {
128 sycocaChangeTimer.start();
129 }
130 });
131
132 rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc"));
133 configWatcher = new KDirWatch(q);
134
135 foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) {
136 configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc"));
137 }
138
139 auto rulesConfigChange = [this, clearCacheAndRefresh] {
140 rulesConfig->reparseConfiguration();
141 clearCacheAndRefresh();
142 };
143
144 QObject::connect(configWatcher, &KDirWatch::dirty, rulesConfigChange);
145 QObject::connect(configWatcher, &KDirWatch::created, rulesConfigChange);
146 QObject::connect(configWatcher, &KDirWatch::deleted, rulesConfigChange);
147
148 auto windowSystem = new XWindowSystemEventBatcher(q);
149
150 QObject::connect(windowSystem, &XWindowSystemEventBatcher::windowAdded, q, [this](WId window) {
151 addWindow(window);
152 });
153
154 QObject::connect(windowSystem, &XWindowSystemEventBatcher::windowRemoved, q, [this](WId window) {
155 removeWindow(window);
156 });
157
158 QObject::connect(windowSystem, &XWindowSystemEventBatcher::windowChanged, q, [this](WId window, NET::Properties properties, NET::Properties2 properties2) {
159 windowChanged(window, properties, properties2);
160 });
161
162 // Update IsActive for previously- and newly-active windows.
163 QObject::connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, q, [this](WId window) {
164 const WId oldActiveWindow = activeWindow;
165
166 const auto leader = transients.value(window, XCB_WINDOW_NONE);
167 if (leader != XCB_WINDOW_NONE) {
168 window = leader;
169 }
170
171 activeWindow = window;
172 lastActivated[activeWindow] = QTime::currentTime();
173
174 int row = windows.indexOf(oldActiveWindow);
175
176 if (row != -1) {
177 dataChanged(oldActiveWindow, QVector<int>{IsActive});
178 }
179
180 row = windows.indexOf(window);
181
182 if (row != -1) {
183 dataChanged(window, QVector<int>{IsActive});
184 }
185 });
186
187 QObject::connect(KWindowSystem::self(), &KWindowSystem::stackingOrderChanged, q, [this]() {
188 cachedStackingOrder = KWindowSystem::stackingOrder();
189 Q_EMIT q->dataChanged(q->index(0, 0), q->index(q->rowCount() - 1, 0), QVector<int>{StackingOrder});
190 });
191
192 activeWindow = KWindowSystem::activeWindow();
193
194 // Add existing windows.
195 foreach (const WId window, KWindowSystem::windows()) {
196 addWindow(window);
197 }
198 }
199
addWindow(WId window)200 void XWindowTasksModel::Private::addWindow(WId window)
201 {
202 // Don't add window twice.
203 if (windows.contains(window)) {
204 return;
205 }
206
207 KWindowInfo info(window, NET::WMWindowType | NET::WMState | NET::WMName | NET::WMVisibleName, NET::WM2TransientFor);
208
209 NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask
210 | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask);
211
212 const WId leader = info.transientFor();
213
214 // Handle transient.
215 if (leader > 0 && leader != window && leader != QX11Info::appRootWindow() && !transients.contains(window) && windows.contains(leader)) {
216 transients.insert(window, leader);
217
218 // Update demands attention state for leader.
219 if (info.hasState(NET::DemandsAttention) && windows.contains(leader)) {
220 transientsDemandingAttention.insertMulti(leader, window);
221 dataChanged(leader, QVector<int>{IsDemandingAttention});
222 }
223
224 return;
225 }
226
227 // Ignore NET::Tool and other special window types; they are not considered tasks.
228 if (wType != NET::Normal && wType != NET::Override && wType != NET::Unknown && wType != NET::Dialog && wType != NET::Utility) {
229 return;
230 }
231
232 const int count = windows.count();
233 q->beginInsertRows(QModelIndex(), count, count);
234 windows.append(window);
235 q->endInsertRows();
236 }
237
removeWindow(WId window)238 void XWindowTasksModel::Private::removeWindow(WId window)
239 {
240 const int row = windows.indexOf(window);
241
242 if (row != -1) {
243 q->beginRemoveRows(QModelIndex(), row, row);
244 windows.removeAt(row);
245 transientsDemandingAttention.remove(window);
246 delete windowInfoCache.take(window);
247 appDataCache.remove(window);
248 delegateGeometries.remove(window);
249 usingFallbackIcon.remove(window);
250 lastActivated.remove(window);
251 q->endRemoveRows();
252 } else { // Could be a transient.
253 // Removing a transient might change the demands attention state of the leader.
254 if (transients.remove(window)) {
255 const WId leader = transientsDemandingAttention.key(window, XCB_WINDOW_NONE);
256
257 if (leader != XCB_WINDOW_NONE) {
258 transientsDemandingAttention.remove(leader, window);
259 dataChanged(leader, QVector<int>{IsDemandingAttention});
260 }
261 }
262 }
263
264 if (activeWindow == window) {
265 activeWindow = -1;
266 }
267 }
268
transientChanged(WId window,NET::Properties properties,NET::Properties2 properties2)269 void XWindowTasksModel::Private::transientChanged(WId window, NET::Properties properties, NET::Properties2 properties2)
270 {
271 // Changes to a transient's state might change demands attention state for leader.
272 if (properties & (NET::WMState | NET::XAWMState)) {
273 const KWindowInfo info(window, NET::WMState | NET::XAWMState, NET::WM2TransientFor);
274 const WId leader = info.transientFor();
275
276 if (!windows.contains(leader)) {
277 return;
278 }
279
280 if (info.hasState(NET::DemandsAttention)) {
281 if (!transientsDemandingAttention.values(leader).contains(window)) {
282 transientsDemandingAttention.insertMulti(leader, window);
283 dataChanged(leader, QVector<int>{IsDemandingAttention});
284 }
285 } else if (transientsDemandingAttention.remove(window)) {
286 dataChanged(leader, QVector<int>{IsDemandingAttention});
287 }
288 // Leader might have changed.
289 } else if (properties2 & NET::WM2TransientFor) {
290 const KWindowInfo info(window, NET::WMState | NET::XAWMState, NET::WM2TransientFor);
291
292 if (info.hasState(NET::DemandsAttention)) {
293 const WId oldLeader = transientsDemandingAttention.key(window, XCB_WINDOW_NONE);
294
295 if (oldLeader != XCB_WINDOW_NONE) {
296 const WId leader = info.transientFor();
297
298 if (leader != oldLeader) {
299 transientsDemandingAttention.remove(oldLeader, window);
300 transientsDemandingAttention.insertMulti(leader, window);
301 dataChanged(oldLeader, QVector<int>{IsDemandingAttention});
302 dataChanged(leader, QVector<int>{IsDemandingAttention});
303 }
304 }
305 }
306 }
307 }
308
windowChanged(WId window,NET::Properties properties,NET::Properties2 properties2)309 void XWindowTasksModel::Private::windowChanged(WId window, NET::Properties properties, NET::Properties2 properties2)
310 {
311 if (transients.contains(window)) {
312 transientChanged(window, properties, properties2);
313 return;
314 }
315
316 bool wipeInfoCache = false;
317 bool wipeAppDataCache = false;
318 QVector<int> changedRoles;
319
320 if (properties & (NET::WMPid) || properties2 & (NET::WM2DesktopFileName | NET::WM2WindowClass)) {
321 wipeInfoCache = true;
322 wipeAppDataCache = true;
323 changedRoles << Qt::DecorationRole << AppId << AppName << GenericName << LauncherUrl << AppPid << SkipTaskbar;
324 }
325
326 if (properties & (NET::WMName | NET::WMVisibleName)) {
327 changedRoles << Qt::DisplayRole;
328 wipeInfoCache = true;
329 }
330
331 if ((properties & NET::WMIcon) && usingFallbackIcon.contains(window)) {
332 wipeAppDataCache = true;
333
334 if (!changedRoles.contains(Qt::DecorationRole)) {
335 changedRoles << Qt::DecorationRole;
336 }
337 }
338
339 // FIXME TODO: It might be worth keeping track of which windows were demanding
340 // attention (or not) to avoid emitting this role on every state change, as
341 // TaskGroupingProxyModel needs to check for group-ability when a change to it
342 // is announced and the queried state is false.
343 if (properties & (NET::WMState | NET::XAWMState)) {
344 wipeInfoCache = true;
345 changedRoles << IsFullScreen << IsMaximized << IsMinimized << IsKeepAbove << IsKeepBelow;
346 changedRoles << IsShaded << IsDemandingAttention << SkipTaskbar << SkipPager;
347 }
348
349 if (properties & NET::WMWindowType) {
350 wipeInfoCache = true;
351 changedRoles << SkipTaskbar;
352 }
353
354 if (properties2 & NET::WM2AllowedActions) {
355 wipeInfoCache = true;
356 changedRoles << IsClosable << IsMovable << IsResizable << IsMaximizable << IsMinimizable;
357 changedRoles << IsFullScreenable << IsShadeable << IsVirtualDesktopsChangeable;
358 }
359
360 if (properties & NET::WMDesktop) {
361 wipeInfoCache = true;
362 changedRoles << VirtualDesktops << IsOnAllVirtualDesktops;
363 }
364
365 if (properties & NET::WMGeometry) {
366 wipeInfoCache = true;
367 changedRoles << Geometry << ScreenGeometry;
368 }
369
370 if (properties2 & NET::WM2Activities) {
371 changedRoles << Activities;
372 }
373
374 if (properties2 & NET::WM2AppMenuServiceName) {
375 changedRoles << ApplicationMenuServiceName;
376 }
377
378 if (properties2 & NET::WM2AppMenuObjectPath) {
379 changedRoles << ApplicationMenuObjectPath;
380 }
381
382 if (wipeInfoCache) {
383 delete windowInfoCache.take(window);
384 }
385
386 if (wipeAppDataCache) {
387 appDataCache.remove(window);
388 usingFallbackIcon.remove(window);
389 }
390
391 if (!changedRoles.isEmpty()) {
392 dataChanged(window, changedRoles);
393 }
394 }
395
dataChanged(WId window,const QVector<int> & roles)396 void XWindowTasksModel::Private::dataChanged(WId window, const QVector<int> &roles)
397 {
398 const int i = windows.indexOf(window);
399
400 if (i == -1) {
401 return;
402 }
403
404 QModelIndex idx = q->index(i);
405 emit q->dataChanged(idx, idx, roles);
406 }
407
windowInfo(WId window)408 KWindowInfo *XWindowTasksModel::Private::windowInfo(WId window)
409 {
410 const auto &it = windowInfoCache.constFind(window);
411
412 if (it != windowInfoCache.constEnd()) {
413 return *it;
414 }
415
416 KWindowInfo *info = new KWindowInfo(window, windowInfoFlags, windowInfoFlags2);
417 windowInfoCache.insert(window, info);
418
419 return info;
420 }
421
appData(WId window)422 AppData XWindowTasksModel::Private::appData(WId window)
423 {
424 const auto &it = appDataCache.constFind(window);
425
426 if (it != appDataCache.constEnd()) {
427 return *it;
428 }
429
430 const AppData &data = appDataFromUrl(windowUrl(window));
431
432 // If we weren't able to derive a launcher URL from the window meta data,
433 // fall back to WM_CLASS Class string as app id. This helps with apps we
434 // can't map to an URL due to existing outside the regular system
435 // environment, e.g. wine clients.
436 if (data.id.isEmpty() && data.url.isEmpty()) {
437 AppData dataCopy = data;
438
439 dataCopy.id = windowInfo(window)->windowClassClass();
440
441 appDataCache.insert(window, dataCopy);
442
443 return dataCopy;
444 }
445
446 appDataCache.insert(window, data);
447
448 return data;
449 }
450
appMenuServiceName(WId window)451 QString XWindowTasksModel::Private::appMenuServiceName(WId window)
452 {
453 const KWindowInfo *info = windowInfo(window);
454 return QString::fromUtf8(info->applicationMenuServiceName());
455 }
456
appMenuObjectPath(WId window)457 QString XWindowTasksModel::Private::appMenuObjectPath(WId window)
458 {
459 const KWindowInfo *info = windowInfo(window);
460 return QString::fromUtf8(info->applicationMenuObjectPath());
461 }
462
icon(WId window)463 QIcon XWindowTasksModel::Private::icon(WId window)
464 {
465 const AppData &app = appData(window);
466
467 if (!app.icon.isNull()) {
468 return app.icon;
469 }
470
471 QIcon icon;
472
473 icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeSmall, KIconLoader::SizeSmall, false));
474 icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium, false));
475 icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeMedium, KIconLoader::SizeMedium, false));
476 icon.addPixmap(KWindowSystem::icon(window, KIconLoader::SizeLarge, KIconLoader::SizeLarge, false));
477
478 appDataCache[window].icon = icon;
479 usingFallbackIcon.insert(window);
480
481 return icon;
482 }
483
mimeType()484 QString XWindowTasksModel::Private::mimeType()
485 {
486 return QStringLiteral("windowsystem/winid");
487 }
488
groupMimeType()489 QString XWindowTasksModel::Private::groupMimeType()
490 {
491 return QStringLiteral("windowsystem/multiple-winids");
492 }
493
windowUrl(WId window)494 QUrl XWindowTasksModel::Private::windowUrl(WId window)
495 {
496 const KWindowInfo *info = windowInfo(window);
497
498 QString desktopFile = QString::fromUtf8(info->desktopFileName());
499
500 if (!desktopFile.isEmpty()) {
501 KService::Ptr service = KService::serviceByStorageId(desktopFile);
502
503 if (service) {
504 const QString &menuId = service->menuId();
505
506 // applications: URLs are used to refer to applications by their KService::menuId
507 // (i.e. .desktop file name) rather than the absolute path to a .desktop file.
508 if (!menuId.isEmpty()) {
509 return QUrl(QStringLiteral("applications:") + menuId);
510 }
511
512 return QUrl::fromLocalFile(service->entryPath());
513 }
514
515 if (!desktopFile.endsWith(QLatin1String(".desktop"))) {
516 desktopFile.append(QLatin1String(".desktop"));
517 }
518
519 if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) {
520 return QUrl::fromLocalFile(desktopFile);
521 }
522 }
523
524 return windowUrlFromMetadata(info->windowClassClass(), info->pid(), rulesConfig, info->windowClassName());
525 }
526
launcherUrl(WId window,bool encodeFallbackIcon)527 QUrl XWindowTasksModel::Private::launcherUrl(WId window, bool encodeFallbackIcon)
528 {
529 const AppData &data = appData(window);
530
531 QUrl url = data.url;
532 if (!encodeFallbackIcon || !data.icon.name().isEmpty()) {
533 return url;
534 }
535
536 // Forego adding the window icon pixmap if the URL is otherwise empty.
537 if (!url.isValid()) {
538 return QUrl();
539 }
540
541 // Only serialize pixmap data if the window pixmap is actually being used.
542 // QIcon::name() used above only returns a themed icon name but nothing when
543 // the icon was created using an absolute path, as can be the case with, e.g.
544 // containerized apps.
545 if (!usingFallbackIcon.contains(window)) {
546 return url;
547 }
548
549 QPixmap pixmap;
550
551 if (!data.icon.isNull()) {
552 pixmap = data.icon.pixmap(KIconLoader::SizeLarge);
553 }
554
555 if (pixmap.isNull()) {
556 pixmap = KWindowSystem::icon(window, KIconLoader::SizeLarge, KIconLoader::SizeLarge, false);
557 }
558
559 if (pixmap.isNull()) {
560 return data.url;
561 }
562 QUrlQuery uQuery(url);
563 QByteArray bytes;
564 QBuffer buffer(&bytes);
565 buffer.open(QIODevice::WriteOnly);
566 pixmap.save(&buffer, "PNG");
567 uQuery.addQueryItem(QStringLiteral("iconData"), bytes.toBase64(QByteArray::Base64UrlEncoding));
568
569 url.setQuery(uQuery);
570
571 return url;
572 }
573
demandsAttention(WId window)574 bool XWindowTasksModel::Private::demandsAttention(WId window)
575 {
576 if (windows.contains(window)) {
577 return ((windowInfo(window)->hasState(NET::DemandsAttention)) || transientsDemandingAttention.contains(window));
578 }
579
580 return false;
581 }
582
XWindowTasksModel(QObject * parent)583 XWindowTasksModel::XWindowTasksModel(QObject *parent)
584 : AbstractWindowTasksModel(parent)
585 , d(new Private(this))
586 {
587 d->init();
588 }
589
~XWindowTasksModel()590 XWindowTasksModel::~XWindowTasksModel()
591 {
592 }
593
data(const QModelIndex & index,int role) const594 QVariant XWindowTasksModel::data(const QModelIndex &index, int role) const
595 {
596 if (!index.isValid() || index.row() >= d->windows.count()) {
597 return QVariant();
598 }
599
600 const WId window = d->windows.at(index.row());
601
602 if (role == Qt::DisplayRole) {
603 return d->windowInfo(window)->visibleName();
604 } else if (role == Qt::DecorationRole) {
605 return d->icon(window);
606 } else if (role == AppId) {
607 return d->appData(window).id;
608 } else if (role == AppName) {
609 return d->appData(window).name;
610 } else if (role == GenericName) {
611 return d->appData(window).genericName;
612 } else if (role == LauncherUrl) {
613 return d->launcherUrl(window);
614 } else if (role == LauncherUrlWithoutIcon) {
615 return d->launcherUrl(window, false /* encodeFallbackIcon */);
616 } else if (role == WinIdList) {
617 return QVariantList() << window;
618 } else if (role == MimeType) {
619 return d->mimeType();
620 } else if (role == MimeData) {
621 return QByteArray((char *)&window, sizeof(window));
622 } else if (role == IsWindow) {
623 return true;
624 } else if (role == IsActive) {
625 return (window == d->activeWindow);
626 } else if (role == IsClosable) {
627 return d->windowInfo(window)->actionSupported(NET::ActionClose);
628 } else if (role == IsMovable) {
629 return d->windowInfo(window)->actionSupported(NET::ActionMove);
630 } else if (role == IsResizable) {
631 return d->windowInfo(window)->actionSupported(NET::ActionResize);
632 } else if (role == IsMaximizable) {
633 return d->windowInfo(window)->actionSupported(NET::ActionMax);
634 } else if (role == IsMaximized) {
635 const KWindowInfo *info = d->windowInfo(window);
636 return info->hasState(NET::MaxHoriz) && info->hasState(NET::MaxVert);
637 } else if (role == IsMinimizable) {
638 return d->windowInfo(window)->actionSupported(NET::ActionMinimize);
639 } else if (role == IsMinimized) {
640 return d->windowInfo(window)->isMinimized();
641 } else if (role == IsHidden) {
642 return d->windowInfo(window)->hasState(NET::Hidden);
643 } else if (role == IsKeepAbove) {
644 return d->windowInfo(window)->hasState(NET::KeepAbove);
645 } else if (role == IsKeepBelow) {
646 return d->windowInfo(window)->hasState(NET::KeepBelow);
647 } else if (role == IsFullScreenable) {
648 return d->windowInfo(window)->actionSupported(NET::ActionFullScreen);
649 } else if (role == IsFullScreen) {
650 return d->windowInfo(window)->hasState(NET::FullScreen);
651 } else if (role == IsShadeable) {
652 return d->windowInfo(window)->actionSupported(NET::ActionShade);
653 } else if (role == IsShaded) {
654 return d->windowInfo(window)->hasState(NET::Shaded);
655 } else if (role == IsVirtualDesktopsChangeable) {
656 return d->windowInfo(window)->actionSupported(NET::ActionChangeDesktop);
657 } else if (role == VirtualDesktops) {
658 return QVariantList() << d->windowInfo(window)->desktop();
659 } else if (role == IsOnAllVirtualDesktops) {
660 return d->windowInfo(window)->onAllDesktops();
661 } else if (role == Geometry) {
662 return d->windowInfo(window)->frameGeometry();
663 } else if (role == ScreenGeometry) {
664 return screenGeometry(d->windowInfo(window)->frameGeometry().center());
665 } else if (role == Activities) {
666 return d->windowInfo(window)->activities();
667 } else if (role == IsDemandingAttention) {
668 return d->demandsAttention(window);
669 } else if (role == SkipTaskbar) {
670 const KWindowInfo *info = d->windowInfo(window);
671 // _NET_WM_WINDOW_TYPE_UTILITY type windows should not be on task bars,
672 // but they should be shown on pagers.
673 return (info->hasState(NET::SkipTaskbar) || info->windowType(NET::UtilityMask) == NET::Utility || d->appData(window).skipTaskbar);
674 } else if (role == SkipPager) {
675 return d->windowInfo(window)->hasState(NET::SkipPager);
676 } else if (role == AppPid) {
677 return d->windowInfo(window)->pid();
678 } else if (role == StackingOrder) {
679 return d->cachedStackingOrder.indexOf(window);
680 } else if (role == LastActivated) {
681 if (d->lastActivated.contains(window)) {
682 return d->lastActivated.value(window);
683 }
684 } else if (role == ApplicationMenuObjectPath) {
685 return d->appMenuObjectPath(window);
686 } else if (role == ApplicationMenuServiceName) {
687 return d->appMenuServiceName(window);
688 }
689
690 return QVariant();
691 }
692
rowCount(const QModelIndex & parent) const693 int XWindowTasksModel::rowCount(const QModelIndex &parent) const
694 {
695 return parent.isValid() ? 0 : d->windows.count();
696 }
697
requestActivate(const QModelIndex & index)698 void XWindowTasksModel::requestActivate(const QModelIndex &index)
699 {
700 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
701 return;
702 }
703
704 if (index.row() >= 0 && index.row() < d->windows.count()) {
705 WId window = d->windows.at(index.row());
706
707 // Pull forward any transient demanding attention.
708 if (d->transientsDemandingAttention.contains(window)) {
709 window = d->transientsDemandingAttention.value(window);
710 // Quote from legacy libtaskmanager:
711 // "this is a work around for (at least?) kwin where a shaded transient will prevent the main
712 // window from being brought forward unless the transient is actually pulled forward, most
713 // easily reproduced by opening a modal file open/save dialog on an app then shading the file
714 // dialog and trying to bring the window forward by clicking on it in a tasks widget
715 // TODO: do we need to check all the transients for shaded?"
716 } else if (!d->transients.isEmpty()) {
717 const auto transients = d->transients.keys(window);
718 for (const auto transient : qAsConst(transients)) {
719 KWindowInfo info(transient, NET::WMState, NET::WM2TransientFor);
720 if (info.valid(true) && info.hasState(NET::Shaded)) {
721 window = transient;
722 break;
723 }
724 }
725 }
726
727 KWindowSystem::forceActiveWindow(window);
728 }
729 }
730
requestNewInstance(const QModelIndex & index)731 void XWindowTasksModel::requestNewInstance(const QModelIndex &index)
732 {
733 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
734 return;
735 }
736
737 runApp(d->appData(d->windows.at(index.row())));
738 }
739
requestOpenUrls(const QModelIndex & index,const QList<QUrl> & urls)740 void XWindowTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
741 {
742 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count() || urls.isEmpty()) {
743 return;
744 }
745
746 runApp(d->appData(d->windows.at(index.row())), urls);
747 }
748
requestClose(const QModelIndex & index)749 void XWindowTasksModel::requestClose(const QModelIndex &index)
750 {
751 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
752 return;
753 }
754
755 NETRootInfo ri(QX11Info::connection(), NET::CloseWindow);
756 ri.closeWindowRequest(d->windows.at(index.row()));
757 }
758
requestMove(const QModelIndex & index)759 void XWindowTasksModel::requestMove(const QModelIndex &index)
760 {
761 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
762 return;
763 }
764
765 const WId window = d->windows.at(index.row());
766 const KWindowInfo *info = d->windowInfo(window);
767
768 bool onCurrent = info->isOnCurrentDesktop();
769
770 if (!onCurrent) {
771 KWindowSystem::setCurrentDesktop(info->desktop());
772 KWindowSystem::forceActiveWindow(window);
773 }
774
775 if (info->isMinimized()) {
776 KWindowSystem::unminimizeWindow(window);
777 }
778
779 const QRect &geom = info->geometry();
780
781 NETRootInfo ri(QX11Info::connection(), NET::WMMoveResize);
782 ri.moveResizeRequest(window, geom.center().x(), geom.center().y(), NET::Move);
783 }
784
requestResize(const QModelIndex & index)785 void XWindowTasksModel::requestResize(const QModelIndex &index)
786 {
787 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
788 return;
789 }
790
791 const WId window = d->windows.at(index.row());
792 const KWindowInfo *info = d->windowInfo(window);
793
794 bool onCurrent = info->isOnCurrentDesktop();
795
796 if (!onCurrent) {
797 KWindowSystem::setCurrentDesktop(info->desktop());
798 KWindowSystem::forceActiveWindow(window);
799 }
800
801 if (info->isMinimized()) {
802 KWindowSystem::unminimizeWindow(window);
803 }
804
805 const QRect &geom = info->geometry();
806
807 NETRootInfo ri(QX11Info::connection(), NET::WMMoveResize);
808 ri.moveResizeRequest(window, geom.bottomRight().x(), geom.bottomRight().y(), NET::BottomRight);
809 }
810
requestToggleMinimized(const QModelIndex & index)811 void XWindowTasksModel::requestToggleMinimized(const QModelIndex &index)
812 {
813 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
814 return;
815 }
816
817 const WId window = d->windows.at(index.row());
818 const KWindowInfo *info = d->windowInfo(window);
819
820 if (index.data(AbstractTasksModel::IsHidden).toBool()) {
821 bool onCurrent = info->isOnCurrentDesktop();
822
823 // FIXME: Move logic up into proxy? (See also others.)
824 if (!onCurrent) {
825 KWindowSystem::setCurrentDesktop(info->desktop());
826 }
827
828 KWindowSystem::unminimizeWindow(window);
829
830 if (onCurrent) {
831 KWindowSystem::forceActiveWindow(window);
832 }
833 } else {
834 KWindowSystem::minimizeWindow(window);
835 }
836 }
837
requestToggleMaximized(const QModelIndex & index)838 void XWindowTasksModel::requestToggleMaximized(const QModelIndex &index)
839 {
840 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
841 return;
842 }
843
844 const WId window = d->windows.at(index.row());
845 const KWindowInfo *info = d->windowInfo(window);
846 bool onCurrent = info->isOnCurrentDesktop();
847 bool restore = (info->hasState(NET::MaxHoriz) && info->hasState(NET::MaxVert));
848
849 // FIXME: Move logic up into proxy? (See also others.)
850 if (!onCurrent) {
851 KWindowSystem::setCurrentDesktop(info->desktop());
852 }
853
854 if (info->isMinimized()) {
855 KWindowSystem::unminimizeWindow(window);
856 }
857
858 NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
859
860 if (restore) {
861 ni.setState(NET::States(), NET::Max);
862 } else {
863 ni.setState(NET::Max, NET::Max);
864 }
865
866 if (!onCurrent) {
867 KWindowSystem::forceActiveWindow(window);
868 }
869 }
870
requestToggleKeepAbove(const QModelIndex & index)871 void XWindowTasksModel::requestToggleKeepAbove(const QModelIndex &index)
872 {
873 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
874 return;
875 }
876
877 const WId window = d->windows.at(index.row());
878 const KWindowInfo *info = d->windowInfo(window);
879
880 NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
881
882 if (info->hasState(NET::KeepAbove)) {
883 ni.setState(NET::States(), NET::KeepAbove);
884 } else {
885 ni.setState(NET::KeepAbove, NET::KeepAbove);
886 }
887 }
888
requestToggleKeepBelow(const QModelIndex & index)889 void XWindowTasksModel::requestToggleKeepBelow(const QModelIndex &index)
890 {
891 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
892 return;
893 }
894
895 const WId window = d->windows.at(index.row());
896 const KWindowInfo *info = d->windowInfo(window);
897
898 NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
899
900 if (info->hasState(NET::KeepBelow)) {
901 ni.setState(NET::States(), NET::KeepBelow);
902 } else {
903 ni.setState(NET::KeepBelow, NET::KeepBelow);
904 }
905 }
906
requestToggleFullScreen(const QModelIndex & index)907 void XWindowTasksModel::requestToggleFullScreen(const QModelIndex &index)
908 {
909 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
910 return;
911 }
912
913 const WId window = d->windows.at(index.row());
914 const KWindowInfo *info = d->windowInfo(window);
915
916 NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
917
918 if (info->hasState(NET::FullScreen)) {
919 ni.setState(NET::States(), NET::FullScreen);
920 } else {
921 ni.setState(NET::FullScreen, NET::FullScreen);
922 }
923 }
924
requestToggleShaded(const QModelIndex & index)925 void XWindowTasksModel::requestToggleShaded(const QModelIndex &index)
926 {
927 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
928 return;
929 }
930
931 const WId window = d->windows.at(index.row());
932 const KWindowInfo *info = d->windowInfo(window);
933
934 NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::WMState, NET::Properties2());
935
936 if (info->hasState(NET::Shaded)) {
937 ni.setState(NET::States(), NET::Shaded);
938 } else {
939 ni.setState(NET::Shaded, NET::Shaded);
940 }
941 }
942
requestVirtualDesktops(const QModelIndex & index,const QVariantList & desktops)943 void XWindowTasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops)
944 {
945 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
946 return;
947 }
948
949 int desktop = 0;
950
951 if (!desktops.isEmpty()) {
952 bool ok = false;
953
954 desktop = desktops.first().toUInt(&ok);
955
956 if (!ok) {
957 return;
958 }
959 }
960
961 if (desktop > KWindowSystem::numberOfDesktops()) {
962 return;
963 }
964
965 const WId window = d->windows.at(index.row());
966 const KWindowInfo *info = d->windowInfo(window);
967
968 if (desktop == 0) {
969 if (info->onAllDesktops()) {
970 KWindowSystem::setOnDesktop(window, KWindowSystem::currentDesktop());
971 KWindowSystem::forceActiveWindow(window);
972 } else {
973 KWindowSystem::setOnAllDesktops(window, true);
974 }
975
976 return;
977 }
978
979 KWindowSystem::setOnDesktop(window, desktop);
980
981 if (desktop == KWindowSystem::currentDesktop()) {
982 KWindowSystem::forceActiveWindow(window);
983 }
984 }
985
requestNewVirtualDesktop(const QModelIndex & index)986 void XWindowTasksModel::requestNewVirtualDesktop(const QModelIndex &index)
987 {
988 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
989 return;
990 }
991
992 const WId window = d->windows.at(index.row());
993 const int desktop = KWindowSystem::numberOfDesktops() + 1;
994
995 // FIXME Arbitrary limit of 20 copied from old code.
996 if (desktop > 20) {
997 return;
998 }
999
1000 NETRootInfo ri(QX11Info::connection(), NET::NumberOfDesktops);
1001 ri.setNumberOfDesktops(desktop);
1002
1003 KWindowSystem::setOnDesktop(window, desktop);
1004 }
1005
requestActivities(const QModelIndex & index,const QStringList & activities)1006 void XWindowTasksModel::requestActivities(const QModelIndex &index, const QStringList &activities)
1007 {
1008 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
1009 return;
1010 }
1011
1012 const WId window = d->windows.at(index.row());
1013
1014 KWindowSystem::setOnActivities(window, activities);
1015 }
1016
requestPublishDelegateGeometry(const QModelIndex & index,const QRect & geometry,QObject * delegate)1017 void XWindowTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate)
1018 {
1019 Q_UNUSED(delegate)
1020
1021 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
1022 return;
1023 }
1024
1025 const WId window = d->windows.at(index.row());
1026
1027 if (d->delegateGeometries.contains(window) && d->delegateGeometries.value(window) == geometry) {
1028 return;
1029 }
1030
1031 NETWinInfo ni(QX11Info::connection(), window, QX11Info::appRootWindow(), NET::Properties(), NET::Properties2());
1032 NETRect rect;
1033
1034 if (geometry.isValid()) {
1035 rect.pos.x = geometry.x();
1036 rect.pos.y = geometry.y();
1037 rect.size.width = geometry.width();
1038 rect.size.height = geometry.height();
1039
1040 d->delegateGeometries.insert(window, geometry);
1041 } else {
1042 d->delegateGeometries.remove(window);
1043 }
1044
1045 ni.setIconGeometry(rect);
1046 }
1047
winIdFromMimeData(const QMimeData * mimeData,bool * ok)1048 WId XWindowTasksModel::winIdFromMimeData(const QMimeData *mimeData, bool *ok)
1049 {
1050 Q_ASSERT(mimeData);
1051
1052 if (ok) {
1053 *ok = false;
1054 }
1055
1056 if (!mimeData->hasFormat(Private::mimeType())) {
1057 return 0;
1058 }
1059
1060 QByteArray data(mimeData->data(Private::mimeType()));
1061 if (data.size() != sizeof(WId)) {
1062 return 0;
1063 }
1064
1065 WId id;
1066 memcpy(&id, data.data(), sizeof(WId));
1067
1068 if (ok) {
1069 *ok = true;
1070 }
1071
1072 return id;
1073 }
1074
winIdsFromMimeData(const QMimeData * mimeData,bool * ok)1075 QList<WId> XWindowTasksModel::winIdsFromMimeData(const QMimeData *mimeData, bool *ok)
1076 {
1077 Q_ASSERT(mimeData);
1078 QList<WId> ids;
1079
1080 if (ok) {
1081 *ok = false;
1082 }
1083
1084 if (!mimeData->hasFormat(Private::groupMimeType())) {
1085 // Try to extract single window id.
1086 bool singularOk;
1087 WId id = winIdFromMimeData(mimeData, &singularOk);
1088
1089 if (ok) {
1090 *ok = singularOk;
1091 }
1092
1093 if (singularOk) {
1094 ids << id;
1095 }
1096
1097 return ids;
1098 }
1099
1100 QByteArray data(mimeData->data(Private::groupMimeType()));
1101 if ((unsigned int)data.size() < sizeof(int) + sizeof(WId)) {
1102 return ids;
1103 }
1104
1105 int count = 0;
1106 memcpy(&count, data.data(), sizeof(int));
1107 if (count < 1 || (unsigned int)data.size() < sizeof(int) + sizeof(WId) * count) {
1108 return ids;
1109 }
1110
1111 WId id;
1112 for (int i = 0; i < count; ++i) {
1113 memcpy(&id, data.data() + sizeof(int) + sizeof(WId) * i, sizeof(WId));
1114 ids << id;
1115 }
1116
1117 if (ok) {
1118 *ok = true;
1119 }
1120
1121 return ids;
1122 }
1123
1124 }
1125