PlasmaActivities

activitymodel.cpp
1/*
2 SPDX-FileCopyrightText: 2012, 2013, 2014, 2015 Ivan Cukic <ivan.cukic(at)kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7// Self
8#include "activitymodel.h"
9
10// Qt
11#include <QByteArray>
12#include <QDBusPendingCall>
13#include <QDBusPendingCallWatcher>
14#include <QDebug>
15#include <QFutureWatcher>
16#include <QHash>
17#include <QIcon>
18#include <QList>
19#include <QModelIndex>
20
21// KDE
22#include <KConfig>
23#include <KConfigGroup>
24#include <KDirWatch>
25
26// Boost
27#include <boost/range/adaptor/filtered.hpp>
28#include <boost/range/algorithm/binary_search.hpp>
29#include <boost/range/algorithm/find_if.hpp>
30
31#include <optional>
32
33// Local
34#include "utils/remove_if.h"
35#define ENABLE_QJSVALUE_CONTINUATION
36#include "utils/continue_with.h"
37#include "utils/model_updaters.h"
38
39using kamd::utils::continue_with;
40
41namespace KActivities
42{
43namespace Imports
44{
45class ActivityModel::Private
46{
47public:
48 DECLARE_RAII_MODEL_UPDATERS(ActivityModel)
49
50 /**
51 * Returns whether the activity has a desired state.
52 * If the state is 0, returns true
53 */
54 template<typename T>
55 static inline bool matchingState(InfoPtr activity, T states)
56 {
57 // Are we filtering activities on their states?
58 if (!states.empty() && !boost::binary_search(states, activity->state())) {
59 return false;
60 }
61
62 return true;
63 }
64
65 /**
66 * Searches for the activity.
67 * Returns an option(index, iterator) for the found activity.
68 */
69 template<typename _Container>
70 static inline std::optional<std::pair<unsigned int, typename _Container::const_iterator>> activityPosition(const _Container &container,
71 const QString &activityId)
72 {
73 using ActivityPosition = decltype(activityPosition(container, activityId));
74 using ContainerElement = typename _Container::value_type;
75
76 auto position = boost::find_if(container, [&](const ContainerElement &activity) {
77 return activity->id() == activityId;
78 });
79
80 return (position != container.end()) ? ActivityPosition(std::make_pair(position - container.begin(), position)) : ActivityPosition();
81 }
82
83 /**
84 * Notifies the model that an activity was updated
85 */
86 template<typename _Model, typename _Container>
87 static inline void emitActivityUpdated(_Model *model, const _Container &container, QObject *activityInfo, int role)
88 {
89 const auto activity = static_cast<Info *>(activityInfo);
90 emitActivityUpdated(model, container, activity->id(), role);
91 }
92
93 /**
94 * Notifies the model that an activity was updated
95 */
96 template<typename _Model, typename _Container>
97 static inline void emitActivityUpdated(_Model *model, const _Container &container, const QString &activity, int role)
98 {
99 auto position = Private::activityPosition(container, activity);
100
101 if (position) {
102 Q_EMIT model->dataChanged(model->index(position->first),
103 model->index(position->first),
104 role == Qt::DecorationRole ? QList<int>{role, ActivityModel::ActivityIcon} : QList<int>{role});
105 }
106 }
107
108 class BackgroundCache
109 {
110 public:
111 BackgroundCache()
112 : initialized(false)
113 , plasmaConfig(QStringLiteral("plasma-org.kde.plasma.desktop-appletsrc"))
114 {
115 using namespace std::placeholders;
116
117 const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + plasmaConfig.name();
118
119 KDirWatch::self()->addFile(configFile);
120
121 connect(KDirWatch::self(), &KDirWatch::dirty, std::bind(&BackgroundCache::settingsFileChanged, this, _1));
122 connect(KDirWatch::self(), &KDirWatch::created, std::bind(&BackgroundCache::settingsFileChanged, this, _1));
123 }
124
125 void settingsFileChanged(const QString &file)
126 {
127 if (!file.endsWith(plasmaConfig.name())) {
128 return;
129 }
130
131 plasmaConfig.reparseConfiguration();
132
133 if (initialized) {
134 reload(false);
135 }
136 }
137
138 void subscribe(ActivityModel *model)
139 {
140 if (!initialized) {
141 reload(true);
142 }
143
144 models << model;
145 }
146
147 void unsubscribe(ActivityModel *model)
148 {
149 models.removeAll(model);
150
151 if (models.isEmpty()) {
152 initialized = false;
153 forActivity.clear();
154 }
155 }
156
157 QString backgroundFromConfig(const KConfigGroup &config) const
158 {
159 auto wallpaperPlugin = config.readEntry("wallpaperplugin");
160 auto wallpaperConfig = config.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General"));
161
162 if (wallpaperConfig.hasKey("Image")) {
163 // Trying for the wallpaper
164 auto wallpaper = wallpaperConfig.readEntry("Image", QString());
165 if (!wallpaper.isEmpty()) {
166 return wallpaper;
167 }
168 }
169 if (wallpaperConfig.hasKey("Color")) {
170 auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0));
171 return backgroundColor.name();
172 }
173
174 return QString();
175 }
176
177 void reload(bool fullReload)
178 {
179 QHash<QString, QString> newBackgrounds;
180
181 if (fullReload) {
182 forActivity.clear();
183 }
184
185 QStringList changedBackgrounds;
186
187 for (const auto &cont : plasmaConfigContainments().groupList()) {
188 auto config = plasmaConfigContainments().group(cont);
189 auto activityId = config.readEntry("activityId", QString());
190
191 // Ignore if it has no assigned activity
192 if (activityId.isEmpty()) {
193 continue;
194 }
195
196 // Ignore if we have already found the background
197 if (newBackgrounds.contains(activityId) && newBackgrounds[activityId][0] != QLatin1Char('#')) {
198 continue;
199 }
200
201 auto newBackground = backgroundFromConfig(config);
202
203 if (forActivity[activityId] != newBackground) {
204 changedBackgrounds << activityId;
205 if (!newBackground.isEmpty()) {
206 newBackgrounds[activityId] = newBackground;
207 }
208 }
209 }
210
211 initialized = true;
212
213 if (!changedBackgrounds.isEmpty()) {
214 forActivity = newBackgrounds;
215
216 for (auto model : models) {
217 model->backgroundsUpdated(changedBackgrounds);
218 }
219 }
220 }
221
222 KConfigGroup plasmaConfigContainments()
223 {
224 return plasmaConfig.group(QStringLiteral("Containments"));
225 }
226
227 QHash<QString, QString> forActivity;
228 QList<ActivityModel *> models;
229
230 bool initialized;
231 KConfig plasmaConfig;
232 };
233
234 static BackgroundCache &backgrounds()
235 {
236 // If you convert this to a shared pointer,
237 // fix the connections to KDirWatcher
238 static BackgroundCache cache;
239 return cache;
240 }
241};
242
243ActivityModel::ActivityModel(QObject *parent)
244 : QAbstractListModel(parent)
245{
246 // Initializing role names for qml
247 connect(&m_service, &Consumer::serviceStatusChanged, this, &ActivityModel::setServiceStatus);
248
249 connect(&m_service, &KActivities::Consumer::activityAdded, this, [this](const QString &id) {
250 onActivityAdded(id);
251 });
252 connect(&m_service, &KActivities::Consumer::activityRemoved, this, &ActivityModel::onActivityRemoved);
253 connect(&m_service, &KActivities::Consumer::currentActivityChanged, this, &ActivityModel::onCurrentActivityChanged);
254
255 setServiceStatus(m_service.serviceStatus());
256
257 Private::backgrounds().subscribe(this);
258}
259
260ActivityModel::~ActivityModel()
261{
262 Private::backgrounds().unsubscribe(this);
263}
264
265QHash<int, QByteArray> ActivityModel::roleNames() const
266{
267 return {{Qt::DisplayRole, "name"},
268 {Qt::DecorationRole, "icon"},
269
270 {ActivityState, "state"},
271 {ActivityId, "id"},
272 {ActivityIcon, "iconSource"},
273 {ActivityDescription, "description"},
274 {ActivityBackground, "background"},
275 {ActivityCurrent, "current"}};
276}
277
278void ActivityModel::setServiceStatus(Consumer::ServiceStatus)
279{
280 replaceActivities(m_service.activities());
281}
282
283void ActivityModel::replaceActivities(const QStringList &activities)
284{
285 // qDebug() << m_shownStatesString << "New list of activities: "
286 // << activities;
287 // qDebug() << m_shownStatesString << " -- RESET MODEL -- ";
288
289 Private::model_reset m(this);
290
291 m_knownActivities.clear();
292 m_shownActivities.clear();
293
294 for (const QString &activity : activities) {
295 onActivityAdded(activity, false);
296 }
297}
298
299void ActivityModel::onActivityAdded(const QString &id, bool notifyClients)
300{
301 auto info = registerActivity(id);
302
303 // qDebug() << m_shownStatesString << "Added a new activity:" << info->id()
304 // << " " << info->name();
305
306 showActivity(info, notifyClients);
307}
308
309void ActivityModel::onActivityRemoved(const QString &id)
310{
311 // qDebug() << m_shownStatesString << "Removed an activity:" << id;
312
313 hideActivity(id);
314 unregisterActivity(id);
315}
316
317void ActivityModel::onCurrentActivityChanged(const QString &id)
318{
319 Q_UNUSED(id);
320
321 for (const auto &activity : m_shownActivities) {
322 Private::emitActivityUpdated(this, m_shownActivities, activity->id(), ActivityCurrent);
323 }
324}
325
326ActivityModel::InfoPtr ActivityModel::registerActivity(const QString &id)
327{
328 auto position = Private::activityPosition(m_knownActivities, id);
329
330 // qDebug() << m_shownStatesString << "Registering activity: " << id
331 // << " new? not " << (bool)position;
332
333 if (position) {
334 return *(position->second);
335
336 } else {
337 auto activityInfo = std::make_shared<Info>(id);
338
339 auto ptr = activityInfo.get();
340
341 connect(ptr, &Info::nameChanged, this, &ActivityModel::onActivityNameChanged);
342 connect(ptr, &Info::descriptionChanged, this, &ActivityModel::onActivityDescriptionChanged);
343 connect(ptr, &Info::iconChanged, this, &ActivityModel::onActivityIconChanged);
344 connect(ptr, &Info::stateChanged, this, &ActivityModel::onActivityStateChanged);
345
346 m_knownActivities.insert(InfoPtr(activityInfo));
347
348 return activityInfo;
349 }
350}
351
352void ActivityModel::unregisterActivity(const QString &id)
353{
354 // qDebug() << m_shownStatesString << "Deregistering activity: " << id;
355
356 auto position = Private::activityPosition(m_knownActivities, id);
357
358 if (position) {
359 if (auto shown = Private::activityPosition(m_shownActivities, id)) {
360 Private::model_remove(this, QModelIndex(), shown->first, shown->first);
361 m_shownActivities.erase(shown->second);
362 }
363
364 m_knownActivities.erase(position->second);
365 }
366}
367
368void ActivityModel::showActivity(InfoPtr activityInfo, bool notifyClients)
369{
370 // Should it really be shown?
371 if (!Private::matchingState(activityInfo, m_shownStates)) {
372 return;
373 }
374
375 // Is it already shown?
376 if (boost::binary_search(m_shownActivities, activityInfo, InfoPtrComparator())) {
377 return;
378 }
379
380 auto registeredPosition = Private::activityPosition(m_knownActivities, activityInfo->id());
381
382 if (!registeredPosition) {
383 qDebug() << "Got a request to show an unknown activity, ignoring";
384 return;
385 }
386
387 auto activityInfoPtr = *(registeredPosition->second);
388
389 // qDebug() << m_shownStatesString << "Setting activity visibility to true:"
390 // << activityInfoPtr->id() << activityInfoPtr->name();
391
392 auto position = m_shownActivities.insert(activityInfoPtr);
393
394 if (notifyClients) {
395 unsigned int index = (position.second ? position.first : m_shownActivities.end()) - m_shownActivities.begin();
396
397 // qDebug() << m_shownStatesString << " -- MODEL INSERT -- " << index;
398 Private::model_insert(this, QModelIndex(), index, index);
399 }
400}
401
402void ActivityModel::hideActivity(const QString &id)
403{
404 auto position = Private::activityPosition(m_shownActivities, id);
405
406 // qDebug() << m_shownStatesString
407 // << "Setting activity visibility to false: " << id;
408
409 if (position) {
410 // qDebug() << m_shownStatesString << " -- MODEL REMOVE -- "
411 // << position->first;
412 Private::model_remove(this, QModelIndex(), position->first, position->first);
413 m_shownActivities.erase(position->second);
414 }
415}
416// clang-format off
417#define CREATE_SIGNAL_EMITTER(What,Role) \
418 void ActivityModel::onActivity##What##Changed(const QString &) \
419 { \
420 Private::emitActivityUpdated(this, m_shownActivities, sender(), Role); \
421 }
422// clang-format on
423
424CREATE_SIGNAL_EMITTER(Name, Qt::DisplayRole)
425CREATE_SIGNAL_EMITTER(Description, ActivityDescription)
426CREATE_SIGNAL_EMITTER(Icon, Qt::DecorationRole)
427
428#undef CREATE_SIGNAL_EMITTER
429
430void ActivityModel::onActivityStateChanged(Info::State state)
431{
432 if (m_shownStates.empty()) {
433 Private::emitActivityUpdated(this, m_shownActivities, sender(), ActivityState);
434
435 } else {
436 auto info = findActivity(sender());
437
438 if (!info) {
439 return;
440 }
441
442 if (boost::binary_search(m_shownStates, state)) {
443 showActivity(info, true);
444 } else {
445 hideActivity(info->id());
446 }
447 }
448}
449
450void ActivityModel::backgroundsUpdated(const QStringList &activities)
451{
452 for (const auto &activity : activities) {
453 Private::emitActivityUpdated(this, m_shownActivities, activity, ActivityBackground);
454 }
455}
456
457void ActivityModel::setShownStates(const QString &states)
458{
459 m_shownStates.clear();
460 m_shownStatesString = states;
461
462 for (const auto &state : states.split(QLatin1Char(','))) {
463 if (state == QLatin1String("Running")) {
464 m_shownStates.insert(Running);
465
466 } else if (state == QLatin1String("Starting")) {
467 m_shownStates.insert(Starting);
468
469 } else if (state == QLatin1String("Stopped")) {
470 m_shownStates.insert(Stopped);
471
472 } else if (state == QLatin1String("Stopping")) {
473 m_shownStates.insert(Stopping);
474 }
475 }
476
477 replaceActivities(m_service.activities());
478
479 Q_EMIT shownStatesChanged(states);
480}
481
482QString ActivityModel::shownStates() const
483{
484 return m_shownStatesString;
485}
486
487int ActivityModel::rowCount(const QModelIndex &parent) const
488{
489 Q_UNUSED(parent);
490
491 return m_shownActivities.size();
492}
493
494QVariant ActivityModel::data(const QModelIndex &index, int role) const
495{
496 const int row = index.row();
497 const auto &item = *(m_shownActivities.cbegin() + row);
498
499 switch (role) {
500 case Qt::DisplayRole:
501 return item->name();
502
504 return QIcon::fromTheme(data(index, ActivityIcon).toString());
505
506 case ActivityId:
507 return item->id();
508
509 case ActivityState:
510 return item->state();
511
512 case ActivityIcon: {
513 const QString &icon = item->icon();
514
515 // We need a default icon for activities
516 return icon.isEmpty() ? QStringLiteral("activities") : icon;
517 }
518
519 case ActivityDescription:
520 return item->description();
521
522 case ActivityCurrent:
523 return m_service.currentActivity() == item->id();
524
525 case ActivityBackground:
526 return Private::backgrounds().forActivity[item->id()];
527
528 default:
529 return QVariant();
530 }
531}
532
533QVariant ActivityModel::headerData(int section, Qt::Orientation orientation, int role) const
534{
535 Q_UNUSED(section);
536 Q_UNUSED(orientation);
537 Q_UNUSED(role);
538
539 return QVariant();
540}
541
542ActivityModel::InfoPtr ActivityModel::findActivity(QObject *ptr) const
543{
544 auto info = boost::find_if(m_knownActivities, [ptr](const InfoPtr &info) {
545 return ptr == info.get();
546 });
547
548 if (info == m_knownActivities.end()) {
549 return nullptr;
550 } else {
551 return *info;
552 }
553}
554
555// clang-format off
556// QFuture<void> Controller::setActivityWhat(id, value)
557#define CREATE_SETTER(What) \
558 void ActivityModel::setActivity##What( \
559 const QString &id, const QString &value, const QJSValue &callback) \
560 { \
561 continue_with(m_service.setActivity##What(id, value), callback); \
562 }
563// clang-format on
564
565CREATE_SETTER(Name)
566CREATE_SETTER(Description)
567CREATE_SETTER(Icon)
568
569#undef CREATE_SETTER
570
571// QFuture<bool> Controller::setCurrentActivity(id)
572void ActivityModel::setCurrentActivity(const QString &id, const QJSValue &callback)
573{
574 continue_with(m_service.setCurrentActivity(id), callback);
575}
576
577// QFuture<QString> Controller::addActivity(name)
578void ActivityModel::addActivity(const QString &name, const QJSValue &callback)
579{
580 continue_with(m_service.addActivity(name), callback);
581}
582
583// QFuture<void> Controller::removeActivity(id)
584void ActivityModel::removeActivity(const QString &id, const QJSValue &callback)
585{
586 continue_with(m_service.removeActivity(id), callback);
587}
588
589// QFuture<void> Controller::stopActivity(id)
590void ActivityModel::stopActivity(const QString &id, const QJSValue &callback)
591{
592 continue_with(m_service.stopActivity(id), callback);
593}
594
595// QFuture<void> Controller::startActivity(id)
596void ActivityModel::startActivity(const QString &id, const QJSValue &callback)
597{
598 continue_with(m_service.startActivity(id), callback);
599}
600
601} // namespace Imports
602} // namespace KActivities
603
604#include "moc_activitymodel.cpp"
void activityAdded(const QString &id)
This signal is emitted when a new activity is added.
void currentActivityChanged(const QString &id)
This signal is emitted when the current activity is changed.
void activityRemoved(const QString &id)
This signal is emitted when an activity has been removed.
KConfigGroup group(const QString &group)
QString readEntry(const char *key, const char *aDefault=nullptr) const
void addFile(const QString &file)
static KDirWatch * self()
void dirty(const QString &path)
void created(const QString &path)
char * toString(const EngineQuery &query)
Namespace for everything in libkactivities.
bool contains(const Key &key) const const
QIcon fromTheme(const QString &name)
bool isEmpty() const const
int row() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString writableLocation(StandardLocation type)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
DecorationRole
Orientation
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 12:01:06 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.