QortalOS Brooklyn for Raspberry Pi 4
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

767 lines
25 KiB

/*
SPDX-FileCopyrightText: 2016 Eike Hein <[email protected]>
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 <KDirWatch>
#include <KSharedConfig>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmawindowmanagement.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
#include <KWindowSystem>
#include <QGuiApplication>
#include <QMimeData>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSet>
#include <QUrl>
#include <QUuid>
#include <QWindow>
namespace TaskManager
{
class Q_DECL_HIDDEN WaylandTasksModel::Private
{
public:
Private(WaylandTasksModel *q);
QList<KWayland::Client::PlasmaWindow *> windows;
QHash<KWayland::Client::PlasmaWindow *, AppData> appDataCache;
QHash<KWayland::Client::PlasmaWindow *, QTime> 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<int> &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<int>{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<int>{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<int>{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<int>{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<int>{role});
}
void WaylandTasksModel::Private::dataChanged(KWayland::Client::PlasmaWindow *window, const QVector<int> &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<QUrl> &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<const QQuickItem *>(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<QUuid> WaylandTasksModel::winIdsFromMimeData(const QMimeData *mimeData, bool *ok)
{
Q_ASSERT(mimeData);
QList<QUuid> 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;
}
}