File indexing completed on 2024-05-05 05:38:34
0001 /* 0002 SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 */ 0006 0007 #include "launchertasksmodel.h" 0008 #include "tasktools.h" 0009 0010 #include <KDesktopFile> 0011 #include <KNotificationJobUiDelegate> 0012 #include <KService> 0013 #include <KSycoca> 0014 #include <KWindowSystem> 0015 0016 #include <PlasmaActivities/Consumer> 0017 #include <PlasmaActivities/ResourceInstance> 0018 0019 #include <KIO/ApplicationLauncherJob> 0020 0021 #include <QHash> 0022 #include <QIcon> 0023 #include <QSet> 0024 #include <QTimer> 0025 #include <QUrlQuery> 0026 0027 #include "launchertasksmodel_p.h" 0028 #include <chrono> 0029 0030 using namespace std::chrono_literals; 0031 0032 namespace TaskManager 0033 { 0034 typedef QSet<QString> ActivitiesSet; 0035 0036 template<typename ActivitiesCollection> 0037 inline bool isOnAllActivities(const ActivitiesCollection &activities) 0038 { 0039 return activities.isEmpty() || activities.contains(NULL_UUID); 0040 } 0041 0042 class Q_DECL_HIDDEN LauncherTasksModel::Private 0043 { 0044 public: 0045 Private(LauncherTasksModel *q); 0046 0047 KActivities::Consumer activitiesConsumer; 0048 0049 QList<QUrl> launchersOrder; 0050 0051 QHash<QUrl, ActivitiesSet> activitiesForLauncher; 0052 inline void setActivitiesForLauncher(const QUrl &url, const ActivitiesSet &activities) 0053 { 0054 if (activities.size() == activitiesConsumer.activities().size()) { 0055 activitiesForLauncher[url] = {NULL_UUID}; 0056 } else { 0057 activitiesForLauncher[url] = activities; 0058 } 0059 } 0060 0061 QHash<QUrl, AppData> appDataCache; 0062 QTimer sycocaChangeTimer; 0063 0064 void init(); 0065 AppData appData(const QUrl &url); 0066 0067 bool requestAddLauncherToActivities(const QUrl &_url, const QStringList &activities); 0068 bool requestRemoveLauncherFromActivities(const QUrl &_url, const QStringList &activities); 0069 0070 private: 0071 LauncherTasksModel *q; 0072 }; 0073 0074 LauncherTasksModel::Private::Private(LauncherTasksModel *q) 0075 : q(q) 0076 { 0077 } 0078 0079 void LauncherTasksModel::Private::init() 0080 { 0081 sycocaChangeTimer.setSingleShot(true); 0082 sycocaChangeTimer.setInterval(100ms); 0083 0084 QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, [this]() { 0085 if (!launchersOrder.count()) { 0086 return; 0087 } 0088 0089 appDataCache.clear(); 0090 0091 // Emit changes of all roles satisfied from app data cache. 0092 Q_EMIT q->dataChanged(q->index(0, 0), 0093 q->index(launchersOrder.count() - 1, 0), 0094 QList<int>{Qt::DisplayRole, 0095 Qt::DecorationRole, 0096 AbstractTasksModel::AppId, 0097 AbstractTasksModel::AppName, 0098 AbstractTasksModel::GenericName, 0099 AbstractTasksModel::LauncherUrl, 0100 AbstractTasksModel::LauncherUrlWithoutIcon}); 0101 }); 0102 0103 QObject::connect(KSycoca::self(), &KSycoca::databaseChanged, q, [this]() { 0104 sycocaChangeTimer.start(); 0105 }); 0106 } 0107 0108 AppData LauncherTasksModel::Private::appData(const QUrl &url) 0109 { 0110 const auto &it = appDataCache.constFind(url); 0111 0112 if (it != appDataCache.constEnd()) { 0113 return *it; 0114 } 0115 0116 const AppData &data = appDataFromUrl(url, QIcon::fromTheme(QLatin1String("unknown"))); 0117 0118 appDataCache.insert(url, data); 0119 0120 return data; 0121 } 0122 0123 bool LauncherTasksModel::Private::requestAddLauncherToActivities(const QUrl &_url, const QStringList &_activities) 0124 { 0125 QUrl url(_url); 0126 if (!isValidLauncherUrl(url)) { 0127 return false; 0128 } 0129 0130 const auto activities = ActivitiesSet(_activities.cbegin(), _activities.cend()); 0131 0132 if (url.isLocalFile() && KDesktopFile::isDesktopFile(url.toLocalFile())) { 0133 KDesktopFile f(url.toLocalFile()); 0134 0135 const KService::Ptr service = KService::serviceByStorageId(f.fileName()); 0136 0137 // Resolve to non-absolute menuId-based URL if possible. 0138 if (service) { 0139 const QString &menuId = service->menuId(); 0140 0141 if (!menuId.isEmpty()) { 0142 url = QUrl(QLatin1String("applications:") + menuId); 0143 } 0144 } 0145 } 0146 0147 // Merge duplicates 0148 int row = -1; 0149 foreach (const QUrl &launcher, launchersOrder) { 0150 ++row; 0151 0152 if (launcherUrlsMatch(url, launcher, IgnoreQueryItems)) { 0153 ActivitiesSet newActivities; 0154 0155 // Use the key we established equivalence to ('launcher'). 0156 if (!activitiesForLauncher.contains(launcher)) { 0157 // If we don't have the activities assigned to this url 0158 // for some reason 0159 newActivities = activities; 0160 0161 } else { 0162 if (isOnAllActivities(activities)) { 0163 // If the new list is empty, or has a null uuid, this 0164 // launcher should be on all activities 0165 newActivities = ActivitiesSet{NULL_UUID}; 0166 0167 } else if (isOnAllActivities(activitiesForLauncher[launcher])) { 0168 // If we have been on all activities before, and we have 0169 // been asked to be on a specific one, lets make an 0170 // exception - we will set the activities to exactly 0171 // what we have been asked 0172 newActivities = activities; 0173 0174 } else { 0175 newActivities += activities; 0176 newActivities += activitiesForLauncher[launcher]; 0177 } 0178 } 0179 0180 if (newActivities != activitiesForLauncher[launcher]) { 0181 setActivitiesForLauncher(launcher, newActivities); 0182 0183 Q_EMIT q->dataChanged(q->index(row, 0), q->index(row, 0)); 0184 0185 Q_EMIT q->launcherListChanged(); 0186 return true; 0187 } 0188 0189 return false; 0190 } 0191 } 0192 0193 // This is a new one 0194 const auto count = launchersOrder.count(); 0195 q->beginInsertRows(QModelIndex(), count, count); 0196 setActivitiesForLauncher(url, activities); 0197 launchersOrder.append(url); 0198 q->endInsertRows(); 0199 0200 Q_EMIT q->launcherListChanged(); 0201 0202 return true; 0203 } 0204 0205 bool LauncherTasksModel::Private::requestRemoveLauncherFromActivities(const QUrl &url, const QStringList &activities) 0206 { 0207 for (int row = 0; row < launchersOrder.count(); ++row) { 0208 const QUrl launcher = launchersOrder.at(row); 0209 0210 if (launcherUrlsMatch(url, launcher, IgnoreQueryItems) || launcherUrlsMatch(url, appData(launcher).url, IgnoreQueryItems)) { 0211 const auto currentActivities = activitiesForLauncher[url]; 0212 ActivitiesSet newActivities; 0213 0214 bool remove = false; 0215 bool update = false; 0216 0217 if (isOnAllActivities(currentActivities)) { 0218 // We are currently on all activities. 0219 // Should we go away, or just remove from the current one? 0220 0221 if (isOnAllActivities(activities)) { 0222 remove = true; 0223 0224 } else { 0225 const auto _activities = activitiesConsumer.activities(); 0226 for (const auto &activity : _activities) { 0227 if (!activities.contains(activity)) { 0228 newActivities << activity; 0229 } else { 0230 update = true; 0231 } 0232 } 0233 } 0234 0235 } else if (isOnAllActivities(activities)) { 0236 remove = true; 0237 0238 } else { 0239 // We weren't on all activities, just remove those that 0240 // we were on 0241 0242 for (const auto &activity : currentActivities) { 0243 if (!activities.contains(activity)) { 0244 newActivities << activity; 0245 } 0246 } 0247 0248 if (newActivities.isEmpty()) { 0249 remove = true; 0250 } else { 0251 update = true; 0252 } 0253 } 0254 0255 if (remove) { 0256 q->beginRemoveRows(QModelIndex(), row, row); 0257 appDataCache.remove(launcher); 0258 launchersOrder.removeAt(row); 0259 activitiesForLauncher.remove(url); 0260 q->endRemoveRows(); 0261 0262 } else if (update) { 0263 setActivitiesForLauncher(url, newActivities); 0264 0265 Q_EMIT q->dataChanged(q->index(row, 0), q->index(row, 0)); 0266 } 0267 0268 if (remove || update) { 0269 Q_EMIT q->launcherListChanged(); 0270 return true; 0271 } 0272 } 0273 } 0274 0275 return false; 0276 } 0277 0278 LauncherTasksModel::LauncherTasksModel(QObject *parent) 0279 : AbstractTasksModel(parent) 0280 , d(new Private(this)) 0281 { 0282 d->init(); 0283 } 0284 0285 LauncherTasksModel::~LauncherTasksModel() 0286 { 0287 } 0288 0289 QVariant LauncherTasksModel::data(const QModelIndex &index, int role) const 0290 { 0291 if (!index.isValid() || index.row() >= d->launchersOrder.count()) { 0292 return QVariant(); 0293 } 0294 0295 const QUrl &url = d->launchersOrder.at(index.row()); 0296 const AppData &data = d->appData(url); 0297 if (role == Qt::DisplayRole) { 0298 return data.name; 0299 } else if (role == Qt::DecorationRole) { 0300 return data.icon; 0301 } else if (role == AppId) { 0302 return data.id; 0303 } else if (role == AppName) { 0304 return data.name; 0305 } else if (role == GenericName) { 0306 return data.genericName; 0307 } else if (role == LauncherUrl) { 0308 // Take resolved URL from cache. 0309 return data.url; 0310 } else if (role == LauncherUrlWithoutIcon) { 0311 // Take resolved URL from cache. 0312 QUrl url = data.url; 0313 0314 if (url.hasQuery()) { 0315 QUrlQuery query(url); 0316 query.removeQueryItem(QLatin1String("iconData")); 0317 url.setQuery(query); 0318 } 0319 0320 return url; 0321 } else if (role == IsLauncher) { 0322 return true; 0323 } else if (role == IsVirtualDesktopsChangeable) { 0324 return false; 0325 } else if (role == IsOnAllVirtualDesktops) { 0326 return true; 0327 } else if (role == Activities) { 0328 return QStringList(d->activitiesForLauncher[url].values()); 0329 } else if (role == CanLaunchNewInstance) { 0330 return false; 0331 } 0332 0333 return AbstractTasksModel::data(index, role); 0334 } 0335 0336 int LauncherTasksModel::rowCount(const QModelIndex &parent) const 0337 { 0338 return parent.isValid() ? 0 : d->launchersOrder.count(); 0339 } 0340 0341 int LauncherTasksModel::rowCountForActivity(const QString &activity) const 0342 { 0343 if (activity == NULL_UUID || activity.isEmpty()) { 0344 return rowCount(); 0345 } 0346 0347 return std::count_if(d->launchersOrder.cbegin(), d->launchersOrder.cend(), [this, &activity](const QUrl &url) { 0348 const auto &set = d->activitiesForLauncher[url]; 0349 return set.contains(NULL_UUID) || set.contains(activity); 0350 }); 0351 } 0352 0353 QStringList LauncherTasksModel::launcherList() const 0354 { 0355 // Serializing the launchers 0356 QStringList result; 0357 0358 for (const auto &launcher : std::as_const(d->launchersOrder)) { 0359 const auto &activities = d->activitiesForLauncher[launcher]; 0360 0361 QString serializedLauncher; 0362 if (isOnAllActivities(activities)) { 0363 serializedLauncher = launcher.toString(); 0364 0365 } else { 0366 serializedLauncher = "[" + d->activitiesForLauncher[launcher].values().join(",") + "]\n" + launcher.toString(); 0367 } 0368 0369 result << serializedLauncher; 0370 } 0371 0372 return result; 0373 } 0374 0375 void LauncherTasksModel::setLauncherList(const QStringList &serializedLaunchers) 0376 { 0377 // Clearing everything 0378 QList<QUrl> newLaunchersOrder; 0379 QHash<QUrl, ActivitiesSet> newActivitiesForLauncher; 0380 0381 // Loading the activity to launchers map 0382 for (const auto &serializedLauncher : serializedLaunchers) { 0383 QStringList _activities; 0384 QUrl url; 0385 0386 std::tie(url, _activities) = deserializeLauncher(serializedLauncher); 0387 0388 auto activities = ActivitiesSet(_activities.cbegin(), _activities.cend()); 0389 0390 // Is url is not valid, ignore it 0391 if (!isValidLauncherUrl(url)) { 0392 continue; 0393 } 0394 0395 // If we have a null uuid, it means we are on all activities 0396 // and we should contain only the null uuid 0397 if (isOnAllActivities(activities)) { 0398 activities = {NULL_UUID}; 0399 0400 } else { 0401 // Filter out invalid activities 0402 const auto allActivities = d->activitiesConsumer.activities(); 0403 ActivitiesSet validActivities; 0404 for (const auto &activity : std::as_const(activities)) { 0405 if (allActivities.contains(activity)) { 0406 validActivities << activity; 0407 } 0408 } 0409 0410 if (validActivities.isEmpty()) { 0411 // If all activities that had this launcher are 0412 // removed, we are killing the launcher as well 0413 continue; 0414 } 0415 0416 activities = validActivities; 0417 } 0418 0419 // Is the url a duplicate? 0420 const auto location = std::find_if(newLaunchersOrder.begin(), newLaunchersOrder.end(), [&url](const QUrl &item) { 0421 return launcherUrlsMatch(url, item, IgnoreQueryItems); 0422 }); 0423 0424 if (location != newLaunchersOrder.end()) { 0425 // It is a duplicate 0426 url = *location; 0427 0428 } else { 0429 // It is not a duplicate, we need to add it 0430 // to the list of registered launchers 0431 newLaunchersOrder << url; 0432 } 0433 0434 if (!newActivitiesForLauncher.contains(url)) { 0435 // This is the first time we got this url 0436 newActivitiesForLauncher[url] = activities; 0437 0438 } else if (newActivitiesForLauncher[url].contains(NULL_UUID)) { 0439 // Do nothing, we are already on all activities 0440 0441 } else if (activities.contains(NULL_UUID)) { 0442 newActivitiesForLauncher[url] = {NULL_UUID}; 0443 0444 } else { 0445 // We are not on all activities, append the new ones 0446 newActivitiesForLauncher[url] += activities; 0447 } 0448 } 0449 0450 if (newLaunchersOrder != d->launchersOrder) { 0451 const bool isOrderChanged = std::all_of(newLaunchersOrder.cbegin(), 0452 newLaunchersOrder.cend(), 0453 [this](const QUrl &url) { 0454 return d->launchersOrder.contains(url); 0455 }) 0456 && newLaunchersOrder.size() == d->launchersOrder.size(); 0457 0458 if (isOrderChanged) { 0459 for (int i = 0; i < newLaunchersOrder.size(); i++) { 0460 int oldRow = d->launchersOrder.indexOf(newLaunchersOrder.at(i)); 0461 0462 if (oldRow != i) { 0463 beginMoveRows(QModelIndex(), oldRow, oldRow, QModelIndex(), i); 0464 d->launchersOrder.move(oldRow, i); 0465 endMoveRows(); 0466 } 0467 } 0468 } else { 0469 // Use Remove/Insert to update the manual sort map in TasksModel 0470 if (!d->launchersOrder.empty()) { 0471 beginRemoveRows(QModelIndex(), 0, d->launchersOrder.size() - 1); 0472 0473 d->launchersOrder.clear(); 0474 d->activitiesForLauncher.clear(); 0475 0476 endRemoveRows(); 0477 } 0478 0479 if (!newLaunchersOrder.empty()) { 0480 beginInsertRows(QModelIndex(), 0, newLaunchersOrder.size() - 1); 0481 0482 d->launchersOrder = newLaunchersOrder; 0483 d->activitiesForLauncher = newActivitiesForLauncher; 0484 0485 endInsertRows(); 0486 } 0487 } 0488 0489 Q_EMIT launcherListChanged(); 0490 0491 } else if (newActivitiesForLauncher != d->activitiesForLauncher) { 0492 for (int i = 0; i < d->launchersOrder.size(); i++) { 0493 const QUrl &url = d->launchersOrder.at(i); 0494 0495 if (d->activitiesForLauncher[url] != newActivitiesForLauncher[url]) { 0496 d->activitiesForLauncher[url] = newActivitiesForLauncher[url]; 0497 Q_EMIT dataChanged(index(i, 0), index(i, 0), {Activities}); 0498 } 0499 } 0500 } 0501 } 0502 0503 bool LauncherTasksModel::requestAddLauncher(const QUrl &url) 0504 { 0505 return d->requestAddLauncherToActivities(url, {NULL_UUID}); 0506 } 0507 0508 bool LauncherTasksModel::requestRemoveLauncher(const QUrl &url) 0509 { 0510 return d->requestRemoveLauncherFromActivities(url, {NULL_UUID}); 0511 } 0512 0513 bool LauncherTasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity) 0514 { 0515 return d->requestAddLauncherToActivities(url, {activity}); 0516 } 0517 0518 bool LauncherTasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity) 0519 { 0520 return d->requestRemoveLauncherFromActivities(url, {activity}); 0521 } 0522 0523 QStringList LauncherTasksModel::launcherActivities(const QUrl &_url) const 0524 { 0525 const auto position = launcherPosition(_url); 0526 0527 if (position == -1) { 0528 // If we do not have this launcher, return an empty list 0529 return {}; 0530 0531 } else { 0532 const auto url = d->launchersOrder.at(position); 0533 0534 // If the launcher is on all activities, return a null uuid 0535 return d->activitiesForLauncher.contains(url) ? d->activitiesForLauncher[url].values() : QStringList{NULL_UUID}; 0536 } 0537 } 0538 0539 int LauncherTasksModel::launcherPosition(const QUrl &url) const 0540 { 0541 for (int i = 0; i < d->launchersOrder.count(); ++i) { 0542 if (launcherUrlsMatch(url, d->appData(d->launchersOrder.at(i)).url, IgnoreQueryItems)) { 0543 return i; 0544 } 0545 } 0546 0547 return -1; 0548 } 0549 0550 void LauncherTasksModel::requestActivate(const QModelIndex &index) 0551 { 0552 requestNewInstance(index); 0553 } 0554 0555 void LauncherTasksModel::requestNewInstance(const QModelIndex &index) 0556 { 0557 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count()) { 0558 return; 0559 } 0560 0561 runApp(d->appData(d->launchersOrder.at(index.row()))); 0562 } 0563 0564 void LauncherTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls) 0565 { 0566 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count() || urls.isEmpty()) { 0567 return; 0568 } 0569 0570 const QUrl &url = d->launchersOrder.at(index.row()); 0571 0572 KService::Ptr service; 0573 0574 if (url.scheme() == QLatin1String("applications")) { 0575 service = KService::serviceByMenuId(url.path()); 0576 } else if (url.scheme() == QLatin1String("preferred")) { 0577 service = KService::serviceByStorageId(defaultApplication(url)); 0578 } else { 0579 service = KService::serviceByDesktopPath(url.toLocalFile()); 0580 } 0581 0582 if (!service || !service->isApplication()) { 0583 return; 0584 } 0585 0586 auto *job = new KIO::ApplicationLauncherJob(service); 0587 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); 0588 job->setUrls(urls); 0589 0590 job->start(); 0591 0592 KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.libtaskmanager")); 0593 } 0594 0595 }