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