KWin
Loading...
Searching...
No Matches
desktopsmodel.cpp
Go to the documentation of this file.
1/*
2 SPDX-FileCopyrightText: 2018 Eike Hein <hein@kde.org>
3 SPDX-FileCopyrightText: 2018 Marco Martin <mart@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6*/
7
8#include "desktopsmodel.h"
9
10#include <cmath>
11
12#include <KLocalizedString>
13
14#include <QDBusArgument>
15#include <QDBusConnection>
16#include <QDBusMessage>
17#include <QDBusMetaType>
18#include <QDBusPendingCall>
19#include <QDBusPendingCallWatcher>
20#include <QDBusPendingReply>
21#include <QDBusServiceWatcher>
22#include <QDBusVariant>
23#include <QMetaEnum>
24#include <QRegularExpression>
25#include <QUuid>
26
27namespace KWin
28{
29
30static const QString s_serviceName(QStringLiteral("org.kde.KWin"));
31static const QString s_virtualDesktopsInterface(QStringLiteral("org.kde.KWin.VirtualDesktopManager"));
32static const QString s_virtDesktopsPath(QStringLiteral("/VirtualDesktopManager"));
33static const QString s_fdoPropertiesInterface(QStringLiteral("org.freedesktop.DBus.Properties"));
34
36 : QAbstractListModel(parent)
37 , m_userModified(false)
38 , m_serverModified(false)
39 , m_serverSideRows(-1)
40 , m_rows(-1)
41{
42 qDBusRegisterMetaType<KWin::DBusDesktopDataStruct>();
43 qDBusRegisterMetaType<KWin::DBusDesktopDataVector>();
44
45 m_serviceWatcher = new QDBusServiceWatcher(s_serviceName,
46 QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange);
47
48 QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered,
49 this, [this]() {
50 reset();
51 });
52
53 QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered,
54 this, [this]() {
55 QDBusConnection::sessionBus().disconnect(
56 s_serviceName,
57 s_virtDesktopsPath,
58 s_virtualDesktopsInterface,
59 QStringLiteral("desktopCreated"),
60 this,
62
63 QDBusConnection::sessionBus().disconnect(
64 s_serviceName,
65 s_virtDesktopsPath,
66 s_virtualDesktopsInterface,
67 QStringLiteral("desktopRemoved"),
68 this,
69 SLOT(desktopRemoved(QString)));
70
71 QDBusConnection::sessionBus().disconnect(
72 s_serviceName,
73 s_virtDesktopsPath,
74 s_virtualDesktopsInterface,
75 QStringLiteral("desktopDataChanged"),
76 this,
78
79 QDBusConnection::sessionBus().disconnect(
80 s_serviceName,
81 s_virtDesktopsPath,
82 s_virtualDesktopsInterface,
83 QStringLiteral("rowsChanged"),
84 this,
85 SLOT(desktopRowsChanged(uint)));
86 });
87
88 reset();
89}
90
94
95QHash<int, QByteArray> DesktopsModel::roleNames() const
96{
97 QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
98
99 QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles"));
100
101 for (int i = 0; i < e.keyCount(); ++i) {
102 roles.insert(e.value(i), e.key(i));
103 }
104
105 return roles;
106}
107
108QVariant DesktopsModel::data(const QModelIndex &index, int role) const
109{
110 if (!index.isValid() || index.row() < 0 || index.row() > (m_desktops.count() - 1)) {
111 return QVariant();
112 }
113
114 if (role == Qt::DisplayRole) {
115 return m_names.value(m_desktops.at(index.row()));
116 } else if (role == Id) {
117 return m_desktops.at(index.row());
118 } else if (role == DesktopRow) {
119 const int rows = std::max(m_rows, 1);
120 const int perRow = std::ceil((qreal)m_desktops.count() / (qreal)rows);
121
122 return (index.row() / perRow) + 1;
123
124 } else if (role == IsDefault) {
125 // According to defaults(), first desktop is default
126 return index.row() == 0;
127 }
128
129 return QVariant();
130}
131
132int DesktopsModel::rowCount(const QModelIndex &parent) const
133{
134 if (parent.isValid()) {
135 return 0;
136 }
137
138 return m_desktops.count();
139}
140
142{
143 return !m_desktops.isEmpty();
144}
145
146QString DesktopsModel::error() const
147{
148 return m_error;
149}
150
152{
153 return m_userModified;
154}
155
157{
158 return m_serverModified;
159}
160
162{
163 return m_rows;
164}
165
167{
168 if (!ready()) {
169 return;
170 }
171
172 if (m_rows != rows) {
173 m_rows = rows;
174
175 Q_EMIT rowsChanged();
176 Q_EMIT dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QList<int>{DesktopRow});
177
179 }
180}
181
183{
184 return rowCount();
185}
186
188{
189 const QStringList nameValues = m_names.values();
190 for (int index = 1;; ++index) {
191 const QString desktopName = i18ncp("A numbered name for virtual desktops", "Desktop %1", "Desktop %1", index);
192 if (!nameValues.contains(desktopName)) {
193 return desktopName;
194 }
195 }
196}
197
199{
200 if (!ready()) {
201 return;
202 }
203
204 beginInsertRows(QModelIndex(), m_desktops.count(), m_desktops.count());
205
206 const QString &dummyId = QUuid::createUuid().toString(QUuid::WithoutBraces);
207
208 m_desktops.append(dummyId);
209 m_names[dummyId] = createDesktopName();
210
211 endInsertRows();
212 Q_EMIT desktopCountChanged();
213
215}
216
217void DesktopsModel::removeDesktop(const QString &id)
218{
219 if (!ready() || !m_desktops.contains(id)) {
220 return;
221 }
222
223 const int desktopIndex = m_desktops.indexOf(id);
224
225 beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex);
226
227 m_desktops.removeAt(desktopIndex);
228 m_names.remove(id);
229
230 endRemoveRows();
231 Q_EMIT desktopCountChanged();
232
234}
235
236void DesktopsModel::setDesktopName(const QString &id, const QString &name)
237{
238 if (!ready() || !m_desktops.contains(id)) {
239 return;
240 }
241
242 m_names[id] = name;
243
244 const QModelIndex &idx = index(m_desktops.indexOf(id), 0);
245
246 Q_EMIT dataChanged(idx, idx, QList<int>{Qt::DisplayRole});
247
249}
250
252{
253 auto callFinished = [this](QDBusPendingCallWatcher *call) {
254 QDBusPendingReply<void> reply = *call;
255
256 if (reply.isError()) {
258 }
259
260 --m_pendingCalls;
261
262 call->deleteLater();
263 };
264
265 if (m_desktops.count() > m_serverSideDesktops.count()) {
266 auto call = QDBusMessage::createMethodCall(
267 s_serviceName,
268 s_virtDesktopsPath,
269 s_virtualDesktopsInterface,
270 QStringLiteral("createDesktop"));
271
272 const int newIndex = m_serverSideDesktops.count();
273
274 call.setArguments({(uint)newIndex, m_names.value(m_desktops.at(newIndex))});
275
276 ++m_pendingCalls;
277 QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
278
279 const auto *watcher = new QDBusPendingCallWatcher(pending, this);
280 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
281
282 return; // The change-handling slot will call syncWithServer() again,
283 // until everything is in sync.
284 }
285
286 if (m_desktops.count() < m_serverSideDesktops.count()) {
287 QStringListIterator i(m_serverSideDesktops);
288
289 i.toBack();
290
291 while (i.hasPrevious()) {
292 const QString &previous = i.previous();
293
294 if (!m_desktops.contains(previous)) {
295 auto call = QDBusMessage::createMethodCall(
296 s_serviceName,
297 s_virtDesktopsPath,
298 s_virtualDesktopsInterface,
299 QStringLiteral("removeDesktop"));
300
301 call.setArguments({previous});
302
303 ++m_pendingCalls;
304 QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
305
306 const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
307 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
308
309 return; // The change-handling slot will call syncWithServer() again,
310 // until everything is in sync.
311 }
312 }
313 }
314
315 // Sync ids. Replace dummy ids in the process.
316 for (int i = 0; i < m_serverSideDesktops.count(); ++i) {
317 const QString oldId = m_desktops.at(i);
318 const QString &newId = m_serverSideDesktops.at(i);
319 m_desktops[i] = newId;
320 m_names[newId] = m_names.take(oldId);
321 }
322
323 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), QList<int>{Qt::DisplayRole});
324
325 // Sync names.
326 if (m_names != m_serverSideNames) {
327 QHashIterator<QString, QString> i(m_names);
328
329 while (i.hasNext()) {
330 i.next();
331
332 if (i.value() != m_serverSideNames.value(i.key())) {
333 auto call = QDBusMessage::createMethodCall(
334 s_serviceName,
335 s_virtDesktopsPath,
336 s_virtualDesktopsInterface,
337 QStringLiteral("setDesktopName"));
338
339 call.setArguments({i.key(), i.value()});
340
341 ++m_pendingCalls;
342 QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
343
344 const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
345 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
346
347 break;
348 }
349 }
350
351 return; // The change-handling slot will call syncWithServer() again,
352 // until everything is in sync..
353 }
354
355 // Sync rows.
356 if (m_rows != m_serverSideRows) {
357 auto call = QDBusMessage::createMethodCall(
358 s_serviceName,
359 s_virtDesktopsPath,
360 s_fdoPropertiesInterface,
361 QStringLiteral("Set"));
362
363 call.setArguments({s_virtualDesktopsInterface,
364 QStringLiteral("rows"), QVariant::fromValue(QDBusVariant(QVariant((uint)m_rows)))});
365
366 ++m_pendingCalls;
367 QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call);
368
369 const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this);
370 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished);
371 }
372}
373
375{
376 auto getAllAndConnectCall = QDBusMessage::createMethodCall(
377 s_serviceName,
378 s_virtDesktopsPath,
379 s_fdoPropertiesInterface,
380 QStringLiteral("GetAll"));
381
382 getAllAndConnectCall.setArguments({s_virtualDesktopsInterface});
383
384 QDBusConnection::sessionBus().callWithCallback(
385 getAllAndConnectCall,
386 this,
387 SLOT(getAllAndConnect(QDBusMessage)),
388 SLOT(handleCallError()));
389}
390
392{
393 return m_userModified;
394}
395
397{
398 return m_rows == 2 && m_desktops.count() == 1;
399}
400
402{
403 beginResetModel();
404 // default is 1 desktop with 2 rows
405 // see kwin/virtualdesktops.cpp VirtualDesktopGrid::VirtualDesktopGrid
406 while (m_desktops.count() > 1) {
407 const auto desktop = m_desktops.takeLast();
408 m_names.remove(desktop);
409 }
410 setRows(2);
411
412 endResetModel();
413
414 m_userModified = true;
416}
417
419{
420 beginResetModel();
421 m_desktops = m_serverSideDesktops;
422 m_names = m_serverSideNames;
423 setRows(m_serverSideRows);
424 endResetModel();
425
426 m_userModified = true;
428}
429
430void DesktopsModel::getAllAndConnect(const QDBusMessage &msg)
431{
432 const QVariantMap &data = qdbus_cast<QVariantMap>(msg.arguments().at(0).value<QDBusArgument>());
433
434 const KWin::DBusDesktopDataVector &desktops = qdbus_cast<KWin::DBusDesktopDataVector>(
435 data.value(QStringLiteral("desktops")).value<QDBusArgument>());
436
437 const int newServerSideRows = data.value(QStringLiteral("rows")).toUInt();
438 QStringList newServerSideDesktops;
439 QHash<QString, QString> newServerSideNames;
440
441 for (const KWin::DBusDesktopDataStruct &d : desktops) {
442 newServerSideDesktops.append(d.id);
443 newServerSideNames[d.id] = d.name;
444 }
445
446 // If the server-side state changed during a KWin restart, and the
447 // user had made notifications, the model should notify about the
448 // change.
449 if (m_serverSideDesktops != newServerSideDesktops
450 || m_serverSideNames != newServerSideNames
451 || m_serverSideRows != newServerSideRows) {
452 if (!m_serverSideDesktops.isEmpty() || m_userModified) {
453 m_serverModified = true;
454 Q_EMIT serverModifiedChanged();
455 }
456
457 m_serverSideDesktops = newServerSideDesktops;
458 m_serverSideNames = newServerSideNames;
459 m_serverSideRows = newServerSideRows;
460 }
461
462 // For the case KWin restarts while the KCM was open: If the user had
463 // made no modifications, just reset to the server data. E.g. perhaps
464 // the user intentionally nuked the KWin config while it was down, so
465 // we should follow.
466 if (!m_userModified || m_desktops.empty()) {
467 beginResetModel();
468 m_desktops = m_serverSideDesktops;
469 m_names = m_serverSideNames;
470 m_rows = m_serverSideRows;
471 endResetModel();
472
473 Q_EMIT rowsChanged();
474 }
475
476 Q_EMIT readyChanged();
477
478 auto handleConnectionError = [this]() {
479 m_error = i18n("There was an error connecting to the compositor.");
480 Q_EMIT errorChanged();
481 };
482
483 bool connected = QDBusConnection::sessionBus().connect(
484 s_serviceName,
485 s_virtDesktopsPath,
486 s_virtualDesktopsInterface,
487 QStringLiteral("desktopCreated"),
488 this,
490
491 if (!connected) {
492 handleConnectionError();
493
494 return;
495 }
496
497 connected = QDBusConnection::sessionBus().connect(
498 s_serviceName,
499 s_virtDesktopsPath,
500 s_virtualDesktopsInterface,
501 QStringLiteral("desktopRemoved"),
502 this,
503 SLOT(desktopRemoved(QString)));
504
505 if (!connected) {
506 handleConnectionError();
507
508 return;
509 }
510
511 connected = QDBusConnection::sessionBus().connect(
512 s_serviceName,
513 s_virtDesktopsPath,
514 s_virtualDesktopsInterface,
515 QStringLiteral("desktopDataChanged"),
516 this,
518
519 if (!connected) {
520 handleConnectionError();
521
522 return;
523 }
524
525 connected = QDBusConnection::sessionBus().connect(
526 s_serviceName,
527 s_virtDesktopsPath,
528 s_virtualDesktopsInterface,
529 QStringLiteral("rowsChanged"),
530 this,
531 SLOT(desktopRowsChanged(uint)));
532
533 if (!connected) {
534 handleConnectionError();
535
536 return;
537 }
538}
539
541{
542 m_serverSideDesktops.insert(data.position, id);
543 m_serverSideNames[data.id] = data.name;
544
545 // If the user didn't make any changes, we can just stay in sync.
546 if (!m_userModified) {
547 beginInsertRows(QModelIndex(), data.position, data.position);
548
549 m_desktops = m_serverSideDesktops;
550 m_names = m_serverSideNames;
551
552 endInsertRows();
553 } else {
554 // Remove dummy data.
555 const QString dummyId = m_desktops.at(data.position);
556 m_desktops[data.position] = id;
557 m_names.remove(dummyId);
558 m_names[id] = data.name;
559 const QModelIndex &idx = index(data.position, 0);
560 Q_EMIT dataChanged(idx, idx, QList<int>{Id});
561
562 updateModifiedState(/* server */ true);
563 }
564}
565
566void DesktopsModel::desktopRemoved(const QString &id)
567{
568 const int desktopIndex = m_serverSideDesktops.indexOf(id);
569
570 m_serverSideDesktops.removeAt(desktopIndex);
571 m_serverSideNames.remove(id);
572
573 // If the user didn't make any changes, we can just stay in sync.
574 if (!m_userModified) {
575 beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex);
576
577 m_desktops = m_serverSideDesktops;
578 m_names = m_serverSideNames;
579
580 endRemoveRows();
581 } else {
582 updateModifiedState(/* server */ true);
583 }
584}
585
587{
588 const int desktopIndex = m_serverSideDesktops.indexOf(id);
589
590 m_serverSideDesktops[desktopIndex] = id;
591 m_serverSideNames[id] = data.name;
592
593 // If the user didn't make any changes, we can just stay in sync.
594 if (!m_userModified) {
595 m_desktops = m_serverSideDesktops;
596 m_names = m_serverSideNames;
597
598 const QModelIndex &idx = index(desktopIndex, 0);
599
600 Q_EMIT dataChanged(idx, idx, QList<int>{Qt::DisplayRole});
601 } else {
602 updateModifiedState(/* server */ true);
603 }
604}
605
607{
608 // Unfortunately we sometimes get this signal from the server with an unchanged value.
609 if ((int)rows == m_serverSideRows) {
610 return;
611 }
612
613 m_serverSideRows = rows;
614
615 // If the user didn't make any changes, we can just stay in sync.
616 if (!m_userModified) {
617 m_rows = m_serverSideRows;
618
619 Q_EMIT rowsChanged();
620 Q_EMIT dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QList<int>{DesktopRow});
621 } else {
622 updateModifiedState(/* server */ true);
623 }
624}
625
627{
628 // Count is the same but contents are not: The user may have
629 // removed and created new desktops in the UI, but there were
630 // no changes to send to the server because number and names
631 // have remained the same. In that case we can just clean
632 // that up here.
633 if (m_desktops.count() == m_serverSideDesktops.count()
634 && m_desktops != m_serverSideDesktops) {
635
636 for (int i = 0; i < m_serverSideDesktops.count(); ++i) {
637 const QString oldId = m_desktops.at(i);
638 const QString &newId = m_serverSideDesktops.at(i);
639 m_desktops[i] = newId;
640 m_names[newId] = m_names.take(oldId);
641 }
642
643 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), QList<int>{Qt::DisplayRole});
644 }
645
646 if (m_desktops == m_serverSideDesktops
647 && m_names == m_serverSideNames
648 && m_rows == m_serverSideRows) {
649
650 m_userModified = false;
651 Q_EMIT userModifiedChanged();
652
653 m_serverModified = false;
654 Q_EMIT serverModifiedChanged();
655 } else {
656 if (m_pendingCalls > 0) {
657 m_serverModified = false;
658 Q_EMIT serverModifiedChanged();
659
661 } else if (server) {
662 m_serverModified = true;
663 Q_EMIT serverModifiedChanged();
664 } else {
665 m_userModified = true;
666 Q_EMIT userModifiedChanged();
667 }
668 }
669}
670
672{
673 if (m_pendingCalls > 0) {
674
675 m_serverModified = false;
676 Q_EMIT serverModifiedChanged();
677
678 m_error = i18n("There was an error saving the settings to the compositor.");
679 Q_EMIT errorChanged();
680 } else {
681 m_error = i18n("There was an error requesting information from the compositor.");
682 Q_EMIT errorChanged();
683 }
684}
685
686}
687
688#include "moc_desktopsmodel.cpp"
void updateModifiedState(bool server=false)
void setRows(int rows)
void serverModifiedChanged() const
void desktopRowsChanged(uint rows)
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
void rowsChanged() const
void desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data)
QString createDesktopName() const
Q_INVOKABLE void syncWithServer()
int rowCount(const QModelIndex &parent={}) const override
DesktopsModel(QObject *parent=nullptr)
void readyChanged() const
void desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data)
Q_INVOKABLE void createDesktop()
void userModifiedChanged() const
QHash< int, QByteArray > roleNames() const override
void desktopRemoved(const QString &id)
Q_INVOKABLE void removeDesktop(const QString &id)
void getAllAndConnect(const QDBusMessage &msg)
Q_INVOKABLE void setDesktopName(const QString &id, const QString &name)
void errorChanged() const
QList< DBusDesktopDataStruct > DBusDesktopDataVector