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 ¤tActivity) 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 }