Plasma-workspace

launchertasksmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7#include "launchertasksmodel.h"
8#include "tasktools.h"
9
10#include <KDesktopFile>
11#include <KNotificationJobUiDelegate>
12#include <KService>
13#include <KSycoca>
14#include <KWindowSystem>
15
16#include <PlasmaActivities/Consumer>
17#include <PlasmaActivities/ResourceInstance>
18
19#include <KIO/ApplicationLauncherJob>
20
21#include <QHash>
22#include <QIcon>
23#include <QSet>
24#include <QTimer>
25#include <QUrlQuery>
26
27#include "launchertasksmodel_p.h"
28#include <chrono>
29
30using namespace std::chrono_literals;
31
32namespace TaskManager
33{
34typedef QSet<QString> ActivitiesSet;
35
36template<typename ActivitiesCollection>
37inline bool isOnAllActivities(const ActivitiesCollection &activities)
38{
39 return activities.isEmpty() || activities.contains(NULL_UUID);
40}
41
42class Q_DECL_HIDDEN LauncherTasksModel::Private
43{
44public:
45 Private(LauncherTasksModel *q);
46
47 KActivities::Consumer activitiesConsumer;
48
49 QList<QUrl> launchersOrder;
50
51 QHash<QUrl, ActivitiesSet> activitiesForLauncher;
52 inline void setActivitiesForLauncher(const QUrl &url, const ActivitiesSet &activities)
53 {
54 if (activities.size() == activitiesConsumer.activities().size()) {
55 activitiesForLauncher[url] = {NULL_UUID};
56 } else {
57 activitiesForLauncher[url] = activities;
58 }
59 }
60
61 QHash<QUrl, AppData> appDataCache;
62 QTimer sycocaChangeTimer;
63
64 void init();
65 AppData appData(const QUrl &url);
66
67 bool requestAddLauncherToActivities(const QUrl &_url, const QStringList &activities);
68 bool requestRemoveLauncherFromActivities(const QUrl &_url, const QStringList &activities);
69
70private:
71 LauncherTasksModel *q;
72};
73
74LauncherTasksModel::Private::Private(LauncherTasksModel *q)
75 : q(q)
76{
77}
78
79void LauncherTasksModel::Private::init()
80{
81 sycocaChangeTimer.setSingleShot(true);
82 sycocaChangeTimer.setInterval(100ms);
83
84 QObject::connect(&sycocaChangeTimer, &QTimer::timeout, q, [this]() {
85 if (!launchersOrder.count()) {
86 return;
87 }
88
89 appDataCache.clear();
90
91 // Emit changes of all roles satisfied from app data cache.
92 Q_EMIT q->dataChanged(q->index(0, 0),
93 q->index(launchersOrder.count() - 1, 0),
94 QList<int>{Qt::DisplayRole,
95 Qt::DecorationRole,
96 AbstractTasksModel::AppId,
97 AbstractTasksModel::AppName,
98 AbstractTasksModel::GenericName,
99 AbstractTasksModel::LauncherUrl,
100 AbstractTasksModel::LauncherUrlWithoutIcon});
101 });
102
104 sycocaChangeTimer.start();
105 });
106}
107
108AppData LauncherTasksModel::Private::appData(const QUrl &url)
109{
110 const auto &it = appDataCache.constFind(url);
111
112 if (it != appDataCache.constEnd()) {
113 return *it;
114 }
115
116 const AppData &data = appDataFromUrl(url, QIcon::fromTheme(QLatin1String("unknown")));
117
118 appDataCache.insert(url, data);
119
120 return data;
121}
122
123bool LauncherTasksModel::Private::requestAddLauncherToActivities(const QUrl &_url, const QStringList &_activities)
124{
125 QUrl url(_url);
126 if (!isValidLauncherUrl(url)) {
127 return false;
128 }
129
130 const auto activities = ActivitiesSet(_activities.cbegin(), _activities.cend());
131
133 KDesktopFile f(url.toLocalFile());
134
135 const KService::Ptr service = KService::serviceByStorageId(f.fileName());
136
137 // Resolve to non-absolute menuId-based URL if possible.
138 if (service) {
139 const QString &menuId = service->menuId();
140
141 if (!menuId.isEmpty()) {
142 url = QUrl(QLatin1String("applications:") + menuId);
143 }
144 }
145 }
146
147 // Merge duplicates
148 int row = -1;
149 for (const QUrl &launcher : std::as_const(launchersOrder)) {
150 ++row;
151
152 if (launcherUrlsMatch(url, launcher, IgnoreQueryItems)) {
153 ActivitiesSet newActivities;
154
155 // Use the key we established equivalence to ('launcher').
156 if (!activitiesForLauncher.contains(launcher)) {
157 // If we don't have the activities assigned to this url
158 // for some reason
159 newActivities = activities;
160
161 } else {
162 if (isOnAllActivities(activities)) {
163 // If the new list is empty, or has a null uuid, this
164 // launcher should be on all activities
165 newActivities = ActivitiesSet{NULL_UUID};
166
167 } else if (isOnAllActivities(activitiesForLauncher[launcher])) {
168 // If we have been on all activities before, and we have
169 // been asked to be on a specific one, lets make an
170 // exception - we will set the activities to exactly
171 // what we have been asked
172 newActivities = activities;
173
174 } else {
175 newActivities += activities;
176 newActivities += activitiesForLauncher[launcher];
177 }
178 }
179
180 if (newActivities != activitiesForLauncher[launcher]) {
181 setActivitiesForLauncher(launcher, newActivities);
182
183 Q_EMIT q->dataChanged(q->index(row, 0), q->index(row, 0));
184
185 Q_EMIT q->launcherListChanged();
186 return true;
187 }
188
189 return false;
190 }
191 }
192
193 // This is a new one
194 const auto count = launchersOrder.count();
195 q->beginInsertRows(QModelIndex(), count, count);
196 setActivitiesForLauncher(url, activities);
197 launchersOrder.append(url);
198 q->endInsertRows();
199
200 Q_EMIT q->launcherListChanged();
201
202 return true;
203}
204
205bool LauncherTasksModel::Private::requestRemoveLauncherFromActivities(const QUrl &url, const QStringList &activities)
206{
207 for (int row = 0; row < launchersOrder.count(); ++row) {
208 const QUrl launcher = launchersOrder.at(row);
209
210 if (launcherUrlsMatch(url, launcher, IgnoreQueryItems) || launcherUrlsMatch(url, appData(launcher).url, IgnoreQueryItems)) {
211 const auto currentActivities = activitiesForLauncher[url];
212 ActivitiesSet newActivities;
213
214 bool remove = false;
215 bool update = false;
216
217 if (isOnAllActivities(currentActivities)) {
218 // We are currently on all activities.
219 // Should we go away, or just remove from the current one?
220
221 if (isOnAllActivities(activities)) {
222 remove = true;
223
224 } else {
225 const auto _activities = activitiesConsumer.activities();
226 for (const auto &activity : _activities) {
227 if (!activities.contains(activity)) {
228 newActivities << activity;
229 } else {
230 update = true;
231 }
232 }
233 }
234
235 } else if (isOnAllActivities(activities)) {
236 remove = true;
237
238 } else {
239 // We weren't on all activities, just remove those that
240 // we were on
241
242 for (const auto &activity : currentActivities) {
243 if (!activities.contains(activity)) {
244 newActivities << activity;
245 }
246 }
247
248 if (newActivities.isEmpty()) {
249 remove = true;
250 } else {
251 update = true;
252 }
253 }
254
255 if (remove) {
256 q->beginRemoveRows(QModelIndex(), row, row);
257 appDataCache.remove(launcher);
258 launchersOrder.removeAt(row);
259 activitiesForLauncher.remove(url);
260 q->endRemoveRows();
261
262 } else if (update) {
263 setActivitiesForLauncher(url, newActivities);
264
265 Q_EMIT q->dataChanged(q->index(row, 0), q->index(row, 0));
266 }
267
268 if (remove || update) {
269 Q_EMIT q->launcherListChanged();
270 return true;
271 }
272 }
273 }
274
275 return false;
276}
277
278LauncherTasksModel::LauncherTasksModel(QObject *parent)
279 : AbstractTasksModel(parent)
280 , d(new Private(this))
281{
282 d->init();
283}
284
285LauncherTasksModel::~LauncherTasksModel()
286{
287}
288
289QVariant LauncherTasksModel::data(const QModelIndex &index, int role) const
290{
291 if (!index.isValid() || index.row() >= d->launchersOrder.count()) {
292 return QVariant();
293 }
294
295 const QUrl &url = d->launchersOrder.at(index.row());
296 const AppData &data = d->appData(url);
297 if (role == Qt::DisplayRole) {
298 return data.name;
299 } else if (role == Qt::DecorationRole) {
300 return data.icon;
301 } else if (role == AppId) {
302 return data.id;
303 } else if (role == AppName) {
304 return data.name;
305 } else if (role == GenericName) {
306 return data.genericName;
307 } else if (role == LauncherUrl) {
308 // Take resolved URL from cache.
309 return data.url;
310 } else if (role == LauncherUrlWithoutIcon) {
311 // Take resolved URL from cache.
312 QUrl url = data.url;
313
314 if (url.hasQuery()) {
315 QUrlQuery query(url);
316 query.removeQueryItem(QLatin1String("iconData"));
317 url.setQuery(query);
318 }
319
320 return url;
321 } else if (role == IsLauncher) {
322 return true;
323 } else if (role == IsVirtualDesktopsChangeable) {
324 return false;
325 } else if (role == IsOnAllVirtualDesktops) {
326 return true;
327 } else if (role == Activities) {
328 return QStringList(d->activitiesForLauncher[url].values());
329 } else if (role == CanLaunchNewInstance) {
330 return false;
331 }
332
333 return AbstractTasksModel::data(index, role);
334}
335
336int LauncherTasksModel::rowCount(const QModelIndex &parent) const
337{
338 return parent.isValid() ? 0 : d->launchersOrder.count();
339}
340
341int LauncherTasksModel::rowCountForActivity(const QString &activity) const
342{
343 if (activity == NULL_UUID || activity.isEmpty()) {
344 return rowCount();
345 }
346
347 return std::count_if(d->launchersOrder.cbegin(), d->launchersOrder.cend(), [this, &activity](const QUrl &url) {
348 const auto &set = d->activitiesForLauncher[url];
349 return set.contains(NULL_UUID) || set.contains(activity);
350 });
351}
352
353QStringList LauncherTasksModel::launcherList() const
354{
355 // Serializing the launchers
356 QStringList result;
357
358 for (const auto &launcher : std::as_const(d->launchersOrder)) {
359 const auto &activities = d->activitiesForLauncher[launcher];
360
361 QString serializedLauncher;
362 if (isOnAllActivities(activities)) {
363 serializedLauncher = launcher.toString();
364
365 } else {
366 serializedLauncher = u'[' + d->activitiesForLauncher[launcher].values().join(u',') + u"]\n" + launcher.toString();
367 }
368
369 result << serializedLauncher;
370 }
371
372 return result;
373}
374
375void LauncherTasksModel::setLauncherList(const QStringList &serializedLaunchers)
376{
377 // Clearing everything
378 QList<QUrl> newLaunchersOrder;
379 QHash<QUrl, ActivitiesSet> newActivitiesForLauncher;
380
381 // Loading the activity to launchers map
382 for (const auto &serializedLauncher : serializedLaunchers) {
383 QList<QStringView> _activities;
384 QUrl url;
385
386 std::tie(url, _activities) = deserializeLauncher(serializedLauncher);
387
388 // Is url is not valid, ignore it
389 if (!isValidLauncherUrl(url)) {
390 continue;
391 }
392
393 // If we have a null uuid, it means we are on all activities
394 // and we should contain only the null uuid
395 ActivitiesSet activities;
396 if (isOnAllActivities(_activities)) {
397 activities = {NULL_UUID};
398
399 } else {
400 // Filter out invalid activities
401 const auto allActivities = d->activitiesConsumer.activities();
402 ActivitiesSet validActivities;
403 for (const QStringView activity : std::as_const(_activities)) {
404 if (allActivities.contains(activity)) {
405 validActivities << activity.toString(); // Need a deep copy
406 }
407 }
408
409 if (validActivities.isEmpty()) {
410 // If all activities that had this launcher are
411 // removed, we are killing the launcher as well
412 continue;
413 }
414
415 activities = std::move(validActivities);
416 }
417
418 // Is the url a duplicate?
419 const auto location = std::find_if(newLaunchersOrder.begin(), newLaunchersOrder.end(), [&url](const QUrl &item) {
420 return launcherUrlsMatch(url, item, IgnoreQueryItems);
421 });
422
423 if (location != newLaunchersOrder.end()) {
424 // It is a duplicate
425 url = *location;
426
427 } else {
428 // It is not a duplicate, we need to add it
429 // to the list of registered launchers
430 newLaunchersOrder << url;
431 }
432
433 if (!newActivitiesForLauncher.contains(url)) {
434 // This is the first time we got this url
435 newActivitiesForLauncher[url] = activities;
436
437 } else if (newActivitiesForLauncher[url].contains(NULL_UUID)) {
438 // Do nothing, we are already on all activities
439
440 } else if (activities.contains(NULL_UUID)) {
441 newActivitiesForLauncher[url] = {NULL_UUID};
442
443 } else {
444 // We are not on all activities, append the new ones
445 newActivitiesForLauncher[url] += activities;
446 }
447 }
448
449 if (newLaunchersOrder != d->launchersOrder) {
450 const bool isOrderChanged = std::all_of(newLaunchersOrder.cbegin(),
451 newLaunchersOrder.cend(),
452 [this](const QUrl &url) {
453 return d->launchersOrder.contains(url);
454 })
455 && newLaunchersOrder.size() == d->launchersOrder.size();
456
457 if (isOrderChanged) {
458 for (int i = 0; i < newLaunchersOrder.size(); i++) {
459 int oldRow = d->launchersOrder.indexOf(newLaunchersOrder.at(i));
460
461 if (oldRow != i) {
462 beginMoveRows(QModelIndex(), oldRow, oldRow, QModelIndex(), i);
463 d->launchersOrder.move(oldRow, i);
464 endMoveRows();
465 }
466 }
467 } else {
468 // Use Remove/Insert to update the manual sort map in TasksModel
469 if (!d->launchersOrder.empty()) {
470 beginRemoveRows(QModelIndex(), 0, d->launchersOrder.size() - 1);
471
472 d->launchersOrder.clear();
473 d->activitiesForLauncher.clear();
474
475 endRemoveRows();
476 }
477
478 if (!newLaunchersOrder.empty()) {
479 beginInsertRows(QModelIndex(), 0, newLaunchersOrder.size() - 1);
480
481 d->launchersOrder = newLaunchersOrder;
482 d->activitiesForLauncher = newActivitiesForLauncher;
483
484 endInsertRows();
485 }
486 }
487
488 Q_EMIT launcherListChanged();
489
490 } else if (newActivitiesForLauncher != d->activitiesForLauncher) {
491 for (int i = 0; i < d->launchersOrder.size(); i++) {
492 const QUrl &url = d->launchersOrder.at(i);
493
494 if (d->activitiesForLauncher[url] != newActivitiesForLauncher[url]) {
495 d->activitiesForLauncher[url] = newActivitiesForLauncher[url];
496 Q_EMIT dataChanged(index(i, 0), index(i, 0), {Activities});
497 }
498 }
499 }
500}
501
502bool LauncherTasksModel::requestAddLauncher(const QUrl &url)
503{
504 return d->requestAddLauncherToActivities(url, {NULL_UUID});
505}
506
507bool LauncherTasksModel::requestRemoveLauncher(const QUrl &url)
508{
509 return d->requestRemoveLauncherFromActivities(url, {NULL_UUID});
510}
511
512bool LauncherTasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity)
513{
514 return d->requestAddLauncherToActivities(url, {activity});
515}
516
517bool LauncherTasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity)
518{
519 return d->requestRemoveLauncherFromActivities(url, {activity});
520}
521
522QStringList LauncherTasksModel::launcherActivities(const QUrl &_url) const
523{
524 const auto position = launcherPosition(_url);
525
526 if (position == -1) {
527 // If we do not have this launcher, return an empty list
528 return {};
529
530 } else {
531 const auto url = d->launchersOrder.at(position);
532
533 // If the launcher is on all activities, return a null uuid
534 return d->activitiesForLauncher.contains(url) ? d->activitiesForLauncher[url].values() : QStringList{NULL_UUID};
535 }
536}
537
538int LauncherTasksModel::launcherPosition(const QUrl &url) const
539{
540 for (int i = 0; i < d->launchersOrder.count(); ++i) {
541 if (launcherUrlsMatch(url, d->appData(d->launchersOrder.at(i)).url, IgnoreQueryItems)) {
542 return i;
543 }
544 }
545
546 return -1;
547}
548
549void LauncherTasksModel::requestActivate(const QModelIndex &index)
550{
551 requestNewInstance(index);
552}
553
554void LauncherTasksModel::requestNewInstance(const QModelIndex &index)
555{
556 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count()) {
557 return;
558 }
559
560 runApp(d->appData(d->launchersOrder.at(index.row())));
561}
562
563void LauncherTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
564{
565 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count() || urls.isEmpty()) {
566 return;
567 }
568
569 const QUrl &url = d->launchersOrder.at(index.row());
570
571 KService::Ptr service;
572
573 if (url.scheme() == QLatin1String("applications")) {
574 service = KService::serviceByMenuId(url.path());
575 } else if (url.scheme() == QLatin1String("preferred")) {
576 service = KService::serviceByStorageId(defaultApplication(url));
577 } else {
579 }
580
581 if (!service || !service->isApplication()) {
582 return;
583 }
584
585 auto *job = new KIO::ApplicationLauncherJob(service);
587 job->setUrls(urls);
588
589 job->start();
590
591 KActivities::ResourceInstance::notifyAccessed(QUrl(QString(u"applications:" + service->storageId())), QStringLiteral("org.kde.libtaskmanager"));
592}
593
594}
static bool isDesktopFile(const QString &path)
static Ptr serviceByStorageId(const QString &_storageId)
static Ptr serviceByMenuId(const QString &_menuId)
static Ptr serviceByDesktopPath(const QString &_path)
static KSycoca * self()
Q_SIGNAL void databaseChanged()
void update(Part *part, const QByteArray &data, qint64 dataSize)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
void init(KXmlGuiWindow *window, KGameDifficulty *difficulty=nullptr)
KGuiItem remove()
bool contains(const Key &key) const const
bool remove(const Key &key)
QIcon fromTheme(const QString &name)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
qsizetype count() const const
bool empty() const const
iterator end()
bool isEmpty() const const
void removeAt(qsizetype i)
qsizetype size() const const
bool isValid() const const
const QAbstractItemModel * model() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool contains(const QSet< T > &other) const const
bool isEmpty() const const
bool isEmpty() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
DisplayRole
void timeout()
bool hasQuery() const const
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
void setQuery(const QString &query, ParsingMode mode)
QString toLocalFile() const const
QString toString(FormattingOptions options) const const
QString url(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:19:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.