Warning, file /plasma/plasma-desktop/imports/activitymanager/sortedactivitiesmodel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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