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"