/* SPDX-FileCopyrightText: 2012, 2013, 2014, 2015 Ivan Cukic SPDX-License-Identifier: GPL-2.0-or-later */ // Self #include "activitymodel.h" // Qt #include #include #include #include #include #include #include #include #include // KDE #include #include #include // Boost #include #include #include #include // Local #include "utils/remove_if.h" #define ENABLE_QJSVALUE_CONTINUATION #include "utils/continue_with.h" #include "utils/model_updaters.h" using kamd::utils::continue_with; namespace KActivities { namespace Imports { class ActivityModel::Private { public: DECLARE_RAII_MODEL_UPDATERS(ActivityModel) /** * Returns whether the activity has a desired state. * If the state is 0, returns true */ template static inline bool matchingState(InfoPtr activity, T states) { // Are we filtering activities on their states? if (!states.empty() && !boost::binary_search(states, activity->state())) { return false; } return true; } /** * Searches for the activity. * Returns an option(index, iterator) for the found activity. */ template static inline std::optional> activityPosition(const _Container &container, const QString &activityId) { using ActivityPosition = decltype(activityPosition(container, activityId)); using ContainerElement = typename _Container::value_type; auto position = boost::find_if(container, [&](const ContainerElement &activity) { return activity->id() == activityId; }); return (position != container.end()) ? ActivityPosition(std::make_pair(position - container.begin(), position)) : ActivityPosition(); } /** * Notifies the model that an activity was updated */ template static inline void emitActivityUpdated(_Model *model, const _Container &container, QObject *activityInfo, int role) { const auto activity = static_cast(activityInfo); emitActivityUpdated(model, container, activity->id(), role); } /** * Notifies the model that an activity was updated */ template static inline void emitActivityUpdated(_Model *model, const _Container &container, const QString &activity, int role) { auto position = Private::activityPosition(container, activity); if (position) { Q_EMIT model->dataChanged(model->index(position->first), model->index(position->first), role == Qt::DecorationRole ? QList{role, ActivityModel::ActivityIcon} : QList{role}); } } class BackgroundCache { public: BackgroundCache() : initialized(false) , plasmaConfig(QStringLiteral("plasma-org.kde.plasma.desktop-appletsrc")) { using namespace std::placeholders; const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + plasmaConfig.name(); KDirWatch::self()->addFile(configFile); connect(KDirWatch::self(), &KDirWatch::dirty, std::bind(&BackgroundCache::settingsFileChanged, this, _1)); connect(KDirWatch::self(), &KDirWatch::created, std::bind(&BackgroundCache::settingsFileChanged, this, _1)); } void settingsFileChanged(const QString &file) { if (!file.endsWith(plasmaConfig.name())) { return; } plasmaConfig.reparseConfiguration(); if (initialized) { reload(false); } } void subscribe(ActivityModel *model) { if (!initialized) { reload(true); } models << model; } void unsubscribe(ActivityModel *model) { models.removeAll(model); if (models.isEmpty()) { initialized = false; forActivity.clear(); } } QString backgroundFromConfig(const KConfigGroup &config) const { auto wallpaperPlugin = config.readEntry("wallpaperplugin"); auto wallpaperConfig = config.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General")); if (wallpaperConfig.hasKey("Image")) { // Trying for the wallpaper auto wallpaper = wallpaperConfig.readEntry("Image", QString()); if (!wallpaper.isEmpty()) { return wallpaper; } } if (wallpaperConfig.hasKey("Color")) { auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0)); return backgroundColor.name(); } return QString(); } void reload(bool fullReload) { QHash newBackgrounds; if (fullReload) { forActivity.clear(); } QStringList changedBackgrounds; for (const auto &cont : plasmaConfigContainments().groupList()) { auto config = plasmaConfigContainments().group(cont); auto activityId = config.readEntry("activityId", QString()); // Ignore if it has no assigned activity if (activityId.isEmpty()) { continue; } // Ignore if we have already found the background if (newBackgrounds.contains(activityId) && newBackgrounds[activityId][0] != QLatin1Char('#')) { continue; } auto newBackground = backgroundFromConfig(config); if (forActivity[activityId] != newBackground) { changedBackgrounds << activityId; if (!newBackground.isEmpty()) { newBackgrounds[activityId] = newBackground; } } } initialized = true; if (!changedBackgrounds.isEmpty()) { forActivity = newBackgrounds; for (auto model : models) { model->backgroundsUpdated(changedBackgrounds); } } } KConfigGroup plasmaConfigContainments() { return plasmaConfig.group(QStringLiteral("Containments")); } QHash forActivity; QList models; bool initialized; KConfig plasmaConfig; }; static BackgroundCache &backgrounds() { // If you convert this to a shared pointer, // fix the connections to KDirWatcher static BackgroundCache cache; return cache; } }; ActivityModel::ActivityModel(QObject *parent) : QAbstractListModel(parent) { // Initializing role names for qml connect(&m_service, &Consumer::serviceStatusChanged, this, &ActivityModel::setServiceStatus); connect(&m_service, &KActivities::Consumer::activityAdded, this, [this](const QString &id) { onActivityAdded(id); }); connect(&m_service, &KActivities::Consumer::activityRemoved, this, &ActivityModel::onActivityRemoved); connect(&m_service, &KActivities::Consumer::currentActivityChanged, this, &ActivityModel::onCurrentActivityChanged); setServiceStatus(m_service.serviceStatus()); Private::backgrounds().subscribe(this); } ActivityModel::~ActivityModel() { Private::backgrounds().unsubscribe(this); } QHash ActivityModel::roleNames() const { return {{Qt::DisplayRole, "name"}, {Qt::DecorationRole, "icon"}, {ActivityState, "state"}, {ActivityId, "id"}, {ActivityIcon, "iconSource"}, {ActivityDescription, "description"}, {ActivityBackground, "background"}, {ActivityCurrent, "current"}}; } void ActivityModel::setServiceStatus(Consumer::ServiceStatus) { replaceActivities(m_service.activities()); } void ActivityModel::replaceActivities(const QStringList &activities) { // qDebug() << m_shownStatesString << "New list of activities: " // << activities; // qDebug() << m_shownStatesString << " -- RESET MODEL -- "; Private::model_reset m(this); m_knownActivities.clear(); m_shownActivities.clear(); for (const QString &activity : activities) { onActivityAdded(activity, false); } } void ActivityModel::onActivityAdded(const QString &id, bool notifyClients) { auto info = registerActivity(id); // qDebug() << m_shownStatesString << "Added a new activity:" << info->id() // << " " << info->name(); showActivity(info, notifyClients); } void ActivityModel::onActivityRemoved(const QString &id) { // qDebug() << m_shownStatesString << "Removed an activity:" << id; hideActivity(id); unregisterActivity(id); } void ActivityModel::onCurrentActivityChanged(const QString &id) { Q_UNUSED(id); for (const auto &activity : m_shownActivities) { Private::emitActivityUpdated(this, m_shownActivities, activity->id(), ActivityCurrent); } } ActivityModel::InfoPtr ActivityModel::registerActivity(const QString &id) { auto position = Private::activityPosition(m_knownActivities, id); // qDebug() << m_shownStatesString << "Registering activity: " << id // << " new? not " << (bool)position; if (position) { return *(position->second); } else { auto activityInfo = std::make_shared(id); auto ptr = activityInfo.get(); connect(ptr, &Info::nameChanged, this, &ActivityModel::onActivityNameChanged); connect(ptr, &Info::descriptionChanged, this, &ActivityModel::onActivityDescriptionChanged); connect(ptr, &Info::iconChanged, this, &ActivityModel::onActivityIconChanged); connect(ptr, &Info::stateChanged, this, &ActivityModel::onActivityStateChanged); m_knownActivities.insert(InfoPtr(activityInfo)); return activityInfo; } } void ActivityModel::unregisterActivity(const QString &id) { // qDebug() << m_shownStatesString << "Deregistering activity: " << id; auto position = Private::activityPosition(m_knownActivities, id); if (position) { if (auto shown = Private::activityPosition(m_shownActivities, id)) { Private::model_remove(this, QModelIndex(), shown->first, shown->first); m_shownActivities.erase(shown->second); } m_knownActivities.erase(position->second); } } void ActivityModel::showActivity(InfoPtr activityInfo, bool notifyClients) { // Should it really be shown? if (!Private::matchingState(activityInfo, m_shownStates)) { return; } // Is it already shown? if (boost::binary_search(m_shownActivities, activityInfo, InfoPtrComparator())) { return; } auto registeredPosition = Private::activityPosition(m_knownActivities, activityInfo->id()); if (!registeredPosition) { qDebug() << "Got a request to show an unknown activity, ignoring"; return; } auto activityInfoPtr = *(registeredPosition->second); // qDebug() << m_shownStatesString << "Setting activity visibility to true:" // << activityInfoPtr->id() << activityInfoPtr->name(); auto position = m_shownActivities.insert(activityInfoPtr); if (notifyClients) { unsigned int index = (position.second ? position.first : m_shownActivities.end()) - m_shownActivities.begin(); // qDebug() << m_shownStatesString << " -- MODEL INSERT -- " << index; Private::model_insert(this, QModelIndex(), index, index); } } void ActivityModel::hideActivity(const QString &id) { auto position = Private::activityPosition(m_shownActivities, id); // qDebug() << m_shownStatesString // << "Setting activity visibility to false: " << id; if (position) { // qDebug() << m_shownStatesString << " -- MODEL REMOVE -- " // << position->first; Private::model_remove(this, QModelIndex(), position->first, position->first); m_shownActivities.erase(position->second); } } // clang-format off #define CREATE_SIGNAL_EMITTER(What,Role) \ void ActivityModel::onActivity##What##Changed(const QString &) \ { \ Private::emitActivityUpdated(this, m_shownActivities, sender(), Role); \ } // clang-format on CREATE_SIGNAL_EMITTER(Name, Qt::DisplayRole) CREATE_SIGNAL_EMITTER(Description, ActivityDescription) CREATE_SIGNAL_EMITTER(Icon, Qt::DecorationRole) #undef CREATE_SIGNAL_EMITTER void ActivityModel::onActivityStateChanged(Info::State state) { if (m_shownStates.empty()) { Private::emitActivityUpdated(this, m_shownActivities, sender(), ActivityState); } else { auto info = findActivity(sender()); if (!info) { return; } if (boost::binary_search(m_shownStates, state)) { showActivity(info, true); } else { hideActivity(info->id()); } } } void ActivityModel::backgroundsUpdated(const QStringList &activities) { for (const auto &activity : activities) { Private::emitActivityUpdated(this, m_shownActivities, activity, ActivityBackground); } } void ActivityModel::setShownStates(const QString &states) { m_shownStates.clear(); m_shownStatesString = states; for (const auto &state : states.split(QLatin1Char(','))) { if (state == QLatin1String("Running")) { m_shownStates.insert(Running); } else if (state == QLatin1String("Starting")) { m_shownStates.insert(Starting); } else if (state == QLatin1String("Stopped")) { m_shownStates.insert(Stopped); } else if (state == QLatin1String("Stopping")) { m_shownStates.insert(Stopping); } } replaceActivities(m_service.activities()); Q_EMIT shownStatesChanged(states); } QString ActivityModel::shownStates() const { return m_shownStatesString; } int ActivityModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_shownActivities.size(); } QVariant ActivityModel::data(const QModelIndex &index, int role) const { const int row = index.row(); const auto &item = *(m_shownActivities.cbegin() + row); switch (role) { case Qt::DisplayRole: return item->name(); case Qt::DecorationRole: return QIcon::fromTheme(data(index, ActivityIcon).toString()); case ActivityId: return item->id(); case ActivityState: return item->state(); case ActivityIcon: { const QString &icon = item->icon(); // We need a default icon for activities return icon.isEmpty() ? QStringLiteral("activities") : icon; } case ActivityDescription: return item->description(); case ActivityCurrent: return m_service.currentActivity() == item->id(); case ActivityBackground: return Private::backgrounds().forActivity[item->id()]; default: return QVariant(); } } QVariant ActivityModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(section); Q_UNUSED(orientation); Q_UNUSED(role); return QVariant(); } ActivityModel::InfoPtr ActivityModel::findActivity(QObject *ptr) const { auto info = boost::find_if(m_knownActivities, [ptr](const InfoPtr &info) { return ptr == info.get(); }); if (info == m_knownActivities.end()) { return nullptr; } else { return *info; } } // clang-format off // QFuture Controller::setActivityWhat(id, value) #define CREATE_SETTER(What) \ void ActivityModel::setActivity##What( \ const QString &id, const QString &value, const QJSValue &callback) \ { \ continue_with(m_service.setActivity##What(id, value), callback); \ } // clang-format on CREATE_SETTER(Name) CREATE_SETTER(Description) CREATE_SETTER(Icon) #undef CREATE_SETTER // QFuture Controller::setCurrentActivity(id) void ActivityModel::setCurrentActivity(const QString &id, const QJSValue &callback) { continue_with(m_service.setCurrentActivity(id), callback); } // QFuture Controller::addActivity(name) void ActivityModel::addActivity(const QString &name, const QJSValue &callback) { continue_with(m_service.addActivity(name), callback); } // QFuture Controller::removeActivity(id) void ActivityModel::removeActivity(const QString &id, const QJSValue &callback) { continue_with(m_service.removeActivity(id), callback); } // QFuture Controller::stopActivity(id) void ActivityModel::stopActivity(const QString &id, const QJSValue &callback) { continue_with(m_service.stopActivity(id), callback); } // QFuture Controller::startActivity(id) void ActivityModel::startActivity(const QString &id, const QJSValue &callback) { continue_with(m_service.startActivity(id), callback); } } // namespace Imports } // namespace KActivities #include "moc_activitymodel.cpp"