File indexing completed on 2022-11-29 13:02:27

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