/* SPDX-FileCopyrightText: 2016 Ivan Cukic SPDX-License-Identifier: GPL-2.0-or-later */ // Self #include "sortedactivitiesmodel.h" // C++ #include #include // Qt #include #include #include #include #include #include // KDE #include #include #include #include static const char *s_plasma_config = "plasma-org.kde.plasma.desktop-appletsrc"; using namespace Qt::Literals; namespace { class BackgroundCache : public QObject { public: BackgroundCache() : initialized(false) , plasmaConfig(KSharedConfig::openConfig(QString::fromLatin1(s_plasma_config))) { using namespace std::placeholders; const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char{'/'} + QLatin1String{s_plasma_config}; KDirWatch::self()->addFile(configFile); QObject::connect(KDirWatch::self(), &KDirWatch::dirty, this, &BackgroundCache::settingsFileChanged, Qt::QueuedConnection); QObject::connect(KDirWatch::self(), &KDirWatch::created, this, &BackgroundCache::settingsFileChanged, Qt::QueuedConnection); } void settingsFileChanged(const QString &file) { if (!file.endsWith(QLatin1String{s_plasma_config})) { return; } if (initialized) { plasmaConfig->reparseConfiguration(); reload(); } } void subscribe(SortedActivitiesModel *model) { if (!initialized) { reload(); } models << model; } void unsubscribe(SortedActivitiesModel *model) { models.removeAll(model); if (models.isEmpty()) { initialized = false; forActivity.clear(); } } QSize parseSize(QStringView str) const { const int index = str.indexOf(QLatin1Char('x')); if (index != -1) { return QSize(str.left(index).toInt(), str.mid(index + 1).toInt()); } return QSize(); } double distance(const QSize &size, const QSize &desired) const { const double desiredAspectRatio = (desired.height() > 0) ? desired.width() / static_cast(desired.height()) : 0; const double candidateAspectRatio = (size.height() > 0) ? size.width() / static_cast(size.height()) : std::numeric_limits::max(); double delta = size.width() - desired.width(); delta = delta >= 0.0 ? delta : -delta * 2; // Penalize for scaling up return std::abs(candidateAspectRatio - desiredAspectRatio) * 25000 + delta; } QString findImageInWallpaperPackage(const QString &folderName) const { QString preferred; const QStringList images = QDir(folderName).entryList(); if (images.empty()) { return preferred; } double best = std::numeric_limits::max(); for (const QString &entry : images) { QSize candidate = parseSize(QFileInfo(entry).baseName()); if (candidate.isEmpty()) { continue; } const double dist = distance(candidate, QSize(1920, 1080)); if (preferred.isEmpty() || dist < best) { preferred = entry; best = dist; } } return folderName + u"/" + preferred; } QString backgroundFromConfig(const KConfigGroup &config) const { auto wallpaperPlugin = config.readEntry("wallpaperplugin"); auto wallpaperConfig = config.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General")); if (wallpaperPlugin == u"org.kde.image") { // we can have three cases: // - Image is a URL to a specific image -> use that // - Image is a URL to a wallpaper package folder -> pick one image from that folder // - Image is empty -> use default wallpaper const QUrl defaultWallpaper = QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"wallpapers/Next"_s, QStandardPaths::LocateDirectory)); const QUrl wallpaper = wallpaperConfig.readEntry("Image", defaultWallpaper); if (QFileInfo(wallpaper.toLocalFile()).isDir()) { return findImageInWallpaperPackage(wallpaper.toLocalFile() + u"/contents/images"); } else { return wallpaper.toString(); } } else if (wallpaperPlugin == u"org.kde.color") { auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(29, 153, 243)); return backgroundColor.name(); } return QString(); } void reload() { auto newForActivity = forActivity; QHash lastScreenForActivity; // contains activities for which the wallpaper // has updated QStringList changedActivities; // Contains activities not covered by any containment QStringList ghostActivities = forActivity.keys(); // Traversing through all containments in search for // containments that define activities in plasma for (const auto &containmentId : plasmaConfigContainments().groupList()) { const auto containment = plasmaConfigContainments().group(containmentId); const auto lastScreen = containment.readEntry("lastScreen", 0); const auto activity = containment.readEntry("activityId", QString()); // Ignore the containment if the activity is not defined if (activity.isEmpty()) continue; // If we have already found the same activity from another // containment, we are using the new one only if // the previous one was a color and not a proper wallpaper, // or if the screen ID is closer to zero const bool processed = !ghostActivities.contains(activity) && newForActivity.contains(activity) && (lastScreenForActivity[activity] <= lastScreen); // qDebug() << "GREPME Searching containment " << containmentId // << "for the wallpaper of the " << activity << " activity - " // << "currently, we think that the wallpaper is " << processed << (processed ? newForActivity[activity] : QString()) // << "last screen is" << lastScreen // ; if (processed && !newForActivity[activity].startsWith(QLatin1Char{'#'})) continue; // Marking the current activity as processed ghostActivities.removeAll(activity); const auto background = backgroundFromConfig(containment); // qDebug() << " GREPME Found wallpaper: " << background; if (background.isEmpty()) continue; // If we got this far and we already had a new wallpaper for // this activity, it means we now have a better one bool foundBetterWallpaper = changedActivities.contains(activity); if (foundBetterWallpaper || newForActivity[activity] != background) { if (!foundBetterWallpaper) { changedActivities << activity; } // qDebug() << " GREPME Setting: " << activity << " = " << background << "," << lastScreen; newForActivity[activity] = background; lastScreenForActivity[activity] = lastScreen; } } initialized = true; // Removing the activities from the list if we haven't found them // while traversing through the containments for (const auto &activity : std::as_const(ghostActivities)) { newForActivity.remove(activity); } // If we have detected the changes, lets notify everyone if (!changedActivities.isEmpty()) { forActivity = newForActivity; for (auto model : std::as_const(models)) { model->onBackgroundsUpdated(changedActivities); } } } KConfigGroup plasmaConfigContainments() { return plasmaConfig->group(QStringLiteral("Containments")); } QHash forActivity; QList models; bool initialized; KSharedConfig::Ptr plasmaConfig; }; static BackgroundCache &backgrounds() { // If you convert this to a shared pointer, // fix the connections to KDirWatcher static BackgroundCache cache; return cache; } } SortedActivitiesModel::SortedActivitiesModel(QObject *parent) : QSortFilterProxyModel(parent) , m_windowTasksModel(new TaskManager::WindowTasksModel(this)) , m_activitiesModel(new KActivities::ActivitiesModel(this)) , m_activities(new KActivities::Consumer(this)) { setSourceModel(m_activitiesModel); setDynamicSortFilter(true); setSortRole(LastTimeUsed); sort(0, Qt::DescendingOrder); backgrounds().subscribe(this); connect(m_windowTasksModel, &TaskManager::WindowTasksModel::rowsInserted, this, &SortedActivitiesModel::onWindowAdded); // Using rowsAboutToBeRemoved because we can't fetch data from already removed rows connect(m_windowTasksModel, &TaskManager::WindowTasksModel::rowsAboutToBeRemoved, this, &SortedActivitiesModel::onWindowRemoved); connect(m_windowTasksModel, &TaskManager::WindowTasksModel::dataChanged, this, &SortedActivitiesModel::onWindowChanged); // Update windows at start onWindowAdded(QModelIndex(), 0, m_windowTasksModel->rowCount()); } SortedActivitiesModel::~SortedActivitiesModel() { backgrounds().unsubscribe(this); } bool SortedActivitiesModel::inhibitUpdates() const { return m_inhibitUpdates; } void SortedActivitiesModel::setInhibitUpdates(bool inhibitUpdates) { if (m_inhibitUpdates != inhibitUpdates) { m_inhibitUpdates = inhibitUpdates; Q_EMIT inhibitUpdatesChanged(m_inhibitUpdates); setDynamicSortFilter(!inhibitUpdates); } } uint SortedActivitiesModel::lastUsedTime(const QString &activity) const { if (m_activities->currentActivity() == activity) { return ~(uint)0; } else { KConfig config(QStringLiteral("plasma/activitiesstaterc"), KConfig::SimpleConfig, QStandardPaths::GenericStateLocation); KConfigGroup times(&config, QStringLiteral("LastUsed")); return times.readEntry(activity, (uint)0); } } bool SortedActivitiesModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const { const auto activityLeft = sourceModel()->data(sourceLeft, KActivities::ActivitiesModel::ActivityId).toString(); const auto activityRight = sourceModel()->data(sourceRight, KActivities::ActivitiesModel::ActivityId).toString(); const auto timeLeft = lastUsedTime(activityLeft); const auto timeRight = lastUsedTime(activityRight); return (timeLeft < timeRight) || (timeLeft == timeRight && activityLeft < activityRight); } QHash SortedActivitiesModel::roleNames() const { if (!sourceModel()) return QHash(); auto roleNames = sourceModel()->roleNames(); roleNames[LastTimeUsed] = QByteArrayLiteral("lastTimeUsed"); roleNames[LastTimeUsedString] = QByteArrayLiteral("lastTimeUsedString"); roleNames[WindowCount] = QByteArrayLiteral("windowCount"); roleNames[HasWindows] = QByteArrayLiteral("hasWindows"); return roleNames; } QVariant SortedActivitiesModel::data(const QModelIndex &index, int role) const { if (role == KActivities::ActivitiesModel::ActivityBackground) { const auto activity = activityIdForIndex(index); return backgrounds().forActivity[activity]; } else if (role == LastTimeUsed || role == LastTimeUsedString) { const auto activity = activityIdForIndex(index); const auto time = lastUsedTime(activity); if (role == LastTimeUsed) { return QVariant(time); } else { const auto now = QDateTime::currentSecsSinceEpoch(); if (time == 0) return i18n("Used some time ago"); auto diff = now - time; // We do not need to be precise diff /= 60; const auto minutes = diff % 60; diff /= 60; const auto hours = diff % 24; diff /= 24; const auto days = diff % 30; diff /= 30; const auto months = diff % 12; diff /= 12; const auto years = diff; return (years > 0) ? i18n("Used more than a year ago") : (months > 0) ? i18ncp("amount in months", "Used a month ago", "Used %1 months ago", months) : (days > 0) ? i18ncp("amount in days", "Used a day ago", "Used %1 days ago", days) : (hours > 0) ? i18ncp("amount in hours", "Used an hour ago", "Used %1 hours ago", hours) : (minutes > 0) ? i18ncp("amount in minutes", "Used a minute ago", "Used %1 minutes ago", minutes) : i18n("Used a moment ago"); } } else if (role == HasWindows || role == WindowCount) { const auto activity = activityIdForIndex(index); if (role == HasWindows) { return (m_activitiesWindows[activity].size() > 0); } else { return m_activitiesWindows[activity].size(); } } else { return QSortFilterProxyModel::data(index, role); } } QString SortedActivitiesModel::activityIdForIndex(const QModelIndex &index) const { return data(index, KActivities::ActivitiesModel::ActivityId).toString(); } QString SortedActivitiesModel::activityIdForRow(int row) const { return activityIdForIndex(index(row, 0)); } int SortedActivitiesModel::rowForActivityId(const QString &activity) const { int position = -1; for (int row = 0; row < rowCount(); ++row) { if (activity == activityIdForRow(row)) { position = row; } } return position; } QString SortedActivitiesModel::relativeActivity(int relative) const { const auto currentActivity = m_activities->currentActivity(); if (!sourceModel()) return QString(); const auto currentRowCount = sourceModel()->rowCount(); // x % 0 is undefined in c++ if (currentRowCount == 0) { return QString(); } int currentActivityRow = 0; for (; currentActivityRow < currentRowCount; currentActivityRow++) { if (activityIdForRow(currentActivityRow) == currentActivity) break; } currentActivityRow = currentActivityRow + relative; // wrap to within bounds for both positive and negative currentActivityRows currentActivityRow = (currentRowCount + (currentActivityRow % currentRowCount)) % currentRowCount; return activityIdForRow(currentActivityRow); } void SortedActivitiesModel::onCurrentActivityChanged(const QString ¤tActivity) { if (m_previousActivity == currentActivity) return; const int previousActivityRow = rowForActivityId(m_previousActivity); rowChanged(previousActivityRow, {LastTimeUsed, LastTimeUsedString}); m_previousActivity = currentActivity; const int currentActivityRow = rowForActivityId(m_previousActivity); rowChanged(currentActivityRow, {LastTimeUsed, LastTimeUsedString}); } void SortedActivitiesModel::onBackgroundsUpdated(const QStringList &activities) { for (const auto &activity : activities) { const int row = rowForActivityId(activity); rowChanged(row, {KActivities::ActivitiesModel::ActivityBackground}); } } void SortedActivitiesModel::onWindowAdded(const QModelIndex &parent, int first, int last) { for (int row = first; row <= last; row++) { auto window = m_windowTasksModel->index(row, 0, parent); const QStringList activities = window.data(TaskManager::AbstractTasksModel::Activities).toStringList(); auto winIds = getWinIdList(parent, row); for (const auto &activity : activities) { if (!m_activitiesWindows[activity].contains(winIds)) { m_activitiesWindows[activity].append(winIds); rowChanged(rowForActivityId(activity), m_activitiesWindows[activity].size() == 1 // ? QList{WindowCount, HasWindows} : QList{WindowCount}); } } } } void SortedActivitiesModel::onWindowRemoved(const QModelIndex &parent, int first, int last) { for (int row = first; row <= last; row++) { auto winIds = getWinIdList(parent, row); for (const auto &activity : m_activitiesWindows.keys()) { if (m_activitiesWindows[activity].contains(winIds)) { m_activitiesWindows[activity].removeAll(winIds); rowChanged(rowForActivityId(activity), m_activitiesWindows[activity].size() == 0 // ? QList{WindowCount, HasWindows} : QList{WindowCount}); } } } } void SortedActivitiesModel::onWindowChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) { // If Activities are changed, remove and add the window again to correct activity if (roles.contains(TaskManager::AbstractTasksModel::Activities) || roles.isEmpty()) { onWindowRemoved(topLeft.parent(), topLeft.row(), bottomRight.row()); onWindowAdded(topLeft.parent(), topLeft.row(), bottomRight.row()); } } QVariant SortedActivitiesModel::getWinIdList(const QModelIndex &parent, int row) { return m_windowTasksModel->index(row, 0, parent).data(TaskManager::AbstractTasksModel::WinIdList); } void SortedActivitiesModel::rowChanged(int row, const QList &roles) { if (row == -1) return; Q_EMIT dataChanged(index(row, 0), index(row, 0), roles); } #include "moc_sortedactivitiesmodel.cpp"