File indexing completed on 2025-01-26 05:06:07
0001 /* 0002 SPDX-FileCopyrightText: 2012, 2013, 2014, 2015 Ivan Cukic <ivan.cukic(at)kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 // Self 0008 #include "activitymodel.h" 0009 0010 // Qt 0011 #include <QByteArray> 0012 #include <QDBusPendingCall> 0013 #include <QDBusPendingCallWatcher> 0014 #include <QDebug> 0015 #include <QFutureWatcher> 0016 #include <QHash> 0017 #include <QIcon> 0018 #include <QList> 0019 #include <QModelIndex> 0020 0021 // KDE 0022 #include <KConfig> 0023 #include <KConfigGroup> 0024 #include <KDirWatch> 0025 0026 // Boost 0027 #include <boost/range/adaptor/filtered.hpp> 0028 #include <boost/range/algorithm/binary_search.hpp> 0029 #include <boost/range/algorithm/find_if.hpp> 0030 0031 #include <optional> 0032 0033 // Local 0034 #include "utils/remove_if.h" 0035 #define ENABLE_QJSVALUE_CONTINUATION 0036 #include "utils/continue_with.h" 0037 #include "utils/model_updaters.h" 0038 0039 using kamd::utils::continue_with; 0040 0041 namespace KActivities 0042 { 0043 namespace Imports 0044 { 0045 class ActivityModel::Private 0046 { 0047 public: 0048 DECLARE_RAII_MODEL_UPDATERS(ActivityModel) 0049 0050 /** 0051 * Returns whether the activity has a desired state. 0052 * If the state is 0, returns true 0053 */ 0054 template<typename T> 0055 static inline bool matchingState(InfoPtr activity, T states) 0056 { 0057 // Are we filtering activities on their states? 0058 if (!states.empty() && !boost::binary_search(states, activity->state())) { 0059 return false; 0060 } 0061 0062 return true; 0063 } 0064 0065 /** 0066 * Searches for the activity. 0067 * Returns an option(index, iterator) for the found activity. 0068 */ 0069 template<typename _Container> 0070 static inline std::optional<std::pair<unsigned int, typename _Container::const_iterator>> activityPosition(const _Container &container, 0071 const QString &activityId) 0072 { 0073 using ActivityPosition = decltype(activityPosition(container, activityId)); 0074 using ContainerElement = typename _Container::value_type; 0075 0076 auto position = boost::find_if(container, [&](const ContainerElement &activity) { 0077 return activity->id() == activityId; 0078 }); 0079 0080 return (position != container.end()) ? ActivityPosition(std::make_pair(position - container.begin(), position)) : ActivityPosition(); 0081 } 0082 0083 /** 0084 * Notifies the model that an activity was updated 0085 */ 0086 template<typename _Model, typename _Container> 0087 static inline void emitActivityUpdated(_Model *model, const _Container &container, QObject *activityInfo, int role) 0088 { 0089 const auto activity = static_cast<Info *>(activityInfo); 0090 emitActivityUpdated(model, container, activity->id(), role); 0091 } 0092 0093 /** 0094 * Notifies the model that an activity was updated 0095 */ 0096 template<typename _Model, typename _Container> 0097 static inline void emitActivityUpdated(_Model *model, const _Container &container, const QString &activity, int role) 0098 { 0099 auto position = Private::activityPosition(container, activity); 0100 0101 if (position) { 0102 Q_EMIT model->dataChanged(model->index(position->first), 0103 model->index(position->first), 0104 role == Qt::DecorationRole ? QList<int>{role, ActivityModel::ActivityIcon} : QList<int>{role}); 0105 } 0106 } 0107 0108 class BackgroundCache 0109 { 0110 public: 0111 BackgroundCache() 0112 : initialized(false) 0113 , plasmaConfig(QStringLiteral("plasma-org.kde.plasma.desktop-appletsrc")) 0114 { 0115 using namespace std::placeholders; 0116 0117 const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + plasmaConfig.name(); 0118 0119 KDirWatch::self()->addFile(configFile); 0120 0121 connect(KDirWatch::self(), &KDirWatch::dirty, std::bind(&BackgroundCache::settingsFileChanged, this, _1)); 0122 connect(KDirWatch::self(), &KDirWatch::created, std::bind(&BackgroundCache::settingsFileChanged, this, _1)); 0123 } 0124 0125 void settingsFileChanged(const QString &file) 0126 { 0127 if (!file.endsWith(plasmaConfig.name())) { 0128 return; 0129 } 0130 0131 plasmaConfig.reparseConfiguration(); 0132 0133 if (initialized) { 0134 reload(false); 0135 } 0136 } 0137 0138 void subscribe(ActivityModel *model) 0139 { 0140 if (!initialized) { 0141 reload(true); 0142 } 0143 0144 models << model; 0145 } 0146 0147 void unsubscribe(ActivityModel *model) 0148 { 0149 models.removeAll(model); 0150 0151 if (models.isEmpty()) { 0152 initialized = false; 0153 forActivity.clear(); 0154 } 0155 } 0156 0157 QString backgroundFromConfig(const KConfigGroup &config) const 0158 { 0159 auto wallpaperPlugin = config.readEntry("wallpaperplugin"); 0160 auto wallpaperConfig = config.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General")); 0161 0162 if (wallpaperConfig.hasKey("Image")) { 0163 // Trying for the wallpaper 0164 auto wallpaper = wallpaperConfig.readEntry("Image", QString()); 0165 if (!wallpaper.isEmpty()) { 0166 return wallpaper; 0167 } 0168 } 0169 if (wallpaperConfig.hasKey("Color")) { 0170 auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0)); 0171 return backgroundColor.name(); 0172 } 0173 0174 return QString(); 0175 } 0176 0177 void reload(bool fullReload) 0178 { 0179 QHash<QString, QString> newBackgrounds; 0180 0181 if (fullReload) { 0182 forActivity.clear(); 0183 } 0184 0185 QStringList changedBackgrounds; 0186 0187 for (const auto &cont : plasmaConfigContainments().groupList()) { 0188 auto config = plasmaConfigContainments().group(cont); 0189 auto activityId = config.readEntry("activityId", QString()); 0190 0191 // Ignore if it has no assigned activity 0192 if (activityId.isEmpty()) { 0193 continue; 0194 } 0195 0196 // Ignore if we have already found the background 0197 if (newBackgrounds.contains(activityId) && newBackgrounds[activityId][0] != QLatin1Char('#')) { 0198 continue; 0199 } 0200 0201 auto newBackground = backgroundFromConfig(config); 0202 0203 if (forActivity[activityId] != newBackground) { 0204 changedBackgrounds << activityId; 0205 if (!newBackground.isEmpty()) { 0206 newBackgrounds[activityId] = newBackground; 0207 } 0208 } 0209 } 0210 0211 initialized = true; 0212 0213 if (!changedBackgrounds.isEmpty()) { 0214 forActivity = newBackgrounds; 0215 0216 for (auto model : models) { 0217 model->backgroundsUpdated(changedBackgrounds); 0218 } 0219 } 0220 } 0221 0222 KConfigGroup plasmaConfigContainments() 0223 { 0224 return plasmaConfig.group(QStringLiteral("Containments")); 0225 } 0226 0227 QHash<QString, QString> forActivity; 0228 QList<ActivityModel *> models; 0229 0230 bool initialized; 0231 KConfig plasmaConfig; 0232 }; 0233 0234 static BackgroundCache &backgrounds() 0235 { 0236 // If you convert this to a shared pointer, 0237 // fix the connections to KDirWatcher 0238 static BackgroundCache cache; 0239 return cache; 0240 } 0241 }; 0242 0243 ActivityModel::ActivityModel(QObject *parent) 0244 : QAbstractListModel(parent) 0245 { 0246 // Initializing role names for qml 0247 connect(&m_service, &Consumer::serviceStatusChanged, this, &ActivityModel::setServiceStatus); 0248 0249 connect(&m_service, &KActivities::Consumer::activityAdded, this, [this](const QString &id) { 0250 onActivityAdded(id); 0251 }); 0252 connect(&m_service, &KActivities::Consumer::activityRemoved, this, &ActivityModel::onActivityRemoved); 0253 connect(&m_service, &KActivities::Consumer::currentActivityChanged, this, &ActivityModel::onCurrentActivityChanged); 0254 0255 setServiceStatus(m_service.serviceStatus()); 0256 0257 Private::backgrounds().subscribe(this); 0258 } 0259 0260 ActivityModel::~ActivityModel() 0261 { 0262 Private::backgrounds().unsubscribe(this); 0263 } 0264 0265 QHash<int, QByteArray> ActivityModel::roleNames() const 0266 { 0267 return {{Qt::DisplayRole, "name"}, 0268 {Qt::DecorationRole, "icon"}, 0269 0270 {ActivityState, "state"}, 0271 {ActivityId, "id"}, 0272 {ActivityIcon, "iconSource"}, 0273 {ActivityDescription, "description"}, 0274 {ActivityBackground, "background"}, 0275 {ActivityCurrent, "current"}}; 0276 } 0277 0278 void ActivityModel::setServiceStatus(Consumer::ServiceStatus) 0279 { 0280 replaceActivities(m_service.activities()); 0281 } 0282 0283 void ActivityModel::replaceActivities(const QStringList &activities) 0284 { 0285 // qDebug() << m_shownStatesString << "New list of activities: " 0286 // << activities; 0287 // qDebug() << m_shownStatesString << " -- RESET MODEL -- "; 0288 0289 Private::model_reset m(this); 0290 0291 m_knownActivities.clear(); 0292 m_shownActivities.clear(); 0293 0294 for (const QString &activity : activities) { 0295 onActivityAdded(activity, false); 0296 } 0297 } 0298 0299 void ActivityModel::onActivityAdded(const QString &id, bool notifyClients) 0300 { 0301 auto info = registerActivity(id); 0302 0303 // qDebug() << m_shownStatesString << "Added a new activity:" << info->id() 0304 // << " " << info->name(); 0305 0306 showActivity(info, notifyClients); 0307 } 0308 0309 void ActivityModel::onActivityRemoved(const QString &id) 0310 { 0311 // qDebug() << m_shownStatesString << "Removed an activity:" << id; 0312 0313 hideActivity(id); 0314 unregisterActivity(id); 0315 } 0316 0317 void ActivityModel::onCurrentActivityChanged(const QString &id) 0318 { 0319 Q_UNUSED(id); 0320 0321 for (const auto &activity : m_shownActivities) { 0322 Private::emitActivityUpdated(this, m_shownActivities, activity->id(), ActivityCurrent); 0323 } 0324 } 0325 0326 ActivityModel::InfoPtr ActivityModel::registerActivity(const QString &id) 0327 { 0328 auto position = Private::activityPosition(m_knownActivities, id); 0329 0330 // qDebug() << m_shownStatesString << "Registering activity: " << id 0331 // << " new? not " << (bool)position; 0332 0333 if (position) { 0334 return *(position->second); 0335 0336 } else { 0337 auto activityInfo = std::make_shared<Info>(id); 0338 0339 auto ptr = activityInfo.get(); 0340 0341 connect(ptr, &Info::nameChanged, this, &ActivityModel::onActivityNameChanged); 0342 connect(ptr, &Info::descriptionChanged, this, &ActivityModel::onActivityDescriptionChanged); 0343 connect(ptr, &Info::iconChanged, this, &ActivityModel::onActivityIconChanged); 0344 connect(ptr, &Info::stateChanged, this, &ActivityModel::onActivityStateChanged); 0345 0346 m_knownActivities.insert(InfoPtr(activityInfo)); 0347 0348 return activityInfo; 0349 } 0350 } 0351 0352 void ActivityModel::unregisterActivity(const QString &id) 0353 { 0354 // qDebug() << m_shownStatesString << "Deregistering activity: " << id; 0355 0356 auto position = Private::activityPosition(m_knownActivities, id); 0357 0358 if (position) { 0359 if (auto shown = Private::activityPosition(m_shownActivities, id)) { 0360 Private::model_remove(this, QModelIndex(), shown->first, shown->first); 0361 m_shownActivities.erase(shown->second); 0362 } 0363 0364 m_knownActivities.erase(position->second); 0365 } 0366 } 0367 0368 void ActivityModel::showActivity(InfoPtr activityInfo, bool notifyClients) 0369 { 0370 // Should it really be shown? 0371 if (!Private::matchingState(activityInfo, m_shownStates)) { 0372 return; 0373 } 0374 0375 // Is it already shown? 0376 if (boost::binary_search(m_shownActivities, activityInfo, InfoPtrComparator())) { 0377 return; 0378 } 0379 0380 auto registeredPosition = Private::activityPosition(m_knownActivities, activityInfo->id()); 0381 0382 if (!registeredPosition) { 0383 qDebug() << "Got a request to show an unknown activity, ignoring"; 0384 return; 0385 } 0386 0387 auto activityInfoPtr = *(registeredPosition->second); 0388 0389 // qDebug() << m_shownStatesString << "Setting activity visibility to true:" 0390 // << activityInfoPtr->id() << activityInfoPtr->name(); 0391 0392 auto position = m_shownActivities.insert(activityInfoPtr); 0393 0394 if (notifyClients) { 0395 unsigned int index = (position.second ? position.first : m_shownActivities.end()) - m_shownActivities.begin(); 0396 0397 // qDebug() << m_shownStatesString << " -- MODEL INSERT -- " << index; 0398 Private::model_insert(this, QModelIndex(), index, index); 0399 } 0400 } 0401 0402 void ActivityModel::hideActivity(const QString &id) 0403 { 0404 auto position = Private::activityPosition(m_shownActivities, id); 0405 0406 // qDebug() << m_shownStatesString 0407 // << "Setting activity visibility to false: " << id; 0408 0409 if (position) { 0410 // qDebug() << m_shownStatesString << " -- MODEL REMOVE -- " 0411 // << position->first; 0412 Private::model_remove(this, QModelIndex(), position->first, position->first); 0413 m_shownActivities.erase(position->second); 0414 } 0415 } 0416 // clang-format off 0417 #define CREATE_SIGNAL_EMITTER(What,Role) \ 0418 void ActivityModel::onActivity##What##Changed(const QString &) \ 0419 { \ 0420 Private::emitActivityUpdated(this, m_shownActivities, sender(), Role); \ 0421 } 0422 // clang-format on 0423 0424 CREATE_SIGNAL_EMITTER(Name, Qt::DisplayRole) 0425 CREATE_SIGNAL_EMITTER(Description, ActivityDescription) 0426 CREATE_SIGNAL_EMITTER(Icon, Qt::DecorationRole) 0427 0428 #undef CREATE_SIGNAL_EMITTER 0429 0430 void ActivityModel::onActivityStateChanged(Info::State state) 0431 { 0432 if (m_shownStates.empty()) { 0433 Private::emitActivityUpdated(this, m_shownActivities, sender(), ActivityState); 0434 0435 } else { 0436 auto info = findActivity(sender()); 0437 0438 if (!info) { 0439 return; 0440 } 0441 0442 if (boost::binary_search(m_shownStates, state)) { 0443 showActivity(info, true); 0444 } else { 0445 hideActivity(info->id()); 0446 } 0447 } 0448 } 0449 0450 void ActivityModel::backgroundsUpdated(const QStringList &activities) 0451 { 0452 for (const auto &activity : activities) { 0453 Private::emitActivityUpdated(this, m_shownActivities, activity, ActivityBackground); 0454 } 0455 } 0456 0457 void ActivityModel::setShownStates(const QString &states) 0458 { 0459 m_shownStates.clear(); 0460 m_shownStatesString = states; 0461 0462 for (const auto &state : states.split(QLatin1Char(','))) { 0463 if (state == QLatin1String("Running")) { 0464 m_shownStates.insert(Running); 0465 0466 } else if (state == QLatin1String("Starting")) { 0467 m_shownStates.insert(Starting); 0468 0469 } else if (state == QLatin1String("Stopped")) { 0470 m_shownStates.insert(Stopped); 0471 0472 } else if (state == QLatin1String("Stopping")) { 0473 m_shownStates.insert(Stopping); 0474 } 0475 } 0476 0477 replaceActivities(m_service.activities()); 0478 0479 Q_EMIT shownStatesChanged(states); 0480 } 0481 0482 QString ActivityModel::shownStates() const 0483 { 0484 return m_shownStatesString; 0485 } 0486 0487 int ActivityModel::rowCount(const QModelIndex &parent) const 0488 { 0489 Q_UNUSED(parent); 0490 0491 return m_shownActivities.size(); 0492 } 0493 0494 QVariant ActivityModel::data(const QModelIndex &index, int role) const 0495 { 0496 const int row = index.row(); 0497 const auto &item = *(m_shownActivities.cbegin() + row); 0498 0499 switch (role) { 0500 case Qt::DisplayRole: 0501 return item->name(); 0502 0503 case Qt::DecorationRole: 0504 return QIcon::fromTheme(data(index, ActivityIcon).toString()); 0505 0506 case ActivityId: 0507 return item->id(); 0508 0509 case ActivityState: 0510 return item->state(); 0511 0512 case ActivityIcon: { 0513 const QString &icon = item->icon(); 0514 0515 // We need a default icon for activities 0516 return icon.isEmpty() ? QStringLiteral("activities") : icon; 0517 } 0518 0519 case ActivityDescription: 0520 return item->description(); 0521 0522 case ActivityCurrent: 0523 return m_service.currentActivity() == item->id(); 0524 0525 case ActivityBackground: 0526 return Private::backgrounds().forActivity[item->id()]; 0527 0528 default: 0529 return QVariant(); 0530 } 0531 } 0532 0533 QVariant ActivityModel::headerData(int section, Qt::Orientation orientation, int role) const 0534 { 0535 Q_UNUSED(section); 0536 Q_UNUSED(orientation); 0537 Q_UNUSED(role); 0538 0539 return QVariant(); 0540 } 0541 0542 ActivityModel::InfoPtr ActivityModel::findActivity(QObject *ptr) const 0543 { 0544 auto info = boost::find_if(m_knownActivities, [ptr](const InfoPtr &info) { 0545 return ptr == info.get(); 0546 }); 0547 0548 if (info == m_knownActivities.end()) { 0549 return nullptr; 0550 } else { 0551 return *info; 0552 } 0553 } 0554 0555 // clang-format off 0556 // QFuture<void> Controller::setActivityWhat(id, value) 0557 #define CREATE_SETTER(What) \ 0558 void ActivityModel::setActivity##What( \ 0559 const QString &id, const QString &value, const QJSValue &callback) \ 0560 { \ 0561 continue_with(m_service.setActivity##What(id, value), callback); \ 0562 } 0563 // clang-format on 0564 0565 CREATE_SETTER(Name) 0566 CREATE_SETTER(Description) 0567 CREATE_SETTER(Icon) 0568 0569 #undef CREATE_SETTER 0570 0571 // QFuture<bool> Controller::setCurrentActivity(id) 0572 void ActivityModel::setCurrentActivity(const QString &id, const QJSValue &callback) 0573 { 0574 continue_with(m_service.setCurrentActivity(id), callback); 0575 } 0576 0577 // QFuture<QString> Controller::addActivity(name) 0578 void ActivityModel::addActivity(const QString &name, const QJSValue &callback) 0579 { 0580 continue_with(m_service.addActivity(name), callback); 0581 } 0582 0583 // QFuture<void> Controller::removeActivity(id) 0584 void ActivityModel::removeActivity(const QString &id, const QJSValue &callback) 0585 { 0586 continue_with(m_service.removeActivity(id), callback); 0587 } 0588 0589 // QFuture<void> Controller::stopActivity(id) 0590 void ActivityModel::stopActivity(const QString &id, const QJSValue &callback) 0591 { 0592 continue_with(m_service.stopActivity(id), callback); 0593 } 0594 0595 // QFuture<void> Controller::startActivity(id) 0596 void ActivityModel::startActivity(const QString &id, const QJSValue &callback) 0597 { 0598 continue_with(m_service.startActivity(id), callback); 0599 } 0600 0601 } // namespace Imports 0602 } // namespace KActivities 0603 0604 #include "moc_activitymodel.cpp"