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 if (left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) {
778 return (left.row() < right.row());
779 } else if (left.data(AbstractTasksModel::IsLauncher).toBool() && !right.data(AbstractTasksModel::IsLauncher).toBool()) {
780 if (launchInPlace) {
781 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
782 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
783
784 if (rightPos != -1) {
785 return (leftPos < rightPos);
786 }
787 }
788
789 return true;
790 } else if (!left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) {
791 if (launchInPlace) {
792 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
793 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
794
795 if (leftPos != -1) {
796 return (leftPos < rightPos);
797 }
798 }
799
800 return false;
801 } else if (launchInPlace) {
802 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
803 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl());
804
805 if (leftPos != -1 && rightPos != -1) {
806 return (leftPos < rightPos);
807 } else if (leftPos != -1 && rightPos == -1) {
808 return true;
809 } else if (leftPos == -1 && rightPos != -1) {
810 return false;
811 }
812 }
813 }
814
815 // If told to stop after launchers we fall through to the existing map if it exists.
816 if (sortOnlyLaunchers && !sortedPreFilterRows.isEmpty()) {
817 return (sortedPreFilterRows.indexOf(left.row()) < sortedPreFilterRows.indexOf(right.row()));
818 }
819
820 // Sort other cases by sort mode.
821 switch (sortMode) {
822 case SortLastActivated: {
823 QDateTime leftSortDateTime, rightSortDateTime;
824
825 // Check if the task is in a group
826 if (left.parent().isValid()) {
827 leftSortDateTime = left.parent().data(AbstractTasksModel::LastActivated).toDateTime();
828 } else {
829 leftSortDateTime = left.data(AbstractTasksModel::LastActivated).toDateTime();
830 }
831
832 if (!leftSortDateTime.isValid()) {
833 leftSortDateTime = left.data(Qt::DisplayRole).toDateTime();
834 }
835
836 if (right.parent().isValid()) {
837 rightSortDateTime = right.parent().data(AbstractTasksModel::LastActivated).toDateTime();
838 } else {
839 rightSortDateTime = right.data(AbstractTasksModel::LastActivated).toDateTime();
840 }
841
842 if (!rightSortDateTime.isValid()) {
843 rightSortDateTime = right.data(Qt::DisplayRole).toDateTime();
844 }
845
846 if (leftSortDateTime != rightSortDateTime) {
847 // Move latest to leftmost
848 return leftSortDateTime > rightSortDateTime;
849 }
850
851 Q_FALLTHROUGH();
852 }
853
854 case SortVirtualDesktop: {
855 const bool leftAll = left.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool();
856 const bool rightAll = right.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool();
857
858 if (leftAll && !rightAll) {
859 return true;
860 } else if (rightAll && !leftAll) {
861 return false;
862 }
863
864 if (!(leftAll && rightAll)) {
865 const QVariantList &leftDesktops = left.data(AbstractTasksModel::VirtualDesktops).toList();
866 QVariant leftDesktop;
867 int leftDesktopPos = virtualDesktopInfo->numberOfDesktops();
868
869 for (const QVariant &desktop : leftDesktops) {
870 const int desktopPos = virtualDesktopInfo->position(desktop);
871
872 if (desktopPos <= leftDesktopPos) {
873 leftDesktop = desktop;
874 leftDesktopPos = desktopPos;
875 }
876 }
877
878 const QVariantList &rightDesktops = right.data(AbstractTasksModel::VirtualDesktops).toList();
879 QVariant rightDesktop;
880 int rightDesktopPos = virtualDesktopInfo->numberOfDesktops();
881
882 for (const QVariant &desktop : rightDesktops) {
883 const int desktopPos = virtualDesktopInfo->position(desktop);
884
885 if (desktopPos <= rightDesktopPos) {
886 rightDesktop = desktop;
887 rightDesktopPos = desktopPos;
888 }
889 }
890
891 if (!leftDesktop.isNull() && !rightDesktop.isNull() && (leftDesktop != rightDesktop)) {
892 return (virtualDesktopInfo->position(leftDesktop) < virtualDesktopInfo->position(rightDesktop));
893 } else if (!leftDesktop.isNull() && rightDesktop.isNull()) {
894 return false;
895 } else if (leftDesktop.isNull() && !rightDesktop.isNull()) {
896 return true;
897 }
898 }
899 }
900 // fall through
901 case SortActivity: {
902 // updateActivityTaskCounts() counts the number of window tasks on each
903 // activity. This will sort tasks by comparing a cumulative score made
904 // up of the task counts for each activity a task is assigned to, and
905 // otherwise fall through to alphabetical sorting.
906 const QStringList &leftActivities = left.data(AbstractTasksModel::Activities).toStringList();
907 int leftScore = std::accumulate(leftActivities.cbegin(), leftActivities.cend(), -1, [this](int a, const QString &activity) {
908 return a + activityTaskCounts[activity];
909 });
910
911 const QStringList &rightActivities = right.data(AbstractTasksModel::Activities).toStringList();
912 int rightScore = std::accumulate(rightActivities.cbegin(), rightActivities.cend(), -1, [this](int a, const QString &activity) {
913 return a + activityTaskCounts[activity];
914 });
915
916 if (leftScore == -1 || rightScore == -1) {
917 const int sumScore = std::accumulate(activityTaskCounts.constBegin(), activityTaskCounts.constEnd(), 0);
918
919 if (leftScore == -1) {
920 leftScore = sumScore;
921 }
922
923 if (rightScore == -1) {
924 rightScore = sumScore;
925 }
926 }
927
928 if (leftScore != rightScore) {
929 return (leftScore > rightScore);
930 }
931 }
932 // Fall through to source order if sorting is disabled or manual, or alphabetical by app name otherwise.
933 // This marker comment makes gcc/clang happy:
934 // fall through
935 default: {
936 if (sortMode == SortDisabled) {
937 return (left.row() < right.row());
938 } else {
939 // The overall goal of alphabetic sorting is to sort tasks belonging to the
940 // same app together, while sorting the resulting sets alphabetically among
941 // themselves by the app name. The following code tries to achieve this by
942 // going for AppName first, and falling back to DisplayRole - which for
943 // window-type tasks generally contains the window title - if AppName is
944 // not available. When comparing tasks with identical resulting sort strings,
945 // we sort them by the source model order (i.e. insertion/creation). Older
946 // versions of this code compared tasks by a concatenation of AppName and
947 // DisplayRole at all times, but always sorting by the window title does more
948 // than our goal description - and can cause tasks within an app's set to move
949 // around when window titles change, which is a nuisance for users (especially
950 // in case of tabbed apps that have the window title reflect the active tab,
951 // e.g. web browsers). To recap, the common case is "sort by AppName, then
952 // insertion order", only swapping out AppName for DisplayRole (i.e. window
953 // title) when necessary.
954
955 QString leftSortString = left.data(AbstractTasksModel::AppName).toString();
956
957 if (leftSortString.isEmpty()) {
958 leftSortString = left.data(Qt::DisplayRole).toString();
959 }
960
961 QString rightSortString = right.data(AbstractTasksModel::AppName).toString();
962
963 if (rightSortString.isEmpty()) {
964 rightSortString = right.data(Qt::DisplayRole).toString();
965 }
966
967 const int sortResult = leftSortString.localeAwareCompare(rightSortString);
968
969 // If the string are identical fall back to source model (creation/append) order.
970 if (sortResult == 0) {
971 return (left.row() < right.row());
972 }
973
974 return (sortResult < 0);
975 }
976 }
977 }
978}
979
980TasksModel::TasksModel(QObject *parent)
981 : QSortFilterProxyModel(parent)
982 , d(new Private(this))
983{
984 d->initModels();
985
986 // Start sorting.
987 sort(0);
988
989 connect(this, &TasksModel::sourceModelChanged, this, &TasksModel::countChanged);
990
991 // Private::updateGroupInline() sets our source model, populating the model. We
992 // delay running this until the QML runtime had a chance to call our implementation
993 // of QQmlParserStatus::classBegin(), setting Private::usedByQml to true. If used
994 // by QML, Private::updateGroupInline() will abort if the component is not yet
995 // complete, instead getting called through QQmlParserStatus::componentComplete()
996 // only after all properties have been set. This avoids delegate churn in Qt Quick
997 // views using the model. If not used by QML, Private::updateGroupInline() will run
998 // directly.
999 QTimer::singleShot(0, this, [this]() {
1000 d->updateGroupInline();
1001 });
1002}
1003
1004TasksModel::~TasksModel()
1005{
1006}
1007
1008QHash<int, QByteArray> TasksModel::roleNames() const
1009{
1010 if (d->windowTasksModel) {
1011 return d->windowTasksModel->roleNames();
1012 }
1013
1014 return QHash<int, QByteArray>();
1015}
1016
1017int TasksModel::rowCount(const QModelIndex &parent) const
1018{
1019 return QSortFilterProxyModel::rowCount(parent);
1020}
1021
1022QVariant TasksModel::data(const QModelIndex &proxyIndex, int role) const
1023{
1024 if (role == AbstractTasksModel::HasLauncher && proxyIndex.isValid() && proxyIndex.row() < rowCount()) {
1025 if (proxyIndex.data(AbstractTasksModel::IsLauncher).toBool()) {
1026 return true;
1027 } else {
1028 if (!d->launcherTasksModel) {
1029 return false;
1030 }
1031 for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) {
1032 const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0);
1033
1034 if (appsMatch(proxyIndex, launcherIndex)) {
1035 return true;
1036 }
1037 }
1038
1039 return false;
1040 }
1041 } else if (rowCount(proxyIndex) && role == AbstractTasksModel::WinIdList) {
1042 QVariantList winIds;
1043
1044 for (int i = 0; i < rowCount(proxyIndex); ++i) {
1045 winIds.append(index(i, 0, proxyIndex).data(AbstractTasksModel::WinIdList).toList());
1046 }
1047
1048 return winIds;
1049 }
1050
1051 return QSortFilterProxyModel::data(proxyIndex, role);
1052}
1053
1054void TasksModel::updateLauncherCount()
1055{
1056 if (!d->launcherTasksModel) {
1057 return;
1058 }
1059
1060 int count = 0;
1061
1062 for (int i = 0; i < rowCount(); ++i) {
1063 if (index(i, 0).data(AbstractTasksModel::IsLauncher).toBool()) {
1064 ++count;
1065 }
1066 }
1067
1068 if (d->launcherCount != count) {
1069 d->launcherCount = count;
1070 Q_EMIT launcherCountChanged();
1071 }
1072}
1073
1074int TasksModel::launcherCount() const
1075{
1076 return d->launcherCount;
1077}
1078
1079bool TasksModel::anyTaskDemandsAttention() const
1080{
1081 return d->anyTaskDemandsAttention;
1082}
1083
1084QVariant TasksModel::virtualDesktop() const
1085{
1086 return d->filterProxyModel->virtualDesktop();
1087}
1088
1089void TasksModel::setVirtualDesktop(const QVariant &desktop)
1090{
1091 d->filterProxyModel->setVirtualDesktop(desktop);
1092}
1093
1094QRect TasksModel::screenGeometry() const
1095{
1096 return d->filterProxyModel->screenGeometry();
1097}
1098
1099void TasksModel::setScreenGeometry(const QRect &geometry)
1100{
1101 d->filterProxyModel->setScreenGeometry(geometry);
1102}
1103
1104QRect TasksModel::regionGeometry() const
1105{
1106 return d->filterProxyModel->regionGeometry();
1107}
1108
1109void TasksModel::setRegionGeometry(const QRect &geometry)
1110{
1111 d->filterProxyModel->setRegionGeometry(geometry);
1112}
1113
1114QString TasksModel::activity() const
1115{
1116 return d->filterProxyModel->activity();
1117}
1118
1119void TasksModel::setActivity(const QString &activity)
1120{
1121 d->filterProxyModel->setActivity(activity);
1122}
1123
1124bool TasksModel::filterByVirtualDesktop() const
1125{
1126 return d->filterProxyModel->filterByVirtualDesktop();
1127}
1128
1129void TasksModel::setFilterByVirtualDesktop(bool filter)
1130{
1131 d->filterProxyModel->setFilterByVirtualDesktop(filter);
1132}
1133
1134bool TasksModel::filterByScreen() const
1135{
1136 return d->filterProxyModel->filterByScreen();
1137}
1138
1139void TasksModel::setFilterByScreen(bool filter)
1140{
1141 d->filterProxyModel->setFilterByScreen(filter);
1142}
1143
1144bool TasksModel::filterByActivity() const
1145{
1146 return d->filterProxyModel->filterByActivity();
1147}
1148
1149void TasksModel::setFilterByActivity(bool filter)
1150{
1151 d->filterProxyModel->setFilterByActivity(filter);
1152}
1153
1154RegionFilterMode::Mode TasksModel::filterByRegion() const
1155{
1156 return d->filterProxyModel->filterByRegion();
1157}
1158
1159void TasksModel::setFilterByRegion(RegionFilterMode::Mode mode)
1160{
1161 d->filterProxyModel->setFilterByRegion(mode);
1162}
1163
1164bool TasksModel::filterMinimized() const
1165{
1166 return d->filterProxyModel->filterMinimized();
1167}
1168
1169void TasksModel::setFilterMinimized(bool filter)
1170{
1171 d->filterProxyModel->setFilterMinimized(filter);
1172}
1173
1174bool TasksModel::filterNotMinimized() const
1175{
1176 return d->filterProxyModel->filterNotMinimized();
1177}
1178
1179void TasksModel::setFilterNotMinimized(bool filter)
1180{
1181 d->filterProxyModel->setFilterNotMinimized(filter);
1182}
1183
1184bool TasksModel::filterNotMaximized() const
1185{
1186 return d->filterProxyModel->filterNotMaximized();
1187}
1188
1189void TasksModel::setFilterNotMaximized(bool filter)
1190{
1191 d->filterProxyModel->setFilterNotMaximized(filter);
1192}
1193
1194bool TasksModel::filterHidden() const
1195{
1196 return d->filterProxyModel->filterHidden();
1197}
1198
1199void TasksModel::setFilterHidden(bool filter)
1200{
1201 d->filterProxyModel->setFilterHidden(filter);
1202}
1203
1204TasksModel::SortMode TasksModel::sortMode() const
1205{
1206 return d->sortMode;
1207}
1208
1209void TasksModel::setSortMode(SortMode mode)
1210{
1211 if (d->sortMode != mode) {
1212 if (mode == SortManual) {
1213 d->updateManualSortMap();
1214 } else if (d->sortMode == SortManual) {
1215 d->sortedPreFilterRows.clear();
1216 }
1217
1218 if (mode == SortVirtualDesktop) {
1219 d->virtualDesktopInfo = virtualDesktopInfo();
1220 setSortRole(AbstractTasksModel::VirtualDesktops);
1221 } else if (d->sortMode == SortVirtualDesktop) {
1222 d->virtualDesktopInfo = nullptr;
1223 setSortRole(Qt::DisplayRole);
1224 }
1225
1226 if (mode == SortActivity) {
1227 d->activityInfo = activityInfo();
1228
1229 d->updateActivityTaskCounts();
1230 setSortRole(AbstractTasksModel::Activities);
1231 } else if (d->sortMode == SortActivity) {
1232 d->activityInfo = nullptr;
1233
1234 d->activityTaskCounts.clear();
1235 setSortRole(Qt::DisplayRole);
1236 }
1237
1238 if (mode == SortLastActivated) {
1239 setSortRole(AbstractTasksModel::LastActivated);
1240 }
1241
1242 d->sortMode = mode;
1243
1244 d->forceResort();
1245
1246 Q_EMIT sortModeChanged();
1247 }
1248}
1249
1250bool TasksModel::separateLaunchers() const
1251{
1252 return d->separateLaunchers;
1253}
1254
1255void TasksModel::setSeparateLaunchers(bool separate)
1256{
1257 if (d->separateLaunchers != separate) {
1258 d->separateLaunchers = separate;
1259
1260 d->updateManualSortMap();
1261 d->forceResort();
1262
1263 Q_EMIT separateLaunchersChanged();
1264 }
1265}
1266
1267bool TasksModel::launchInPlace() const
1268{
1269 return d->launchInPlace;
1270}
1271
1272void TasksModel::setLaunchInPlace(bool launchInPlace)
1273{
1274 if (d->launchInPlace != launchInPlace) {
1275 d->launchInPlace = launchInPlace;
1276
1277 d->forceResort();
1278
1279 Q_EMIT launchInPlaceChanged();
1280 }
1281}
1282
1283TasksModel::GroupMode TasksModel::groupMode() const
1284{
1285 if (!d->groupingProxyModel) {
1286 return GroupDisabled;
1287 }
1288
1289 return d->groupingProxyModel->groupMode();
1290}
1291
1292bool TasksModel::hideActivatedLaunchers() const
1293{
1294 return d->hideActivatedLaunchers;
1295}
1296
1297void TasksModel::setHideActivatedLaunchers(bool hideActivatedLaunchers)
1298{
1299 if (d->hideActivatedLaunchers != hideActivatedLaunchers) {
1300 d->hideActivatedLaunchers = hideActivatedLaunchers;
1301
1302 d->updateManualSortMap();
1303 d->forceResort();
1304
1305 Q_EMIT hideActivatedLaunchersChanged();
1306 }
1307}
1308
1309void TasksModel::setGroupMode(GroupMode mode)
1310{
1311 if (d->groupingProxyModel) {
1312 if (mode == GroupDisabled && d->flattenGroupsProxyModel) {
1313 d->flattenGroupsProxyModel->setSourceModel(nullptr);
1314 }
1315
1316 d->groupingProxyModel->setGroupMode(mode);
1317 d->updateGroupInline();
1318 }
1319}
1320
1321bool TasksModel::groupInline() const
1322{
1323 return d->groupInline;
1324}
1325
1326void TasksModel::setGroupInline(bool groupInline)
1327{
1328 if (d->groupInline != groupInline) {
1329 d->groupInline = groupInline;
1330
1331 d->updateGroupInline();
1332
1333 Q_EMIT groupInlineChanged();
1334 }
1335}
1336
1337int TasksModel::groupingWindowTasksThreshold() const
1338{
1339 return d->groupingWindowTasksThreshold;
1340}
1341
1342void TasksModel::setGroupingWindowTasksThreshold(int threshold)
1343{
1344 if (d->groupingWindowTasksThreshold != threshold) {
1345 d->groupingWindowTasksThreshold = threshold;
1346
1347 if (!d->groupInline && d->groupingProxyModel) {
1348 d->groupingProxyModel->setWindowTasksThreshold(threshold);
1349 }
1350
1351 Q_EMIT groupingWindowTasksThresholdChanged();
1352 }
1353}
1354
1355QStringList TasksModel::groupingAppIdBlacklist() const
1356{
1357 if (!d->groupingProxyModel) {
1358 return QStringList();
1359 }
1360
1361 return d->groupingProxyModel->blacklistedAppIds();
1362}
1363
1364void TasksModel::setGroupingAppIdBlacklist(const QStringList &list)
1365{
1366 if (d->groupingProxyModel) {
1367 d->groupingProxyModel->setBlacklistedAppIds(list);
1368 }
1369}
1370
1371QStringList TasksModel::groupingLauncherUrlBlacklist() const
1372{
1373 if (!d->groupingProxyModel) {
1374 return QStringList();
1375 }
1376
1377 return d->groupingProxyModel->blacklistedLauncherUrls();
1378}
1379
1380void TasksModel::setGroupingLauncherUrlBlacklist(const QStringList &list)
1381{
1382 if (d->groupingProxyModel) {
1383 d->groupingProxyModel->setBlacklistedLauncherUrls(list);
1384 }
1385}
1386
1387bool TasksModel::taskReorderingEnabled() const
1388{
1389 return dynamicSortFilter();
1390}
1391
1392void TasksModel::setTaskReorderingEnabled(bool enabled)
1393{
1394 enabled ? setDynamicSortFilter(true) : setDynamicSortFilter(false);
1395
1396 Q_EMIT taskReorderingEnabledChanged();
1397}
1398
1399QStringList TasksModel::launcherList() const
1400{
1401 if (d->launcherTasksModel) {
1402 return d->launcherTasksModel->launcherList();
1403 }
1404
1405 return QStringList();
1406}
1407
1408void TasksModel::setLauncherList(const QStringList &launchers)
1409{
1410 d->initLauncherTasksModel();
1411 d->launcherTasksModel->setLauncherList(launchers);
1412 d->launchersEverSet = true;
1413}
1414
1415bool TasksModel::requestAddLauncher(const QUrl &url)
1416{
1417 d->initLauncherTasksModel();
1418
1419 bool added = d->launcherTasksModel->requestAddLauncher(url);
1420
1421 // If using manual and launch-in-place sorting with separate launchers,
1422 // we need to trigger a sort map update to move any window tasks to
1423 // their launcher position now.
1424 if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1425 d->updateManualSortMap();
1426 d->forceResort();
1427 }
1428
1429 return added;
1430}
1431
1432bool TasksModel::requestRemoveLauncher(const QUrl &url)
1433{
1434 if (d->launcherTasksModel) {
1435 bool removed = d->launcherTasksModel->requestRemoveLauncher(url);
1436
1437 // If using manual and launch-in-place sorting with separate launchers,
1438 // we need to trigger a sort map update to move any window tasks no
1439 // longer backed by a launcher out of the launcher area.
1440 if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1441 d->updateManualSortMap();
1442 d->forceResort();
1443 }
1444
1445 return removed;
1446 }
1447
1448 return false;
1449}
1450
1451bool TasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity)
1452{
1453 d->initLauncherTasksModel();
1454
1455 bool added = d->launcherTasksModel->requestAddLauncherToActivity(url, activity);
1456
1457 // If using manual and launch-in-place sorting with separate launchers,
1458 // we need to trigger a sort map update to move any window tasks to
1459 // their launcher position now.
1460 if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1461 d->updateManualSortMap();
1462 d->forceResort();
1463 }
1464
1465 return added;
1466}
1467
1468bool TasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity)
1469{
1470 if (d->launcherTasksModel) {
1471 bool removed = d->launcherTasksModel->requestRemoveLauncherFromActivity(url, activity);
1472
1473 // If using manual and launch-in-place sorting with separate launchers,
1474 // we need to trigger a sort map update to move any window tasks no
1475 // longer backed by a launcher out of the launcher area.
1476 if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) {
1477 d->updateManualSortMap();
1478 d->forceResort();
1479 }
1480
1481 return removed;
1482 }
1483
1484 return false;
1485}
1486
1487QStringList TasksModel::launcherActivities(const QUrl &url)
1488{
1489 if (d->launcherTasksModel) {
1490 return d->launcherTasksModel->launcherActivities(url);
1491 }
1492
1493 return {};
1494}
1495
1496int TasksModel::launcherPosition(const QUrl &url) const
1497{
1498 if (d->launcherTasksModel) {
1499 return d->launcherTasksModel->launcherPosition(url);
1500 }
1501
1502 return -1;
1503}
1504
1505void TasksModel::requestActivate(const QModelIndex &index)
1506{
1507 if (index.isValid() && index.model() == this) {
1508 d->abstractTasksSourceModel->requestActivate(mapToSource(index));
1509 }
1510}
1511
1512void TasksModel::requestNewInstance(const QModelIndex &index)
1513{
1514 if (index.isValid() && index.model() == this) {
1515 d->abstractTasksSourceModel->requestNewInstance(mapToSource(index));
1516 }
1517}
1518
1519void TasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
1520{
1521 if (index.isValid() && index.model() == this) {
1522 d->abstractTasksSourceModel->requestOpenUrls(mapToSource(index), urls);
1523 }
1524}
1525
1526void TasksModel::requestClose(const QModelIndex &index)
1527{
1528 if (index.isValid() && index.model() == this) {
1529 d->abstractTasksSourceModel->requestClose(mapToSource(index));
1530 }
1531}
1532
1533void TasksModel::requestMove(const QModelIndex &index)
1534{
1535 if (index.isValid() && index.model() == this) {
1536 d->abstractTasksSourceModel->requestMove(mapToSource(index));
1537 }
1538}
1539
1540void TasksModel::requestResize(const QModelIndex &index)
1541{
1542 if (index.isValid() && index.model() == this) {
1543 d->abstractTasksSourceModel->requestResize(mapToSource(index));
1544 }
1545}
1546
1547void TasksModel::requestToggleMinimized(const QModelIndex &index)
1548{
1549 if (index.isValid() && index.model() == this) {
1550 d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index));
1551 }
1552}
1553
1554void TasksModel::requestToggleMaximized(const QModelIndex &index)
1555{
1556 if (index.isValid() && index.model() == this) {
1557 d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index));
1558 }
1559}
1560
1561void TasksModel::requestToggleKeepAbove(const QModelIndex &index)
1562{
1563 if (index.isValid() && index.model() == this) {
1564 d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index));
1565 }
1566}
1567
1568void TasksModel::requestToggleKeepBelow(const QModelIndex &index)
1569{
1570 if (index.isValid() && index.model() == this) {
1571 d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index));
1572 }
1573}
1574
1575void TasksModel::requestToggleFullScreen(const QModelIndex &index)
1576{
1577 if (index.isValid() && index.model() == this) {
1578 d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index));
1579 }
1580}
1581
1582void TasksModel::requestToggleShaded(const QModelIndex &index)
1583{
1584 if (index.isValid() && index.model() == this) {
1585 d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index));
1586 }
1587}
1588
1589void TasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops)
1590{
1591 if (index.isValid() && index.model() == this) {
1592 d->abstractTasksSourceModel->requestVirtualDesktops(mapToSource(index), desktops);
1593 }
1594}
1595
1596void TasksModel::requestNewVirtualDesktop(const QModelIndex &index)
1597{
1598 if (index.isValid() && index.model() == this) {
1599 d->abstractTasksSourceModel->requestNewVirtualDesktop(mapToSource(index));
1600 }
1601}
1602
1603void TasksModel::requestActivities(const QModelIndex &index, const QStringList &activities)
1604{
1605 if (index.isValid() && index.model() == this) {
1606 d->groupingProxyModel->requestActivities(mapToSource(index), activities);
1607 }
1608}
1609
1610void TasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate)
1611{
1612 if (!index.isValid() || index.model() != this || !index.data(AbstractTasksModel::IsWindow).toBool()) {
1613 return;
1614 }
1615 d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate);
1616}
1617
1618void TasksModel::requestToggleGrouping(const QModelIndex &index)
1619{
1620 if (index.isValid() && index.model() == this) {
1621 const QModelIndex &target = (d->flattenGroupsProxyModel ? d->flattenGroupsProxyModel->mapToSource(mapToSource(index)) : mapToSource(index));
1622 d->groupingProxyModel->requestToggleGrouping(target);
1623 }
1624}
1625
1626bool TasksModel::move(int row, int newPos, const QModelIndex &parent)
1627{
1628 /*
1629 * NOTE After doing any modification in TasksModel::move, make sure fixes listed below are not regressed.
1630 * - https://bugs.kde.org/444816
1631 * - https://bugs.kde.org/448912
1632 * - https://invent.kde.org/plasma/plasma-workspace/-/commit/ea51795e8c571513e1ff583350ab8649bc857fc2
1633 */
1634
1635 if (d->sortMode != SortManual || row == newPos || newPos < 0 || newPos >= rowCount(parent)) {
1636 return false;
1637 }
1638
1639 const QModelIndex &idx = index(row, 0, parent);
1640 bool isLauncherMove = false;
1641
1642 // Figure out if we're moving a launcher so we can run barrier checks.
1643 if (idx.isValid()) {
1644 if (idx.data(AbstractTasksModel::IsLauncher).toBool()) {
1645 isLauncherMove = true;
1646 // When using launch-in-place sorting, launcher-backed window tasks act as launchers.
1647 } else if ((d->launchInPlace || !d->separateLaunchers) && idx.data(AbstractTasksModel::IsWindow).toBool()) {
1648 const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1649 const int launcherPos = launcherPosition(launcherUrl);
1650
1651 if (launcherPos != -1) {
1652 isLauncherMove = true;
1653 }
1654 }
1655 } else {
1656 return false;
1657 }
1658
1659 if (d->separateLaunchers && !parent.isValid() /* Exclude tasks in a group */) {
1660 int firstTask = 0;
1661 if (d->launcherTasksModel) {
1662 if (d->launchInPlace) {
1663 firstTask = d->launcherTasksModel->rowCountForActivity(activity());
1664 } else {
1665 firstTask = launcherCount();
1666 }
1667 }
1668
1669 // Don't allow launchers to be moved past the last launcher.
1670 if (isLauncherMove && newPos >= firstTask) {
1671 return false;
1672 }
1673
1674 // Don't allow tasks to be moved into the launchers.
1675 if (!isLauncherMove && newPos < firstTask) {
1676 return false;
1677 }
1678 }
1679
1680 // Treat flattened-out groups as single items.
1681 if (d->flattenGroupsProxyModel) {
1682 QModelIndex groupingRowIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(row, 0)));
1683 const QModelIndex &groupingRowIndexParent = groupingRowIndex.parent();
1684 QModelIndex groupingNewPosIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(newPos, 0)));
1685 const QModelIndex &groupingNewPosIndexParent = groupingNewPosIndex.parent();
1686
1687 // Disallow moves within a flattened-out group (TODO: for now, anyway).
1688 if (groupingRowIndexParent.isValid() && (groupingRowIndexParent == groupingNewPosIndex || groupingRowIndexParent == groupingNewPosIndexParent)) {
1689 return false;
1690 }
1691
1692 int offset = 0;
1693 int extraChildCount = 0;
1694
1695 if (groupingRowIndexParent.isValid()) {
1696 offset = groupingRowIndex.row();
1697 extraChildCount = d->groupingProxyModel->rowCount(groupingRowIndexParent) - 1;
1698 groupingRowIndex = groupingRowIndexParent;
1699 }
1700
1701 if (groupingNewPosIndexParent.isValid()) {
1702 int extra = d->groupingProxyModel->rowCount(groupingNewPosIndexParent) - 1;
1703
1704 if (newPos > row) {
1705 newPos += extra;
1706 newPos -= groupingNewPosIndex.row();
1707 groupingNewPosIndex = groupingNewPosIndexParent.model()->index(extra, 0, groupingNewPosIndexParent);
1708 } else {
1709 newPos -= groupingNewPosIndex.row();
1710 groupingNewPosIndex = groupingNewPosIndexParent;
1711 }
1712 }
1713
1714 beginMoveRows(QModelIndex(), (row - offset), (row - offset) + extraChildCount, QModelIndex(), (newPos > row) ? newPos + 1 : newPos);
1715
1716 row = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingRowIndex)).row());
1717 newPos = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingNewPosIndex)).row());
1718
1719 // Update sort mappings.
1720 d->sortedPreFilterRows.move(row, newPos);
1721
1722 endMoveRows();
1723
1724 if (groupingRowIndexParent.isValid()) {
1725 d->consolidateManualSortMapForGroup(groupingRowIndexParent);
1726 }
1727
1728 } else {
1729 beginMoveRows(parent, row, row, parent, (newPos > row) ? newPos + 1 : newPos);
1730
1731 // Translate to sort map indices.
1732 const QModelIndex &groupingRowIndex = mapToSource(index(row, 0, parent));
1733 const QModelIndex &preFilterRowIndex = d->preFilterIndex(groupingRowIndex);
1734
1735 const bool groupNotDisabled = !parent.isValid() && groupMode() != GroupDisabled;
1736 QModelIndex adjacentGroupingRowIndex; // Also consolidate the adjacent group parent
1737 if (groupNotDisabled) {
1738 if (newPos > row && row + 1 < rowCount(parent)) {
1739 adjacentGroupingRowIndex = mapToSource(index(row + 1, 0, parent) /* task on the right */);
1740 } else if (newPos < row && row - 1 >= 0) {
1741 adjacentGroupingRowIndex = mapToSource(index(row - 1, 0, parent) /* task on the left */);
1742 }
1743 }
1744
1745 row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row());
1746 newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0, parent))).row());
1747
1748 // Update sort mapping.
1749 d->sortedPreFilterRows.move(row, newPos);
1750
1751 endMoveRows();
1752
1753 // If we moved a group parent, consolidate sort map for children.
1754 if (groupNotDisabled) {
1755 if (d->groupingProxyModel->rowCount(groupingRowIndex)) {
1756 d->consolidateManualSortMapForGroup(groupingRowIndex);
1757 }
1758 // Special case: Before moving, the task at newPos is a group parent
1759 // Before moving: [Task] [Group parent] [Other task in group]
1760 // After moving: [Group parent (not consolidated yet)] [Task, newPos] [Other task in group]
1761 if (int childCount = d->groupingProxyModel->rowCount(adjacentGroupingRowIndex); childCount && adjacentGroupingRowIndex.isValid()) {
1762 d->consolidateManualSortMapForGroup(adjacentGroupingRowIndex);
1763 if (newPos > row) {
1764 newPos += childCount - 1;
1765 // After consolidation: [Group parent (not consolidated yet)] [Other task in group] [Task, newPos]
1766 }
1767 // No need to consider newPos < row
1768 // Before moving: [Group parent, newPos] [Other task in group] [Task]
1769 // After moving: [Task, newPos] [Group parent] [Other task in group]
1770 }
1771 }
1772 }
1773
1774 // Resort.
1775 d->forceResort();
1776
1777 if (!d->separateLaunchers) {
1778 if (isLauncherMove) {
1779 const QModelIndex &idx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos), 0);
1780 const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1781
1782 // Move launcher for launcher-backed task along with task if launchers
1783 // are not being kept separate.
1784 // We don't need to resort again because the launcher is implicitly hidden
1785 // at this time.
1786 if (!idx.data(AbstractTasksModel::IsLauncher).toBool()) {
1787 const int launcherPos = d->launcherTasksModel->launcherPosition(launcherUrl);
1788 const QModelIndex &launcherIndex = d->launcherTasksModel->index(launcherPos, 0);
1789 const int sortIndex = d->sortedPreFilterRows.indexOf(d->concatProxyModel->mapFromSource(launcherIndex).row());
1790 d->sortedPreFilterRows.move(sortIndex, newPos);
1791
1792 if (row > newPos && newPos >= 1) {
1793 const QModelIndex beforeIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos - 1), 0);
1794 if (beforeIdx.data(AbstractTasksModel::IsLauncher).toBool()) {
1795 // Search forward to skip grouped tasks
1796 int afterPos = newPos + 1;
1797 for (; afterPos < d->sortedPreFilterRows.size(); ++afterPos) {
1798 const QModelIndex tempIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(afterPos), 0);
1799 if (!appsMatch(idx, tempIdx)) {
1800 break;
1801 }
1802 }
1803
1804 const QModelIndex afterIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(afterPos), 0);
1805 if (appsMatch(beforeIdx, afterIdx)) {
1806 d->sortedPreFilterRows.move(newPos - 1, afterPos - 1);
1807 }
1808 }
1809 }
1810 // Otherwise move matching windows to after the launcher task (they are
1811 // currently hidden but might be on another virtual desktop).
1812 } else {
1813 for (int i = (d->sortedPreFilterRows.count() - 1); i >= 0; --i) {
1814 const QModelIndex &concatProxyIndex = d->concatProxyModel->index(d->sortedPreFilterRows.at(i), 0);
1815
1816 if (launcherUrl == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) {
1817 d->sortedPreFilterRows.move(i, newPos);
1818
1819 if (newPos > i) {
1820 --newPos;
1821 }
1822 }
1823 }
1824 }
1825 } else if (newPos > 0 && newPos < d->sortedPreFilterRows.size() - 1) {
1826 /*
1827 * When dragging an unpinned task, a pinned task can also be moved.
1828 * In this case, sortedPreFilterRows is like:
1829 * - before moving: [pinned 1 (launcher item)] [pinned 1 (window)] [unpinned]
1830 * - after moving: [pinned 1 (launcher item)] [unpinned] [pinned 1 (window)]
1831 * So also check the indexes before and after the unpinned task.
1832 */
1833 const QModelIndex beforeIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos - 1), 0, parent);
1834 const QModelIndex afterIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos + 1), 0, parent);
1835 // BUG 462508: check if any item is a launcher
1836 const bool hasLauncher = beforeIdx.data(AbstractTasksModel::IsLauncher).toBool() || afterIdx.data(AbstractTasksModel::IsLauncher).toBool();
1837
1838 if (hasLauncher && appsMatch(beforeIdx, afterIdx)) {
1839 // after adjusting: [unpinned] [pinned 1 (launcher item)] [pinned 1]
1840 d->sortedPreFilterRows.move(newPos, newPos + (row < newPos ? 1 : -1));
1841 }
1842 }
1843 }
1844
1845 // Setup for syncLaunchers().
1846 d->launcherSortingDirty = isLauncherMove;
1847
1848 return true;
1849}
1850
1851void TasksModel::syncLaunchers()
1852{
1853 // Writes the launcher order exposed through the model back to the launcher
1854 // tasks model, committing any move() operations to persistent state.
1855
1856 if (!d->launcherTasksModel || !d->launcherSortingDirty) {
1857 return;
1858 }
1859
1860 QMap<int, QString> sortedShownLaunchers;
1861 QStringList sortedHiddenLaunchers;
1862
1863 for (const auto launchers = launcherList(); const QString &launcherUrlStr : launchers) {
1864 int row = -1;
1865 QList<QStringView> activities;
1866 QUrl launcherUrl;
1867
1868 std::tie(launcherUrl, activities) = deserializeLauncher(launcherUrlStr);
1869
1870 for (int i = 0; i < rowCount(); ++i) {
1871 const QUrl &rowLauncherUrl = index(i, 0).data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1872
1873 // `LauncherTasksModel::launcherList()` returns data in a format suitable for writing
1874 // to persistent configuration storage, e.g. `preferred://browser`. We mean to compare
1875 // this last "save state" to a higher, resolved URL representation to compute the delta
1876 // so we need to move the unresolved URLs through `TaskTools::appDataFromUrl()` first.
1877 // TODO: This bypasses an existing lookup cache for the resolved app data that exists
1878 // in LauncherTasksModel. It's likely a good idea to eventually move these caches out
1879 // of the various models and share them among users of `TaskTools::appDataFromUrl()`,
1880 // and then also do resolution implicitly in `TaskTools::launcherUrlsMatch`, to speed
1881 // things up slightly and make the models simpler (central cache eviction, ...).
1882 if (launcherUrlsMatch(appDataFromUrl(launcherUrl).url, rowLauncherUrl, IgnoreQueryItems)) {
1883 row = i;
1884 break;
1885 }
1886 }
1887
1888 if (row != -1) {
1889 sortedShownLaunchers.insert(row, launcherUrlStr);
1890 } else {
1891 sortedHiddenLaunchers << launcherUrlStr;
1892 }
1893 }
1894
1895 // Prep sort map for source model data changes.
1896 if (d->sortMode == SortManual) {
1897 QList<int> sortMapIndices;
1898 QList<int> preFilterRows;
1899
1900 for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) {
1901 const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0);
1902 const QModelIndex &concatIndex = d->concatProxyModel->mapFromSource(launcherIndex);
1903 sortMapIndices << d->sortedPreFilterRows.indexOf(concatIndex.row());
1904 preFilterRows << concatIndex.row();
1905 }
1906
1907 // We're going to write back launcher model entries in the sort
1908 // map in concat model order, matching the reordered launcher list
1909 // we're about to pass down.
1910 std::sort(sortMapIndices.begin(), sortMapIndices.end());
1911
1912 for (int i = 0; i < sortMapIndices.count(); ++i) {
1913 d->sortedPreFilterRows.replace(sortMapIndices.at(i), preFilterRows.at(i));
1914 }
1915 }
1916
1917 setLauncherList(sortedShownLaunchers.values() + sortedHiddenLaunchers);
1918
1919 // The accepted rows are outdated after the item order is changed
1920 invalidateFilter();
1921 d->forceResort();
1922
1923 d->launcherSortingDirty = false;
1924}
1925
1926QModelIndex TasksModel::activeTask() const
1927{
1928 for (int i = 0; i < rowCount(); ++i) {
1929 const QModelIndex &idx = index(i, 0);
1930
1931 if (idx.data(AbstractTasksModel::IsActive).toBool()) {
1932 if (groupMode() != GroupDisabled && rowCount(idx)) {
1933 for (int j = 0; j < rowCount(idx); ++j) {
1934 const QModelIndex &child = index(j, 0, idx);
1935
1936 if (child.data(AbstractTasksModel::IsActive).toBool()) {
1937 return child;
1938 }
1939 }
1940 } else {
1941 return idx;
1942 }
1943 }
1944 }
1945
1946 return QModelIndex();
1947}
1948
1949QModelIndex TasksModel::makeModelIndex(int row, int childRow) const
1950{
1951 if (row < 0 || row >= rowCount()) {
1952 return QModelIndex();
1953 }
1954
1955 if (childRow == -1) {
1956 return index(row, 0);
1957 } else {
1958 const QModelIndex &parent = index(row, 0);
1959
1960 if (childRow < rowCount(parent)) {
1961 return index(childRow, 0, parent);
1962 }
1963 }
1964
1965 return QModelIndex();
1966}
1967
1968QPersistentModelIndex TasksModel::makePersistentModelIndex(int row, int childCount) const
1969{
1970 return QPersistentModelIndex(makeModelIndex(row, childCount));
1971}
1972
1973void TasksModel::classBegin()
1974{
1975 d->usedByQml = true;
1976}
1977
1978void TasksModel::componentComplete()
1979{
1980 d->componentComplete = true;
1981
1982 // Sets our source model, populating the model.
1983 d->updateGroupInline();
1984}
1985
1986bool TasksModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
1987{
1988 // All our filtering occurs at the top-level; anything below always
1989 // goes through.
1990 if (sourceParent.isValid()) {
1991 return true;
1992 }
1993
1994 const QModelIndex &sourceIndex = sourceModel()->index(sourceRow, 0);
1995
1996 // In inline grouping mode, filter out group parents.
1997 if (d->groupInline && d->flattenGroupsProxyModel && sourceIndex.data(AbstractTasksModel::IsGroupParent).toBool()) {
1998 return false;
1999 }
2000
2001 const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString();
2002 const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString();
2003
2004 // Filter startup tasks we already have a window task for.
2005 if (sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) {
2006 for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) {
2007 const QModelIndex &filterIndex = d->filterProxyModel->index(i, 0);
2008
2009 if (!filterIndex.data(AbstractTasksModel::IsWindow).toBool()) {
2010 continue;
2011 }
2012
2013 if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString())
2014 || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) {
2015 return false;
2016 }
2017 }
2018 }
2019
2020 // Filter launcher tasks we already have a startup or window task for (that
2021 // got through filtering).
2022 if (d->hideActivatedLaunchers && sourceIndex.data(AbstractTasksModel::IsLauncher).toBool()) {
2023 for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) {
2024 const QModelIndex &filteredIndex = d->filterProxyModel->index(i, 0);
2025
2026 if (!filteredIndex.data(AbstractTasksModel::IsWindow).toBool() && !filteredIndex.data(AbstractTasksModel::IsStartup).toBool()) {
2027 continue;
2028 }
2029
2030 if (appsMatch(sourceIndex, filteredIndex)) {
2031 return false;
2032 }
2033 }
2034 }
2035
2036 return true;
2037}
2038
2039bool TasksModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
2040{
2041 // In manual sort mode, sort by map.
2042 if (d->sortMode == SortManual) {
2043 return (d->sortedPreFilterRows.indexOf(d->preFilterIndex(left).row()) < d->sortedPreFilterRows.indexOf(d->preFilterIndex(right).row()));
2044 }
2045
2046 return d->lessThan(left, right);
2047}
2048
2049std::shared_ptr<VirtualDesktopInfo> TasksModel::virtualDesktopInfo() const
2050{
2051 static std::weak_ptr<VirtualDesktopInfo> s_virtualDesktopInfo;
2052 if (s_virtualDesktopInfo.expired()) {
2053 auto ptr = std::make_shared<VirtualDesktopInfo>();
2054 s_virtualDesktopInfo = ptr;
2055 return ptr;
2056 }
2057 return s_virtualDesktopInfo.lock();
2058}
2059
2060std::shared_ptr<ActivityInfo> TasksModel::activityInfo() const
2061{
2062 static std::weak_ptr<ActivityInfo> s_activityInfo;
2063 if (s_activityInfo.expired()) {
2064 auto ptr = std::make_shared<ActivityInfo>();
2065 s_activityInfo = ptr;
2066 return ptr;
2067 }
2068 return s_activityInfo.lock();
2069}
2070}
2071
2072#include "moc_tasksmodel.cpp"
Q_SCRIPTABLE Q_NOREPLY void start()
const QList< QKeySequence > & end()
QCA_EXPORT QString appName()
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
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual QVariant data(const QModelIndex &index, int role) const const override
virtual int rowCount(const QModelIndex &parent) const const override
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
QString toString() const const
QStringList toStringList() const const
QUrl toUrl() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 20 2024 12:00:00 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.