/* SPDX-FileCopyrightText: 2016 Eike Hein SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include "waylandtasksmodel.h" #include "tasktools.h" #include "virtualdesktopinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace TaskManager { class Q_DECL_HIDDEN WaylandTasksModel::Private { public: Private(WaylandTasksModel *q); QList windows; QHash appDataCache; QHash lastActivated; KWayland::Client::PlasmaWindowManagement *windowManagement = nullptr; KSharedConfig::Ptr rulesConfig; KDirWatch *configWatcher = nullptr; VirtualDesktopInfo *virtualDesktopInfo = nullptr; static QUuid uuid; void init(); void initWayland(); void addWindow(KWayland::Client::PlasmaWindow *window); AppData appData(KWayland::Client::PlasmaWindow *window); QIcon icon(KWayland::Client::PlasmaWindow *window); static QString mimeType(); static QString groupMimeType(); void dataChanged(KWayland::Client::PlasmaWindow *window, int role); void dataChanged(KWayland::Client::PlasmaWindow *window, const QVector &roles); private: WaylandTasksModel *q; }; QUuid WaylandTasksModel::Private::uuid = QUuid::createUuid(); WaylandTasksModel::Private::Private(WaylandTasksModel *q) : q(q) { } void WaylandTasksModel::Private::init() { auto clearCacheAndRefresh = [this] { if (!windows.count()) { return; } appDataCache.clear(); // Emit changes of all roles satisfied from app data cache. Q_EMIT q->dataChanged(q->index(0, 0), q->index(windows.count() - 1, 0), QVector{Qt::DecorationRole, AbstractTasksModel::AppId, AbstractTasksModel::AppName, AbstractTasksModel::GenericName, AbstractTasksModel::LauncherUrl, AbstractTasksModel::LauncherUrlWithoutIcon, AbstractTasksModel::CanLaunchNewInstance, AbstractTasksModel::SkipTaskbar}); }; rulesConfig = KSharedConfig::openConfig(QStringLiteral("taskmanagerrulesrc")); configWatcher = new KDirWatch(q); foreach (const QString &location, QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)) { configWatcher->addFile(location + QLatin1String("/taskmanagerrulesrc")); } auto rulesConfigChange = [this, clearCacheAndRefresh] { rulesConfig->reparseConfiguration(); clearCacheAndRefresh(); }; QObject::connect(configWatcher, &KDirWatch::dirty, rulesConfigChange); QObject::connect(configWatcher, &KDirWatch::created, rulesConfigChange); QObject::connect(configWatcher, &KDirWatch::deleted, rulesConfigChange); virtualDesktopInfo = new VirtualDesktopInfo(q); initWayland(); } void WaylandTasksModel::Private::initWayland() { if (!KWindowSystem::isPlatformWayland()) { return; } KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(q); if (!connection) { return; } KWayland::Client::Registry *registry = new KWayland::Client::Registry(q); registry->create(connection); QObject::connect(registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced, q, [this, registry](quint32 name, quint32 version) { windowManagement = registry->createPlasmaWindowManagement(name, version, q); QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::interfaceAboutToBeReleased, q, [this] { q->beginResetModel(); windows.clear(); q->endResetModel(); }); QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated, q, [this](KWayland::Client::PlasmaWindow *window) { addWindow(window); }); QObject::connect(windowManagement, &KWayland::Client::PlasmaWindowManagement::stackingOrderUuidsChanged, q, [this]() { for (const auto window : qAsConst(windows)) { this->dataChanged(window, StackingOrder); } }); const auto windows = windowManagement->windows(); for (auto it = windows.constBegin(); it != windows.constEnd(); ++it) { addWindow(*it); } }); registry->setup(); } void WaylandTasksModel::Private::addWindow(KWayland::Client::PlasmaWindow *window) { if (windows.indexOf(window) != -1) { return; } const int count = windows.count(); q->beginInsertRows(QModelIndex(), count, count); windows.append(window); q->endInsertRows(); auto removeWindow = [window, this] { const int row = windows.indexOf(window); if (row != -1) { q->beginRemoveRows(QModelIndex(), row, row); windows.removeAt(row); appDataCache.remove(window); lastActivated.remove(window); q->endRemoveRows(); } }; QObject::connect(window, &KWayland::Client::PlasmaWindow::unmapped, q, removeWindow); QObject::connect(window, &QObject::destroyed, q, removeWindow); QObject::connect(window, &KWayland::Client::PlasmaWindow::titleChanged, q, [window, this] { this->dataChanged(window, Qt::DisplayRole); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::iconChanged, q, [window, this] { // The icon in the AppData struct might come from PlasmaWindow if it wasn't // filled in by windowUrlFromMetadata+appDataFromUrl. // TODO: Don't evict the cache unnecessarily if this isn't the case. As icons // are currently very static on Wayland, this eviction is unlikely to happen // frequently as of now. appDataCache.remove(window); this->dataChanged(window, Qt::DecorationRole); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::appIdChanged, q, [window, this] { // The AppData struct in the cache is derived from this and needs // to be evicted in favor of a fresh struct based on the changed // window metadata. appDataCache.remove(window); // Refresh roles satisfied from the app data cache. this->dataChanged(window, QVector{AppId, AppName, GenericName, LauncherUrl, LauncherUrlWithoutIcon, SkipTaskbar, CanLaunchNewInstance}); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::activeChanged, q, [window, this] { if (window->isActive()) { lastActivated[window] = QTime::currentTime(); } this->dataChanged(window, IsActive); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::closeableChanged, q, [window, this] { this->dataChanged(window, IsClosable); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::movableChanged, q, [window, this] { this->dataChanged(window, IsMovable); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::resizableChanged, q, [window, this] { this->dataChanged(window, IsResizable); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::fullscreenableChanged, q, [window, this] { this->dataChanged(window, IsFullScreenable); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::fullscreenChanged, q, [window, this] { this->dataChanged(window, IsFullScreen); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::maximizeableChanged, q, [window, this] { this->dataChanged(window, IsMaximizable); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::maximizedChanged, q, [window, this] { this->dataChanged(window, IsMaximized); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::minimizeableChanged, q, [window, this] { this->dataChanged(window, IsMinimizable); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::minimizedChanged, q, [window, this] { this->dataChanged(window, IsMinimized); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::keepAboveChanged, q, [window, this] { this->dataChanged(window, IsKeepAbove); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::keepBelowChanged, q, [window, this] { this->dataChanged(window, IsKeepBelow); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::shadeableChanged, q, [window, this] { this->dataChanged(window, IsShadeable); }); // FIXME // QObject::connect(window, &KWayland::Client::PlasmaWindow::virtualDesktopChangeableChanged, q, // // TODO: This is marked deprecated in KWayland, but (IMHO) shouldn't be. // [window, this] { this->dataChanged(window, IsVirtualDesktopsChangeable); } // ); QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopEntered, q, [window, this] { this->dataChanged(window, VirtualDesktops); // If the count has changed from 0, the window may no longer be on all virtual // desktops. if (window->plasmaVirtualDesktops().count() > 0) { this->dataChanged(window, IsOnAllVirtualDesktops); } }); QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopLeft, q, [window, this] { this->dataChanged(window, VirtualDesktops); // If the count has changed to 0, the window is now on all virtual desktops. if (window->plasmaVirtualDesktops().count() == 0) { this->dataChanged(window, IsOnAllVirtualDesktops); } }); QObject::connect(window, &KWayland::Client::PlasmaWindow::geometryChanged, q, [window, this] { this->dataChanged(window, QVector{Geometry, ScreenGeometry}); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::demandsAttentionChanged, q, [window, this] { this->dataChanged(window, IsDemandingAttention); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::skipTaskbarChanged, q, [window, this] { this->dataChanged(window, SkipTaskbar); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::applicationMenuChanged, q, [window, this] { this->dataChanged(window, QVector{ApplicationMenuServiceName, ApplicationMenuObjectPath}); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaActivityEntered, q, [window, this] { this->dataChanged(window, Activities); }); QObject::connect(window, &KWayland::Client::PlasmaWindow::plasmaActivityLeft, q, [window, this] { this->dataChanged(window, Activities); }); } AppData WaylandTasksModel::Private::appData(KWayland::Client::PlasmaWindow *window) { const auto &it = appDataCache.constFind(window); if (it != appDataCache.constEnd()) { return *it; } const AppData &data = appDataFromUrl(windowUrlFromMetadata(window->appId(), window->pid(), rulesConfig)); appDataCache.insert(window, data); return data; } QIcon WaylandTasksModel::Private::icon(KWayland::Client::PlasmaWindow *window) { const AppData &app = appData(window); if (!app.icon.isNull()) { return app.icon; } appDataCache[window].icon = window->icon(); return window->icon(); } QString WaylandTasksModel::Private::mimeType() { // Use a unique format id to make this intentionally useless for // cross-process DND. return QStringLiteral("windowsystem/winid+") + uuid.toString(); } QString WaylandTasksModel::Private::groupMimeType() { // Use a unique format id to make this intentionally useless for // cross-process DND. return QStringLiteral("windowsystem/multiple-winids+") + uuid.toString(); } void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, int role) { QModelIndex idx = q->index(windows.indexOf(window)); Q_EMIT q->dataChanged(idx, idx, QVector{role}); } void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, const QVector &roles) { QModelIndex idx = q->index(windows.indexOf(window)); Q_EMIT q->dataChanged(idx, idx, roles); } WaylandTasksModel::WaylandTasksModel(QObject *parent) : AbstractWindowTasksModel(parent) , d(new Private(this)) { d->init(); } WaylandTasksModel::~WaylandTasksModel() = default; QVariant WaylandTasksModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= d->windows.count()) { return QVariant(); } KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); if (role == Qt::DisplayRole) { return window->title(); } else if (role == Qt::DecorationRole) { return d->icon(window); } else if (role == AppId) { const QString &id = d->appData(window).id; if (id.isEmpty()) { return window->appId(); } else { return id; } } else if (role == AppName) { return d->appData(window).name; } else if (role == GenericName) { return d->appData(window).genericName; } else if (role == LauncherUrl || role == LauncherUrlWithoutIcon) { return d->appData(window).url; } else if (role == WinIdList) { return QVariantList() << window->uuid(); } else if (role == MimeType) { return d->mimeType(); } else if (role == MimeData) { return window->uuid(); } else if (role == IsWindow) { return true; } else if (role == IsActive) { return window->isActive(); } else if (role == IsClosable) { return window->isCloseable(); } else if (role == IsMovable) { return window->isMovable(); } else if (role == IsResizable) { return window->isResizable(); } else if (role == IsMaximizable) { return window->isMaximizeable(); } else if (role == IsMaximized) { return window->isMaximized(); } else if (role == IsMinimizable) { return window->isMinimizeable(); } else if (role == IsMinimized || role == IsHidden) { return window->isMinimized(); } else if (role == IsKeepAbove) { return window->isKeepAbove(); } else if (role == IsKeepBelow) { return window->isKeepBelow(); } else if (role == IsFullScreenable) { return window->isFullscreenable(); } else if (role == IsFullScreen) { return window->isFullscreen(); } else if (role == IsShadeable) { return window->isShadeable(); } else if (role == IsShaded) { return window->isShaded(); } else if (role == IsVirtualDesktopsChangeable) { // FIXME Currently not implemented in KWayland. return true; } else if (role == VirtualDesktops) { return window->plasmaVirtualDesktops(); } else if (role == IsOnAllVirtualDesktops) { return window->plasmaVirtualDesktops().isEmpty(); } else if (role == Geometry) { return window->geometry(); } else if (role == ScreenGeometry) { return screenGeometry(window->geometry().center()); } else if (role == Activities) { return window->plasmaActivities(); } else if (role == IsDemandingAttention) { return window->isDemandingAttention(); } else if (role == SkipTaskbar) { return window->skipTaskbar() || d->appData(window).skipTaskbar; } else if (role == SkipPager) { // FIXME Implement. } else if (role == AppPid) { return window->pid(); } else if (role == StackingOrder) { return d->windowManagement->stackingOrderUuids().indexOf(window->uuid()); } else if (role == LastActivated) { if (d->lastActivated.contains(window)) { return d->lastActivated.value(window); } } else if (role == ApplicationMenuObjectPath) { return window->applicationMenuObjectPath(); } else if (role == ApplicationMenuServiceName) { return window->applicationMenuServiceName(); } else if (role == CanLaunchNewInstance) { return canLauchNewInstance(d->appData(window)); } return QVariant(); } int WaylandTasksModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : d->windows.count(); } QModelIndex WaylandTasksModel::index(int row, int column, const QModelIndex &parent) const { return hasIndex(row, column, parent) ? createIndex(row, column, d->windows.at(row)) : QModelIndex(); } void WaylandTasksModel::requestActivate(const QModelIndex &index) { // FIXME Lacks transient handling of the XWindows version. if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestActivate(); } void WaylandTasksModel::requestNewInstance(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } runApp(d->appData(d->windows.at(index.row()))); } void WaylandTasksModel::requestOpenUrls(const QModelIndex &index, const QList &urls) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count() || urls.isEmpty()) { return; } runApp(d->appData(d->windows.at(index.row())), urls); } void WaylandTasksModel::requestClose(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestClose(); } void WaylandTasksModel::requestMove(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); window->requestActivate(); window->requestMove(); } void WaylandTasksModel::requestResize(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); window->requestActivate(); window->requestResize(); } void WaylandTasksModel::requestToggleMinimized(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestToggleMinimized(); } void WaylandTasksModel::requestToggleMaximized(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); window->requestActivate(); window->requestToggleMaximized(); } void WaylandTasksModel::requestToggleKeepAbove(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestToggleKeepAbove(); } void WaylandTasksModel::requestToggleKeepBelow(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestToggleKeepBelow(); } void WaylandTasksModel::requestToggleFullScreen(const QModelIndex &index) { Q_UNUSED(index) // FIXME Implement. } void WaylandTasksModel::requestToggleShaded(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestToggleShaded(); } void WaylandTasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops) { // FIXME TODO: Lacks the "if we've requested the current desktop, force-activate // the window" logic from X11 version. This behavior should be in KWin rather than // libtm however. if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); if (desktops.isEmpty()) { const QStringList virtualDesktops = window->plasmaVirtualDesktops(); for (const QString &desktop : virtualDesktops) { window->requestLeaveVirtualDesktop(desktop); } } else { const QStringList &now = window->plasmaVirtualDesktops(); QStringList next; for (const QVariant &desktop : desktops) { const QString &desktopId = desktop.toString(); if (!desktopId.isEmpty()) { next << desktopId; if (!now.contains(desktopId)) { window->requestEnterVirtualDesktop(desktopId); } } } for (const QString &desktop : now) { if (!next.contains(desktop)) { window->requestLeaveVirtualDesktop(desktop); } } } } void WaylandTasksModel::requestNewVirtualDesktop(const QModelIndex &index) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } d->windows.at(index.row())->requestEnterNewVirtualDesktop(); } void WaylandTasksModel::requestActivities(const QModelIndex &index, const QStringList &activities) { if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } auto *const window = d->windows.at(index.row()); const auto newActivities = QSet(activities.begin(), activities.end()); const auto plasmaActivities = window->plasmaActivities(); const auto oldActivities = QSet(plasmaActivities.begin(), plasmaActivities.end()); const auto activitiesToAdd = newActivities - oldActivities; for (const auto &activity : activitiesToAdd) { window->requestEnterActivity(activity); } const auto activitiesToRemove = oldActivities - newActivities; for (const auto &activity : activitiesToRemove) { window->requestLeaveActivity(activity); } } void WaylandTasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) { /* FIXME: This introduces the dependency on Qt::Quick. I might prefer reversing this and publishing the window pointer through the model, then calling PlasmaWindow::setMinimizeGeometry in the applet backend, rather than hand delegate items into the lib, keeping the lib more UI- agnostic. */ Q_UNUSED(geometry) if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->windows.count()) { return; } const QQuickItem *item = qobject_cast(delegate); if (!item || !item->parentItem() || !item->window()) { return; } QWindow *itemWindow = item->window(); if (!itemWindow) { return; } using namespace KWayland::Client; Surface *surface = Surface::fromWindow(itemWindow); if (!surface) { return; } QRect rect(item->x(), item->y(), item->width(), item->height()); rect.moveTopLeft(item->parentItem()->mapToScene(rect.topLeft()).toPoint()); KWayland::Client::PlasmaWindow *window = d->windows.at(index.row()); window->setMinimizedGeometry(surface, rect); } QUuid WaylandTasksModel::winIdFromMimeData(const QMimeData *mimeData, bool *ok) { Q_ASSERT(mimeData); if (ok) { *ok = false; } if (!mimeData->hasFormat(Private::mimeType())) { return 0; } QUuid id(mimeData->data(Private::mimeType())); *ok = !id.isNull(); return id; } QList WaylandTasksModel::winIdsFromMimeData(const QMimeData *mimeData, bool *ok) { Q_ASSERT(mimeData); QList ids; if (ok) { *ok = false; } if (!mimeData->hasFormat(Private::groupMimeType())) { // Try to extract single window id. bool singularOk; QUuid id = winIdFromMimeData(mimeData, &singularOk); if (ok) { *ok = singularOk; } if (singularOk) { ids << id; } return ids; } // FIXME: Extracting multiple winids is still unimplemented; // TaskGroupingProxy::data(..., ::MimeData) can't produce // a payload with them anyways. return ids; } }