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 foreach (const QUrl &launcher, 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 = "[" + d->activitiesForLauncher[launcher].values().join(",") + "]\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 QStringList _activities;
384 QUrl url;
385
386 std::tie(url, _activities) = deserializeLauncher(serializedLauncher);
387
388 auto activities = ActivitiesSet(_activities.cbegin(), _activities.cend());
389
390 // Is url is not valid, ignore it
391 if (!isValidLauncherUrl(url)) {
392 continue;
393 }
394
395 // If we have a null uuid, it means we are on all activities
396 // and we should contain only the null uuid
397 if (isOnAllActivities(activities)) {
398 activities = {NULL_UUID};
399
400 } else {
401 // Filter out invalid activities
402 const auto allActivities = d->activitiesConsumer.activities();
403 ActivitiesSet validActivities;
404 for (const auto &activity : std::as_const(activities)) {
405 if (allActivities.contains(activity)) {
406 validActivities << activity;
407 }
408 }
409
410 if (validActivities.isEmpty()) {
411 // If all activities that had this launcher are
412 // removed, we are killing the launcher as well
413 continue;
414 }
415
416 activities = validActivities;
417 }
418
419 // Is the url a duplicate?
420 const auto location = std::find_if(newLaunchersOrder.begin(), newLaunchersOrder.end(), [&url](const QUrl &item) {
421 return launcherUrlsMatch(url, item, IgnoreQueryItems);
422 });
423
424 if (location != newLaunchersOrder.end()) {
425 // It is a duplicate
426 url = *location;
427
428 } else {
429 // It is not a duplicate, we need to add it
430 // to the list of registered launchers
431 newLaunchersOrder << url;
432 }
433
434 if (!newActivitiesForLauncher.contains(url)) {
435 // This is the first time we got this url
436 newActivitiesForLauncher[url] = activities;
437
438 } else if (newActivitiesForLauncher[url].contains(NULL_UUID)) {
439 // Do nothing, we are already on all activities
440
441 } else if (activities.contains(NULL_UUID)) {
442 newActivitiesForLauncher[url] = {NULL_UUID};
443
444 } else {
445 // We are not on all activities, append the new ones
446 newActivitiesForLauncher[url] += activities;
447 }
448 }
449
450 if (newLaunchersOrder != d->launchersOrder) {
451 const bool isOrderChanged = std::all_of(newLaunchersOrder.cbegin(),
452 newLaunchersOrder.cend(),
453 [this](const QUrl &url) {
454 return d->launchersOrder.contains(url);
455 })
456 && newLaunchersOrder.size() == d->launchersOrder.size();
457
458 if (isOrderChanged) {
459 for (int i = 0; i < newLaunchersOrder.size(); i++) {
460 int oldRow = d->launchersOrder.indexOf(newLaunchersOrder.at(i));
461
462 if (oldRow != i) {
463 beginMoveRows(QModelIndex(), oldRow, oldRow, QModelIndex(), i);
464 d->launchersOrder.move(oldRow, i);
465 endMoveRows();
466 }
467 }
468 } else {
469 // Use Remove/Insert to update the manual sort map in TasksModel
470 if (!d->launchersOrder.empty()) {
471 beginRemoveRows(QModelIndex(), 0, d->launchersOrder.size() - 1);
472
473 d->launchersOrder.clear();
474 d->activitiesForLauncher.clear();
475
476 endRemoveRows();
477 }
478
479 if (!newLaunchersOrder.empty()) {
480 beginInsertRows(QModelIndex(), 0, newLaunchersOrder.size() - 1);
481
482 d->launchersOrder = newLaunchersOrder;
483 d->activitiesForLauncher = newActivitiesForLauncher;
484
485 endInsertRows();
486 }
487 }
488
489 Q_EMIT launcherListChanged();
490
491 } else if (newActivitiesForLauncher != d->activitiesForLauncher) {
492 for (int i = 0; i < d->launchersOrder.size(); i++) {
493 const QUrl &url = d->launchersOrder.at(i);
494
495 if (d->activitiesForLauncher[url] != newActivitiesForLauncher[url]) {
496 d->activitiesForLauncher[url] = newActivitiesForLauncher[url];
497 Q_EMIT dataChanged(index(i, 0), index(i, 0), {Activities});
498 }
499 }
500 }
501}
502
503bool LauncherTasksModel::requestAddLauncher(const QUrl &url)
504{
505 return d->requestAddLauncherToActivities(url, {NULL_UUID});
506}
507
508bool LauncherTasksModel::requestRemoveLauncher(const QUrl &url)
509{
510 return d->requestRemoveLauncherFromActivities(url, {NULL_UUID});
511}
512
513bool LauncherTasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity)
514{
515 return d->requestAddLauncherToActivities(url, {activity});
516}
517
518bool LauncherTasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity)
519{
520 return d->requestRemoveLauncherFromActivities(url, {activity});
521}
522
523QStringList LauncherTasksModel::launcherActivities(const QUrl &_url) const
524{
525 const auto position = launcherPosition(_url);
526
527 if (position == -1) {
528 // If we do not have this launcher, return an empty list
529 return {};
530
531 } else {
532 const auto url = d->launchersOrder.at(position);
533
534 // If the launcher is on all activities, return a null uuid
535 return d->activitiesForLauncher.contains(url) ? d->activitiesForLauncher[url].values() : QStringList{NULL_UUID};
536 }
537}
538
539int LauncherTasksModel::launcherPosition(const QUrl &url) const
540{
541 for (int i = 0; i < d->launchersOrder.count(); ++i) {
542 if (launcherUrlsMatch(url, d->appData(d->launchersOrder.at(i)).url, IgnoreQueryItems)) {
543 return i;
544 }
545 }
546
547 return -1;
548}
549
550void LauncherTasksModel::requestActivate(const QModelIndex &index)
551{
552 requestNewInstance(index);
553}
554
555void LauncherTasksModel::requestNewInstance(const QModelIndex &index)
556{
557 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count()) {
558 return;
559 }
560
561 runApp(d->appData(d->launchersOrder.at(index.row())));
562}
563
564void LauncherTasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
565{
566 if (!index.isValid() || index.model() != this || index.row() < 0 || index.row() >= d->launchersOrder.count() || urls.isEmpty()) {
567 return;
568 }
569
570 const QUrl &url = d->launchersOrder.at(index.row());
571
572 KService::Ptr service;
573
574 if (url.scheme() == QLatin1String("applications")) {
575 service = KService::serviceByMenuId(url.path());
576 } else if (url.scheme() == QLatin1String("preferred")) {
577 service = KService::serviceByStorageId(defaultApplication(url));
578 } else {
580 }
581
582 if (!service || !service->isApplication()) {
583 return;
584 }
585
586 auto *job = new KIO::ApplicationLauncherJob(service);
588 job->setUrls(urls);
589
590 job->start();
591
592 KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.libtaskmanager"));
593}
594
595}
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)
KGuiItem remove()
QCA_EXPORT void init()
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 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 Tue Mar 26 2024 11:17:42 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.