PlasmaActivitiesStats

resultmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2015, 2016 Ivan Cukic <ivan.cukic(at)kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6
7// Self
8#include "resultmodel.h"
9
10// Qt
11#include <QCoreApplication>
12#include <QDateTime>
13#include <QDebug>
14#include <QFile>
15#include <QTimer>
16#include <QPointer>
17
18// STL
19#include <functional>
20#include <thread>
21
22// KDE
23#include <KConfigGroup>
24#include <KSharedConfig>
25
26// Local
27#include "cleaning.h"
28#include "plasma-activities-stats-logsettings.h"
29#include "plasmaactivities/consumer.h"
30#include "resultset.h"
31#include "resultwatcher.h"
32#include <common/database/Database.h>
33#include <utils/member_matcher.h>
34#include <utils/qsqlquery_iterator.h>
35#include <utils/slide.h>
36
37#include <common/specialvalues.h>
38
39constexpr int s_defaultCacheSize = 50;
40
41#define QDBG qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "PlasmaActivitiesStats(" << (void *)this << ")"
42
43namespace KActivities
44{
45namespace Stats
46{
47using Common::Database;
48
49class ResultModelPrivate
50{
51public:
52 ResultModelPrivate(Query query, const QString &clientId, ResultModel *parent)
53 : cache(this, clientId, query.limit())
54 , query(query)
55 , watcher(query)
56 , hasMore(true)
57 , database(Database::instance(Database::ResourcesDatabase, Database::ReadOnly))
58 , q(parent)
59 {
60 s_privates << this;
61 }
62
63 ~ResultModelPrivate()
64 {
65 s_privates.removeAll(this);
66 }
67
68 enum Fetch {
69 FetchReset, // Remove old data and reload
70 FetchReload, // Update all data
71 FetchMore, // Load more data if there is any
72 };
73
74 class Cache
75 { //_
76 public:
77 typedef QList<ResultSet::Result> Items;
78
79 Cache(ResultModelPrivate *d, const QString &clientId, int limit)
80 : d(d)
81 , m_countLimit(limit)
82 , m_clientId(clientId)
83 {
84 if (!m_clientId.isEmpty()) {
85 m_configFile = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
86 }
87 }
88
89 ~Cache()
90 {
91 }
92
93 inline int size() const
94 {
95 return m_items.size();
96 }
97
98 inline void setLinkedResultPosition(const QString &resourcePath, int position)
99 {
100 if (!m_orderingConfig.isValid()) {
101 qCWarning(PLASMA_ACTIVITIES_STATS_LOG) << "We can not reorder the results, no clientId was specified";
102 return;
103 }
104
105 // Preconditions:
106 // - cache is ordered properly, first on the user's desired order,
107 // then on the query specified order
108 // - the resource that needs to be moved is a linked resource, not
109 // one that comes from the stats (there are overly many
110 // corner-cases that need to be covered in order to support
111 // reordering of the statistics-based resources)
112 // - the new position for the resource is not outside of the cache
113
114 auto resourcePosition = find(resourcePath);
115
116 if (resourcePosition) {
117 if (resourcePosition.index == position) {
118 return;
119 }
120 if (resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) {
121 return;
122 }
123 }
124
125 // Lets make a list of linked items - we can only reorder them,
126 // not others
127 QStringList linkedItems;
128
129 for (const ResultSet::Result &item : std::as_const(m_items)) {
130 if (item.linkStatus() == ResultSet::Result::NotLinked) {
131 break;
132 }
133 linkedItems << item.resource();
134 }
135
136 // We have two options:
137 // - we are planning to add an item to the desired position,
138 // but the item is not yet in the model
139 // - we want to move an existing item
140 if (!resourcePosition || resourcePosition.iterator->linkStatus() == ResultSet::Result::NotLinked) {
141 linkedItems.insert(position, resourcePath);
142
143 m_fixedOrderedItems = linkedItems;
144
145 } else {
146 // We can not accept the new position to be outside
147 // of the linked items area
148 if (position >= linkedItems.size()) {
149 position = linkedItems.size() - 1;
150 }
151
152 Q_ASSERT(resourcePosition.index == linkedItems.indexOf(resourcePath));
153 auto oldPosition = linkedItems.indexOf(resourcePath);
154
155 kamd::utils::move_one(linkedItems.begin() + oldPosition, linkedItems.begin() + position);
156
157 // When we change this, the cache is not valid anymore,
158 // destinationFor will fail and we can not use it
159 m_fixedOrderedItems = linkedItems;
160
161 // We are prepared to reorder the cache
162 d->repositionResult(resourcePosition, d->destinationFor(*resourcePosition));
163 }
164
165 m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems);
166 m_orderingConfig.sync();
167
168 // We need to notify others to reload
169 for (const auto &other : std::as_const(s_privates)) {
170 if (other != d && other->cache.m_clientId == m_clientId) {
171 other->fetch(FetchReset);
172 }
173 }
174 }
175
176 inline void debug() const
177 {
178 for (const auto &item : m_items) {
179 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Item: " << item;
180 }
181 }
182
183 void loadOrderingConfig(const QString &activityTag)
184 {
185 if (!m_configFile) {
186 qCDebug(PLASMA_ACTIVITIES_STATS_LOG) << "Nothing to load - the client id is empty";
187 return;
188 }
189
190 m_orderingConfig = KConfigGroup(m_configFile, QStringLiteral("ResultModel-OrderingFor-") + m_clientId + activityTag);
191
192 if (m_orderingConfig.hasKey("kactivitiesLinkedItemsOrder")) {
193 // If we have the ordering defined, use it
194 m_fixedOrderedItems = m_orderingConfig.readEntry("kactivitiesLinkedItemsOrder", QStringList());
195 } else {
196 // Otherwise, copy the order from the previous activity to this one
197 m_orderingConfig.writeEntry("kactivitiesLinkedItemsOrder", m_fixedOrderedItems);
198 m_orderingConfig.sync();
199 }
200 }
201
202 private:
203 ResultModelPrivate *const d;
204
205 QList<ResultSet::Result> m_items;
206 int m_countLimit;
207
208 QString m_clientId;
209 KSharedConfig::Ptr m_configFile;
210 KConfigGroup m_orderingConfig;
211 QStringList m_fixedOrderedItems;
212
213 friend QDebug operator<<(QDebug out, const Cache &cache)
214 {
215 for (const auto &item : cache.m_items) {
216 out << "Cache item: " << item << "\n";
217 }
218
219 return out;
220 }
221
222 public:
223 inline const QStringList &fixedOrderedItems() const
224 {
225 return m_fixedOrderedItems;
226 }
227
228 //_ Fancy iterator, find, lowerBound
229 struct FindCacheResult {
230 Cache *const cache;
231 Items::iterator iterator;
232 int index;
233
234 FindCacheResult(Cache *cache, Items::iterator iterator)
235 : cache(cache)
236 , iterator(iterator)
237 , index(std::distance(cache->m_items.begin(), iterator))
238 {
239 }
240
241 operator bool() const
242 {
243 return iterator != cache->m_items.end();
244 }
245
246 ResultSet::Result &operator*() const
247 {
248 return *iterator;
249 }
250
251 ResultSet::Result *operator->() const
252 {
253 return &(*iterator);
254 }
255 };
256
257 inline FindCacheResult find(const QString &resource)
258 {
259 using namespace kamd::utils::member_matcher;
260
261 // Non-const iterator because the result is constructed from it
262 return FindCacheResult(this, std::find_if(m_items.begin(), m_items.end(), member(&ResultSet::Result::resource) == resource));
263 }
264
265 template<typename Predicate>
266 inline FindCacheResult lowerBoundWithSkippedResource(Predicate &&lessThanPredicate)
267 {
268 using namespace kamd::utils::member_matcher;
269 const int count = std::count_if(m_items.cbegin(), m_items.cend(), [&](const ResultSet::Result &result) {
270 return lessThanPredicate(result, _);
271 });
272
273 return FindCacheResult(this, m_items.begin() + count);
274
275 // using namespace kamd::utils::member_matcher;
276 //
277 // const auto position =
278 // std::lower_bound(m_items.begin(), m_items.end(),
279 // _, std::forward<Predicate>(lessThanPredicate));
280 //
281 // // We seem to have found the position for the item.
282 // // The problem is that we might have found the same position
283 // // we were previously at. Since this function is usually used
284 // // to reposition the result, we might not be in a completely
285 // // sorted collection, so the next item(s) could be less than us.
286 // // We could do this with count_if, but it would be slower
287 //
288 // if (position >= m_items.cend() - 1) {
289 // return FindCacheResult(this, position);
290 //
291 // } else if (lessThanPredicate(_, *(position + 1))) {
292 // return FindCacheResult(this, position);
293 //
294 // } else {
295 // return FindCacheResult(
296 // this, std::lower_bound(position + 1, m_items.end(),
297 // _, std::forward<Predicate>(lessThanPredicate)));
298 // }
299 }
300 //^
301
302 inline void insertAt(const FindCacheResult &at, const ResultSet::Result &result)
303 {
304 m_items.insert(at.iterator, result);
305 }
306
307 inline void removeAt(const FindCacheResult &at)
308 {
309 m_items.removeAt(at.index);
310 }
311
312 inline const ResultSet::Result &operator[](int index) const
313 {
314 return m_items[index];
315 }
316
317 inline void clear()
318 {
319 if (m_items.size() == 0) {
320 return;
321 }
322
323 d->q->beginRemoveRows(QModelIndex(), 0, m_items.size() - 1);
324 m_items.clear();
325 d->q->endRemoveRows();
326 }
327
328 // Algorithm to calculate the edit operations to allow
329 //_ replaceing items without model reset
330 inline void replace(const Items &newItems, int from = 0)
331 {
332 using namespace kamd::utils::member_matcher;
333
334#if 0
335 QDBG << "======";
336 QDBG << "Old items {";
337 for (const auto &item : m_items) {
338 QDBG << item;
339 }
340 QDBG << "}";
341
342 QDBG << "New items to be added at " << from << " {";
343 for (const auto &item : newItems) {
344 QDBG << item;
345 }
346 QDBG << "}";
347#endif
348
349 // Based on 'The string to string correction problem
350 // with block moves' paper by Walter F. Tichy
351 //
352 // In essence, it goes like this:
353 //
354 // Take the first element from the new list, and try to find
355 // it in the old one. If you can not find it, it is a new item
356 // item - send the 'inserted' event.
357 // If you did find it, test whether the following items also
358 // match. This detects blocks of items that have moved.
359 //
360 // In this example, we find 'b', and then detect the rest of the
361 // moved block 'b' 'c' 'd'
362 //
363 // Old items: a[b c d]e f g
364 // ^
365 // /
366 // New items: [b c d]a f g
367 //
368 // After processing one block, just repeat until the end of the
369 // new list is reached.
370 //
371 // Then remove all remaining elements from the old list.
372 //
373 // The main addition here compared to the original papers is that
374 // our 'strings' can not hold two instances of the same element,
375 // and that we support updating from arbitrary position.
376
377 auto newBlockStart = newItems.cbegin();
378
379 // How many items should we add?
380 // This should remove the need for post-replace-trimming
381 // in the case where somebody called this with too much new items.
382 const int maxToReplace = m_countLimit - from;
383
384 if (maxToReplace <= 0) {
385 return;
386 }
387
388 const auto newItemsEnd = newItems.size() <= maxToReplace ? newItems.cend() : newItems.cbegin() + maxToReplace;
389
390 // Finding the blocks until we reach the end of the newItems list
391 //
392 // from = 4
393 // Old items: X Y Z U a b c d e f g
394 // ^ oldBlockStart points to the first element
395 // of the currently processed block in the old list
396 //
397 // New items: _ _ _ _ b c d a f g
398 // ^ newBlockStartIndex is the index of the first
399 // element of the block that is currently being
400 // processed (with 'from' offset)
401
402 while (newBlockStart != newItemsEnd) {
403 const int newBlockStartIndex = from + std::distance(newItems.cbegin(), newBlockStart);
404
405 const auto oldBlockStart =
406 std::find_if(m_items.begin() + from, m_items.end(), member(&ResultSet::Result::resource) == newBlockStart->resource());
407
408 if (oldBlockStart == m_items.end()) {
409 // This item was not found in the old cache, so we are
410 // inserting a new item at the same position it had in
411 // the newItems array
412 d->q->beginInsertRows(QModelIndex(), newBlockStartIndex, newBlockStartIndex);
413
414 m_items.insert(newBlockStartIndex, *newBlockStart);
415 d->q->endInsertRows();
416
417 // This block contained only one item, move on to find
418 // the next block - it starts from the next item
419 ++newBlockStart;
420
421 } else {
422 // We are searching for a block of matching items.
423 // This is a reimplementation of std::mismatch that
424 // accepts two complete ranges that is available only
425 // since C++14, so we can not use it.
426 auto newBlockEnd = newBlockStart;
427 auto oldBlockEnd = oldBlockStart;
428
429 while (newBlockEnd != newItemsEnd && oldBlockEnd != m_items.end() && newBlockEnd->resource() == oldBlockEnd->resource()) {
430 ++newBlockEnd;
431 ++oldBlockEnd;
432 }
433
434 // We have found matching blocks
435 // [newBlockStart, newBlockEnd) and [oldBlockStart, newBlockEnd)
436 const int oldBlockStartIndex = std::distance(m_items.begin() + from, oldBlockStart);
437
438 const int blockSize = std::distance(oldBlockStart, oldBlockEnd);
439
440 if (oldBlockStartIndex != newBlockStartIndex) {
441 // If these blocks do not have the same start,
442 // we need to send the move event.
443
444 // Note: If there is a crash here, it means we
445 // are getting a bad query which has duplicate
446 // results
447
448 d->q->beginMoveRows(QModelIndex(), oldBlockStartIndex, oldBlockStartIndex + blockSize - 1, QModelIndex(), newBlockStartIndex);
449
450 // Moving the items from the old location to the new one
451 kamd::utils::slide(oldBlockStart, oldBlockEnd, m_items.begin() + newBlockStartIndex);
452
453 d->q->endMoveRows();
454 }
455
456 // Skip all the items in this block, and continue with
457 // the search
458 newBlockStart = newBlockEnd;
459 }
460 }
461
462 // We have avoided the need for trimming for the most part,
463 // but if the newItems list was shorter than needed, we still
464 // need to trim the rest.
465 trim(from + newItems.size());
466
467 // Check whether we got an item representing a non-existent file,
468 // if so, schedule its removal from the database
469 // we want to do this async so that we don't block
470 QPointer model{d->q};
471 std::thread([model, newItems] {
472 QList<QString> missingResources;
473 for (const auto &item : newItems) {
474 // QFile.exists() can be incredibly slow (eg. if resource is on remote filesystem)
475 if (item.resource().startsWith(QLatin1Char('/')) && !QFile(item.resource()).exists()) {
476 missingResources << item.resource();
477 }
478 }
479
480 if (missingResources.empty()) {
481 return;
482 }
483
484 QTimer::singleShot(0, model, [missingResources, model] {
485 if (model) {
486 model->forgetResources(missingResources);
487 }
488 });
489 }).detach();
490 }
491 //^
492
493 inline void trim()
494 {
495 trim(m_countLimit);
496 }
497
498 inline void trim(int limit)
499 {
500 if (m_items.size() <= limit) {
501 return;
502 }
503
504 // Example:
505 // limit is 5,
506 // current cache (0, 1, 2, 3, 4, 5, 6, 7), size = 8
507 // We need to delete from 5 to 7
508
509 d->q->beginRemoveRows(QModelIndex(), limit, m_items.size() - 1);
510 m_items.erase(m_items.begin() + limit, m_items.end());
511 d->q->endRemoveRows();
512 }
513
514 } cache; //^
515
516 struct FixedItemsLessThan {
517 //_ Compartor that orders the linked items by user-specified order
518 typedef kamd::utils::member_matcher::placeholder placeholder;
519
520 enum Ordering {
521 PartialOrdering,
522 FullOrdering,
523 };
524
525 FixedItemsLessThan(Ordering ordering, const Cache &cache, const QString &matchResource = QString())
526 : cache(cache)
527 , matchResource(matchResource)
528 , ordering(ordering)
529 {
530 }
531
532 bool lessThan(const QString &leftResource, const QString &rightResource) const
533 {
534 const auto fixedOrderedItems = cache.fixedOrderedItems();
535
536 const auto indexLeft = fixedOrderedItems.indexOf(leftResource);
537 const auto indexRight = fixedOrderedItems.indexOf(rightResource);
538
539 const bool hasLeft = indexLeft != -1;
540 const bool hasRight = indexRight != -1;
541
542 return (hasLeft && !hasRight) ? true
543 : (!hasLeft && hasRight) ? false
544 : (hasLeft && hasRight) ? indexLeft < indexRight
545 : (ordering == PartialOrdering ? false : leftResource < rightResource);
546 }
547
548 template<typename T>
549 bool operator()(const T &left, placeholder) const
550 {
551 return lessThan(left.resource(), matchResource);
552 }
553
554 template<typename T>
555 bool operator()(placeholder, const T &right) const
556 {
557 return lessThan(matchResource, right.resource());
558 }
559
560 template<typename T, typename V>
561 bool operator()(const T &left, const V &right) const
562 {
563 return lessThan(left.resource(), right.resource());
564 }
565
566 const Cache &cache;
567 const QString matchResource;
568 Ordering ordering;
569 //^
570 };
571
572 inline Cache::FindCacheResult destinationFor(const ResultSet::Result &result)
573 {
574 using namespace kamd::utils::member_matcher;
575 using namespace Terms;
576
577 const auto resource = result.resource();
578 const auto score = result.score();
579 const auto firstUpdate = result.firstUpdate();
580 const auto lastUpdate = result.lastUpdate();
581 const auto linkStatus = result.linkStatus();
582
583#define FIXED_ITEMS_LESS_THAN FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, cache, resource)
584#define ORDER_BY(Field) member(&ResultSet::Result::Field) > Field
585#define ORDER_BY_FULL(Field) \
586 (query.selection() == Terms::AllResources \
587 ? cache.lowerBoundWithSkippedResource(FIXED_ITEMS_LESS_THAN && ORDER_BY(linkStatus) && ORDER_BY(Field) && ORDER_BY(resource)) \
588 : cache.lowerBoundWithSkippedResource(FIXED_ITEMS_LESS_THAN && ORDER_BY(Field) && ORDER_BY(resource)))
589
590 const auto destination = query.ordering() == HighScoredFirst ? ORDER_BY_FULL(score)
591 : query.ordering() == RecentlyUsedFirst ? ORDER_BY_FULL(lastUpdate)
592 : query.ordering() == RecentlyCreatedFirst ? ORDER_BY_FULL(firstUpdate)
593 :
594 /* otherwise */ ORDER_BY_FULL(resource);
595#undef ORDER_BY
596#undef ORDER_BY_FULL
597#undef FIXED_ITEMS_LESS_THAN
598
599 return destination;
600 }
601
602 inline void removeResult(const Cache::FindCacheResult &result)
603 {
604 q->beginRemoveRows(QModelIndex(), result.index, result.index);
605 cache.removeAt(result);
606 q->endRemoveRows();
607
608 if (query.selection() != Terms::LinkedResources) {
609 fetch(cache.size(), 1);
610 }
611 }
612
613 inline void repositionResult(const Cache::FindCacheResult &result, const Cache::FindCacheResult &destination)
614 {
615 // We already have the resource in the cache
616 // So, it is the time for a reshuffle
617 const int oldPosition = result.index;
618 int position = destination.index;
619
620 Q_EMIT q->dataChanged(q->index(oldPosition), q->index(oldPosition));
621
622 if (oldPosition == position) {
623 return;
624 }
625
626 if (position > oldPosition) {
627 position++;
628 }
629
630 bool moving = q->beginMoveRows(QModelIndex(), oldPosition, oldPosition, QModelIndex(), position);
631
632 kamd::utils::move_one(result.iterator, destination.iterator);
633
634 if (moving) {
635 q->endMoveRows();
636 }
637 }
638
639 void reload()
640 {
641 fetch(FetchReload);
642 }
643
644 void init()
645 {
646 using namespace std::placeholders;
647
648 QObject::connect(&watcher, &ResultWatcher::resultScoreUpdated, q, std::bind(&ResultModelPrivate::onResultScoreUpdated, this, _1, _2, _3, _4));
649 QObject::connect(&watcher, &ResultWatcher::resultRemoved, q, std::bind(&ResultModelPrivate::onResultRemoved, this, _1));
650 QObject::connect(&watcher, &ResultWatcher::resultLinked, q, std::bind(&ResultModelPrivate::onResultLinked, this, _1));
651 QObject::connect(&watcher, &ResultWatcher::resultUnlinked, q, std::bind(&ResultModelPrivate::onResultUnlinked, this, _1));
652
653 QObject::connect(&watcher, &ResultWatcher::resourceTitleChanged, q, std::bind(&ResultModelPrivate::onResourceTitleChanged, this, _1, _2));
654 QObject::connect(&watcher, &ResultWatcher::resourceMimetypeChanged, q, std::bind(&ResultModelPrivate::onResourceMimetypeChanged, this, _1, _2));
655
656 QObject::connect(&watcher, &ResultWatcher::resultsInvalidated, q, std::bind(&ResultModelPrivate::reload, this));
657
658 if (query.activities().contains(CURRENT_ACTIVITY_TAG)) {
659 QObject::connect(&activities,
661 q,
662 std::bind(&ResultModelPrivate::onCurrentActivityChanged, this, _1));
663 }
664
665 fetch(FetchReset);
666 }
667
668 void fetch(const int from, int count)
669 {
670 using namespace Terms;
671
672 if (from + count > query.limit()) {
673 count = query.limit() - from;
674 }
675
676 if (count <= 0) {
677 return;
678 }
679
680 // In order to see whether there are more results, we need to pass
681 // the count increased by one
682 ResultSet results(query | Offset(from) | Limit(count + 1));
683
684 auto it = results.begin();
685
686 Cache::Items newItems;
687
688 while (count-- > 0 && it != results.end()) {
689 newItems << *it;
690 ++it;
691 }
692
693 hasMore = (it != results.end());
694
695 // We need to sort the new items for the linked resources
696 // user-defined reordering. This needs only to be a partial sort,
697 // the main sorting is done by sqlite
698 if (query.selection() != Terms::UsedResources) {
699 std::stable_sort(newItems.begin(), newItems.end(), FixedItemsLessThan(FixedItemsLessThan::PartialOrdering, cache));
700 }
701
702 cache.replace(newItems, from);
703 }
704
705 void fetch(Fetch mode)
706 {
707 if (mode == FetchReset) {
708 // Removing the previously cached data
709 // and loading all from scratch
710 cache.clear();
711
712 const QString activityTag = query.activities().contains(CURRENT_ACTIVITY_TAG) //
713 ? (QStringLiteral("-ForActivity-") + activities.currentActivity())
714 : QStringLiteral("-ForAllActivities");
715
716 cache.loadOrderingConfig(activityTag);
717
718 // If the user has requested less than 50 entries, only fetch those. If more, they should be fetched in subsequent batches
719 fetch(0, qMin(s_defaultCacheSize, query.limit()));
720
721 } else if (mode == FetchReload) {
722 if (cache.size() > s_defaultCacheSize) {
723 // If the cache is big, we are pretending
724 // we were asked to reset the model
725 fetch(FetchReset);
726
727 } else {
728 // We are only updating the currently
729 // cached items, nothing more
730 fetch(0, cache.size());
731 }
732
733 } else { // FetchMore
734 // Load a new batch of data
735 fetch(cache.size(), s_defaultCacheSize);
736 }
737 }
738
739 void onResultScoreUpdated(const QString &resource, double score, uint lastUpdate, uint firstUpdate)
740 {
741 QDBG << "ResultModelPrivate::onResultScoreUpdated "
742 << "result added:" << resource << "score:" << score << "last:" << lastUpdate << "first:" << firstUpdate;
743
744 // This can also be called when the resource score
745 // has been updated, so we need to check whether
746 // we already have it in the cache
747 const auto result = cache.find(resource);
748
749 ResultSet::Result::LinkStatus linkStatus = result ? result->linkStatus()
750 : query.selection() != Terms::UsedResources ? ResultSet::Result::Unknown
751 : query.selection() != Terms::LinkedResources ? ResultSet::Result::Linked
752 : ResultSet::Result::NotLinked;
753
754 if (result) {
755 // We are only updating a result we already had,
756 // lets fill out the data and send the update signal.
757 // Move it if necessary.
758
759 auto &item = *result.iterator;
760
761 item.setScore(score);
762 item.setLinkStatus(linkStatus);
763 item.setLastUpdate(lastUpdate);
764 item.setFirstUpdate(firstUpdate);
765
766 repositionResult(result, destinationFor(item));
767
768 } else {
769 // We do not have the resource in the cache,
770 // lets fill out the data and insert it
771 // at the desired position
772
773 ResultSet::Result result;
774 result.setResource(resource);
775
776 result.setTitle(QStringLiteral(" "));
777 result.setMimetype(QStringLiteral(" "));
778 fillTitleAndMimetype(result);
779
780 result.setScore(score);
781 result.setLinkStatus(linkStatus);
782 result.setLastUpdate(lastUpdate);
783 result.setFirstUpdate(firstUpdate);
784
785 const auto destination = destinationFor(result);
786
787 q->beginInsertRows(QModelIndex(), destination.index, destination.index);
788
789 cache.insertAt(destination, result);
790
791 q->endInsertRows();
792
793 cache.trim();
794 }
795 }
796
797 void onResultRemoved(const QString &resource)
798 {
799 const auto result = cache.find(resource);
800
801 if (!result) {
802 return;
803 }
804
805 if (query.selection() == Terms::UsedResources || result->linkStatus() != ResultSet::Result::Linked) {
806 removeResult(result);
807 }
808 }
809
810 void onResultLinked(const QString &resource)
811 {
812 if (query.selection() != Terms::UsedResources) {
813 onResultScoreUpdated(resource, 0, 0, 0);
814 }
815 }
816
817 void onResultUnlinked(const QString &resource)
818 {
819 const auto result = cache.find(resource);
820
821 if (!result) {
822 return;
823 }
824
825 if (query.selection() == Terms::LinkedResources) {
826 removeResult(result);
827
828 } else if (query.selection() == Terms::AllResources) {
829 // When the result is unlinked, it might go away or not
830 // depending on its previous usage
831 reload();
832 }
833 }
834
835 Query query;
836 ResultWatcher watcher;
837 bool hasMore;
838
839 KActivities::Consumer activities;
840 Common::Database::Ptr database;
841
842 //_ Title and mimetype functions
843 void fillTitleAndMimetype(ResultSet::Result &result)
844 {
845 if (!database) {
846 return;
847 }
848
849 auto query = database->execQuery(QStringLiteral("SELECT "
850 "title, mimetype "
851 "FROM "
852 "ResourceInfo "
853 "WHERE "
854 "targettedResource = '")
855 + result.resource() + QStringLiteral("'"));
856
857 // Only one item at most
858 for (const auto &item : query) {
859 result.setTitle(item[QStringLiteral("title")].toString());
860 result.setMimetype(item[QStringLiteral("mimetype")].toString());
861 }
862 }
863
864 void onResourceTitleChanged(const QString &resource, const QString &title)
865 {
866 const auto result = cache.find(resource);
867
868 if (!result) {
869 return;
870 }
871
872 result->setTitle(title);
873
874 Q_EMIT q->dataChanged(q->index(result.index), q->index(result.index));
875 }
876
877 void onResourceMimetypeChanged(const QString &resource, const QString &mimetype)
878 {
879 // TODO: This can add or remove items from the model
880
881 const auto result = cache.find(resource);
882
883 if (!result) {
884 return;
885 }
886
887 result->setMimetype(mimetype);
888
889 Q_EMIT q->dataChanged(q->index(result.index), q->index(result.index));
890 }
891 //^
892
893 void onCurrentActivityChanged(const QString &activity)
894 {
895 Q_UNUSED(activity);
896 // If the current activity has changed, and
897 // the query lists items for the ':current' one,
898 // reset the model (not a simple refresh this time)
899 if (query.activities().contains(CURRENT_ACTIVITY_TAG)) {
900 fetch(FetchReset);
901 }
902 }
903
904private:
905 ResultModel *const q;
906 static QList<ResultModelPrivate *> s_privates;
907};
908
909QList<ResultModelPrivate *> ResultModelPrivate::s_privates;
910
911ResultModel::ResultModel(Query query, QObject *parent)
912 : QAbstractListModel(parent)
913 , d(new ResultModelPrivate(query, QString(), this))
914{
915 d->init();
916}
917
918ResultModel::ResultModel(Query query, const QString &clientId, QObject *parent)
919 : QAbstractListModel(parent)
920 , d(new ResultModelPrivate(query, clientId, this))
921{
922 d->init();
923}
924
925ResultModel::~ResultModel()
926{
927 delete d;
928}
929
930QHash<int, QByteArray> ResultModel::roleNames() const
931{
932 return {
933 {ResourceRole, "resource"},
934 {TitleRole, "title"},
935 {ScoreRole, "score"},
936 {FirstUpdateRole, "created"},
937 {LastUpdateRole, "modified"},
938 {LinkStatusRole, "linkStatus"},
939 {LinkedActivitiesRole, "linkedActivities"},
940 {MimeType, "mimeType"},
941 };
942}
943
944QVariant ResultModel::data(const QModelIndex &item, int role) const
945{
946 const auto row = item.row();
947
948 if (row < 0 || row >= d->cache.size()) {
949 return QVariant();
950 }
951
952 const auto &result = d->cache[row];
953
954 return role == Qt::DisplayRole ? QString(result.title() + QStringLiteral(" ") + result.resource() + QStringLiteral(" - ")
955 + QString::number(result.linkStatus()) + QStringLiteral(" - ") + QString::number(result.score()))
956 : role == ResourceRole ? result.resource()
957 : role == TitleRole ? result.title()
958 : role == ScoreRole ? result.score()
959 : role == FirstUpdateRole ? result.firstUpdate()
960 : role == LastUpdateRole ? result.lastUpdate()
961 : role == LinkStatusRole ? result.linkStatus()
962 : role == LinkedActivitiesRole ? result.linkedActivities()
963 : role == MimeType ? result.mimetype()
964 : role == Agent ? result.agent()
965 : QVariant();
966}
967
968QVariant ResultModel::headerData(int section, Qt::Orientation orientation, int role) const
969{
970 Q_UNUSED(section);
971 Q_UNUSED(orientation);
972 Q_UNUSED(role);
973 return QVariant();
974}
975
976int ResultModel::rowCount(const QModelIndex &parent) const
977{
978 return parent.isValid() ? 0 : d->cache.size();
979}
980
981void ResultModel::fetchMore(const QModelIndex &parent)
982{
983 if (parent.isValid()) {
984 return;
985 }
986 d->fetch(ResultModelPrivate::FetchMore);
987}
988
989bool ResultModel::canFetchMore(const QModelIndex &parent) const
990{
991 return parent.isValid() ? false : d->cache.size() >= d->query.limit() ? false : d->hasMore;
992}
993
995{
996 const auto lstActivities = d->query.activities();
997 for (const QString &activity : lstActivities) {
998 const auto lstAgents = d->query.agents();
999 for (const QString &agent : lstAgents) {
1000 for (const QString &resource : resources) {
1001 Stats::forgetResource(activity, agent == CURRENT_AGENT_TAG ? QCoreApplication::applicationName() : agent, resource);
1002 }
1003 }
1004 }
1005}
1006
1008{
1009 ResultModel::forgetResources({resource});
1010}
1011
1013{
1014 if (row >= d->cache.size()) {
1015 return;
1016 }
1017 const auto lstActivities = d->query.activities();
1018 for (const QString &activity : lstActivities) {
1019 const auto lstAgents = d->query.agents();
1020 for (const QString &agent : lstAgents) {
1021 Stats::forgetResource(activity, agent == CURRENT_AGENT_TAG ? QCoreApplication::applicationName() : agent, d->cache[row].resource());
1022 }
1023 }
1024}
1025
1027{
1028 Stats::forgetResources(d->query);
1029}
1030
1031void ResultModel::setResultPosition(const QString &resource, int position)
1032{
1033 d->cache.setLinkedResultPosition(resource, position);
1034}
1035
1037{
1038 // TODO
1039 Q_UNUSED(sortOrder);
1040}
1041
1042void ResultModel::linkToActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent)
1043{
1044 d->watcher.linkToActivity(resource, activity, agent);
1045}
1046
1047void ResultModel::unlinkFromActivity(const QUrl &resource, const Terms::Activity &activity, const Terms::Agent &agent)
1048{
1049 d->watcher.unlinkFromActivity(resource, activity, agent);
1050}
1051
1052} // namespace Stats
1053} // namespace KActivities
1054
1055#include "moc_resultmodel.cpp"
void currentActivityChanged(const QString &id)
The activities system tracks resources (documents, contacts, etc.) that the user has used.
Definition query.h:54
void sortItems(Qt::SortOrder sortOrder)
Sort the items by title.
void forgetResource(const QString &resource)
Removes the specified resource from the history.
void setResultPosition(const QString &resource, int position)
Moves the resource to the specified position.
void forgetResources(const QList< QString > &resources)
Removes specified list of resources from the history.
void forgetAllResources()
Clears the history of all resources that match the current model query.
QString resource() const
String representation of resource (can represent an url or a path)
void resourceMimetypeChanged(const QString &resource, const QString &mimetype)
Emitted when the mimetype of a resource has been changed.
void resultsInvalidated()
Emitted when the client should forget about all the results it knew about and reload them.
void resultLinked(const QString &resource)
Emitted when a result has been linked to the activity.
void resultUnlinked(const QString &resource)
Emitted when a result has been unlinked from the activity.
void resultScoreUpdated(const QString &resource, double score, uint lastUpdate, uint firstUpdate)
Emitted when a result has been added or updated.
void resourceTitleChanged(const QString &resource, const QString &title)
Emitted when the title of a resource has been changed.
void resultRemoved(const QString &resource)
Emitted when a result has been added or updated.
char * toString(const EngineQuery &query)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
const QList< QKeySequence > & begin()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
iterator begin()
bool empty() const const
iterator insert(const_iterator before, parameter_type value)
qsizetype size() const const
bool isValid() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString number(double n, char format, int precision)
qsizetype indexOf(const QRegularExpression &re, qsizetype from) const const
DisplayRole
Orientation
SortOrder
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
Term to filter the resources according the activity in which they were accessed.
Definition terms.h:139
Term to filter the resources according the agent (application) which accessed it.
Definition terms.h:106
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 12:01:02 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.