File indexing completed on 2024-11-10 04:56:48

0001 /*
0002     SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
0003     SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "desktopsmodel.h"
0009 
0010 #include <cmath>
0011 
0012 #include <KLocalizedString>
0013 
0014 #include <QDBusArgument>
0015 #include <QDBusConnection>
0016 #include <QDBusMessage>
0017 #include <QDBusMetaType>
0018 #include <QDBusPendingCall>
0019 #include <QDBusPendingCallWatcher>
0020 #include <QDBusPendingReply>
0021 #include <QDBusServiceWatcher>
0022 #include <QDBusVariant>
0023 #include <QMetaEnum>
0024 #include <QRegularExpression>
0025 #include <QUuid>
0026 
0027 namespace KWin
0028 {
0029 
0030 static const QString s_serviceName(QStringLiteral("org.kde.KWin"));
0031 static const QString s_virtualDesktopsInterface(QStringLiteral("org.kde.KWin.VirtualDesktopManager"));
0032 static const QString s_virtDesktopsPath(QStringLiteral("/VirtualDesktopManager"));
0033 static const QString s_fdoPropertiesInterface(QStringLiteral("org.freedesktop.DBus.Properties"));
0034 
0035 DesktopsModel::DesktopsModel(QObject *parent)
0036     : QAbstractListModel(parent)
0037     , m_userModified(false)
0038     , m_serverModified(false)
0039     , m_serverSideRows(-1)
0040     , m_rows(-1)
0041 {
0042     qDBusRegisterMetaType<KWin::DBusDesktopDataStruct>();
0043     qDBusRegisterMetaType<KWin::DBusDesktopDataVector>();
0044 
0045     m_serviceWatcher = new QDBusServiceWatcher(s_serviceName,
0046                                                QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange);
0047 
0048     QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered,
0049                      this, [this]() {
0050                          reset();
0051                      });
0052 
0053     QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered,
0054                      this, [this]() {
0055                          QDBusConnection::sessionBus().disconnect(
0056                              s_serviceName,
0057                              s_virtDesktopsPath,
0058                              s_virtualDesktopsInterface,
0059                              QStringLiteral("desktopCreated"),
0060                              this,
0061                              SLOT(desktopCreated(QString, KWin::DBusDesktopDataStruct)));
0062 
0063                          QDBusConnection::sessionBus().disconnect(
0064                              s_serviceName,
0065                              s_virtDesktopsPath,
0066                              s_virtualDesktopsInterface,
0067                              QStringLiteral("desktopRemoved"),
0068                              this,
0069                              SLOT(desktopRemoved(QString)));
0070 
0071                          QDBusConnection::sessionBus().disconnect(
0072                              s_serviceName,
0073                              s_virtDesktopsPath,
0074                              s_virtualDesktopsInterface,
0075                              QStringLiteral("desktopDataChanged"),
0076                              this,
0077                              SLOT(desktopDataChanged(QString, KWin::DBusDesktopDataStruct)));
0078 
0079                          QDBusConnection::sessionBus().disconnect(
0080                              s_serviceName,
0081                              s_virtDesktopsPath,
0082                              s_virtualDesktopsInterface,
0083                              QStringLiteral("rowsChanged"),
0084                              this,
0085                              SLOT(desktopRowsChanged(uint)));
0086                      });
0087 
0088     reset();
0089 }
0090 
0091 DesktopsModel::~DesktopsModel()
0092 {
0093 }
0094 
0095 QHash<int, QByteArray> DesktopsModel::roleNames() const
0096 {
0097     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
0098 
0099     QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles"));
0100 
0101     for (int i = 0; i < e.keyCount(); ++i) {
0102         roles.insert(e.value(i), e.key(i));
0103     }
0104 
0105     return roles;
0106 }
0107 
0108 QVariant DesktopsModel::data(const QModelIndex &index, int role) const
0109 {
0110     if (!index.isValid() || index.row() < 0 || index.row() > (m_desktops.count() - 1)) {
0111         return QVariant();
0112     }
0113 
0114     if (role == Qt::DisplayRole) {
0115         return m_names.value(m_desktops.at(index.row()));
0116     } else if (role == Id) {
0117         return m_desktops.at(index.row());
0118     } else if (role == DesktopRow) {
0119         const int rows = std::max(m_rows, 1);
0120         const int perRow = std::ceil((qreal)m_desktops.count() / (qreal)rows);
0121 
0122         return (index.row() / perRow) + 1;
0123 
0124     } else if (role == IsDefault) {
0125         // According to defaults(), first desktop is default
0126         return index.row() == 0;
0127     }
0128 
0129     return QVariant();
0130 }
0131 
0132 int DesktopsModel::rowCount(const QModelIndex &parent) const
0133 {
0134     if (parent.isValid()) {
0135         return 0;
0136     }
0137 
0138     return m_desktops.count();
0139 }
0140 
0141 bool DesktopsModel::ready() const
0142 {
0143     return !m_desktops.isEmpty();
0144 }
0145 
0146 QString DesktopsModel::error() const
0147 {
0148     return m_error;
0149 }
0150 
0151 bool DesktopsModel::userModified() const
0152 {
0153     return m_userModified;
0154 }
0155 
0156 bool DesktopsModel::serverModified() const
0157 {
0158     return m_serverModified;
0159 }
0160 
0161 int DesktopsModel::rows() const
0162 {
0163     return m_rows;
0164 }
0165 
0166 void DesktopsModel::setRows(int rows)
0167 {
0168     if (!ready()) {
0169         return;
0170     }
0171 
0172     if (m_rows != rows) {
0173         m_rows = rows;
0174 
0175         Q_EMIT rowsChanged();
0176         Q_EMIT dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QList<int>{DesktopRow});
0177 
0178         updateModifiedState();
0179     }
0180 }
0181 
0182 int DesktopsModel::desktopCount() const
0183 {
0184     return rowCount();
0185 }
0186 
0187 QString DesktopsModel::createDesktopName() const
0188 {
0189     const QStringList nameValues = m_names.values();
0190     for (int index = 1;; ++index) {
0191         const QString desktopName = i18ncp("A numbered name for virtual desktops", "Desktop %1", "Desktop %1", index);
0192         if (!nameValues.contains(desktopName)) {
0193             return desktopName;
0194         }
0195     }
0196 }
0197 
0198 void DesktopsModel::createDesktop()
0199 {
0200     if (!ready()) {
0201         return;
0202     }
0203 
0204     beginInsertRows(QModelIndex(), m_desktops.count(), m_desktops.count());
0205 
0206     const QString &dummyId = QUuid::createUuid().toString(QUuid::WithoutBraces);
0207 
0208     m_desktops.append(dummyId);
0209     m_names[dummyId] = createDesktopName();
0210 
0211     endInsertRows();
0212     Q_EMIT desktopCountChanged();
0213 
0214     updateModifiedState();
0215 }
0216 
0217 void DesktopsModel::removeDesktop(const QString &id)
0218 {
0219     if (!ready() || !m_desktops.contains(id)) {
0220         return;
0221     }
0222 
0223     const int desktopIndex = m_desktops.indexOf(id);
0224 
0225     beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex);
0226 
0227     m_desktops.removeAt(desktopIndex);
0228     m_names.remove(id);
0229 
0230     endRemoveRows();
0231     Q_EMIT desktopCountChanged();
0232 
0233     updateModifiedState();
0234 }
0235 
0236 void DesktopsModel::setDesktopName(const QString &id, const QString &name)
0237 {
0238     if (!ready() || !m_desktops.contains(id)) {
0239         return;
0240     }
0241 
0242     m_names[id] = name;
0243 
0244     const QModelIndex &idx = index(m_desktops.indexOf(id), 0);
0245 
0246     Q_EMIT dataChanged(idx, idx, QList<int>{Qt::DisplayRole});
0247 
0248     updateModifiedState();
0249 }
0250 
0251 void DesktopsModel::syncWithServer()
0252 {
0253     auto callFinished = [this](QDBusPendingCallWatcher *call) {
0254         QDBusPendingReply<void> reply = *call;
0255 
0256         if (reply.isError()) {
0257             handleCallError();
0258         }
0259 
0260         --m_pendingCalls;
0261 
0262         call->deleteLater();
0263     };
0264 
0265     if (m_desktops.count() > m_serverSideDesktops.count()) {
0266         auto call = QDBusMessage::createMethodCall(
0267             s_serviceName,
0268             s_virtDesktopsPath,
0269             s_virtualDesktopsInterface,
0270             QStringLiteral("createDesktop"));
0271 
0272         const int newIndex = m_serverSideDesktops.count();
0273 
0274         call.setArguments({(uint)newIndex, m_names.value(m_desktops.at(newIndex))});
0275 
0276         ++m_pendingCalls;
0277         QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
0278 
0279         const auto *watcher = new QDBusPendingCallWatcher(pending, this);
0280         QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
0281 
0282         return; // The change-handling slot will call syncWithServer() again,
0283                 // until everything is in sync.
0284     }
0285 
0286     if (m_desktops.count() < m_serverSideDesktops.count()) {
0287         QStringListIterator i(m_serverSideDesktops);
0288 
0289         i.toBack();
0290 
0291         while (i.hasPrevious()) {
0292             const QString &previous = i.previous();
0293 
0294             if (!m_desktops.contains(previous)) {
0295                 auto call = QDBusMessage::createMethodCall(
0296                     s_serviceName,
0297                     s_virtDesktopsPath,
0298                     s_virtualDesktopsInterface,
0299                     QStringLiteral("removeDesktop"));
0300 
0301                 call.setArguments({previous});
0302 
0303                 ++m_pendingCalls;
0304                 QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
0305 
0306                 const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
0307                 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
0308 
0309                 return; // The change-handling slot will call syncWithServer() again,
0310                         // until everything is in sync.
0311             }
0312         }
0313     }
0314 
0315     // Sync ids. Replace dummy ids in the process.
0316     for (int i = 0; i < m_serverSideDesktops.count(); ++i) {
0317         const QString oldId = m_desktops.at(i);
0318         const QString &newId = m_serverSideDesktops.at(i);
0319         m_desktops[i] = newId;
0320         m_names[newId] = m_names.take(oldId);
0321     }
0322 
0323     Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), QList<int>{Qt::DisplayRole});
0324 
0325     // Sync names.
0326     if (m_names != m_serverSideNames) {
0327         QHashIterator<QString, QString> i(m_names);
0328 
0329         while (i.hasNext()) {
0330             i.next();
0331 
0332             if (i.value() != m_serverSideNames.value(i.key())) {
0333                 auto call = QDBusMessage::createMethodCall(
0334                     s_serviceName,
0335                     s_virtDesktopsPath,
0336                     s_virtualDesktopsInterface,
0337                     QStringLiteral("setDesktopName"));
0338 
0339                 call.setArguments({i.key(), i.value()});
0340 
0341                 ++m_pendingCalls;
0342                 QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
0343 
0344                 const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
0345                 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
0346 
0347                 break;
0348             }
0349         }
0350 
0351         return; // The change-handling slot will call syncWithServer() again,
0352                 // until everything is in sync..
0353     }
0354 
0355     // Sync rows.
0356     if (m_rows != m_serverSideRows) {
0357         auto call = QDBusMessage::createMethodCall(
0358             s_serviceName,
0359             s_virtDesktopsPath,
0360             s_fdoPropertiesInterface,
0361             QStringLiteral("Set"));
0362 
0363         call.setArguments({s_virtualDesktopsInterface,
0364                            QStringLiteral("rows"), QVariant::fromValue(QDBusVariant(QVariant((uint)m_rows)))});
0365 
0366         ++m_pendingCalls;
0367         QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
0368 
0369         const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
0370         QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
0371     }
0372 }
0373 
0374 void DesktopsModel::reset()
0375 {
0376     auto getAllAndConnectCall = QDBusMessage::createMethodCall(
0377         s_serviceName,
0378         s_virtDesktopsPath,
0379         s_fdoPropertiesInterface,
0380         QStringLiteral("GetAll"));
0381 
0382     getAllAndConnectCall.setArguments({s_virtualDesktopsInterface});
0383 
0384     QDBusConnection::sessionBus().callWithCallback(
0385         getAllAndConnectCall,
0386         this,
0387         SLOT(getAllAndConnect(QDBusMessage)),
0388         SLOT(handleCallError()));
0389 }
0390 
0391 bool DesktopsModel::needsSave() const
0392 {
0393     return m_userModified;
0394 }
0395 
0396 bool DesktopsModel::isDefaults() const
0397 {
0398     return m_rows == 2 && m_desktops.count() == 1;
0399 }
0400 
0401 void DesktopsModel::defaults()
0402 {
0403     beginResetModel();
0404     // default is 1 desktop with 2 rows
0405     // see kwin/virtualdesktops.cpp  VirtualDesktopGrid::VirtualDesktopGrid
0406     while (m_desktops.count() > 1) {
0407         const auto desktop = m_desktops.takeLast();
0408         m_names.remove(desktop);
0409     }
0410     setRows(2);
0411 
0412     endResetModel();
0413 
0414     m_userModified = true;
0415     updateModifiedState();
0416 }
0417 
0418 void DesktopsModel::load()
0419 {
0420     beginResetModel();
0421     m_desktops = m_serverSideDesktops;
0422     m_names = m_serverSideNames;
0423     setRows(m_serverSideRows);
0424     endResetModel();
0425 
0426     m_userModified = true;
0427     updateModifiedState();
0428 }
0429 
0430 void DesktopsModel::getAllAndConnect(const QDBusMessage &msg)
0431 {
0432     const QVariantMap &data = qdbus_cast<QVariantMap>(msg.arguments().at(0).value<QDBusArgument>());
0433 
0434     const KWin::DBusDesktopDataVector &desktops = qdbus_cast<KWin::DBusDesktopDataVector>(
0435         data.value(QStringLiteral("desktops")).value<QDBusArgument>());
0436 
0437     const int newServerSideRows = data.value(QStringLiteral("rows")).toUInt();
0438     QStringList newServerSideDesktops;
0439     QHash<QString, QString> newServerSideNames;
0440 
0441     for (const KWin::DBusDesktopDataStruct &d : desktops) {
0442         newServerSideDesktops.append(d.id);
0443         newServerSideNames[d.id] = d.name;
0444     }
0445 
0446     // If the server-side state changed during a KWin restart, and the
0447     // user had made notifications, the model should notify about the
0448     // change.
0449     if (m_serverSideDesktops != newServerSideDesktops
0450         || m_serverSideNames != newServerSideNames
0451         || m_serverSideRows != newServerSideRows) {
0452         if (!m_serverSideDesktops.isEmpty() || m_userModified) {
0453             m_serverModified = true;
0454             Q_EMIT serverModifiedChanged();
0455         }
0456 
0457         m_serverSideDesktops = newServerSideDesktops;
0458         m_serverSideNames = newServerSideNames;
0459         m_serverSideRows = newServerSideRows;
0460     }
0461 
0462     // For the case KWin restarts while the KCM was open: If the user had
0463     // made no modifications, just reset to the server data. E.g. perhaps
0464     // the user intentionally nuked the KWin config while it was down, so
0465     // we should follow.
0466     if (!m_userModified || m_desktops.empty()) {
0467         beginResetModel();
0468         m_desktops = m_serverSideDesktops;
0469         m_names = m_serverSideNames;
0470         m_rows = m_serverSideRows;
0471         endResetModel();
0472 
0473         Q_EMIT rowsChanged();
0474     }
0475 
0476     Q_EMIT readyChanged();
0477 
0478     auto handleConnectionError = [this]() {
0479         m_error = i18n("There was an error connecting to the compositor.");
0480         Q_EMIT errorChanged();
0481     };
0482 
0483     bool connected = QDBusConnection::sessionBus().connect(
0484         s_serviceName,
0485         s_virtDesktopsPath,
0486         s_virtualDesktopsInterface,
0487         QStringLiteral("desktopCreated"),
0488         this,
0489         SLOT(desktopCreated(QString, KWin::DBusDesktopDataStruct)));
0490 
0491     if (!connected) {
0492         handleConnectionError();
0493 
0494         return;
0495     }
0496 
0497     connected = QDBusConnection::sessionBus().connect(
0498         s_serviceName,
0499         s_virtDesktopsPath,
0500         s_virtualDesktopsInterface,
0501         QStringLiteral("desktopRemoved"),
0502         this,
0503         SLOT(desktopRemoved(QString)));
0504 
0505     if (!connected) {
0506         handleConnectionError();
0507 
0508         return;
0509     }
0510 
0511     connected = QDBusConnection::sessionBus().connect(
0512         s_serviceName,
0513         s_virtDesktopsPath,
0514         s_virtualDesktopsInterface,
0515         QStringLiteral("desktopDataChanged"),
0516         this,
0517         SLOT(desktopDataChanged(QString, KWin::DBusDesktopDataStruct)));
0518 
0519     if (!connected) {
0520         handleConnectionError();
0521 
0522         return;
0523     }
0524 
0525     connected = QDBusConnection::sessionBus().connect(
0526         s_serviceName,
0527         s_virtDesktopsPath,
0528         s_virtualDesktopsInterface,
0529         QStringLiteral("rowsChanged"),
0530         this,
0531         SLOT(desktopRowsChanged(uint)));
0532 
0533     if (!connected) {
0534         handleConnectionError();
0535 
0536         return;
0537     }
0538 }
0539 
0540 void DesktopsModel::desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data)
0541 {
0542     m_serverSideDesktops.insert(data.position, id);
0543     m_serverSideNames[data.id] = data.name;
0544 
0545     // If the user didn't make any changes, we can just stay in sync.
0546     if (!m_userModified) {
0547         beginInsertRows(QModelIndex(), data.position, data.position);
0548 
0549         m_desktops = m_serverSideDesktops;
0550         m_names = m_serverSideNames;
0551 
0552         endInsertRows();
0553     } else {
0554         // Remove dummy data.
0555         const QString dummyId = m_desktops.at(data.position);
0556         m_desktops[data.position] = id;
0557         m_names.remove(dummyId);
0558         m_names[id] = data.name;
0559         const QModelIndex &idx = index(data.position, 0);
0560         Q_EMIT dataChanged(idx, idx, QList<int>{Id});
0561 
0562         updateModifiedState(/* server */ true);
0563     }
0564 }
0565 
0566 void DesktopsModel::desktopRemoved(const QString &id)
0567 {
0568     const int desktopIndex = m_serverSideDesktops.indexOf(id);
0569 
0570     m_serverSideDesktops.removeAt(desktopIndex);
0571     m_serverSideNames.remove(id);
0572 
0573     // If the user didn't make any changes, we can just stay in sync.
0574     if (!m_userModified) {
0575         beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex);
0576 
0577         m_desktops = m_serverSideDesktops;
0578         m_names = m_serverSideNames;
0579 
0580         endRemoveRows();
0581     } else {
0582         updateModifiedState(/* server */ true);
0583     }
0584 }
0585 
0586 void DesktopsModel::desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data)
0587 {
0588     const int desktopIndex = m_serverSideDesktops.indexOf(id);
0589 
0590     m_serverSideDesktops[desktopIndex] = id;
0591     m_serverSideNames[id] = data.name;
0592 
0593     // If the user didn't make any changes, we can just stay in sync.
0594     if (!m_userModified) {
0595         m_desktops = m_serverSideDesktops;
0596         m_names = m_serverSideNames;
0597 
0598         const QModelIndex &idx = index(desktopIndex, 0);
0599 
0600         Q_EMIT dataChanged(idx, idx, QList<int>{Qt::DisplayRole});
0601     } else {
0602         updateModifiedState(/* server */ true);
0603     }
0604 }
0605 
0606 void DesktopsModel::desktopRowsChanged(uint rows)
0607 {
0608     // Unfortunately we sometimes get this signal from the server with an unchanged value.
0609     if ((int)rows == m_serverSideRows) {
0610         return;
0611     }
0612 
0613     m_serverSideRows = rows;
0614 
0615     // If the user didn't make any changes, we can just stay in sync.
0616     if (!m_userModified) {
0617         m_rows = m_serverSideRows;
0618 
0619         Q_EMIT rowsChanged();
0620         Q_EMIT dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QList<int>{DesktopRow});
0621     } else {
0622         updateModifiedState(/* server */ true);
0623     }
0624 }
0625 
0626 void DesktopsModel::updateModifiedState(bool server)
0627 {
0628     // Count is the same but contents are not: The user may have
0629     // removed and created new desktops in the UI, but there were
0630     // no changes to send to the server because number and names
0631     // have remained the same. In that case we can just clean
0632     // that up here.
0633     if (m_desktops.count() == m_serverSideDesktops.count()
0634         && m_desktops != m_serverSideDesktops) {
0635 
0636         for (int i = 0; i < m_serverSideDesktops.count(); ++i) {
0637             const QString oldId = m_desktops.at(i);
0638             const QString &newId = m_serverSideDesktops.at(i);
0639             m_desktops[i] = newId;
0640             m_names[newId] = m_names.take(oldId);
0641         }
0642 
0643         Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), QList<int>{Qt::DisplayRole});
0644     }
0645 
0646     if (m_desktops == m_serverSideDesktops
0647         && m_names == m_serverSideNames
0648         && m_rows == m_serverSideRows) {
0649 
0650         m_userModified = false;
0651         Q_EMIT userModifiedChanged();
0652 
0653         m_serverModified = false;
0654         Q_EMIT serverModifiedChanged();
0655     } else {
0656         if (m_pendingCalls > 0) {
0657             m_serverModified = false;
0658             Q_EMIT serverModifiedChanged();
0659 
0660             syncWithServer();
0661         } else if (server) {
0662             m_serverModified = true;
0663             Q_EMIT serverModifiedChanged();
0664         } else {
0665             m_userModified = true;
0666             Q_EMIT userModifiedChanged();
0667         }
0668     }
0669 }
0670 
0671 void DesktopsModel::handleCallError()
0672 {
0673     if (m_pendingCalls > 0) {
0674 
0675         m_serverModified = false;
0676         Q_EMIT serverModifiedChanged();
0677 
0678         m_error = i18n("There was an error saving the settings to the compositor.");
0679         Q_EMIT errorChanged();
0680     } else {
0681         m_error = i18n("There was an error requesting information from the compositor.");
0682         Q_EMIT errorChanged();
0683     }
0684 }
0685 
0686 }
0687 
0688 #include "moc_desktopsmodel.cpp"