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 }