Plasma-workspace

tasksmodel.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 "tasksmodel.h"
8#include "activityinfo.h"
9#include "concatenatetasksproxymodel.h"
10#include "flattentaskgroupsproxymodel.h"
11#include "taskfilterproxymodel.h"
12#include "taskgroupingproxymodel.h"
13#include "tasktools.h"
14#include "virtualdesktopinfo.h"
15
16#include "launchertasksmodel.h"
17#include "startuptasksmodel.h"
18#include "windowtasksmodel.h"
19
20#include "launchertasksmodel_p.h"
21
22#include <QDateTime>
23#include <QGuiApplication>
24#include <QList>
25#include <QTimer>
26#include <QUrl>
27
28#include <numeric>
29
30namespace TaskManager
31{
32class Q_DECL_HIDDEN TasksModel::Private
33{
34public:
35 Private(TasksModel *q);
36 ~Private();
37
38 static int instanceCount;
39
40 static WindowTasksModel *windowTasksModel;
41 static StartupTasksModel *startupTasksModel;
42 LauncherTasksModel *launcherTasksModel = nullptr;
43 ConcatenateTasksProxyModel *concatProxyModel = nullptr;
44 TaskFilterProxyModel *filterProxyModel = nullptr;
45 TaskGroupingProxyModel *groupingProxyModel = nullptr;
46 FlattenTaskGroupsProxyModel *flattenGroupsProxyModel = nullptr;
47 AbstractTasksModelIface *abstractTasksSourceModel = nullptr;
48
49 bool anyTaskDemandsAttention = false;
50
51 int launcherCount = 0;
52
53 SortMode sortMode = SortAlpha;
54 bool separateLaunchers = true;
55 bool launchInPlace = false;
56 bool hideActivatedLaunchers = true;
57 bool launchersEverSet = false;
58 bool launcherSortingDirty = false;
59 bool launcherCheckNeeded = false;
60 QList<int> sortedPreFilterRows;
61 QList<int> sortRowInsertQueue;
62 bool sortRowInsertQueueStale = false;
63 std::shared_ptr<VirtualDesktopInfo> virtualDesktopInfo;
64 QHash<QString, int> activityTaskCounts;
65 std::shared_ptr<ActivityInfo> activityInfo;
66
67 bool groupInline = false;
68 int groupingWindowTasksThreshold = -1;
69
70 bool usedByQml = false;
71 bool componentComplete = false;
72
73 void initModels();
74 void initLauncherTasksModel();
75 void updateAnyTaskDemandsAttention();
76 void updateManualSortMap();
77 void consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex);
78 void updateGroupInline();
79 QModelIndex preFilterIndex(const QModelIndex &sourceIndex) const;
80 void updateActivityTaskCounts();
81 void forceResort();
82 bool lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers = false) const;
83
84private:
85 TasksModel *const q;
86};
87
88class TasksModel::TasksModelLessThan
89{
90public:
91 inline TasksModelLessThan(const QAbstractItemModel *s, TasksModel *p, bool sortOnlyLaunchers)
92 : sourceModel(s)
93 , tasksModel(p)
94 , sortOnlyLaunchers(sortOnlyLaunchers)
95 {
96 }
97
98 inline bool operator()(int r1, int r2) const
99 {
100 QModelIndex i1 = sourceModel->index(r1, 0);
101 QModelIndex i2 = sourceModel->index(r2, 0);
102 return tasksModel->d->lessThan(i1, i2, sortOnlyLaunchers);
103 }
104
105private:
106 const QAbstractItemModel *sourceModel;
107 const TasksModel *tasksModel;
108 bool sortOnlyLaunchers;
109};
110
111int TasksModel::Private::instanceCount = 0;
112WindowTasksModel *TasksModel::Private::windowTasksModel = nullptr;
113StartupTasksModel *TasksModel::Private::startupTasksModel = nullptr;
114
115TasksModel::Private::Private(TasksModel *q)
116 : q(q)
117{
118 ++instanceCount;
119}
120
121TasksModel::Private::~Private()
122{
123 --instanceCount;
124
125 if (!instanceCount) {
126 delete windowTasksModel;
127 windowTasksModel = nullptr;
128 delete startupTasksModel;
129 startupTasksModel = nullptr;
130 }
131}
132
133void TasksModel::Private::initModels()
134{
135 // NOTE: Overview over the entire model chain assembled here:
136 // WindowTasksModel, StartupTasksModel, LauncherTasksModel
137 // -> concatProxyModel concatenates them into a single list.
138 // -> filterProxyModel filters by state (e.g. virtual desktop).
139 // -> groupingProxyModel groups by application (we go from flat list to tree).
140 // -> flattenGroupsProxyModel (optionally, if groupInline == true) flattens groups out.
141 // -> TasksModel collapses (top-level) items into task lifecycle abstraction; sorts.
142
143 concatProxyModel = new ConcatenateTasksProxyModel(q);
144
145 if (!windowTasksModel) {
146 windowTasksModel = new WindowTasksModel();
147 }
148
149 concatProxyModel->addSourceModel(windowTasksModel);
150
151 QObject::connect(windowTasksModel, &QAbstractItemModel::rowsInserted, q, [this]() {
152 if (sortMode == SortActivity) {
153 updateActivityTaskCounts();
154 }
155 });
156
157 QObject::connect(windowTasksModel, &QAbstractItemModel::rowsRemoved, q, [this]() {
158 if (sortMode == SortActivity) {
159 updateActivityTaskCounts();
160 forceResort();
161 }
162 // the active task may have potentially changed, so signal that so that users
163 // will recompute it
164 Q_EMIT q->activeTaskChanged();
165 });
166
167 QObject::connect(windowTasksModel,
169 q,
170 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
171 Q_UNUSED(topLeft)
172 Q_UNUSED(bottomRight)
173
174 if (sortMode == SortActivity && roles.contains(AbstractTasksModel::Activities)) {
175 updateActivityTaskCounts();
176 }
177
178 if (roles.contains(AbstractTasksModel::IsActive)) {
179 Q_EMIT q->activeTaskChanged();
180 }
181
182 // In manual sort mode, updateManualSortMap() may consult the sortRowInsertQueue
183 // for new tasks to sort in. Hidden tasks remain in the queue to potentially sort
184 // them later, when they are are actually revealed to the user.
185 // This is particularly useful in concert with taskmanagerrulesrc's SkipTaskbar
186 // key, which is used to hide window tasks which update from bogus to useful
187 // window metadata early in startup. The role change then coincides with positive
188 // app identification, which is when updateManualSortMap() becomes able to sort the
189 // task adjacent to its launcher when required to do so.
190 if (sortMode == SortManual && roles.contains(AbstractTasksModel::SkipTaskbar)) {
191 updateManualSortMap();
192 }
193 });
194
195 if (!startupTasksModel) {
196 startupTasksModel = new StartupTasksModel();
197 }
198
199 concatProxyModel->addSourceModel(startupTasksModel);
200
201 // If we're in manual sort mode, we need to seed the sort map on pending row
202 // insertions.
203 QObject::connect(concatProxyModel, &QAbstractItemModel::rowsAboutToBeInserted, q, [this](const QModelIndex &parent, int start, int end) {
204 Q_UNUSED(parent)
205
206 if (sortMode != SortManual) {
207 return;
208 }
209
210 const int delta = (end - start) + 1;
211 for (auto it = sortedPreFilterRows.begin(); it != sortedPreFilterRows.end(); ++it) {
212 if ((*it) >= start) {
213 *it += delta;
214 }
215 }
216
217 for (int i = start; i <= end; ++i) {
218 sortedPreFilterRows.append(i);
219
220 if (!separateLaunchers) {
221 if (sortRowInsertQueueStale) {
222 sortRowInsertQueue.clear();
223 sortRowInsertQueueStale = false;
224 }
225
226 sortRowInsertQueue.append(sortedPreFilterRows.count() - 1);
227 }
228 }
229 });
230
231 // If we're in manual sort mode, we need to update the sort map on row insertions.
232 QObject::connect(concatProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int start, int end) {
233 Q_UNUSED(parent)
234 Q_UNUSED(start)
235 Q_UNUSED(end)
236
237 if (sortMode == SortManual) {
238 updateManualSortMap();
239 }
240 });
241
242 // If we're in manual sort mode, we need to update the sort map after row removals.
243 QObject::connect(concatProxyModel, &QAbstractItemModel::rowsRemoved, q, [this](const QModelIndex &parent, int first, int last) {
244 Q_UNUSED(parent)
245
246 if (sortMode != SortManual) {
247 return;
248 }
249
250 if (sortRowInsertQueueStale) {
251 sortRowInsertQueue.clear();
252 sortRowInsertQueueStale = false;
253 }
254
255 for (int i = first; i <= last; ++i) {
256 sortedPreFilterRows.removeOne(i);
257 }
258
259 const int delta = (last - first) + 1;
260 for (auto it = sortedPreFilterRows.begin(); it != sortedPreFilterRows.end(); ++it) {
261 if ((*it) > last) {
262 *it -= delta;
263 }
264 }
265 });
266
267 filterProxyModel = new TaskFilterProxyModel(q);
268 filterProxyModel->setSourceModel(concatProxyModel);
269 QObject::connect(filterProxyModel, &TaskFilterProxyModel::virtualDesktopChanged, q, &TasksModel::virtualDesktopChanged);
270 QObject::connect(filterProxyModel, &TaskFilterProxyModel::screenGeometryChanged, q, &TasksModel::screenGeometryChanged);
271 QObject::connect(filterProxyModel, &TaskFilterProxyModel::regionGeometryChanged, q, &TasksModel::regionGeometryChanged);
272 QObject::connect(filterProxyModel, &TaskFilterProxyModel::activityChanged, q, &TasksModel::activityChanged);
273 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByVirtualDesktopChanged, q, &TasksModel::filterByVirtualDesktopChanged);
274 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByScreenChanged, q, &TasksModel::filterByScreenChanged);
275 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByActivityChanged, q, &TasksModel::filterByActivityChanged);
276 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByRegionChanged, q, &TasksModel::filterByRegionChanged);
277 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterMinimizedChanged, q, &TasksModel::filterMinimizedChanged);
278 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMinimizedChanged, q, &TasksModel::filterNotMinimizedChanged);
279 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMaximizedChanged, q, &TasksModel::filterNotMaximizedChanged);
280 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterHiddenChanged, q, &TasksModel::filterHiddenChanged);
281
282 groupingProxyModel = new TaskGroupingProxyModel(q);
283 groupingProxyModel->setSourceModel(filterProxyModel);
284 QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::groupModeChanged, q, &TasksModel::groupModeChanged);
285 QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedAppIdsChanged, q, &TasksModel::groupingAppIdBlacklistChanged);
286 QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedLauncherUrlsChanged, q, &TasksModel::groupingLauncherUrlBlacklistChanged);
287
288 QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int first, int last) {
289 if (parent.isValid()) {
290 if (sortMode == SortManual) {
291 consolidateManualSortMapForGroup(parent);
292 }
293
294 // Existence of a group means everything below this has already been done.
295 return;
296 }
297
298 bool demandsAttentionUpdateNeeded = false;
299
300 for (int i = first; i <= last; ++i) {
301 const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0);
302 const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString();
303
304 if (sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) {
305 demandsAttentionUpdateNeeded = true;
306 }
307
308 // When we get a window we have a startup for, cause the startup to be re-filtered.
309 if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) {
310 const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString();
311
312 for (int j = 0; j < filterProxyModel->rowCount(); ++j) {
313 QModelIndex filterIndex = filterProxyModel->index(j, 0);
314
315 if (!filterIndex.data(AbstractTasksModel::IsStartup).toBool()) {
316 continue;
317 }
318
319 if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString())
320 || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) {
321 Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex);
322 }
323 }
324 }
325
326 // When we get a window or startup we have a launcher for, cause the launcher to be re-filtered.
327 if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() || sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) {
328 for (int j = 0; j < filterProxyModel->rowCount(); ++j) {
329 const QModelIndex &filterIndex = filterProxyModel->index(j, 0);
330
331 if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) {
332 continue;
333 }
334
335 if (appsMatch(sourceIndex, filterIndex)) {
336 Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex);
337 }
338 }
339 }
340 }
341
342 if (!anyTaskDemandsAttention && demandsAttentionUpdateNeeded) {
343 updateAnyTaskDemandsAttention();
344 }
345 });
346
347 QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsAboutToBeRemoved, q, [this](const QModelIndex &parent, int first, int last) {
348 // We can ignore group members.
349 if (parent.isValid()) {
350 return;
351 }
352
353 for (int i = first; i <= last; ++i) {
354 const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0);
355
356 // When a window or startup task is removed, we have to trigger a re-filter of
357 // our launchers to (possibly) pop them back in.
358 // NOTE: An older revision of this code compared the window and startup tasks
359 // to the launchers to figure out which launchers should be re-filtered. This
360 // was fine until we discovered that certain applications (e.g. Google Chrome)
361 // change their window metadata specifically during tear-down, sometimes
362 // breaking TaskTools::appsMatch (it's a race) and causing the associated
363 // launcher to remain hidden. Therefore we now consider any top-level window or
364 // startup task removal a trigger to re-filter all launchers. We don't do this
365 // in response to the window metadata changes (even though it would be strictly
366 // more correct, as then-ending identity match-up was what caused the launcher
367 // to be hidden) because we don't want the launcher and window/startup task to
368 // briefly co-exist in the model.
369 if (!launcherCheckNeeded && launcherTasksModel
370 && (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() || sourceIndex.data(AbstractTasksModel::IsStartup).toBool())) {
371 launcherCheckNeeded = true;
372 }
373 }
374 });
375
376 QObject::connect(filterProxyModel, &QAbstractItemModel::rowsRemoved, q, [this](const QModelIndex &parent, int first, int last) {
377 Q_UNUSED(parent)
378 Q_UNUSED(first)
379 Q_UNUSED(last)
380
381 if (launcherCheckNeeded) {
382 for (int i = 0; i < filterProxyModel->rowCount(); ++i) {
383 const QModelIndex &idx = filterProxyModel->index(i, 0);
384
385 if (idx.data(AbstractTasksModel::IsLauncher).toBool()) {
386 Q_EMIT filterProxyModel->dataChanged(idx, idx);
387 }
388 }
389
390 launcherCheckNeeded = false;
391 }
392
393 // One of the removed tasks might have been demanding attention, but
394 // we can't check the state after the window has been closed already,
395 // so we always have to do a full update.
396 if (anyTaskDemandsAttention) {
397 updateAnyTaskDemandsAttention();
398 }
399 });
400
401 // Update anyTaskDemandsAttention on source data changes.
402 QObject::connect(groupingProxyModel,
404 q,
405 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
406 Q_UNUSED(bottomRight)
407
408 // We can ignore group members.
409 if (topLeft.parent().isValid()) {
410 return;
411 }
412
413 if (roles.isEmpty() || roles.contains(AbstractTasksModel::IsDemandingAttention)) {
414 updateAnyTaskDemandsAttention();
415 }
416
417 if (roles.isEmpty() || roles.contains(AbstractTasksModel::AppId)) {
418 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
419 const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0);
420
421 // When a window task changes identity to one we have a launcher for, cause
422 // the launcher to be re-filtered.
423 if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) {
424 for (int i = 0; i < filterProxyModel->rowCount(); ++i) {
425 const QModelIndex &filterIndex = filterProxyModel->index(i, 0);
426
427 if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) {
428 continue;
429 }
430
431 if (appsMatch(sourceIndex, filterIndex)) {
432 Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex);
433 }
434 }
435 }
436 }
437 }
438 });
439
440 // Update anyTaskDemandsAttention on source model resets.
441 QObject::connect(groupingProxyModel, &QAbstractItemModel::modelReset, q, [this]() {
442 updateAnyTaskDemandsAttention();
443 });
444}
445
446void TasksModel::Private::updateAnyTaskDemandsAttention()
447{
448 bool taskFound = false;
449
450 for (int i = 0; i < groupingProxyModel->rowCount(); ++i) {
451 if (groupingProxyModel->index(i, 0).data(AbstractTasksModel::IsDemandingAttention).toBool()) {
452 taskFound = true;
453 break;
454 }
455 }
456
457 if (taskFound != anyTaskDemandsAttention) {
458 anyTaskDemandsAttention = taskFound;
459 Q_EMIT q->anyTaskDemandsAttentionChanged();
460 }
461}
462
463void TasksModel::Private::initLauncherTasksModel()
464{
465 if (launcherTasksModel) {
466 return;
467 }
468
469 launcherTasksModel = new LauncherTasksModel(q);
470 QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, &TasksModel::launcherListChanged);
471 QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, &TasksModel::updateLauncherCount);
472
473 // TODO: On the assumptions that adding/removing launchers is a rare event and
474 // the HasLaunchers data role is rarely used, this refreshes it for all rows in
475 // the model. If those assumptions are proven wrong later, this could be
476 // optimized to only refresh non-launcher rows matching the inserted or about-
477 // to-be-removed launcherTasksModel rows using TaskTools::appsMatch().
478 QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, [this]() {
479 Q_EMIT q->dataChanged(q->index(0, 0), q->index(q->rowCount() - 1, 0), QList<int>{AbstractTasksModel::HasLauncher});
480 });
481
482 // data() implements AbstractTasksModel::HasLauncher by checking with
483 // TaskTools::appsMatch, which evaluates ::AppId and ::LauncherUrlWithoutIcon.
484 QObject::connect(q, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
485 if (roles.contains(AbstractTasksModel::AppId) || roles.contains(AbstractTasksModel::LauncherUrlWithoutIcon)) {
486 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
487 const QModelIndex &index = q->index(i, 0);
488
489 if (!index.data(AbstractTasksModel::IsLauncher).toBool()) {
490 Q_EMIT q->dataChanged(index, index, QList<int>{AbstractTasksModel::HasLauncher});
491 }
492 }
493 }
494 });
495
496 concatProxyModel->addSourceModel(launcherTasksModel);
497}
498
499void TasksModel::Private::updateManualSortMap()
500{
501 // Empty map; full sort.
502 if (sortedPreFilterRows.isEmpty()) {
503 sortedPreFilterRows.reserve(concatProxyModel->rowCount());
504
505 for (int i = 0; i < concatProxyModel->rowCount(); ++i) {
506 sortedPreFilterRows.append(i);
507 }
508
509 // Full sort.
510 TasksModelLessThan lt(concatProxyModel, q, false);
511 std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt);
512
513 // Consolidate sort map entries for groups.
514 if (q->groupMode() != GroupDisabled) {
515 for (int i = 0; i < groupingProxyModel->rowCount(); ++i) {
516 const QModelIndex &groupingIndex = groupingProxyModel->index(i, 0);
517
518 if (groupingIndex.data(AbstractTasksModel::IsGroupParent).toBool()) {
519 consolidateManualSortMapForGroup(groupingIndex);
520 }
521 }
522 }
523
524 return;
525 }
526
527 // Existing map; check whether launchers need sorting by launcher list position.
528 if (separateLaunchers) {
529 // Sort only launchers.
530 TasksModelLessThan lt(concatProxyModel, q, true);
531 std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt);
532 // Otherwise process any entries in the insert queue and move them intelligently
533 // in the sort map.
534 } else {
535 QMutableListIterator<int> i(sortRowInsertQueue);
536
537 while (i.hasNext()) {
538 i.next();
539
540 const int row = i.value();
541 const QModelIndex &idx = concatProxyModel->index(sortedPreFilterRows.at(row), 0);
542
543 // If a window task is currently hidden, we may want to keep it in the queue
544 // to sort it in later once it gets revealed.
545 // This is important in concert with taskmanagerrulesrc's SkipTaskbar key, which
546 // is used to hide window tasks which update from bogus to useful window metadata
547 // early in startup. Once the task no longer uses bogus metadata listed in the
548 // config key, its SkipTaskbar role changes to false, and then is it possible to
549 // sort the task adjacent to its launcher in the code below.
550 if (idx.data(AbstractTasksModel::IsWindow).toBool() && idx.data(AbstractTasksModel::SkipTaskbar).toBool()) {
551 // Since we're going to keep a row in the queue for now, make sure to
552 // mark the queue as stale so it's cleared on appends or row removals
553 // when they follow this sorting attempt. This frees us from having to
554 // update the indices in the queue to keep them valid.
555 // This means windowing system changes such as the opening or closing
556 // of a window task which happen during the time period that a window
557 // task has known bogus metadata, can upset what we're trying to
558 // achieve with this exception. However, due to the briefness of the
559 // time period and usage patterns, this is improbable, making this
560 // likely good enough. If it turns out not to be, this decision may be
561 // revisited later.
562 sortRowInsertQueueStale = true;
563
564 break;
565 } else {
566 i.remove();
567 }
568
569 bool moved = false;
570
571 // Try to move the task up to its right-most app sibling, unless this
572 // is us sorting in a launcher list for the first time.
573 if (launchersEverSet && !idx.data(AbstractTasksModel::IsLauncher).toBool()) {
574 for (int j = (row - 1); j >= 0; --j) {
575 const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(j), 0);
576
577 // Once we got a match, check if the filter model accepts the potential
578 // sibling. We don't want to sort new tasks in next to tasks it will
579 // filter out once it sees it anyway.
580 if (appsMatch(concatProxyIndex, idx) && filterProxyModel->acceptsRow(concatProxyIndex.row())) {
581 sortedPreFilterRows.move(row, j + 1);
582 moved = true;
583
584 break;
585 }
586 }
587 }
588
589 int insertPos = 0;
590
591 // If unsuccessful or skipped, and the new task is a launcher, put after
592 // the rightmost launcher or launcher-backed task in the map, or failing
593 // that at the start of the map.
594 if (!moved && idx.data(AbstractTasksModel::IsLauncher).toBool()) {
595 for (int j = 0; j < row; ++j) {
596 const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(j), 0);
597
598 if (concatProxyIndex.data(AbstractTasksModel::IsLauncher).toBool()
599 || launcherTasksModel->launcherPosition(concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) != -1) {
600 insertPos = j + 1;
601 } else {
602 break;
603 }
604 }
605
606 sortedPreFilterRows.move(row, insertPos);
607 moved = true;
608 }
609
610 // If we sorted in a launcher and it's the first time we're sorting in a
611 // launcher list, move existing windows to the launcher position now.
612 if (moved && !launchersEverSet) {
613 for (int j = (sortedPreFilterRows.count() - 1); j >= 0; --j) {
614 const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(j), 0);
615
616 if (!concatProxyIndex.data(AbstractTasksModel::IsLauncher).toBool()
617 && idx.data(AbstractTasksModel::LauncherUrlWithoutIcon) == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon)) {
618 sortedPreFilterRows.move(j, insertPos);
619
620 if (insertPos > j) {
621 --insertPos;
622 }
623 }
624 }
625 }
626 }
627 }
628}
629
630void TasksModel::Private::consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex)
631{
632 // Consolidates sort map entries for a group's items to be contiguous
633 // after the group's first item and the same order as in groupingProxyModel.
634
635 const int childCount = groupingProxyModel->rowCount(groupingProxyIndex);
636
637 if (!childCount) {
638 return;
639 }
640
641 const QModelIndex &leader = groupingProxyModel->index(0, 0, groupingProxyIndex);
642 const QModelIndex &preFilterLeader = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(leader));
643
644 // We're moving the trailing children to the sort map position of
645 // the first child, so we're skipping the first child.
646 for (int i = 1; i < childCount; ++i) {
647 const QModelIndex &child = groupingProxyModel->index(i, 0, groupingProxyIndex);
648 const QModelIndex &preFilterChild = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(child));
649 const int leaderPos = sortedPreFilterRows.indexOf(preFilterLeader.row());
650 const int childPos = sortedPreFilterRows.indexOf(preFilterChild.row());
651 const int insertPos = (leaderPos + i) + ((leaderPos + i) > childPos ? -1 : 0);
652 sortedPreFilterRows.move(childPos, insertPos);
653 }
654}
655
656void TasksModel::Private::updateGroupInline()
657{
658 if (usedByQml && !componentComplete) {
659 return;
660 }
661
662 bool hadSourceModel = (q->sourceModel() != nullptr);
663
664 if (q->groupMode() != GroupDisabled && groupInline) {
665 if (flattenGroupsProxyModel) {
666 return;
667 }
668
669 // Exempting tasks which demand attention from grouping is not
670 // necessary when all group children are shown inline anyway
671 // and would interfere with our sort-tasks-together goals.
672 groupingProxyModel->setGroupDemandingAttention(true);
673
674 // Likewise, ignore the window tasks threshold when making
675 // grouping decisions.
676 groupingProxyModel->setWindowTasksThreshold(-1);
677
678 flattenGroupsProxyModel = new FlattenTaskGroupsProxyModel(q);
679 flattenGroupsProxyModel->setSourceModel(groupingProxyModel);
680
681 abstractTasksSourceModel = flattenGroupsProxyModel;
682 q->setSourceModel(flattenGroupsProxyModel);
683
684 if (sortMode == SortManual) {
685 forceResort();
686 }
687 } else {
688 if (hadSourceModel && !flattenGroupsProxyModel) {
689 return;
690 }
691
692 groupingProxyModel->setGroupDemandingAttention(false);
693 groupingProxyModel->setWindowTasksThreshold(groupingWindowTasksThreshold);
694
695 abstractTasksSourceModel = groupingProxyModel;
696 q->setSourceModel(groupingProxyModel);
697
698 delete flattenGroupsProxyModel;
699 flattenGroupsProxyModel = nullptr;
700
701 if (hadSourceModel && sortMode == SortManual) {
702 forceResort();
703 }
704 }
705
706 // Minor optimization: We only make these connections after we populate for
707 // the first time to avoid some churn.
708 if (!hadSourceModel) {
709 QObject::connect(q, &QAbstractItemModel::rowsInserted, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection);
710 QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection);
711 QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection);
712
715 QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::countChanged, Qt::UniqueConnection);
716 }
717}
718
719QModelIndex TasksModel::Private::preFilterIndex(const QModelIndex &sourceIndex) const
720{
721 // Only in inline grouping mode, we have an additional proxy layer.
722 if (flattenGroupsProxyModel) {
723 return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(flattenGroupsProxyModel->mapToSource(sourceIndex)));
724 } else {
725 return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(sourceIndex));
726 }
727}
728
729void TasksModel::Private::updateActivityTaskCounts()
730{
731 // Collects the number of window tasks on each activity.
732
733 activityTaskCounts.clear();
734
735 if (!windowTasksModel || !activityInfo) {
736 return;
737 }
738
739 for (const auto activities = activityInfo->runningActivities(); const QString &activity : activities) {
740 activityTaskCounts.insert(activity, 0);
741 }
742
743 for (int i = 0; i < windowTasksModel->rowCount(); ++i) {
744 const QModelIndex &windowIndex = windowTasksModel->index(i, 0);
745 const QStringList &activities = windowIndex.data(AbstractTasksModel::Activities).toStringList();
746
747 if (activities.isEmpty()) {
748 QMutableHashIterator<QString, int> it(activityTaskCounts);
749
750 while (it.hasNext()) {
751 it.next();
752 it.setValue(it.value() + 1);
753 }
754 } else {
755 for (const QString &activity : activities) {
756 ++activityTaskCounts[activity];
757 }
758 }
759 }
760}
761
762void TasksModel::Private::forceResort()
763{
764 // HACK: This causes QSortFilterProxyModel to run all rows through
765 // our lessThan() implementation again.
766 q->setDynamicSortFilter(false);
767 q->setDynamicSortFilter(true);
768}
769
770bool TasksModel::Private::lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers) const
771{
772 // Launcher tasks go first.
773 // When launchInPlace is enabled, startup and window tasks are sorted
774 // as the launchers they replace (see also move()).
775
776 if (separateLaunchers) {
777 const bool leftIsLauncher = left.data(AbstractTasksModel::IsLauncher).toBool();
778 const bool rightIsLauncher = right.data(AbstractTasksModel::IsLauncher).toBool();
779
780 if (leftIsLauncher && rightIsLauncher) {
781 return (left.row() < right.row());
782 }
783
784 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
785 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
786
787 if (leftIsLauncher && !rightIsLauncher) {
788 if (launchInPlace) {
789 if (rightPos != -1) {
790 return (leftPos < rightPos);
791 }
792 }
793
794 return true;
795 }
796
797 if (!leftIsLauncher && rightIsLauncher) {
798 if (launchInPlace) {
799 if (leftPos != -1) {
800 return (leftPos < rightPos);
801 }
802 }
803
804 return false;
805 }
806
807 // neither is a launcher
808 if (launchInPlace) {
809 if (leftPos != -1 && rightPos != -1) {
810 return (leftPos < rightPos);
811 } else if (leftPos != -1 && rightPos == -1) {
812 return true;
813 } else if (leftPos == -1 && rightPos != -1) {
814 return false;
815 }
816 }
817 }
818
819 // If told to stop after launchers we fall through to the existing map if it exists.
820 if (sortOnlyLaunchers && !sortedPreFilterRows.isEmpty()) {
821 return (sortedPreFilterRows.indexOf(left.row()) < sortedPreFilterRows.indexOf(right.row()));
822 }
823
824 // Sort other cases by sort mode.
825 switch (sortMode) {
826 case SortDisabled: {
827 return (left.row() < right.row());
828 }
829
830 case SortLastActivated: {
831 const auto getSortDateTime = [](const QModelIndex &model) {
832 // Check if the task is in a group
833 const QModelIndex topMost = model.parent().isValid() ? model.parent() : model;
834 QDateTime sortDateTime = topMost.data(AbstractTasksModel::LastActivated).toDateTime();
835 if (!sortDateTime.isValid()) {
836 sortDateTime = model.data(Qt::DisplayRole).toDateTime();
837 }
838 return sortDateTime;
839 };
840
841 const QDateTime leftSortDateTime = getSortDateTime(left);
842 const QDateTime rightSortDateTime = getSortDateTime(right);
843
844 if (leftSortDateTime != rightSortDateTime) {
845 // Move latest to leftmost
846 return leftSortDateTime > rightSortDateTime;
847 }
848
849 Q_FALLTHROUGH();
850 }
851 // fall through
852 case SortVirtualDesktop: {
853 const bool leftAll = left.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool();
854 const bool rightAll = right.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool();
855
856 if (leftAll && !rightAll) {
857 return true;
858 }
859
860 if (!leftAll && rightAll) {
861 return false;
862 }
863
864 if (!leftAll && !rightAll) {
865 const auto getDesktop = [this](const QModelIndex &model) {
866 const QVariantList modelDesktops = model.data(AbstractTasksModel::VirtualDesktops).toList();
867 QVariant modelDesktop;
868 int modelDesktopPos = virtualDesktopInfo->numberOfDesktops();
869 for (const QVariant &desktop : modelDesktops) {
870 const int desktopPos = virtualDesktopInfo->position(desktop);
871
872 if (desktopPos <= modelDesktopPos) {
873 modelDesktop = desktop;
874 modelDesktopPos = desktopPos;
875 }
876 }
877 return modelDesktop;
878 };
879
880 const QVariant leftDesktop = getDesktop(left);
881 const QVariant rightDesktop = getDesktop(right);
882
883 if (!leftDesktop.isNull() && !rightDesktop.isNull() && (leftDesktop != rightDesktop)) {
884 return (virtualDesktopInfo->position(leftDesktop) < virtualDesktopInfo->position(rightDesktop));
885 } else if (!leftDesktop.isNull() && rightDesktop.isNull()) {
886 return false;
887 } else if (leftDesktop.isNull() && !rightDesktop.isNull()) {
888 return true;
889 }
890 }
891 }
892 // fall through
893 case SortActivity: {
894 // updateActivityTaskCounts() counts the number of window tasks on each
895 // activity. This will sort tasks by comparing a cumulative score made
896 // up of the task counts for each activity a task is assigned to, and
897 // otherwise fall through to alphabetical sorting.
898 const auto getScore = [this](const QModelIndex &model) {
899 const QStringList activities = model.data(AbstractTasksModel::Activities).toStringList();
900 const int score = std::accumulate(activities.cbegin(), activities.cend(), -1, [this](int a, const QString &activity) {
901 return a + activityTaskCounts[activity];
902 });
903 return score;
904 };
905
906 int leftScore = getScore(left);
907 int rightScore = getScore(right);
908
909 if (leftScore == -1 || rightScore == -1) {
910 const int sumScore = std::accumulate(activityTaskCounts.constBegin(), activityTaskCounts.constEnd(), 0);
911
912 if (leftScore == -1) {
913 leftScore = sumScore;
914 }
915
916 if (rightScore == -1) {
917 rightScore = sumScore;
918 }
919 }
920
921 if (leftScore != rightScore) {
922 return (leftScore > rightScore);
923 }
924 }
925 // fall through
926 case SortAlpha:
927 // fall through
928 default: {
929 // The overall goal of alphabetic sorting is to sort tasks belonging to the
930 // same app together, while sorting the resulting sets alphabetically among
931 // themselves by the app name. The following code tries to achieve this by
932 // going for AppName first, and falling back to DisplayRole - which for
933 // window-type tasks generally contains the window title - if AppName is
934 // not available. When comparing tasks with identical resulting sort strings,
935 // we sort them by the source model order (i.e. insertion/creation). Older
936 // versions of this code compared tasks by a concatenation of AppName and
937 // DisplayRole at all times, but always sorting by the window title does more
938 // than our goal description - and can cause tasks within an app's set to move
939 // around when window titles change, which is a nuisance for users (especially
940 // in case of tabbed apps that have the window title reflect the active tab,
941 // e.g. web browsers). To recap, the common case is "sort by AppName, then
942 // insertion order", only swapping out AppName for DisplayRole (i.e. window
943 // title) when necessary.
944
945 QString leftSortString = left.data(AbstractTasksModel::AppName).toString();
946
947 if (leftSortString.isEmpty()) {
948 leftSortString = left.data(Qt::DisplayRole).toString();
949 }
950
951 QString rightSortString = right.data(AbstractTasksModel::AppName).toString();
952
953 if (rightSortString.isEmpty()) {
954 rightSortString = right.data(Qt::DisplayRole).toString();
955 }
956
957 const int sortResult = leftSortString.localeAwareCompare(rightSortString);
958
959 // If the string are identical fall back to source model (creation/append) order.
960 if (sortResult == 0) {
961 return (left.row() < right.row());
962 }
963
964 return (sortResult < 0);
965 }
966 }
967}
968
969TasksModel::TasksModel(QObject *parent)
970 : QSortFilterProxyModel(parent)
971 , d(new Private(this))
972{
973 d->initModels();
974
975 // Start sorting.
976 sort(0);
977
978 connect(this, &TasksModel::sourceModelChanged, this, &TasksModel::countChanged);
979
980 // Private::updateGroupInline() sets our source model, populating the model. We
981 // delay running this until the QML runtime had a chance to call our implementation
982 // of QQmlParserStatus::classBegin(), setting Private::usedByQml to true. If used
983 // by QML, Private::updateGroupInline() will abort if the component is not yet
984 // complete, instead getting called through QQmlParserStatus::componentComplete()
985 // only after all properties have been set. This avoids delegate churn in Qt Quick
986 // views using the model. If not used by QML, Private::updateGroupInline() will run
987 // directly.
988 QTimer::singleShot(0, this, [this]() {
989 d->updateGroupInline();
990 });
991}
992
993TasksModel::~TasksModel()
994{
995}
996
997QHash<int, QByteArray> TasksModel::roleNames() const
998{
999 if (d->windowTasksModel) {
1000 return d->windowTasksModel->roleNames();
1001 }
1002
1003 return QHash<int, QByteArray>();
1004}
1005
1006int TasksModel::rowCount(const QModelIndex &parent) const
1007{
1008 return QSortFilterProxyModel::rowCount(parent);
1009}
1010
1011QVariant TasksModel::data(const QModelIndex &proxyIndex, int role) const
1012{
1013 if (role == AbstractTasksModel::HasLauncher && proxyIndex.isValid() && proxyIndex.row() < rowCount()) {
1014 if (proxyIndex.data(AbstractTasksModel::IsLauncher).toBool()) {
1015 return true;
1016 } else {
1017 if (!d->launcherTasksModel) {
1018 return false;
1019 }
1020 for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) {
1021 const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0);
1022
1023 if (appsMatch(proxyIndex, launcherIndex)) {
1024 return true;
1025 }
1026 }
1027
1028 return false;
1029 }
1030 } else if (rowCount(proxyIndex) && role == AbstractTasksModel::WinIdList) {
1031 QVariantList winIds;
1032
1033 for (int i = 0; i < rowCount(proxyIndex); ++i) {
1034 winIds.append(index(i, 0, proxyIndex).data(AbstractTasksModel::WinIdList).toList());
1035 }
1036
1037 return winIds;
1038 }
1039
1040 return QSortFilterProxyModel::data(proxyIndex, role);
1041}
1042
1043void TasksModel::updateLauncherCount()
1044{
1045 if (!d->launcherTasksModel) {
1046 return;
1047 }
1048
1049 int count = 0;
1050
1051 for (int i = 0; i < rowCount(); ++i) {
1052 if (index(i, 0).data(AbstractTasksModel::IsLauncher).toBool()) {
1053 ++count;
1054 }
1055 }
1056
1057 if (d->launcherCount != count) {
1058 d->launcherCount = count;
1059 Q_EMIT launcherCountChanged();
1060 }
1061}
1062
1063int TasksModel::launcherCount() const
1064{
1065 return d->launcherCount;
1066}
1067
1068bool TasksModel::anyTaskDemandsAttention() const
1069{
1070 return d->anyTaskDemandsAttention;
1071}
1072
1073QVariant TasksModel::virtualDesktop() const
1074{
1075 return d->filterProxyModel->virtualDesktop();
1076}
1077
1079{
1080 d->filterProxyModel->setVirtualDesktop(desktop);
1081}
1082
1083QRect TasksModel::screenGeometry() const
1084{
1085 return d->filterProxyModel->screenGeometry();
1086}
1087
1089{
1090 d->filterProxyModel->setScreenGeometry(geometry);
1091}
1092
1093QRect TasksModel::regionGeometry() const
1094{
1095 return d->filterProxyModel->regionGeometry();
1096}
1097
1099{
1100 d->filterProxyModel->setRegionGeometry(geometry);
1101}
1102
1103QString TasksModel::activity() const
1104{
1105 return d->filterProxyModel->activity();
1106}
1107
1109{
1110 d->filterProxyModel->setActivity(activity);
1111}
1112
1113bool TasksModel::filterByVirtualDesktop() const
1114{
1115 return d->filterProxyModel->filterByVirtualDesktop();
1116}
1117
1119{
1120 d->filterProxyModel->setFilterByVirtualDesktop(filter);
1121}
1122
1123bool TasksModel::filterByScreen() const
1124{
1125 return d->filterProxyModel->filterByScreen();
1126}
1127
1129{
1130 d->filterProxyModel->setFilterByScreen(filter);
1131}
1132
1133bool TasksModel::filterByActivity() const
1134{
1135 return d->filterProxyModel->filterByActivity();
1136}
1137
1139{
1140 d->filterProxyModel->setFilterByActivity(filter);
1141}
1142
1143RegionFilterMode::Mode TasksModel::filterByRegion() const
1144{
1145 return d->filterProxyModel->filterByRegion();
1146}
1147
1148void TasksModel::setFilterByRegion(RegionFilterMode::Mode mode)
1149{
1150 d->filterProxyModel->setFilterByRegion(mode);
1151}
1152
1153bool TasksModel::filterMinimized() const
1154{
1155 return d->filterProxyModel->filterMinimized();
1156}
1157
1159{
1160 d->filterProxyModel->setFilterMinimized(filter);
1161}
1162
1163bool TasksModel::filterNotMinimized() const
1164{
1165 return d->filterProxyModel->filterNotMinimized();
1166}
1167
1169{
1170 d->filterProxyModel->setFilterNotMinimized(filter);
1171}
1172
1173bool TasksModel::filterNotMaximized() const
1174{
1175 return d->filterProxyModel->filterNotMaximized();
1176}
1177
1179{
1180 d->filterProxyModel->setFilterNotMaximized(filter);
1181}
1182
1183bool TasksModel::filterHidden() const
1184{
1185 return d->filterProxyModel->filterHidden();
1186}
1187
1189{
1190 d->filterProxyModel->setFilterHidden(filter);
1191}
1192
1193TasksModel::SortMode TasksModel::sortMode() const
1194{
1195 return d->sortMode;
1196}
1197
1199{
1200 if (d->sortMode != mode) {
1201 if (mode == SortManual) {
1202 d->updateManualSortMap();
1203 } else if (d->sortMode == SortManual) {
1204 d->sortedPreFilterRows.clear();
1205 }
1206
1207 if (mode == SortVirtualDesktop) {
1208 d->virtualDesktopInfo = virtualDesktopInfo();
1210 } else if (d->sortMode == SortVirtualDesktop) {
1211 d->virtualDesktopInfo = nullptr;
1213 }
1214
1215 if (mode == SortActivity) {
1216 d->activityInfo = activityInfo();
1217
1218 d->updateActivityTaskCounts();
1220 } else if (d->sortMode == SortActivity) {
1221 d->activityInfo = nullptr;
1222
1223 d->activityTaskCounts.clear();
1225 }
1226
1227 if (mode == SortLastActivated) {
1229 }
1230
1231 d->sortMode = mode;
1232
1233 d->forceResort();
1234
1235 Q_EMIT sortModeChanged();
1236 }
1237}
1238
1239bool TasksModel::separateLaunchers() const
1240{
1241 return d->separateLaunchers;
1242}
1243
1245{
1246 if (d->separateLaunchers != separate) {
1247 d->separateLaunchers = separate;
1248
1249 d->updateManualSortMap();
1250 d->forceResort();
1251
1252 Q_EMIT separateLaunchersChanged();
1253 }
1254}
1255
1256bool TasksModel::launchInPlace() const
1257{
1258 return d->launchInPlace;
1259}
1260
1261void TasksModel::setLaunchInPlace(bool launchInPlace)
1262{
1263 if (d->launchInPlace != launchInPlace) {
1264 d->launchInPlace = launchInPlace;
1265
1266 d->forceResort();
1267
1268 Q_EMIT launchInPlaceChanged();
1269 }
1270}
1271
1272TasksModel::GroupMode TasksModel::groupMode() const
1273{
1274 if (!d->groupingProxyModel) {
1275 return GroupDisabled;
1276 }
1277
1278 return d->groupingProxyModel->groupMode();
1279}
1280
1281bool TasksModel::hideActivatedLaunchers() const
1282{
1283 return d->hideActivatedLaunchers;
1284}
1285
1286void TasksModel::setHideActivatedLaunchers(bool hideActivatedLaunchers)
1287{
1288 if (d->hideActivatedLaunchers != hideActivatedLaunchers) {
1289 d->hideActivatedLaunchers = hideActivatedLaunchers;
1290
1291 d->updateManualSortMap();
1293 d->forceResort();
1294
1295 Q_EMIT hideActivatedLaunchersChanged();
1296 }
1297}
1298
1300{
1301 if (d->groupingProxyModel) {
1302 if (mode == GroupDisabled && d->flattenGroupsProxyModel) {
1303 d->flattenGroupsProxyModel->setSourceModel(nullptr);
1304 }
1305
1306 d->groupingProxyModel->setGroupMode(mode);
1307 d->updateGroupInline();
1308 }
1309}
1310
1311bool TasksModel::groupInline() const
1312{
1313 return d->groupInline;
1314}
1315
1316void TasksModel::setGroupInline(bool groupInline)
1317{
1318 if (d->groupInline != groupInline) {
1319 d->groupInline = groupInline;
1320
1321 d->updateGroupInline();
1322
1323 Q_EMIT groupInlineChanged();
1324 }
1325}
1326
1327int TasksModel::groupingWindowTasksThreshold() const
1328{
1329 return d->groupingWindowTasksThreshold;
1330}
1331
1333{
1334 if (d->groupingWindowTasksThreshold != threshold) {
1335 d->groupingWindowTasksThreshold = threshold;
1336
1337 if (!d->groupInline && d->groupingProxyModel) {
1338 d->groupingProxyModel->setWindowTasksThreshold(threshold);
1339 }
1340
1341 Q_EMIT groupingWindowTasksThresholdChanged();
1342 }
1343}
1344
1345QStringList TasksModel::groupingAppIdBlacklist() const
1346{
1347 if (!d->groupingProxyModel) {
1348 return QStringList();
1349 }
1350
1351 return d->groupingProxyModel->blacklistedAppIds();
1352}
1353
1355{
1356 if (d->groupingProxyModel) {
1357 d->groupingProxyModel->setBlacklistedAppIds(list);
1358 }
1359}
1360
1361QStringList TasksModel::groupingLauncherUrlBlacklist() const
1362{
1363 if (!d->groupingProxyModel) {
1364 return QStringList();
1365 }
1366
1367 return d->groupingProxyModel->blacklistedLauncherUrls();
1368}
1369
1371{
1372 if (d->groupingProxyModel) {
1373 d->groupingProxyModel->setBlacklistedLauncherUrls(list);
1374 }
1375}
1376
1377bool TasksModel::taskReorderingEnabled() const
1378{
1379 return dynamicSortFilter();
1380}
1381
1383{
1384 enabled ? setDynamicSortFilter(true) : setDynamicSortFilter(false);
1385
1386 Q_EMIT taskReorderingEnabledChanged();
1387}
1388
1389QStringList TasksModel::launcherList() const
1390{
1391 if (d->launcherTasksModel) {
1392 return d->launcherTasksModel->launcherList();
1393 }
1394
1395 return QStringList();
1396}
1397
1399{
1400 d->initLauncherTasksModel();
1401 d->launcherTasksModel->setLauncherList(launchers);
1402 d->launchersEverSet = true;
1403}
1404
1406{
1407 d->initLauncherTasksModel();
1408
1409 bool added = d->launcherTasksModel->requestAddLauncher(url);
1410
1411 // If using manual and launch-in-place sorting with separate launchers,
1412 // we need to trigger a sort map update to move any window tasks to
1413 // their launcher position now.
1414 if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1415 d->updateManualSortMap();
1416 d->forceResort();
1417 }
1418
1419 return added;
1420}
1421
1423{
1424 if (d->launcherTasksModel) {
1425 bool removed = d->launcherTasksModel->requestRemoveLauncher(url);
1426
1427 // If using manual and launch-in-place sorting with separate launchers,
1428 // we need to trigger a sort map update to move any window tasks no
1429 // longer backed by a launcher out of the launcher area.
1430 if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1431 d->updateManualSortMap();
1432 d->forceResort();
1433 }
1434
1435 return removed;
1436 }
1437
1438 return false;
1439}
1440
1442{
1443 d->initLauncherTasksModel();
1444
1445 bool added = d->launcherTasksModel->requestAddLauncherToActivity(url, activity);
1446
1447 // If using manual and launch-in-place sorting with separate launchers,
1448 // we need to trigger a sort map update to move any window tasks to
1449 // their launcher position now.
1450 if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1451 d->updateManualSortMap();
1452 d->forceResort();
1453 }
1454
1455 return added;
1456}
1457
1459{
1460 if (d->launcherTasksModel) {
1461 bool removed = d->launcherTasksModel->requestRemoveLauncherFromActivity(url, activity);
1462
1463 // If using manual and launch-in-place sorting with separate launchers,
1464 // we need to trigger a sort map update to move any window tasks no
1465 // longer backed by a launcher out of the launcher area.
1466 if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1467 d->updateManualSortMap();
1468 d->forceResort();
1469 }
1470
1471 return removed;
1472 }
1473
1474 return false;
1475}
1476
1478{
1479 if (d->launcherTasksModel) {
1480 return d->launcherTasksModel->launcherActivities(url);
1481 }
1482
1483 return {};
1484}
1485
1487{
1488 if (d->launcherTasksModel) {
1489 return d->launcherTasksModel->launcherPosition(url);
1490 }
1491
1492 return -1;
1493}
1494
1496{
1497 if (index.isValid() && index.model() == this) {
1498 d->abstractTasksSourceModel->requestActivate(mapToSource(index));
1499 }
1500}
1501
1503{
1504 if (index.isValid() && index.model() == this) {
1505 d->abstractTasksSourceModel->requestNewInstance(mapToSource(index));
1506 }
1507}
1508
1510{
1511 if (index.isValid() && index.model() == this) {
1512 d->abstractTasksSourceModel->requestOpenUrls(mapToSource(index), urls);
1513 }
1514}
1515
1517{
1518 if (index.isValid() && index.model() == this) {
1519 d->abstractTasksSourceModel->requestClose(mapToSource(index));
1520 }
1521}
1522
1524{
1525 if (index.isValid() && index.model() == this) {
1526 d->abstractTasksSourceModel->requestMove(mapToSource(index));
1527 }
1528}
1529
1531{
1532 if (index.isValid() && index.model() == this) {
1533 d->abstractTasksSourceModel->requestResize(mapToSource(index));
1534 }
1535}
1536
1538{
1539 if (index.isValid() && index.model() == this) {
1540 d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index));
1541 }
1542}
1543
1545{
1546 if (index.isValid() && index.model() == this) {
1547 d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index));
1548 }
1549}
1550
1552{
1553 if (index.isValid() && index.model() == this) {
1554 d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index));
1555 }
1556}
1557
1559{
1560 if (index.isValid() && index.model() == this) {
1561 d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index));
1562 }
1563}
1564
1566{
1567 if (index.isValid() && index.model() == this) {
1568 d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index));
1569 }
1570}
1571
1573{
1574 if (index.isValid() && index.model() == this) {
1575 d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index));
1576 }
1577}
1578
1579void TasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops)
1580{
1581 if (index.isValid() && index.model() == this) {
1582 d->abstractTasksSourceModel->requestVirtualDesktops(mapToSource(index), desktops);
1583 }
1584}
1585
1587{
1588 if (index.isValid() && index.model() == this) {
1589 d->abstractTasksSourceModel->requestNewVirtualDesktop(mapToSource(index));
1590 }
1591}
1592
1594{
1595 if (index.isValid() && index.model() == this) {
1596 d->groupingProxyModel->requestActivities(mapToSource(index), activities);
1597 }
1598}
1599
1601{
1602 if (!index.isValid() || index.model() != this || !index.data(AbstractTasksModel::IsWindow).toBool()) {
1603 return;
1604 }
1605 d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate);
1606}
1607
1609{
1610 if (index.isValid() && index.model() == this) {
1611 const QModelIndex &target = (d->flattenGroupsProxyModel ? d->flattenGroupsProxyModel->mapToSource(mapToSource(index)) : mapToSource(index));
1612 d->groupingProxyModel->requestToggleGrouping(target);
1613 }
1614}
1615
1616bool TasksModel::move(int row, int newPos, const QModelIndex &parent)
1617{
1618 /*
1619 * NOTE After doing any modification in TasksModel::move, make sure fixes listed below are not regressed.
1620 * - https://bugs.kde.org/444816
1621 * - https://bugs.kde.org/448912
1622 * - https://invent.kde.org/plasma/plasma-workspace/-/commit/ea51795e8c571513e1ff583350ab8649bc857fc2
1623 */
1624
1625 if (d->sortMode != SortManual || row == newPos || newPos < 0 || newPos >= rowCount(parent)) {
1626 return false;
1627 }
1628
1629 const QModelIndex &idx = index(row, 0, parent);
1630 bool isLauncherMove = false;
1631
1632 // Figure out if we're moving a launcher so we can run barrier checks.
1633 if (idx.isValid()) {
1635 isLauncherMove = true;
1636 // When using launch-in-place sorting, launcher-backed window tasks act as launchers.
1637 } else if ((d->launchInPlace || !d->separateLaunchers) && idx.data(AbstractTasksModel::IsWindow).toBool()) {
1638 const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1639 const int launcherPos = launcherPosition(launcherUrl);
1640
1641 if (launcherPos != -1) {
1642 isLauncherMove = true;
1643 }
1644 }
1645 } else {
1646 return false;
1647 }
1648
1649 if (d->separateLaunchers && !parent.isValid() /* Exclude tasks in a group */) {
1650 int firstTask = 0;
1651 if (d->launcherTasksModel) {
1652 if (d->launchInPlace) {
1653 firstTask = d->launcherTasksModel->rowCountForActivity(activity());
1654 } else {
1655 firstTask = launcherCount();
1656 }
1657 }
1658
1659 // Don't allow launchers to be moved past the last launcher.
1660 if (isLauncherMove && newPos >= firstTask) {
1661 return false;
1662 }
1663
1664 // Don't allow tasks to be moved into the launchers.
1665 if (!isLauncherMove && newPos < firstTask) {
1666 return false;
1667 }
1668 }
1669
1670 // Treat flattened-out groups as single items.
1671 if (d->flattenGroupsProxyModel) {
1672 QModelIndex groupingRowIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(row, 0)));
1673 const QModelIndex &groupingRowIndexParent = groupingRowIndex.parent();
1674 QModelIndex groupingNewPosIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(newPos, 0)));
1675 const QModelIndex &groupingNewPosIndexParent = groupingNewPosIndex.parent();
1676
1677 // Disallow moves within a flattened-out group (TODO: for now, anyway).
1678 if (groupingRowIndexParent.isValid() && (groupingRowIndexParent == groupingNewPosIndex || groupingRowIndexParent == groupingNewPosIndexParent)) {
1679 return false;
1680 }
1681
1682 int offset = 0;
1683 int extraChildCount = 0;
1684
1685 if (groupingRowIndexParent.isValid()) {
1686 offset = groupingRowIndex.row();
1687 extraChildCount = d->groupingProxyModel->rowCount(groupingRowIndexParent) - 1;
1688 groupingRowIndex = groupingRowIndexParent;
1689 }
1690
1691 if (groupingNewPosIndexParent.isValid()) {
1692 int extra = d->groupingProxyModel->rowCount(groupingNewPosIndexParent) - 1;
1693
1694 if (newPos > row) {
1695 newPos += extra;
1696 newPos -= groupingNewPosIndex.row();
1697 groupingNewPosIndex = groupingNewPosIndexParent.model()->index(extra, 0, groupingNewPosIndexParent);
1698 } else {
1699 newPos -= groupingNewPosIndex.row();
1700 groupingNewPosIndex = groupingNewPosIndexParent;
1701 }
1702 }
1703
1704 beginMoveRows(QModelIndex(), (row - offset), (row - offset) + extraChildCount, QModelIndex(), (newPos > row) ? newPos + 1 : newPos);
1705
1706 row = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingRowIndex)).row());
1707 newPos = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingNewPosIndex)).row());
1708
1709 // Update sort mappings.
1710 d->sortedPreFilterRows.move(row, newPos);
1711
1712 endMoveRows();
1713
1714 if (groupingRowIndexParent.isValid()) {
1715 d->consolidateManualSortMapForGroup(groupingRowIndexParent);
1716 }
1717
1718 } else {
1719 beginMoveRows(parent, row, row, parent, (newPos > row) ? newPos + 1 : newPos);
1720
1721 // Translate to sort map indices.
1722 const QModelIndex &groupingRowIndex = mapToSource(index(row, 0, parent));
1723 const QModelIndex &preFilterRowIndex = d->preFilterIndex(groupingRowIndex);
1724
1725 const bool groupNotDisabled = !parent.isValid() && groupMode() != GroupDisabled;
1726 QModelIndex adjacentGroupingRowIndex; // Also consolidate the adjacent group parent
1727 if (groupNotDisabled) {
1728 if (newPos > row && row + 1 < rowCount(parent)) {
1729 adjacentGroupingRowIndex = mapToSource(index(row + 1, 0, parent) /* task on the right */);
1730 } else if (newPos < row && row - 1 >= 0) {
1731 adjacentGroupingRowIndex = mapToSource(index(row - 1, 0, parent) /* task on the left */);
1732 }
1733 }
1734
1735 row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row());
1736 newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0, parent))).row());
1737
1738 // Update sort mapping.
1739 d->sortedPreFilterRows.move(row, newPos);
1740
1741 endMoveRows();
1742
1743 // If we moved a group parent, consolidate sort map for children.
1744 if (groupNotDisabled) {
1745 if (d->groupingProxyModel->rowCount(groupingRowIndex)) {
1746 d->consolidateManualSortMapForGroup(groupingRowIndex);
1747 }
1748 // Special case: Before moving, the task at newPos is a group parent
1749 // Before moving: [Task] [Group parent] [Other task in group]
1750 // After moving: [Group parent (not consolidated yet)] [Task, newPos] [Other task in group]
1751 if (int childCount = d->groupingProxyModel->rowCount(adjacentGroupingRowIndex); childCount && adjacentGroupingRowIndex.isValid()) {
1752 d->consolidateManualSortMapForGroup(adjacentGroupingRowIndex);
1753 if (newPos > row) {
1754 newPos += childCount - 1;
1755 // After consolidation: [Group parent (not consolidated yet)] [Other task in group] [Task, newPos]
1756 }
1757 // No need to consider newPos < row
1758 // Before moving: [Group parent, newPos] [Other task in group] [Task]
1759 // After moving: [Task, newPos] [Group parent] [Other task in group]
1760 }
1761 }
1762 }
1763
1764 // Resort.
1765 d->forceResort();
1766
1767 if (!d->separateLaunchers) {
1768 if (isLauncherMove) {
1769 const QModelIndex &idx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos), 0);
1770 const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1771
1772 // Move launcher for launcher-backed task along with task if launchers
1773 // are not being kept separate.
1774 // We don't need to resort again because the launcher is implicitly hidden
1775 // at this time.
1777 const int launcherPos = d->launcherTasksModel->launcherPosition(launcherUrl);
1778 const QModelIndex &launcherIndex = d->launcherTasksModel->index(launcherPos, 0);
1779 const int sortIndex = d->sortedPreFilterRows.indexOf(d->concatProxyModel->mapFromSource(launcherIndex).row());
1780 d->sortedPreFilterRows.move(sortIndex, newPos);
1781
1782 if (row > newPos && newPos >= 1) {
1783 const QModelIndex beforeIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos - 1), 0);
1784 if (beforeIdx.data(AbstractTasksModel::IsLauncher).toBool()) {
1785 // Search forward to skip grouped tasks
1786 int afterPos = newPos + 1;
1787 for (; afterPos < d->sortedPreFilterRows.size(); ++afterPos) {
1788 const QModelIndex tempIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(afterPos), 0);
1789 if (!appsMatch(idx, tempIdx)) {
1790 break;
1791 }
1792 }
1793
1794 const QModelIndex afterIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(afterPos), 0);
1795 if (appsMatch(beforeIdx, afterIdx)) {
1796 d->sortedPreFilterRows.move(newPos - 1, afterPos - 1);
1797 }
1798 }
1799 }
1800 // Otherwise move matching windows to after the launcher task (they are
1801 // currently hidden but might be on another virtual desktop).
1802 } else {
1803 for (int i = (d->sortedPreFilterRows.count() - 1); i >= 0; --i) {
1804 const QModelIndex &concatProxyIndex = d->concatProxyModel->index(d->sortedPreFilterRows.at(i), 0);
1805
1806 if (launcherUrl == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) {
1807 d->sortedPreFilterRows.move(i, newPos);
1808
1809 if (newPos > i) {
1810 --newPos;
1811 }
1812 }
1813 }
1814 }
1815 } else if (newPos > 0 && newPos < d->sortedPreFilterRows.size() - 1) {
1816 /*
1817 * When dragging an unpinned task, a pinned task can also be moved.
1818 * In this case, sortedPreFilterRows is like:
1819 * - before moving: [pinned 1 (launcher item)] [pinned 1 (window)] [unpinned]
1820 * - after moving: [pinned 1 (launcher item)] [unpinned] [pinned 1 (window)]
1821 * So also check the indexes before and after the unpinned task.
1822 */
1823 const QModelIndex beforeIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos - 1), 0, parent);
1824 const QModelIndex afterIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos + 1), 0, parent);
1825 // BUG 462508: check if any item is a launcher
1826 const bool hasLauncher = beforeIdx.data(AbstractTasksModel::IsLauncher).toBool() || afterIdx.data(AbstractTasksModel::IsLauncher).toBool();
1827
1828 if (hasLauncher && appsMatch(beforeIdx, afterIdx)) {
1829 // after adjusting: [unpinned] [pinned 1 (launcher item)] [pinned 1]
1830 d->sortedPreFilterRows.move(newPos, newPos + (row < newPos ? 1 : -1));
1831 }
1832 }
1833 }
1834
1835 // Setup for syncLaunchers().
1836 d->launcherSortingDirty = isLauncherMove;
1837
1838 return true;
1839}
1840
1842{
1843 // Writes the launcher order exposed through the model back to the launcher
1844 // tasks model, committing any move() operations to persistent state.
1845
1846 if (!d->launcherTasksModel || !d->launcherSortingDirty) {
1847 return;
1848 }
1849
1850 QMap<int, QString> sortedShownLaunchers;
1851 QStringList sortedHiddenLaunchers;
1852
1853 for (const auto launchers = launcherList(); const QString &launcherUrlStr : launchers) {
1854 int row = -1;
1855 QList<QStringView> activities;
1856 QUrl launcherUrl;
1857
1858 std::tie(launcherUrl, activities) = deserializeLauncher(launcherUrlStr);
1859
1860 for (int i = 0; i < rowCount(); ++i) {
1861 const QUrl &rowLauncherUrl = index(i, 0).data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1862
1863 // `LauncherTasksModel::launcherList()` returns data in a format suitable for writing
1864 // to persistent configuration storage, e.g. `preferred://browser`. We mean to compare
1865 // this last "save state" to a higher, resolved URL representation to compute the delta
1866 // so we need to move the unresolved URLs through `TaskTools::appDataFromUrl()` first.
1867 // TODO: This bypasses an existing lookup cache for the resolved app data that exists
1868 // in LauncherTasksModel. It's likely a good idea to eventually move these caches out
1869 // of the various models and share them among users of `TaskTools::appDataFromUrl()`,
1870 // and then also do resolution implicitly in `TaskTools::launcherUrlsMatch`, to speed
1871 // things up slightly and make the models simpler (central cache eviction, ...).
1872 if (launcherUrlsMatch(appDataFromUrl(launcherUrl).url, rowLauncherUrl, IgnoreQueryItems)) {
1873 row = i;
1874 break;
1875 }
1876 }
1877
1878 if (row != -1) {
1879 sortedShownLaunchers.insert(row, launcherUrlStr);
1880 } else {
1881 sortedHiddenLaunchers << launcherUrlStr;
1882 }
1883 }
1884
1885 // Prep sort map for source model data changes.
1886 if (d->sortMode == SortManual) {
1887 QList<int> sortMapIndices;
1888 QList<int> preFilterRows;
1889
1890 for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) {
1891 const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0);
1892 const QModelIndex &concatIndex = d->concatProxyModel->mapFromSource(launcherIndex);
1893 sortMapIndices << d->sortedPreFilterRows.indexOf(concatIndex.row());
1894 preFilterRows << concatIndex.row();
1895 }
1896
1897 // We're going to write back launcher model entries in the sort
1898 // map in concat model order, matching the reordered launcher list
1899 // we're about to pass down.
1900 std::sort(sortMapIndices.begin(), sortMapIndices.end());
1901
1902 for (int i = 0; i < sortMapIndices.count(); ++i) {
1903 d->sortedPreFilterRows.replace(sortMapIndices.at(i), preFilterRows.at(i));
1904 }
1905 }
1906
1907 setLauncherList(sortedShownLaunchers.values() + sortedHiddenLaunchers);
1908
1909 // The accepted rows are outdated after the item order is changed
1911 d->forceResort();
1912
1913 d->launcherSortingDirty = false;
1914}
1915
1916QModelIndex TasksModel::activeTask() const
1917{
1918 for (int i = 0; i < rowCount(); ++i) {
1919 const QModelIndex &idx = index(i, 0);
1920
1922 if (groupMode() != GroupDisabled && rowCount(idx)) {
1923 for (int j = 0; j < rowCount(idx); ++j) {
1924 const QModelIndex &child = index(j, 0, idx);
1925
1927 return child;
1928 }
1929 }
1930 } else {
1931 return idx;
1932 }
1933 }
1934 }
1935
1936 return QModelIndex();
1937}
1938
1939QModelIndex TasksModel::makeModelIndex(int row, int childRow) const
1940{
1941 if (row < 0 || row >= rowCount()) {
1942 return QModelIndex();
1943 }
1944
1945 if (childRow == -1) {
1946 return index(row, 0);
1947 } else {
1948 const QModelIndex &parent = index(row, 0);
1949
1950 if (childRow < rowCount(parent)) {
1951 return index(childRow, 0, parent);
1952 }
1953 }
1954
1955 return QModelIndex();
1956}
1957
1959{
1960 return QPersistentModelIndex(makeModelIndex(row, childCount));
1961}
1962
1963void TasksModel::classBegin()
1964{
1965 d->usedByQml = true;
1966}
1967
1968void TasksModel::componentComplete()
1969{
1970 d->componentComplete = true;
1971
1972 // Sets our source model, populating the model.
1973 d->updateGroupInline();
1974}
1975
1976bool TasksModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
1977{
1978 // All our filtering occurs at the top-level; anything below always
1979 // goes through.
1980 if (sourceParent.isValid()) {
1981 return true;
1982 }
1983
1984 const QModelIndex &sourceIndex = sourceModel()->index(sourceRow, 0);
1985
1986 // In inline grouping mode, filter out group parents.
1987 if (d->groupInline && d->flattenGroupsProxyModel && sourceIndex.data(AbstractTasksModel::IsGroupParent).toBool()) {
1988 return false;
1989 }
1990
1991 const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString();
1992 const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString();
1993
1994 // Filter startup tasks we already have a window task for.
1995 if (sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) {
1996 for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) {
1997 const QModelIndex &filterIndex = d->filterProxyModel->index(i, 0);
1998
1999 if (!filterIndex.data(AbstractTasksModel::IsWindow).toBool()) {
2000 continue;
2001 }
2002
2003 if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString())
2004 || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) {
2005 return false;
2006 }
2007 }
2008 }
2009
2010 // Filter launcher tasks we already have a startup or window task for (that
2011 // got through filtering).
2012 if (d->hideActivatedLaunchers && sourceIndex.data(AbstractTasksModel::IsLauncher).toBool()) {
2013 for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) {
2014 const QModelIndex &filteredIndex = d->filterProxyModel->index(i, 0);
2015
2016 if (!filteredIndex.data(AbstractTasksModel::IsWindow).toBool() && !filteredIndex.data(AbstractTasksModel::IsStartup).toBool()) {
2017 continue;
2018 }
2019
2020 if (appsMatch(sourceIndex, filteredIndex)) {
2021 return false;
2022 }
2023 }
2024 }
2025
2026 return true;
2027}
2028
2029bool TasksModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
2030{
2031 // In manual sort mode, sort by map.
2032 if (d->sortMode == SortManual) {
2033 return (d->sortedPreFilterRows.indexOf(d->preFilterIndex(left).row()) < d->sortedPreFilterRows.indexOf(d->preFilterIndex(right).row()));
2034 }
2035
2036 return d->lessThan(left, right);
2037}
2038
2039std::shared_ptr<VirtualDesktopInfo> TasksModel::virtualDesktopInfo() const
2040{
2041 static std::weak_ptr<VirtualDesktopInfo> s_virtualDesktopInfo;
2042 if (s_virtualDesktopInfo.expired()) {
2043 auto ptr = std::make_shared<VirtualDesktopInfo>();
2044 s_virtualDesktopInfo = ptr;
2045 return ptr;
2046 }
2047 return s_virtualDesktopInfo.lock();
2048}
2049
2050std::shared_ptr<ActivityInfo> TasksModel::activityInfo() const
2051{
2052 static std::weak_ptr<ActivityInfo> s_activityInfo;
2053 if (s_activityInfo.expired()) {
2054 auto ptr = std::make_shared<ActivityInfo>();
2055 s_activityInfo = ptr;
2056 return ptr;
2057 }
2058 return s_activityInfo.lock();
2059}
2060}
2061
2062#include "moc_tasksmodel.cpp"
@ IsLauncher
This is a launcher task.
@ Activities
Activities for the task (i.e.
@ IsActive
This is the currently active task.
@ IsWindow
This is a window task.
@ VirtualDesktops
Virtual desktops for the task (i.e.
@ LauncherUrlWithoutIcon
Special path to get a launcher URL while skipping fallback icon encoding.
@ LastActivated
The timestamp of the last time a task was the active task.
A tasks model for startup notifications.
A unified tasks model.
Definition tasksmodel.h:48
void setFilterNotMinimized(bool filter)
Set whether non-minimized tasks should be filtered.
void setLaunchInPlace(bool launchInPlace)
Sets whether window tasks should be sorted as their associated launcher tasks or separately.
Q_INVOKABLE int launcherPosition(const QUrl &url) const
Return the position of the launcher with the given URL.
Q_INVOKABLE void requestToggleMinimized(const QModelIndex &index) override
Request toggling the minimized state of the task at the given index.
Q_INVOKABLE void requestMove(const QModelIndex &index) override
Request starting an interactive move for the task at the given index.
void setFilterByVirtualDesktop(bool filter)
Set whether tasks should be filtered by virtual desktop.
void setFilterByActivity(bool filter)
Set whether tasks should be filtered by activity.
void setFilterByRegion(RegionFilterMode::Mode mode)
Set whether tasks should be filtered by region.
void setRegionGeometry(const QRect &geometry)
Set the geometry of the screen to use in filtering by region.
@ GroupDisabled
No grouping is done.
Definition tasksmodel.h:101
Q_INVOKABLE bool requestAddLauncher(const QUrl &url)
Request adding a launcher with the given URL.
void setGroupingWindowTasksThreshold(int threshold)
Sets the number of window tasks (AbstractTasksModel::IsWindow) above which groups will be formed,...
Q_INVOKABLE void requestToggleGrouping(const QModelIndex &index)
Request toggling whether the task at the given index, along with any tasks matching its kind,...
Q_INVOKABLE bool move(int row, int newPos, const QModelIndex &parent=QModelIndex())
Moves a task to a new position in the list.
Q_INVOKABLE void requestToggleKeepBelow(const QModelIndex &index) override
Request toggling the keep-below state of the task at the given index.
Q_INVOKABLE bool requestRemoveLauncher(const QUrl &url)
Request removing the launcher with the given URL.
Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(int row, int childRow=-1) const
Given a row in the model, returns a QPersistentModelIndex for it.
void setGroupingAppIdBlacklist(const QStringList &list)
Sets the blacklist of app ids (AbstractTasksModel::AppId) that is consulted before grouping a task.
void setGroupingLauncherUrlBlacklist(const QStringList &list)
Sets the blacklist of launcher URLs (AbstractTasksModel::LauncherUrl) that is consulted before groupi...
Q_INVOKABLE bool requestAddLauncherToActivity(const QUrl &url, const QString &activity)
Request adding a launcher with the given URL to current activity.
std::shared_ptr< ActivityInfo > activityInfo() const
Q_INVOKABLE void requestActivities(const QModelIndex &index, const QStringList &activities) override
Request moving the task at the given index to the specified activities.
Q_INVOKABLE bool requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity)
Request removing the launcher with the given URL from the current activity.
void setLauncherList(const QStringList &launchers)
Replace the list of launcher URL strings.
Q_INVOKABLE void requestActivate(const QModelIndex &index) override
Request activation of the task at the given index.
void setFilterNotMaximized(bool filter)
Set whether non-maximized tasks should be filtered.
std::shared_ptr< VirtualDesktopInfo > virtualDesktopInfo() const
Q_INVOKABLE void requestToggleFullScreen(const QModelIndex &index) override
Request toggling the fullscreen state of the task at the given index.
Q_INVOKABLE void requestToggleKeepAbove(const QModelIndex &index) override
Request toggling the keep-above state of the task at the given index.
void setFilterMinimized(bool filter)
Sets whether non-minimized tasks should be filtered out.
void setTaskReorderingEnabled(bool enabled)
Enables or disables tasks reordering.
Q_INVOKABLE void requestResize(const QModelIndex &index) override
Request starting an interactive resize for the task at the given index.
void setGroupInline(bool groupInline)
Sets whether grouping is done "inline" or not, i.e.
void setFilterHidden(bool filter)
Set whether hidden tasks should be filtered.
Q_INVOKABLE void requestNewInstance(const QModelIndex &index) override
Request an additional instance of the application backing the task at the given index.
Q_INVOKABLE void requestNewVirtualDesktop(const QModelIndex &index) override
Request entering the window at the given index on a new virtual desktop, which is created in response...
@ SortLastActivated
Tasks are sorted by the last time they were active.
Definition tasksmodel.h:96
@ SortManual
Tasks can be moved with move() and syncLaunchers().
Definition tasksmodel.h:92
@ SortActivity
Tasks are sorted by the number of tasks on the activities they're on.
Definition tasksmodel.h:95
@ SortVirtualDesktop
Tasks are sorted by the virtual desktop they are on.
Definition tasksmodel.h:94
void setVirtualDesktop(const QVariant &desktop=QVariant())
Set the id of the virtual desktop to use in filtering by virtual desktop.
Q_INVOKABLE void requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops) override
Request entering the window at the given index on the specified virtual desktops.
void setSeparateLaunchers(bool separate)
Sets whether launchers are kept separate from other kinds of tasks.
Q_INVOKABLE void requestToggleMaximized(const QModelIndex &index) override
Request toggling the maximized state of the task at the given index.
void setHideActivatedLaunchers(bool hideActivatedLaunchers)
Sets whether launchers should be hidden after they have been activated.
Q_INVOKABLE void requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate=nullptr) override
Request informing the window manager of new geometry for a visual delegate for the task at the given ...
void setActivity(const QString &activity)
Set the id of the activity to use in filtering by activity.
void setGroupMode(TasksModel::GroupMode mode)
Sets the group mode, i.e.
Q_INVOKABLE void syncLaunchers()
Updates the launcher list to reflect the new order after calls to move(), if needed.
void setSortMode(SortMode mode)
Sets the sort mode used in sorting tasks.
void setFilterByScreen(bool filter)
Set whether tasks should be filtered by screen.
Q_INVOKABLE void requestClose(const QModelIndex &index) override
Request the task at the given index be closed.
Q_INVOKABLE void requestToggleShaded(const QModelIndex &index) override
Request toggling the shaded state of the task at the given index.
Q_INVOKABLE QStringList launcherActivities(const QUrl &url)
Return the list of activities the launcher belongs to.
Q_INVOKABLE void requestOpenUrls(const QModelIndex &index, const QList< QUrl > &urls) override
Requests to open the given URLs with the application backing the task at the given index.
Q_INVOKABLE QModelIndex makeModelIndex(int row, int childRow=-1) const
Given a row in the model, returns a QModelIndex for it.
void setScreenGeometry(const QRect &geometry)
Set the geometry of the screen to use in filtering by screen.
Q_SCRIPTABLE QString start(QString train="")
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QCA_EXPORT QString appName()
QAbstractItemModel(QObject *parent)
bool beginMoveRows(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destinationParent, int destinationChild)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
bool isValid() const const
const_reference at(qsizetype i) const const
iterator begin()
const_iterator cbegin() const const
const_iterator cend() const const
bool contains(const AT &value) const const
qsizetype count() const const
pointer data()
iterator end()
qsizetype indexOf(const AT &value, qsizetype from) const const
bool isEmpty() const const
iterator insert(const Key &key, const T &value)
QList< T > values() const const
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
int row() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual QVariant data(const QModelIndex &index, int role) const const override
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const const override
virtual QModelIndex parent(const QModelIndex &child) const const override
virtual int rowCount(const QModelIndex &parent) const const override
void setSortRole(int role)
bool isEmpty() const const
int localeAwareCompare(QStringView s1, QStringView s2)
UniqueConnection
DisplayRole
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isNull() const const
bool toBool() const const
QDateTime toDateTime() const const
QString toString() const const
QStringList toStringList() const const
QUrl toUrl() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:51:10 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.