1 /*
2     SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 #include "waylandtasksmodel.h"
8 #include "tasktools.h"
9 #include "virtualdesktopinfo.h"
10 
11 #include <KDirWatch>
12 #include <KSharedConfig>
13 #include <KWayland/Client/connection_thread.h>
14 #include <KWayland/Client/plasmawindowmanagement.h>
15 #include <KWayland/Client/registry.h>
16 #include <KWayland/Client/surface.h>
17 #include <KWindowSystem>
18 
19 #include <QGuiApplication>
20 #include <QMimeData>
21 #include <QQuickItem>
22 #include <QQuickWindow>
23 #include <QSet>
24 #include <QUrl>
25 #include <QUuid>
26 #include <QWindow>
27 
28 namespace TaskManager
29 {
30 class Q_DECL_HIDDEN WaylandTasksModel::Private
31 {
32 public:
33     Private(WaylandTasksModel *q);
34     QList<KWayland::Client::PlasmaWindow *> windows;
35     QHash<KWayland::Client::PlasmaWindow *, AppData> appDataCache;
36     QHash<KWayland::Client::PlasmaWindow *, QTime> lastActivated;
37     KWayland::Client::PlasmaWindowManagement *windowManagement = nullptr;
38     KSharedConfig::Ptr rulesConfig;
39     KDirWatch *configWatcher = nullptr;
40     VirtualDesktopInfo *virtualDesktopInfo = nullptr;
41     static QUuid uuid;
42 
43     void init();
44     void initWayland();
45     void addWindow(KWayland::Client::PlasmaWindow *window);
46 
47     AppData appData(KWayland::Client::PlasmaWindow *window);
48 
49     QIcon icon(KWayland::Client::PlasmaWindow *window);
50 
51     static QString mimeType();
52     static QString groupMimeType();
53 
54     void dataChanged(KWayland::Client::PlasmaWindow *window, int role);
55     void dataChanged(KWayland::Client::PlasmaWindow *window, const QVector<int> &roles);
56 
57 private:
58     WaylandTasksModel *q;
59 };
60 
61 QUuid WaylandTasksModel::Private::uuid = QUuid::createUuid();
62 
Private(WaylandTasksModel * q)63 WaylandTasksModel::Private::Private(WaylandTasksModel *q)
64     : q(q)
65 {
66 }
67 
init()68 void WaylandTasksModel::Private::init()
69 {
70     auto clearCacheAndRefresh = [this] {
71         if (!windows.count()) {
72             return;
73         }
74 
75         appDataCache.clear();
76 
77         // Emit changes of all roles satisfied from app data cache.
78         Q_EMIT q->dataChanged(q->index(0, 0),
79                               q->index(windows.count() - 1, 0),
80                               QVector<int>{Qt::DecorationRole,
81                                            AbstractTasksModel::AppId,
82                                            AbstractTasksModel::AppName,
83                                            AbstractTasksModel::GenericName,
84                                            AbstractTasksModel::LauncherUrl,
85                                            AbstractTasksModel::LauncherUrlWithoutIcon,
86                                            AbstractTasksModel::SkipTaskbar});
87     };
88 
89     rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc"));
90     configWatcher = new KDirWatch(q);
91 
92     foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) {
93         configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc"));
94     }
95 
96     auto rulesConfigChange = [this, clearCacheAndRefresh] {
97         rulesConfig->reparseConfiguration();
98         clearCacheAndRefresh();
99     };
100 
101     QObject::connect(configWatcher, &KDirWatch::dirty, rulesConfigChange);
102     QObject::connect(configWatcher, &KDirWatch::created, rulesConfigChange);
103     QObject::connect(configWatcher, &KDirWatch::deleted, rulesConfigChange);
104 
105     virtualDesktopInfo = new VirtualDesktopInfo(q);
106 
107     initWayland();
108 }
109 
initWayland()110 void WaylandTasksModel::Private::initWayland()
111 {
112     if (!KWindowSystem::isPlatformWayland()) {
113         return;
114     }
115 
116     KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(q);
117 
118     if (!connection) {
119         return;
120     }
121 
122     KWayland::Client::Registry *registry = new KWayland::Client::Registry(q);
123     registry->create(connection);
124 
125     QObject::connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, q, [this, registry](quint32 name, quint32 version) {
126         windowManagement = registry->createPlasmaWindowManagement(name, version, q);
127 
128         QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::interfaceAboutToBeReleased, q, [this] {
129             q->beginResetModel();
130             windows.clear();
131             q->endResetModel();
132         });
133 
134         QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, q, [this](KWayland::Client::PlasmaWindow *window) {
135             addWindow(window);
136         });
137 
138         QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::stackingOrderUuidsChanged, q, [this]() {
139             for (const auto window : qAsConst(windows)) {
140                 this->dataChanged(window, StackingOrder);
141             }
142         });
143 
144         const auto windows = windowManagement->windows();
145         for (auto it = windows.constBegin(); it != windows.constEnd(); ++it) {
146             addWindow(*it);
147         }
148     });
149 
150     registry->setup();
151 }
152 
addWindow(KWayland::Client::PlasmaWindow * window)153 void WaylandTasksModel::Private::addWindow(KWayland::Client::PlasmaWindow *window)
154 {
155     if (windows.indexOf(window) != -1) {
156         return;
157     }
158 
159     const int count = windows.count();
160 
161     q->beginInsertRows(QModelIndex(), count, count);
162 
163     windows.append(window);
164 
165     q->endInsertRows();
166 
167     auto removeWindow = [window, this] {
168         const int row = windows.indexOf(window);
169         if (row != -1) {
170             q->beginRemoveRows(QModelIndex(), row, row);
171             windows.removeAt(row);
172             appDataCache.remove(window);
173             lastActivated.remove(window);
174             q->endRemoveRows();
175         }
176     };
177 
178     QObject::connect(window, &KWayland::Client::PlasmaWindow::unmapped, q, removeWindow);
179     QObject::connect(window, &QObject::destroyed, q, removeWindow);
180 
181     QObject::connect(window, &KWayland::Client::PlasmaWindow::titleChanged, q, [window, this] {
182         this->dataChanged(window, Qt::DisplayRole);
183     });
184 
185     QObject::connect(window, &KWayland::Client::PlasmaWindow::iconChanged, q, [window, this] {
186         // The icon in the AppData struct might come from PlasmaWindow if it wasn't
187         // filled in by windowUrlFromMetadata+appDataFromUrl.
188         // TODO: Don't evict the cache unnecessarily if this isn't the case. As icons
189         // are currently very static on Wayland, this eviction is unlikely to happen
190         // frequently as of now.
191         appDataCache.remove(window);
192 
193         this->dataChanged(window, Qt::DecorationRole);
194     });
195 
196     QObject::connect(window, &KWayland::Client::PlasmaWindow::appIdChanged, q, [window, this] {
197         // The AppData struct in the cache is derived from this and needs
198         // to be evicted in favor of a fresh struct based on the changed
199         // window metadata.
200         appDataCache.remove(window);
201 
202         // Refresh roles satisfied from the app data cache.
203         this->dataChanged(window, QVector<int>{AppId, AppName, GenericName, LauncherUrl, LauncherUrlWithoutIcon, SkipTaskbar});
204     });
205 
206     QObject::connect(window, &KWayland::Client::PlasmaWindow::activeChanged, q, [window, this] {
207         if (window->isActive()) {
208             lastActivated[window] = QTime::currentTime();
209         }
210         this->dataChanged(window, IsActive);
211     });
212 
213     QObject::connect(window, &KWayland::Client::PlasmaWindow::closeableChanged, q, [window, this] {
214         this->dataChanged(window, IsClosable);
215     });
216 
217     QObject::connect(window, &KWayland::Client::PlasmaWindow::movableChanged, q, [window, this] {
218         this->dataChanged(window, IsMovable);
219     });
220 
221     QObject::connect(window, &KWayland::Client::PlasmaWindow::resizableChanged, q, [window, this] {
222         this->dataChanged(window, IsResizable);
223     });
224 
225     QObject::connect(window, &KWayland::Client::PlasmaWindow::fullscreenableChanged, q, [window, this] {
226         this->dataChanged(window, IsFullScreenable);
227     });
228 
229     QObject::connect(window, &KWayland::Client::PlasmaWindow::fullscreenChanged, q, [window, this] {
230         this->dataChanged(window, IsFullScreen);
231     });
232 
233     QObject::connect(window, &KWayland::Client::PlasmaWindow::maximizeableChanged, q, [window, this] {
234         this->dataChanged(window, IsMaximizable);
235     });
236 
237     QObject::connect(window, &KWayland::Client::PlasmaWindow::maximizedChanged, q, [window, this] {
238         this->dataChanged(window, IsMaximized);
239     });
240 
241     QObject::connect(window, &KWayland::Client::PlasmaWindow::minimizeableChanged, q, [window, this] {
242         this->dataChanged(window, IsMinimizable);
243     });
244 
245     QObject::connect(window, &KWayland::Client::PlasmaWindow::minimizedChanged, q, [window, this] {
246         this->dataChanged(window, IsMinimized);
247     });
248 
249     QObject::connect(window, &KWayland::Client::PlasmaWindow::keepAboveChanged, q, [window, this] {
250         this->dataChanged(window, IsKeepAbove);
251     });
252 
253     QObject::connect(window, &KWayland::Client::PlasmaWindow::keepBelowChanged, q, [window, this] {
254         this->dataChanged(window, IsKeepBelow);
255     });
256 
257     QObject::connect(window, &KWayland::Client::PlasmaWindow::shadeableChanged, q, [window, this] {
258         this->dataChanged(window, IsShadeable);
259     });
260 
261     // FIXME
262     //     QObject::connect(window, &KWayland::Client::PlasmaWindow::virtualDesktopChangeableChanged, q,
263     //         // TODO: This is marked deprecated in KWayland, but (IMHO) shouldn't be.
264     //         [window, this] { this->dataChanged(window, IsVirtualDesktopsChangeable); }
265     //     );
266 
267     QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopEntered, q, [window, this] {
268         this->dataChanged(window, VirtualDesktops);
269 
270         // If the count has changed from 0, the window may no longer be on all virtual
271         // desktops.
272         if (window->plasmaVirtualDesktops().count() > 0) {
273             this->dataChanged(window, IsOnAllVirtualDesktops);
274         }
275     });
276 
277     QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopLeft, q, [window, this] {
278         this->dataChanged(window, VirtualDesktops);
279 
280         // If the count has changed to 0, the window is now on all virtual desktops.
281         if (window->plasmaVirtualDesktops().count() == 0) {
282             this->dataChanged(window, IsOnAllVirtualDesktops);
283         }
284     });
285 
286     QObject::connect(window, &KWayland::Client::PlasmaWindow::geometryChanged, q, [window, this] {
287         this->dataChanged(window, QVector<int>{Geometry, ScreenGeometry});
288     });
289 
290     QObject::connect(window, &KWayland::Client::PlasmaWindow::demandsAttentionChanged, q, [window, this] {
291         this->dataChanged(window, IsDemandingAttention);
292     });
293 
294     QObject::connect(window, &KWayland::Client::PlasmaWindow::skipTaskbarChanged, q, [window, this] {
295         this->dataChanged(window, SkipTaskbar);
296     });
297 
298     QObject::connect(window, &KWayland::Client::PlasmaWindow::applicationMenuChanged, q, [window, this] {
299         this->dataChanged(window, QVector<int>{ApplicationMenuServiceName, ApplicationMenuObjectPath});
300     });
301 
302     QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaActivityEntered, q, [window, this] {
303         this->dataChanged(window, Activities);
304     });
305 
306     QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaActivityLeft, q, [window, this] {
307         this->dataChanged(window, Activities);
308     });
309 }
310 
appData(KWayland::Client::PlasmaWindow * window)311 AppData WaylandTasksModel::Private::appData(KWayland::Client::PlasmaWindow *window)
312 {
313     const auto &it = appDataCache.constFind(window);
314 
315     if (it != appDataCache.constEnd()) {
316         return *it;
317     }
318 
319     const AppData &data = appDataFromUrl(windowUrlFromMetadata(window->appId(), window->pid(), rulesConfig));
320 
321     appDataCache.insert(window, data);
322 
323     return data;
324 }
325 
icon(KWayland::Client::PlasmaWindow * window)326 QIcon WaylandTasksModel::Private::icon(KWayland::Client::PlasmaWindow *window)
327 {
328     const AppData &app = appData(window);
329 
330     if (!app.icon.isNull()) {
331         return app.icon;
332     }
333 
334     appDataCache[window].icon = window->icon();
335 
336     return window->icon();
337 }
338 
mimeType()339 QString WaylandTasksModel::Private::mimeType()
340 {
341     // Use a unique format id to make this intentionally useless for
342     // cross-process DND.
343     return QStringLiteral("windowsystem/winid+") + uuid.toString();
344 }
345 
groupMimeType()346 QString WaylandTasksModel::Private::groupMimeType()
347 {
348     // Use a unique format id to make this intentionally useless for
349     // cross-process DND.
350     return QStringLiteral("windowsystem/multiple-winids+") + uuid.toString();
351 }
352 
dataChanged(KWayland::Client::PlasmaWindow * window,int role)353 void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, int role)
354 {
355     QModelIndex idx = q->index(windows.indexOf(window));
356     emit q->dataChanged(idx, idx, QVector<int>{role});
357 }
358 
dataChanged(KWayland::Client::PlasmaWindow * window,const QVector<int> & roles)359 void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, const QVector<int> &roles)
360 {
361     QModelIndex idx = q->index(windows.indexOf(window));
362     emit q->dataChanged(idx, idx, roles);
363 }
364 
WaylandTasksModel(QObject * parent)365 WaylandTasksModel::WaylandTasksModel(QObject *parent)
366     : AbstractWindowTasksModel(parent)
367     , d(new Private(this))
368 {
369     d->init();
370 }
371 
372 WaylandTasksModel::~WaylandTasksModel() = default;
373 
data(const QModelIndex & index,int role) const374 QVariant WaylandTasksModel::data(const QModelIndex &index, int role) const
375 {
376     if (!index.isValid() || index.row() >= d->windows.count()) {
377         return QVariant();
378     }
379 
380     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
381 
382     if (role == Qt::DisplayRole) {
383         return window->title();
384     } else if (role == Qt::DecorationRole) {
385         return d->icon(window);
386     } else if (role == AppId) {
387         const QString &id = d->appData(window).id;
388 
389         if (id.isEmpty()) {
390             return window->appId();
391         } else {
392             return id;
393         }
394     } else if (role == AppName) {
395         return d->appData(window).name;
396     } else if (role == GenericName) {
397         return d->appData(window).genericName;
398     } else if (role == LauncherUrl || role == LauncherUrlWithoutIcon) {
399         return d->appData(window).url;
400     } else if (role == WinIdList) {
401         return QVariantList() << window->uuid();
402     } else if (role == MimeType) {
403         return d->mimeType();
404     } else if (role == MimeData) {
405         return window->uuid();
406     } else if (role == IsWindow) {
407         return true;
408     } else if (role == IsActive) {
409         return window->isActive();
410     } else if (role == IsClosable) {
411         return window->isCloseable();
412     } else if (role == IsMovable) {
413         return window->isMovable();
414     } else if (role == IsResizable) {
415         return window->isResizable();
416     } else if (role == IsMaximizable) {
417         return window->isMaximizeable();
418     } else if (role == IsMaximized) {
419         return window->isMaximized();
420     } else if (role == IsMinimizable) {
421         return window->isMinimizeable();
422     } else if (role == IsMinimized || role == IsHidden) {
423         return window->isMinimized();
424     } else if (role == IsKeepAbove) {
425         return window->isKeepAbove();
426     } else if (role == IsKeepBelow) {
427         return window->isKeepBelow();
428     } else if (role == IsFullScreenable) {
429         return window->isFullscreenable();
430     } else if (role == IsFullScreen) {
431         return window->isFullscreen();
432     } else if (role == IsShadeable) {
433         return window->isShadeable();
434     } else if (role == IsShaded) {
435         return window->isShaded();
436     } else if (role == IsVirtualDesktopsChangeable) {
437         // FIXME Currently not implemented in KWayland.
438         return true;
439     } else if (role == VirtualDesktops) {
440         return window->plasmaVirtualDesktops();
441     } else if (role == IsOnAllVirtualDesktops) {
442         return window->plasmaVirtualDesktops().isEmpty();
443     } else if (role == Geometry) {
444         return window->geometry();
445     } else if (role == ScreenGeometry) {
446         return screenGeometry(window->geometry().center());
447     } else if (role == Activities) {
448         return window->plasmaActivities();
449     } else if (role == IsDemandingAttention) {
450         return window->isDemandingAttention();
451     } else if (role == SkipTaskbar) {
452         return window->skipTaskbar() || d->appData(window).skipTaskbar;
453     } else if (role == SkipPager) {
454         // FIXME Implement.
455     } else if (role == AppPid) {
456         return window->pid();
457     } else if (role == StackingOrder) {
458         return d->windowManagement->stackingOrderUuids().indexOf(window->uuid());
459     } else if (role == LastActivated) {
460         if (d->lastActivated.contains(window)) {
461             return d->lastActivated.value(window);
462         }
463     } else if (role == ApplicationMenuObjectPath) {
464         return window->applicationMenuObjectPath();
465     } else if (role == ApplicationMenuServiceName) {
466         return window->applicationMenuServiceName();
467     }
468 
469     return QVariant();
470 }
471 
rowCount(const QModelIndex & parent) const472 int WaylandTasksModel::rowCount(const QModelIndex &parent) const
473 {
474     return parent.isValid() ? 0 : d->windows.count();
475 }
476 
index(int row,int column,const QModelIndex & parent) const477 QModelIndex WaylandTasksModel::index(int row, int column, const QModelIndex &parent) const
478 {
479     return hasIndex(row, column, parent) ? createIndex(row, column, d->windows.at(row)) : QModelIndex();
480 }
481 
requestActivate(const QModelIndex & index)482 void WaylandTasksModel::requestActivate(const QModelIndex &index)
483 {
484     // FIXME Lacks transient handling of the XWindows version.
485 
486     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
487         return;
488     }
489 
490     d->windows.at(index.row())->requestActivate();
491 }
492 
requestNewInstance(const QModelIndex & index)493 void WaylandTasksModel::requestNewInstance(const QModelIndex &index)
494 {
495     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
496         return;
497     }
498 
499     runApp(d->appData(d->windows.at(index.row())));
500 }
501 
requestOpenUrls(const QModelIndex & index,const QList<QUrl> & urls)502 void WaylandTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
503 {
504     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count() || urls.isEmpty()) {
505         return;
506     }
507 
508     runApp(d->appData(d->windows.at(index.row())), urls);
509 }
510 
requestClose(const QModelIndex & index)511 void WaylandTasksModel::requestClose(const QModelIndex &index)
512 {
513     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
514         return;
515     }
516 
517     d->windows.at(index.row())->requestClose();
518 }
519 
requestMove(const QModelIndex & index)520 void WaylandTasksModel::requestMove(const QModelIndex &index)
521 {
522     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
523         return;
524     }
525 
526     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
527 
528     const QString &currentDesktop = d->virtualDesktopInfo->currentDesktop().toString();
529 
530     if (!currentDesktop.isEmpty()) {
531         window->requestEnterVirtualDesktop(currentDesktop);
532     }
533 
534     window->requestMove();
535 }
536 
requestResize(const QModelIndex & index)537 void WaylandTasksModel::requestResize(const QModelIndex &index)
538 {
539     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
540         return;
541     }
542 
543     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
544 
545     const QString &currentDesktop = d->virtualDesktopInfo->currentDesktop().toString();
546 
547     if (!currentDesktop.isEmpty()) {
548         window->requestEnterVirtualDesktop(currentDesktop);
549     }
550 
551     window->requestResize();
552 }
553 
requestToggleMinimized(const QModelIndex & index)554 void WaylandTasksModel::requestToggleMinimized(const QModelIndex &index)
555 {
556     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
557         return;
558     }
559 
560     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
561 
562     const QString &currentDesktop = d->virtualDesktopInfo->currentDesktop().toString();
563 
564     if (!currentDesktop.isEmpty()) {
565         window->requestEnterVirtualDesktop(currentDesktop);
566     }
567 
568     window->requestToggleMinimized();
569 }
570 
requestToggleMaximized(const QModelIndex & index)571 void WaylandTasksModel::requestToggleMaximized(const QModelIndex &index)
572 {
573     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
574         return;
575     }
576 
577     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
578 
579     const QString &currentDesktop = d->virtualDesktopInfo->currentDesktop().toString();
580 
581     if (!currentDesktop.isEmpty()) {
582         window->requestEnterVirtualDesktop(currentDesktop);
583     }
584 
585     window->requestToggleMaximized();
586 }
587 
requestToggleKeepAbove(const QModelIndex & index)588 void WaylandTasksModel::requestToggleKeepAbove(const QModelIndex &index)
589 {
590     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
591         return;
592     }
593 
594     d->windows.at(index.row())->requestToggleKeepAbove();
595 }
596 
requestToggleKeepBelow(const QModelIndex & index)597 void WaylandTasksModel::requestToggleKeepBelow(const QModelIndex &index)
598 {
599     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
600         return;
601     }
602 
603     d->windows.at(index.row())->requestToggleKeepBelow();
604 }
605 
requestToggleFullScreen(const QModelIndex & index)606 void WaylandTasksModel::requestToggleFullScreen(const QModelIndex &index)
607 {
608     Q_UNUSED(index)
609 
610     // FIXME Implement.
611 }
612 
requestToggleShaded(const QModelIndex & index)613 void WaylandTasksModel::requestToggleShaded(const QModelIndex &index)
614 {
615     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
616         return;
617     }
618 
619     d->windows.at(index.row())->requestToggleShaded();
620 }
621 
requestVirtualDesktops(const QModelIndex & index,const QVariantList & desktops)622 void WaylandTasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops)
623 {
624     // FIXME TODO: Lacks the "if we've requested the current desktop, force-activate
625     // the window" logic from X11 version. This behavior should be in KWin rather than
626     // libtm however.
627 
628     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
629         return;
630     }
631 
632     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
633 
634     if (desktops.isEmpty()) {
635         const QStringList virtualDesktops = window->plasmaVirtualDesktops();
636         for (const QString &desktop : virtualDesktops) {
637             window->requestLeaveVirtualDesktop(desktop);
638         }
639     } else {
640         const QStringList &now = window->plasmaVirtualDesktops();
641         QStringList next;
642 
643         for (const QVariant &desktop : desktops) {
644             const QString &desktopId = desktop.toString();
645 
646             if (!desktopId.isEmpty()) {
647                 next << desktopId;
648 
649                 if (!now.contains(desktopId)) {
650                     window->requestEnterVirtualDesktop(desktopId);
651                 }
652             }
653         }
654 
655         for (const QString &desktop : now) {
656             if (!next.contains(desktop)) {
657                 window->requestLeaveVirtualDesktop(desktop);
658             }
659         }
660     }
661 }
662 
requestNewVirtualDesktop(const QModelIndex & index)663 void WaylandTasksModel::requestNewVirtualDesktop(const QModelIndex &index)
664 {
665     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
666         return;
667     }
668 
669     d->windows.at(index.row())->requestEnterNewVirtualDesktop();
670 }
671 
requestActivities(const QModelIndex & index,const QStringList & activities)672 void WaylandTasksModel::requestActivities(const QModelIndex &index, const QStringList &activities)
673 {
674     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
675         return;
676     }
677 
678     auto *const window = d->windows.at(index.row());
679     const auto newActivities = QSet(activities.begin(), activities.end());
680     const auto plasmaActivities = window->plasmaActivities();
681     const auto oldActivities = QSet(plasmaActivities.begin(), plasmaActivities.end());
682 
683     const auto activitiesToAdd = newActivities - oldActivities;
684     for (const auto &activity : activitiesToAdd) {
685         window->requestEnterActivity(activity);
686     }
687 
688     const auto activitiesToRemove = oldActivities - newActivities;
689     for (const auto &activity : activitiesToRemove) {
690         window->requestLeaveActivity(activity);
691     }
692 }
693 
requestPublishDelegateGeometry(const QModelIndex & index,const QRect & geometry,QObject * delegate)694 void WaylandTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate)
695 {
696     /*
697     FIXME: This introduces the dependency on Qt::Quick. I might prefer
698     reversing this and publishing the window pointer through the model,
699     then calling PlasmaWindow::setMinimizeGeometry in the applet backend,
700     rather than hand delegate items into the lib, keeping the lib more UI-
701     agnostic.
702     */
703 
704     Q_UNUSED(geometry)
705 
706     if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) {
707         return;
708     }
709 
710     const QQuickItem *item = qobject_cast<const QQuickItem *>(delegate);
711 
712     if (!item || !item->parentItem() || !item->window()) {
713         return;
714     }
715 
716     QWindow *itemWindow = item->window();
717 
718     if (!itemWindow) {
719         return;
720     }
721 
722     using namespace KWayland::Client;
723     Surface *surface = Surface::fromWindow(itemWindow);
724 
725     if (!surface) {
726         return;
727     }
728 
729     QRect rect(item->x(), item->y(), item->width(), item->height());
730     rect.moveTopLeft(item->parentItem()->mapToScene(rect.topLeft()).toPoint());
731 
732     KWayland::Client::PlasmaWindow *window = d->windows.at(index.row());
733 
734     window->setMinimizedGeometry(surface, rect);
735 }
736 
winIdFromMimeData(const QMimeData * mimeData,bool * ok)737 QUuid WaylandTasksModel::winIdFromMimeData(const QMimeData *mimeData, bool *ok)
738 {
739     Q_ASSERT(mimeData);
740 
741     if (ok) {
742         *ok = false;
743     }
744 
745     if (!mimeData->hasFormat(Private::mimeType())) {
746         return 0;
747     }
748 
749     QUuid id(mimeData->data(Private::mimeType()));
750     *ok = !id.isNull();
751 
752     return id;
753 }
754 
winIdsFromMimeData(const QMimeData * mimeData,bool * ok)755 QList<QUuid> WaylandTasksModel::winIdsFromMimeData(const QMimeData *mimeData, bool *ok)
756 {
757     Q_ASSERT(mimeData);
758     QList<QUuid> ids;
759 
760     if (ok) {
761         *ok = false;
762     }
763 
764     if (!mimeData->hasFormat(Private::groupMimeType())) {
765         // Try to extract single window id.
766         bool singularOk;
767         QUuid id = winIdFromMimeData(mimeData, &singularOk);
768 
769         if (ok) {
770             *ok = singularOk;
771         }
772 
773         if (singularOk) {
774             ids << id;
775         }
776 
777         return ids;
778     }
779 
780     // FIXME: Extracting multiple winids is still unimplemented;
781     // TaskGroupingProxy::data(..., ::MimeData) can't produce
782     // a payload with them anyways.
783 
784     return ids;
785 }
786 
787 }
788