kwin/kcmkwin/kwindesktop/desktopsmodel.cpp
Vlad Zahorodnii 1fb9f6f13a Switch to SPDX license markers
The main advantage of SPDX license identifiers over the traditional
license headers is that it's more difficult to overlook inappropriate
licenses for kwin, for example GPL 3. We also don't have to copy a
lot of boilerplate text.

In order to create this change, I ran licensedigger -r -c from the
toplevel source directory.
2020-08-07 19:57:56 +00:00

665 lines
18 KiB
C++

/*
* SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
* SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "desktopsmodel.h"
#include <cmath>
#include <KLocalizedString>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusServiceWatcher>
#include <QDBusVariant>
#include <QMetaEnum>
#include <QUuid>
namespace KWin
{
static const QString s_serviceName(QStringLiteral("org.kde.KWin"));
static const QString s_virtualDesktopsInterface(QStringLiteral("org.kde.KWin.VirtualDesktopManager"));
static const QString s_virtDesktopsPath(QStringLiteral("/VirtualDesktopManager"));
static const QString s_fdoPropertiesInterface(QStringLiteral("org.freedesktop.DBus.Properties"));
DesktopsModel::DesktopsModel(QObject *parent)
: QAbstractListModel(parent)
, m_userModified(false)
, m_serverModified(false)
, m_serverSideRows(-1)
, m_rows(-1)
, m_synchronizing(false)
{
qDBusRegisterMetaType<KWin::DBusDesktopDataStruct>();
qDBusRegisterMetaType<KWin::DBusDesktopDataVector>();
m_serviceWatcher = new QDBusServiceWatcher(s_serviceName,
QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange);
QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered,
this, [this]() { reset(); });
QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered,
this, [this]() {
QDBusConnection::sessionBus().disconnect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("desktopCreated"),
this,
SLOT(desktopCreated(QString,KWin::DBusDesktopDataStruct)));
QDBusConnection::sessionBus().disconnect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("desktopRemoved"),
this,
SLOT(desktopRemoved(QString)));
QDBusConnection::sessionBus().disconnect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("desktopDataChanged"),
this,
SLOT(desktopDataChanged(QString,KWin::DBusDesktopDataStruct)));
QDBusConnection::sessionBus().disconnect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("rowsChanged"),
this,
SLOT(desktopRowsChanged(uint)));
}
);
reset();
}
DesktopsModel::~DesktopsModel()
{
}
QHash<int, QByteArray> DesktopsModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles"));
for (int i = 0; i < e.keyCount(); ++i) {
roles.insert(e.value(i), e.key(i));
}
return roles;
}
QVariant DesktopsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() > (m_desktops.count() - 1)) {
return QVariant();
}
if (role == Qt::DisplayRole) {
return m_names.value(m_desktops.at(index.row()));
} else if (role == Id) {
return m_desktops.at(index.row());
} else if (role == DesktopRow) {
const int rows = std::max(m_rows, 1);
const int perRow = std::ceil((qreal)m_desktops.count() / (qreal)rows);
return (index.row() / perRow) + 1;
}
return QVariant();
}
int DesktopsModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_desktops.count();
}
bool DesktopsModel::ready() const
{
return !m_desktops.isEmpty();
}
QString DesktopsModel::error() const
{
return m_error;
}
bool DesktopsModel::userModified() const
{
return m_userModified;
}
bool DesktopsModel::serverModified() const
{
return m_serverModified;
}
int DesktopsModel::rows() const
{
return m_rows;
}
void DesktopsModel::setRows(int rows)
{
if (!ready()) {
return;
}
if (m_rows != rows) {
m_rows = rows;
emit rowsChanged();
emit dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QVector<int>{DesktopRow});
updateModifiedState();
}
}
void DesktopsModel::createDesktop(const QString &name)
{
if (!ready()) {
return;
}
beginInsertRows(QModelIndex(), m_desktops.count(), m_desktops.count());
const QString &dummyId = QUuid::createUuid().toString(QUuid::WithoutBraces);
m_desktops.append(dummyId);
m_names[dummyId] = name;
endInsertRows();
updateModifiedState();
}
void DesktopsModel::removeDesktop(const QString &id)
{
if (!ready() || !m_desktops.contains(id)) {
return;
}
const int desktopIndex = m_desktops.indexOf(id);
beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex);
m_desktops.removeAt(desktopIndex);
m_names.remove(id);
endRemoveRows();
updateModifiedState();
}
void DesktopsModel::setDesktopName(const QString &id, const QString &name)
{
if (!ready() || !m_desktops.contains(id)) {
return;
}
m_names[id] = name;
const QModelIndex &idx = index(m_desktops.indexOf(id), 0);
dataChanged(idx, idx, QVector<int>{Qt::DisplayRole});
updateModifiedState();
}
void DesktopsModel::syncWithServer()
{
m_synchronizing = true;
auto callFinished = [this](QDBusPendingCallWatcher *call) {
QDBusPendingReply<void> reply = *call;
if (reply.isError()) {
handleCallError();
}
call->deleteLater();
};
if (m_desktops.count() > m_serverSideDesktops.count()) {
auto call = QDBusMessage::createMethodCall(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("createDesktop"));
const int newIndex = m_serverSideDesktops.count();
call.setArguments({(uint)newIndex, m_names.value(m_desktops.at(newIndex))});
QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
const auto *watcher = new QDBusPendingCallWatcher(pending, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
return; // The change-handling slot will call syncWithServer() again,
// until everything is in sync.
}
if (m_desktops.count() < m_serverSideDesktops.count()) {
QStringListIterator i(m_serverSideDesktops);
i.toBack();
while (i.hasPrevious()) {
const QString &previous = i.previous();
if (!m_desktops.contains(previous)) {
auto call = QDBusMessage::createMethodCall(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("removeDesktop"));
call.setArguments({previous});
QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
return; // The change-handling slot will call syncWithServer() again,
// until everything is in sync.
}
}
}
// Sync ids. Replace dummy ids in the process.
for (int i = 0; i < m_serverSideDesktops.count(); ++i) {
const QString oldId = m_desktops.at(i);
const QString &newId = m_serverSideDesktops.at(i);
m_desktops[i] = newId;
m_names[newId] = m_names.take(oldId);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0), QVector<int>{Qt::DisplayRole});
// Sync names.
if (m_names != m_serverSideNames) {
QHashIterator<QString, QString> i(m_names);
while (i.hasNext()) {
i.next();
if (i.value() != m_serverSideNames.value(i.key())) {
auto call = QDBusMessage::createMethodCall(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("setDesktopName"));
call.setArguments({i.key(), i.value()});
QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
break;
}
}
return; // The change-handling slot will call syncWithServer() again,
// until everything is in sync..
}
// Sync rows.
if (m_rows != m_serverSideRows) {
auto call = QDBusMessage::createMethodCall(
s_serviceName,
s_virtDesktopsPath,
s_fdoPropertiesInterface,
QStringLiteral("Set"));
call.setArguments({s_virtualDesktopsInterface,
QStringLiteral("rows"), QVariant::fromValue(QDBusVariant(QVariant((uint)m_rows)))});
QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
}
}
void DesktopsModel::reset()
{
m_synchronizing = false; // Sanity.
auto getAllAndConnectCall = QDBusMessage::createMethodCall(
s_serviceName,
s_virtDesktopsPath,
s_fdoPropertiesInterface,
QStringLiteral("GetAll"));
getAllAndConnectCall.setArguments({s_virtualDesktopsInterface});
QDBusConnection::sessionBus().callWithCallback(
getAllAndConnectCall,
this,
SLOT(getAllAndConnect(QDBusMessage)),
SLOT(handleCallError()));
}
bool DesktopsModel::needsSave() const
{
return m_userModified;
}
bool DesktopsModel::isDefaults() const
{
return m_rows == 2 && m_desktops.count() == 1;
}
void DesktopsModel::defaults()
{
beginResetModel();
// default is 1 desktop with 2 rows
// see kwin/virtualdesktops.cpp VirtualDesktopGrid::VirtualDesktopGrid
while (m_desktops.count() > 1) {
const auto desktop = m_desktops.takeLast();
m_names.remove(desktop);
}
m_rows = 2;
endResetModel();
m_userModified = true;
updateModifiedState();
}
void DesktopsModel::load()
{
beginResetModel();
m_desktops = m_serverSideDesktops;
m_names = m_serverSideNames;
m_rows = m_serverSideRows;
endResetModel();
m_userModified = true;
updateModifiedState();
}
void DesktopsModel::getAllAndConnect(const QDBusMessage &msg)
{
const QVariantMap &data = qdbus_cast<QVariantMap>(msg.arguments().at(0).value<QDBusArgument>());
const KWin::DBusDesktopDataVector &desktops = qdbus_cast<KWin::DBusDesktopDataVector>(
data.value(QStringLiteral("desktops")).value<QDBusArgument>()
);
const int newServerSideRows = data.value(QStringLiteral("rows")).toUInt();
QStringList newServerSideDesktops;
QHash<QString,QString> newServerSideNames;
for (const KWin::DBusDesktopDataStruct &d : desktops) {
newServerSideDesktops.append(d.id);
newServerSideNames[d.id] = d.name;
}
// If the server-side state changed during a KWin restart, and the
// user had made notifications, the model should notify about the
// change.
if (m_serverSideDesktops != newServerSideDesktops
|| m_serverSideNames != newServerSideNames
|| m_serverSideRows != newServerSideRows) {
if (!m_serverSideDesktops.isEmpty() || m_userModified) {
m_serverModified = true;
emit serverModifiedChanged();
}
m_serverSideDesktops = newServerSideDesktops;
m_serverSideNames = newServerSideNames;
m_serverSideRows = newServerSideRows;
}
// For the case KWin restarts while the KCM was open: If the user had
// made no modifications, just reset to the server data. E.g. perhaps
// the user intentionally nuked the KWin config while it was down, so
// we should follow.
if (!m_userModified || m_desktops.empty()) {
beginResetModel();
m_desktops = m_serverSideDesktops;
m_names = m_serverSideNames;
m_rows = m_serverSideRows;
endResetModel();
}
emit readyChanged();
auto handleConnectionError = [this]() {
m_error = i18n("There was an error connecting to the compositor.");
emit errorChanged();
};
bool connected = QDBusConnection::sessionBus().connect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("desktopCreated"),
this,
SLOT(desktopCreated(QString,KWin::DBusDesktopDataStruct)));
if (!connected) {
handleConnectionError();
return;
}
connected = QDBusConnection::sessionBus().connect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("desktopRemoved"),
this,
SLOT(desktopRemoved(QString)));
if (!connected) {
handleConnectionError();
return;
}
connected = QDBusConnection::sessionBus().connect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("desktopDataChanged"),
this,
SLOT(desktopDataChanged(QString,KWin::DBusDesktopDataStruct)));
if (!connected) {
handleConnectionError();
return;
}
connected = QDBusConnection::sessionBus().connect(
s_serviceName,
s_virtDesktopsPath,
s_virtualDesktopsInterface,
QStringLiteral("rowsChanged"),
this,
SLOT(desktopRowsChanged(uint)));
if (!connected) {
handleConnectionError();
return;
}
}
void DesktopsModel::desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data)
{
m_serverSideDesktops.insert(data.position, id);
m_serverSideNames[data.id] = data.name;
// If the user didn't make any changes, we can just stay in sync.
if (!m_userModified) {
beginInsertRows(QModelIndex(), data.position, data.position);
m_desktops = m_serverSideDesktops;
m_names = m_serverSideNames;
endInsertRows();
} else {
// Remove dummy data.
const QString dummyId = m_desktops.at(data.position);
m_desktops[data.position] = id;
m_names.remove(dummyId);
m_names[id] = data.name;
const QModelIndex &idx = index(data.position, 0);
emit dataChanged(idx, idx, QVector<int>{Id});
updateModifiedState(/* server */ true);
}
}
void DesktopsModel::desktopRemoved(const QString &id)
{
const int desktopIndex = m_serverSideDesktops.indexOf(id);
m_serverSideDesktops.removeAt(desktopIndex);
m_serverSideNames.remove(id);
// If the user didn't make any changes, we can just stay in sync.
if (!m_userModified) {
beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex);
m_desktops = m_serverSideDesktops;
m_names = m_serverSideNames;
endRemoveRows();
} else {
updateModifiedState(/* server */ true);
}
}
void DesktopsModel::desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data)
{
const int desktopIndex = m_serverSideDesktops.indexOf(id);
m_serverSideDesktops[desktopIndex] = id;
m_serverSideNames[id] = data.name;
// If the user didn't make any changes, we can just stay in sync.
if (!m_userModified) {
m_desktops = m_serverSideDesktops;
m_names = m_serverSideNames;
const QModelIndex &idx = index(desktopIndex, 0);
dataChanged(idx, idx, QVector<int>{Qt::DisplayRole});
} else {
updateModifiedState(/* server */ true);
}
}
void DesktopsModel::desktopRowsChanged(uint rows)
{
// Unfortunately we sometimes get this signal from the server with an unchanged value.
if ((int)rows == m_serverSideRows) {
return;
}
m_serverSideRows = rows;
// If the user didn't make any changes, we can just stay in sync.
if (!m_userModified) {
m_rows = m_serverSideRows;
emit rowsChanged();
emit dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QVector<int>{DesktopRow});
} else {
updateModifiedState(/* server */ true);
}
}
void DesktopsModel::updateModifiedState(bool server)
{
// Count is the same but contents are not: The user may have
// removed and created new desktops in the UI, but there were
// no changes to send to the server because number and names
// have remained the same. In that case we can just clean
// that up here.
if (m_desktops.count() == m_serverSideDesktops.count()
&& m_desktops != m_serverSideDesktops) {
for (int i = 0; i < m_serverSideDesktops.count(); ++i) {
const QString oldId = m_desktops.at(i);
const QString &newId = m_serverSideDesktops.at(i);
m_desktops[i] = newId;
m_names[newId] = m_names.take(oldId);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0), QVector<int>{Qt::DisplayRole});
}
if (m_desktops == m_serverSideDesktops
&& m_names == m_serverSideNames
&& m_rows == m_serverSideRows) {
m_userModified = false;
emit userModifiedChanged();
m_serverModified = false;
emit serverModifiedChanged();
m_synchronizing = false;
} else {
if (m_synchronizing) {
m_serverModified = false;
emit serverModifiedChanged();
syncWithServer();
} else if (server) {
m_serverModified = true;
emit serverModifiedChanged();
} else {
m_userModified = true;
emit userModifiedChanged();
}
}
}
void DesktopsModel::handleCallError()
{
if (m_synchronizing) {
m_synchronizing = false;
m_serverModified = false;
emit serverModifiedChanged();
m_error = i18n("There was an error saving the settings to the compositor.");
emit errorChanged();
} else {
m_error = i18n("There was an error requesting information from the compositor.");
emit errorChanged();
}
}
}