File indexing completed on 2024-05-12 05:35:36

0001 /*
0002     SPDX-FileCopyrightText: 2016 Ivan Cukic <ivan.cukic(at)kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 // Self
0008 #include "sortedactivitiesmodel.h"
0009 
0010 // C++
0011 #include <abstracttasksmodel.h>
0012 #include <windowtasksmodel.h>
0013 
0014 // Qt
0015 #include <QColor>
0016 #include <QObject>
0017 #include <QTimer>
0018 
0019 // KDE
0020 #include <KConfigGroup>
0021 #include <KDirWatch>
0022 #include <KLocalizedString>
0023 #include <KSharedConfig>
0024 
0025 static const char *s_plasma_config = "plasma-org.kde.plasma.desktop-appletsrc";
0026 
0027 namespace
0028 {
0029 class BackgroundCache : public QObject
0030 {
0031 public:
0032     BackgroundCache()
0033         : initialized(false)
0034         , plasmaConfig(KSharedConfig::openConfig(QString::fromLatin1(s_plasma_config)))
0035     {
0036         using namespace std::placeholders;
0037 
0038         const QString configFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char{'/'} + QLatin1String{s_plasma_config};
0039 
0040         KDirWatch::self()->addFile(configFile);
0041 
0042         QObject::connect(KDirWatch::self(), &KDirWatch::dirty, this, &BackgroundCache::settingsFileChanged, Qt::QueuedConnection);
0043         QObject::connect(KDirWatch::self(), &KDirWatch::created, this, &BackgroundCache::settingsFileChanged, Qt::QueuedConnection);
0044     }
0045 
0046     void settingsFileChanged(const QString &file)
0047     {
0048         if (!file.endsWith(QLatin1String{s_plasma_config})) {
0049             return;
0050         }
0051 
0052         if (initialized) {
0053             plasmaConfig->reparseConfiguration();
0054             reload();
0055         }
0056     }
0057 
0058     void subscribe(SortedActivitiesModel *model)
0059     {
0060         if (!initialized) {
0061             reload();
0062         }
0063 
0064         models << model;
0065     }
0066 
0067     void unsubscribe(SortedActivitiesModel *model)
0068     {
0069         models.removeAll(model);
0070 
0071         if (models.isEmpty()) {
0072             initialized = false;
0073             forActivity.clear();
0074         }
0075     }
0076 
0077     QString backgroundFromConfig(const KConfigGroup &config) const
0078     {
0079         auto wallpaperPlugin = config.readEntry("wallpaperplugin");
0080         auto wallpaperConfig = config.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General"));
0081 
0082         if (wallpaperConfig.hasKey("Image")) {
0083             // Trying for the wallpaper
0084             auto wallpaper = wallpaperConfig.readEntry("Image", QString());
0085             if (!wallpaper.isEmpty()) {
0086                 return wallpaper;
0087             }
0088         }
0089         if (wallpaperConfig.hasKey("Color")) {
0090             auto backgroundColor = wallpaperConfig.readEntry("Color", QColor(0, 0, 0));
0091             return backgroundColor.name();
0092         }
0093 
0094         return QString();
0095     }
0096 
0097     void reload()
0098     {
0099         auto newForActivity = forActivity;
0100         QHash<QString, int> lastScreenForActivity;
0101 
0102         // contains activities for which the wallpaper
0103         // has updated
0104         QStringList changedActivities;
0105 
0106         // Contains activities not covered by any containment
0107         QStringList ghostActivities = forActivity.keys();
0108 
0109         // Traversing through all containments in search for
0110         // containments that define activities in plasma
0111         for (const auto &containmentId : plasmaConfigContainments().groupList()) {
0112             const auto containment = plasmaConfigContainments().group(containmentId);
0113             const auto lastScreen = containment.readEntry("lastScreen", 0);
0114             const auto activity = containment.readEntry("activityId", QString());
0115 
0116             // Ignore the containment if the activity is not defined
0117             if (activity.isEmpty())
0118                 continue;
0119 
0120             // If we have already found the same activity from another
0121             // containment, we are using the new one only if
0122             // the previous one was a color and not a proper wallpaper,
0123             // or if the screen ID is closer to zero
0124             const bool processed = !ghostActivities.contains(activity) && newForActivity.contains(activity) && (lastScreenForActivity[activity] <= lastScreen);
0125 
0126             // qDebug() << "GREPME Searching containment " << containmentId
0127             //          << "for the wallpaper of the " << activity << " activity - "
0128             //          << "currently, we think that the wallpaper is " << processed << (processed ? newForActivity[activity] : QString())
0129             //          << "last screen is" << lastScreen
0130             //          ;
0131 
0132             if (processed && !newForActivity[activity].startsWith(QLatin1Char{'#'}))
0133                 continue;
0134 
0135             // Marking the current activity as processed
0136             ghostActivities.removeAll(activity);
0137 
0138             const auto background = backgroundFromConfig(containment);
0139 
0140             // qDebug() << "        GREPME Found wallpaper: " << background;
0141 
0142             if (background.isEmpty())
0143                 continue;
0144 
0145             // If we got this far and we already had a new wallpaper for
0146             // this activity, it means we now have a better one
0147             bool foundBetterWallpaper = changedActivities.contains(activity);
0148 
0149             if (foundBetterWallpaper || newForActivity[activity] != background) {
0150                 if (!foundBetterWallpaper) {
0151                     changedActivities << activity;
0152                 }
0153 
0154                 // qDebug() << "        GREPME Setting: " << activity << " = " << background << "," << lastScreen;
0155                 newForActivity[activity] = background;
0156                 lastScreenForActivity[activity] = lastScreen;
0157             }
0158         }
0159 
0160         initialized = true;
0161 
0162         // Removing the activities from the list if we haven't found them
0163         // while traversing through the containments
0164         for (const auto &activity : ghostActivities) {
0165             newForActivity.remove(activity);
0166         }
0167 
0168         // If we have detected the changes, lets notify everyone
0169         if (!changedActivities.isEmpty()) {
0170             forActivity = newForActivity;
0171 
0172             for (auto model : models) {
0173                 model->onBackgroundsUpdated(changedActivities);
0174             }
0175         }
0176     }
0177 
0178     KConfigGroup plasmaConfigContainments()
0179     {
0180         return plasmaConfig->group(QStringLiteral("Containments"));
0181     }
0182 
0183     QHash<QString, QString> forActivity;
0184     QList<SortedActivitiesModel *> models;
0185 
0186     bool initialized;
0187     KSharedConfig::Ptr plasmaConfig;
0188 };
0189 
0190 static BackgroundCache &backgrounds()
0191 {
0192     // If you convert this to a shared pointer,
0193     // fix the connections to KDirWatcher
0194     static BackgroundCache cache;
0195     return cache;
0196 }
0197 
0198 }
0199 
0200 SortedActivitiesModel::SortedActivitiesModel(const QList<KActivities::Info::State> &states, QObject *parent)
0201     : QSortFilterProxyModel(parent)
0202     , m_windowTasksModel(new TaskManager::WindowTasksModel(this))
0203     , m_activitiesModel(new KActivities::ActivitiesModel(states, this))
0204     , m_activities(new KActivities::Consumer(this))
0205 {
0206     setSourceModel(m_activitiesModel);
0207 
0208     setDynamicSortFilter(true);
0209     setSortRole(LastTimeUsed);
0210     sort(0, Qt::DescendingOrder);
0211 
0212     backgrounds().subscribe(this);
0213 
0214     connect(m_windowTasksModel, &TaskManager::WindowTasksModel::rowsInserted, this, &SortedActivitiesModel::onWindowAdded);
0215     // Using rowsAboutToBeRemoved because we can't fetch data from already removed rows
0216     connect(m_windowTasksModel, &TaskManager::WindowTasksModel::rowsAboutToBeRemoved, this, &SortedActivitiesModel::onWindowRemoved);
0217     connect(m_windowTasksModel, &TaskManager::WindowTasksModel::dataChanged, this, &SortedActivitiesModel::onWindowChanged);
0218 
0219     // Update windows at start
0220     onWindowAdded(QModelIndex(), 0, m_windowTasksModel->rowCount());
0221 }
0222 
0223 SortedActivitiesModel::~SortedActivitiesModel()
0224 {
0225     backgrounds().unsubscribe(this);
0226 }
0227 
0228 bool SortedActivitiesModel::inhibitUpdates() const
0229 {
0230     return m_inhibitUpdates;
0231 }
0232 
0233 void SortedActivitiesModel::setInhibitUpdates(bool inhibitUpdates)
0234 {
0235     if (m_inhibitUpdates != inhibitUpdates) {
0236         m_inhibitUpdates = inhibitUpdates;
0237         Q_EMIT inhibitUpdatesChanged(m_inhibitUpdates);
0238 
0239         setDynamicSortFilter(!inhibitUpdates);
0240     }
0241 }
0242 
0243 uint SortedActivitiesModel::lastUsedTime(const QString &activity) const
0244 {
0245     if (m_activities->currentActivity() == activity) {
0246         return ~(uint)0;
0247 
0248     } else {
0249         KConfig config(QStringLiteral("kactivitymanagerd-switcher"), KConfig::SimpleConfig);
0250         KConfigGroup times(&config, QStringLiteral("LastUsed"));
0251 
0252         return times.readEntry(activity, (uint)0);
0253     }
0254 }
0255 
0256 bool SortedActivitiesModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
0257 {
0258     const auto activityLeft = sourceModel()->data(sourceLeft, KActivities::ActivitiesModel::ActivityId).toString();
0259     const auto activityRight = sourceModel()->data(sourceRight, KActivities::ActivitiesModel::ActivityId).toString();
0260 
0261     const auto timeLeft = lastUsedTime(activityLeft);
0262     const auto timeRight = lastUsedTime(activityRight);
0263 
0264     return (timeLeft < timeRight) || (timeLeft == timeRight && activityLeft < activityRight);
0265 }
0266 
0267 QHash<int, QByteArray> SortedActivitiesModel::roleNames() const
0268 {
0269     if (!sourceModel())
0270         return QHash<int, QByteArray>();
0271 
0272     auto roleNames = sourceModel()->roleNames();
0273 
0274     roleNames[LastTimeUsed] = "lastTimeUsed";
0275     roleNames[LastTimeUsedString] = "lastTimeUsedString";
0276     roleNames[WindowCount] = "windowCount";
0277     roleNames[HasWindows] = "hasWindows";
0278 
0279     return roleNames;
0280 }
0281 
0282 QVariant SortedActivitiesModel::data(const QModelIndex &index, int role) const
0283 {
0284     if (role == KActivities::ActivitiesModel::ActivityBackground) {
0285         const auto activity = activityIdForIndex(index);
0286 
0287         return backgrounds().forActivity[activity];
0288 
0289     } else if (role == LastTimeUsed || role == LastTimeUsedString) {
0290         const auto activity = activityIdForIndex(index);
0291 
0292         const auto time = lastUsedTime(activity);
0293 
0294         if (role == LastTimeUsed) {
0295             return QVariant(time);
0296 
0297         } else {
0298             const auto now = QDateTime::currentDateTime().toSecsSinceEpoch();
0299 
0300             if (time == 0)
0301                 return i18n("Used some time ago");
0302 
0303             auto diff = now - time;
0304 
0305             // We do not need to be precise
0306             diff /= 60;
0307             const auto minutes = diff % 60;
0308             diff /= 60;
0309             const auto hours = diff % 24;
0310             diff /= 24;
0311             const auto days = diff % 30;
0312             diff /= 30;
0313             const auto months = diff % 12;
0314             diff /= 12;
0315             const auto years = diff;
0316 
0317             return (years > 0)  ? i18n("Used more than a year ago")
0318                 : (months > 0)  ? i18ncp("amount in months", "Used a month ago", "Used %1 months ago", months)
0319                 : (days > 0)    ? i18ncp("amount in days", "Used a day ago", "Used %1 days ago", days)
0320                 : (hours > 0)   ? i18ncp("amount in hours", "Used an hour ago", "Used %1 hours ago", hours)
0321                 : (minutes > 0) ? i18ncp("amount in minutes", "Used a minute ago", "Used %1 minutes ago", minutes)
0322                                 : i18n("Used a moment ago");
0323         }
0324 
0325     } else if (role == HasWindows || role == WindowCount) {
0326         const auto activity = activityIdForIndex(index);
0327 
0328         if (role == HasWindows) {
0329             return (m_activitiesWindows[activity].size() > 0);
0330         } else {
0331             return m_activitiesWindows[activity].size();
0332         }
0333 
0334     } else {
0335         return QSortFilterProxyModel::data(index, role);
0336     }
0337 }
0338 
0339 QString SortedActivitiesModel::activityIdForIndex(const QModelIndex &index) const
0340 {
0341     return data(index, KActivities::ActivitiesModel::ActivityId).toString();
0342 }
0343 
0344 QString SortedActivitiesModel::activityIdForRow(int row) const
0345 {
0346     return activityIdForIndex(index(row, 0));
0347 }
0348 
0349 int SortedActivitiesModel::rowForActivityId(const QString &activity) const
0350 {
0351     int position = -1;
0352 
0353     for (int row = 0; row < rowCount(); ++row) {
0354         if (activity == activityIdForRow(row)) {
0355             position = row;
0356         }
0357     }
0358 
0359     return position;
0360 }
0361 
0362 QString SortedActivitiesModel::relativeActivity(int relative) const
0363 {
0364     const auto currentActivity = m_activities->currentActivity();
0365 
0366     if (!sourceModel())
0367         return QString();
0368 
0369     const auto currentRowCount = sourceModel()->rowCount();
0370 
0371     // x % 0 is undefined in c++
0372     if (currentRowCount == 0) {
0373         return QString();
0374     }
0375 
0376     int currentActivityRow = 0;
0377 
0378     for (; currentActivityRow < currentRowCount; currentActivityRow++) {
0379         if (activityIdForRow(currentActivityRow) == currentActivity)
0380             break;
0381     }
0382 
0383     currentActivityRow = currentActivityRow + relative;
0384 
0385     // wrap to within bounds for both positive and negative currentActivityRows
0386     currentActivityRow = (currentRowCount + (currentActivityRow % currentRowCount)) % currentRowCount;
0387 
0388     return activityIdForRow(currentActivityRow);
0389 }
0390 
0391 void SortedActivitiesModel::onCurrentActivityChanged(const QString &currentActivity)
0392 {
0393     if (m_previousActivity == currentActivity)
0394         return;
0395 
0396     const int previousActivityRow = rowForActivityId(m_previousActivity);
0397     rowChanged(previousActivityRow, {LastTimeUsed, LastTimeUsedString});
0398 
0399     m_previousActivity = currentActivity;
0400 
0401     const int currentActivityRow = rowForActivityId(m_previousActivity);
0402     rowChanged(currentActivityRow, {LastTimeUsed, LastTimeUsedString});
0403 }
0404 
0405 void SortedActivitiesModel::onBackgroundsUpdated(const QStringList &activities)
0406 {
0407     for (const auto &activity : activities) {
0408         const int row = rowForActivityId(activity);
0409         rowChanged(row, {KActivities::ActivitiesModel::ActivityBackground});
0410     }
0411 }
0412 
0413 void SortedActivitiesModel::onWindowAdded(const QModelIndex &parent, int first, int last)
0414 {
0415     for (int row = first; row <= last; row++) {
0416         auto window = m_windowTasksModel->index(row, 0, parent);
0417         const QStringList activities = window.data(TaskManager::AbstractTasksModel::Activities).toStringList();
0418         auto winIds = getWinIdList(parent, row);
0419 
0420         for (const auto &activity : activities) {
0421             if (!m_activitiesWindows[activity].contains(winIds)) {
0422                 m_activitiesWindows[activity].append(winIds);
0423 
0424                 rowChanged(rowForActivityId(activity),
0425                            m_activitiesWindows[activity].size() == 1 //
0426                                ? QList<int>{WindowCount, HasWindows}
0427                                : QList<int>{WindowCount});
0428             }
0429         }
0430     }
0431 }
0432 
0433 void SortedActivitiesModel::onWindowRemoved(const QModelIndex &parent, int first, int last)
0434 {
0435     for (int row = first; row <= last; row++) {
0436         auto winIds = getWinIdList(parent, row);
0437 
0438         for (const auto &activity : m_activitiesWindows.keys()) {
0439             if (m_activitiesWindows[activity].contains(winIds)) {
0440                 m_activitiesWindows[activity].removeAll(winIds);
0441 
0442                 rowChanged(rowForActivityId(activity),
0443                            m_activitiesWindows[activity].size() == 0 //
0444                                ? QList<int>{WindowCount, HasWindows}
0445                                : QList<int>{WindowCount});
0446             }
0447         }
0448     }
0449 }
0450 
0451 void SortedActivitiesModel::onWindowChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
0452 {
0453     // If Activities are changed, remove and add the window again to correct activity
0454     if (roles.contains(TaskManager::AbstractTasksModel::Activities) || roles.isEmpty()) {
0455         onWindowRemoved(topLeft.parent(), topLeft.row(), bottomRight.row());
0456         onWindowAdded(topLeft.parent(), topLeft.row(), bottomRight.row());
0457     }
0458 }
0459 
0460 QVariant SortedActivitiesModel::getWinIdList(const QModelIndex &parent, int row)
0461 {
0462     return m_windowTasksModel->index(row, 0, parent).data(TaskManager::AbstractTasksModel::WinIdList);
0463 }
0464 
0465 void SortedActivitiesModel::rowChanged(int row, const QList<int> &roles)
0466 {
0467     if (row == -1)
0468         return;
0469     Q_EMIT dataChanged(index(row, 0), index(row, 0), roles);
0470 }