File indexing completed on 2024-06-09 05:30:57

0001 /*
0002     SPDX-FileCopyrightText: 2014-2015 Eike Hein <hein@kde.org>
0003     SPDX-FileCopyrightText: 2016-2017 Ivan Cukic <ivan.cukic@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "kastatsfavoritesmodel.h"
0009 #include "actionlist.h"
0010 #include "appentry.h"
0011 #include "debug.h"
0012 #include "fileentry.h"
0013 
0014 #include <QFileInfo>
0015 #include <QSortFilterProxyModel>
0016 #include <QTimer>
0017 
0018 #include <KConfigGroup>
0019 #include <KLocalizedString>
0020 #include <KProtocolInfo>
0021 #include <KSharedConfig>
0022 #include <KStringHandler>
0023 #include <KSycoca>
0024 
0025 #include <PlasmaActivities/Consumer>
0026 #include <PlasmaActivities/Stats/Query>
0027 #include <PlasmaActivities/Stats/ResultSet>
0028 #include <PlasmaActivities/Stats/ResultWatcher>
0029 #include <PlasmaActivities/Stats/Terms>
0030 
0031 #include "config-KDECI_BUILD.h"
0032 
0033 namespace KAStats = KActivities::Stats;
0034 
0035 using namespace KAStats;
0036 using namespace KAStats::Terms;
0037 
0038 #define AGENT_APPLICATIONS QStringLiteral("org.kde.plasma.favorites.applications")
0039 #define AGENT_DOCUMENTS QStringLiteral("org.kde.plasma.favorites.documents")
0040 
0041 QString agentForUrl(const QString &url)
0042 {
0043     QUrl u(url);
0044     // clang-format off
0045     return url.startsWith(QLatin1String("preferred:"))
0046                 ? AGENT_APPLICATIONS
0047          : url.startsWith(QLatin1String("applications:"))
0048                 ? AGENT_APPLICATIONS
0049          : (url.startsWith(QLatin1Char('/')) && !url.endsWith(QLatin1String(".desktop")))
0050                 ? AGENT_DOCUMENTS
0051          : (url.startsWith(QLatin1String("file:/")) && !url.endsWith(QLatin1String(".desktop")))
0052                 ? AGENT_DOCUMENTS
0053          : (u.scheme() != QLatin1String("file") && !u.scheme().isEmpty() && KProtocolInfo::isKnownProtocol(u.scheme()))
0054                   ? AGENT_DOCUMENTS
0055          // use applications as the default
0056                 : AGENT_APPLICATIONS;
0057     // clang-format on
0058 }
0059 
0060 class KAStatsFavoritesModel::Private : public QAbstractListModel
0061 {
0062 public:
0063     class NormalizedId
0064     {
0065     public:
0066         NormalizedId()
0067         {
0068         }
0069 
0070         NormalizedId(const Private *parent, const QString &id)
0071         {
0072             if (id.isEmpty())
0073                 return;
0074 
0075             std::shared_ptr<AbstractEntry> entry = nullptr;
0076 
0077             if (parent->m_itemEntries.contains(id)) {
0078                 entry = parent->m_itemEntries[id];
0079             } else {
0080                 // This entry is not cached - it is temporary,
0081                 // so let's clean up when we exit this function
0082                 entry = parent->entryForResource(id);
0083             }
0084 
0085             if (!entry || !entry->isValid()) {
0086                 qCWarning(KICKER_DEBUG) << "Entry is not valid" << id << entry.get();
0087                 m_id = id;
0088                 return;
0089             }
0090 
0091             const auto url = entry->url();
0092 
0093             qCDebug(KICKER_DEBUG) << "Original id is: " << id << ", and the url is" << url;
0094 
0095             // Preferred applications need special handling
0096             if (entry->id().startsWith(QLatin1String("preferred:"))) {
0097                 m_id = entry->id();
0098                 return;
0099             }
0100 
0101             // If this is an application, use the applications:-format url
0102             auto appEntry = dynamic_cast<AppEntry *>(entry.get());
0103             if (appEntry && !appEntry->menuId().isEmpty()) {
0104                 m_id = QLatin1String("applications:") + appEntry->menuId();
0105                 return;
0106             }
0107 
0108             // We want to resolve symbolic links not to have two paths
0109             // refer to the same .desktop file
0110             if (url.isLocalFile()) {
0111                 QFileInfo file(url.toLocalFile());
0112 
0113                 if (file.exists()) {
0114                     m_id = QUrl::fromLocalFile(file.canonicalFilePath()).toString();
0115                     return;
0116                 }
0117             }
0118 
0119             // If this is a file, we should have already covered it
0120             if (url.scheme() == QLatin1String("file")) {
0121                 return;
0122             }
0123 
0124             m_id = url.toString();
0125         }
0126 
0127         const QString &value() const
0128         {
0129             return m_id;
0130         }
0131 
0132         bool operator==(const NormalizedId &other) const
0133         {
0134             return m_id == other.m_id;
0135         }
0136 
0137     private:
0138         QString m_id;
0139     };
0140 
0141     NormalizedId normalizedId(const QString &id) const
0142     {
0143         return NormalizedId(this, id);
0144     }
0145 
0146     std::shared_ptr<AbstractEntry> entryForResource(const QString &resource, const QString &mimeType = QString()) const
0147     {
0148         using SP = std::shared_ptr<AbstractEntry>;
0149 
0150         const auto agent = agentForUrl(resource);
0151 
0152         if (agent == AGENT_DOCUMENTS) {
0153             if (resource.startsWith(QLatin1String("/"))) {
0154                 return SP(new FileEntry(q, QUrl::fromLocalFile(resource), mimeType));
0155             } else {
0156                 return SP(new FileEntry(q, QUrl(resource), mimeType));
0157             }
0158 
0159         } else if (agent == AGENT_APPLICATIONS) {
0160             if (resource.startsWith(QLatin1String("applications:"))) {
0161                 return SP(new AppEntry(q, resource.mid(13)));
0162             } else {
0163                 return SP(new AppEntry(q, resource));
0164             }
0165 
0166         } else {
0167             return {};
0168         }
0169     }
0170 
0171     Private(KAStatsFavoritesModel *parent, const QString &clientId)
0172         : q(parent)
0173         , m_query(LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_DOCUMENTS} | Type::any() | Activity::current() | Activity::global() | Limit(100))
0174         , m_watcher(m_query)
0175         , m_clientId(clientId)
0176     {
0177         // Connecting the watcher
0178         connect(&m_watcher, &ResultWatcher::resultLinked, [this](const QString &resource) {
0179             addResult(resource, -1);
0180         });
0181 
0182         connect(&m_watcher, &ResultWatcher::resultUnlinked, [this](const QString &resource) {
0183             removeResult(resource);
0184         });
0185         connect(KSycoca::self(), &KSycoca::databaseChanged, this, [this]() {
0186             QStringList keys;
0187             for (auto it = m_itemEntries.cbegin(); it != m_itemEntries.cend(); it++) {
0188                 if (it.value() && !it.value()->isValid()) {
0189                     keys << it.key();
0190                 }
0191             }
0192             if (!keys.isEmpty()) {
0193                 for (const QString &key : keys) {
0194                     removeResult(key);
0195                 }
0196             }
0197         });
0198 
0199         // Loading the items order
0200         const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
0201 
0202         // We want first to check whether we have an ordering for this activity.
0203         // If not, we will try to get a global one for this applet
0204 
0205         const QString thisGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + m_activities.currentActivity();
0206         const QString globalGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-global");
0207 
0208         KConfigGroup thisCfgGroup(cfg, thisGroupName);
0209         KConfigGroup globalCfgGroup(cfg, globalGroupName);
0210 
0211         QStringList ordering = thisCfgGroup.readEntry("ordering", QStringList()) + globalCfgGroup.readEntry("ordering", QStringList());
0212         // Normalizing all the ids
0213         std::transform(ordering.begin(), ordering.end(), ordering.begin(), [&](const QString &item) {
0214             return normalizedId(item).value();
0215         });
0216 
0217         qCDebug(KICKER_DEBUG) << "Loading the ordering " << ordering;
0218 
0219         // Loading the results without emitting any model signals
0220         qCDebug(KICKER_DEBUG) << "Query is" << m_query;
0221         ResultSet results(m_query);
0222 
0223         for (const auto &result : results) {
0224             qCDebug(KICKER_DEBUG) << "Got " << result.resource() << " -->";
0225             addResult(result.resource(), -1, false, result.mimetype());
0226         }
0227 
0228         if (ordering.length() == 0) {
0229             // try again with other applets with highest instance number which has the matching apps
0230             qCDebug(KICKER_DEBUG) << "No ordering for this applet found, trying others";
0231             const auto allGroups = cfg->groupList();
0232             int instanceHighest = -1;
0233             for (const auto &groupName : allGroups) {
0234                 if (groupName.contains(QStringLiteral(".favorites.instance-"))
0235                     && (groupName.endsWith(QStringLiteral("-global")) || groupName.endsWith(m_activities.currentActivity()))) {
0236                     // the group names look like "Favorites-org.kde.plasma.kicker.favorites.instance-58-1bd5bb42-187c-4c77-a746-c9644c5da866"
0237                     const QStringList split = groupName.split(QStringLiteral("-"));
0238                     if (split.length() >= 3) {
0239                         bool ok;
0240                         int instanceN = split[2].toInt(&ok);
0241                         if (!ok) {
0242                             continue;
0243                         }
0244                         auto groupOrdering = KConfigGroup(cfg, groupName).readEntry("ordering", QStringList());
0245                         if (groupOrdering.length() != m_items.length()) {
0246                             continue;
0247                         }
0248                         std::transform(groupOrdering.begin(), groupOrdering.end(), groupOrdering.begin(), [&](const QString &item) {
0249                             return normalizedId(item).value();
0250                         });
0251                         for (auto item : m_items) {
0252                             if (!groupOrdering.contains(item.value())) {
0253                                 continue;
0254                             }
0255                         }
0256                         if (instanceHighest == instanceN) {
0257                             // we got a -global as well as -{activity uuid}
0258                             // we add them
0259                             if (groupName.endsWith(QStringLiteral("-global"))) {
0260                                 ordering += groupOrdering;
0261                             } else {
0262                                 ordering = groupOrdering + ordering;
0263                             }
0264                             qCDebug(KICKER_DEBUG) << "adding ordering from: " << groupName;
0265                         } else if (instanceN > instanceHighest) {
0266                             instanceHighest = instanceN;
0267                             ordering = (groupOrdering.length() != 0) ? groupOrdering : ordering;
0268                             qCDebug(KICKER_DEBUG) << "taking ordering from: " << groupName;
0269                         }
0270                     }
0271                 }
0272             }
0273         }
0274 
0275         // Sorting the items in the cache
0276         std::sort(m_items.begin(), m_items.end(), [&](const NormalizedId &left, const NormalizedId &right) {
0277             auto leftIndex = ordering.indexOf(left.value());
0278             auto rightIndex = ordering.indexOf(right.value());
0279             // clang-format off
0280                     return (leftIndex == -1 && rightIndex == -1) ?
0281                                left.value() < right.value() :
0282 
0283                            (leftIndex == -1) ?
0284                                false :
0285 
0286                            (rightIndex == -1) ?
0287                                true :
0288 
0289                            // otherwise
0290                                leftIndex < rightIndex;
0291             // clang-format on
0292         });
0293 
0294         // Debugging:
0295         QList<QString> itemStrings(m_items.size());
0296         std::transform(m_items.cbegin(), m_items.cend(), itemStrings.begin(), [](const NormalizedId &item) {
0297             return item.value();
0298         });
0299         qCDebug(KICKER_DEBUG) << "After ordering: " << itemStrings;
0300     }
0301 
0302     void addResult(const QString &_resource, int index, bool notifyModel = true, const QString &mimeType = QString())
0303     {
0304         // We want even files to have a proper URL
0305         const auto resource = _resource.startsWith(QLatin1Char('/')) ? QUrl::fromLocalFile(_resource).toString() : _resource;
0306 
0307         qCDebug(KICKER_DEBUG) << "Adding result" << resource << "already present?" << m_itemEntries.contains(resource);
0308 
0309         if (m_itemEntries.contains(resource))
0310             return;
0311 
0312         auto entry = entryForResource(resource, mimeType);
0313 
0314         if (!entry || !entry->isValid()) {
0315             qCDebug(KICKER_DEBUG) << "Entry is not valid!" << resource;
0316             return;
0317         }
0318 
0319         // TODO Remove a few releases after Plasma 5.25 as this should become dead code, once users have run this code at least once
0320         // Converts old-style applications favorites with path (/usrshare/applications/org.kde.dolphin.desktop) or storageId (org.kde.dolphin.desktop)
0321         if ((_resource.startsWith(QLatin1String("/")) || QUrl(_resource).scheme().isEmpty()) && _resource.endsWith(QLatin1String(".desktop"))) {
0322             m_watcher.unlinkFromActivity(QUrl(_resource), QStringLiteral(":any"), agentForUrl(resource));
0323 
0324             const auto normalized = normalizedId(resource).value();
0325             qCDebug(KICKER_DEBUG) << "Converting old-style application favorite entry" << _resource << "to" << normalized;
0326 
0327             m_watcher.linkToActivity(QUrl(normalized), QStringLiteral(":any"), agentForUrl(resource));
0328             return;
0329         }
0330 
0331         if (index == -1) {
0332             index = m_items.count();
0333         }
0334 
0335         if (notifyModel) {
0336             beginInsertRows(QModelIndex(), index, index);
0337         }
0338 
0339         auto url = entry->url();
0340 
0341         m_itemEntries[resource] = m_itemEntries[entry->id()] = m_itemEntries[url.toString()] = entry;
0342         if (!url.toLocalFile().isEmpty()) {
0343             m_itemEntries[url.toLocalFile()] = entry;
0344         }
0345 
0346         auto normalized = normalizedId(resource);
0347         m_items.insert(index, normalized);
0348         m_itemEntries[normalized.value()] = entry;
0349 
0350         if (notifyModel) {
0351             endInsertRows();
0352             saveOrdering();
0353         }
0354     }
0355 
0356     void removeResult(const QString &resource)
0357     {
0358         const auto normalized = normalizedId(resource);
0359 
0360         // If we know this item will not really be removed,
0361         // but only that activities it is on have changed,
0362         // lets leave it
0363         if (m_ignoredItems.contains(normalized.value())) {
0364             m_ignoredItems.removeAll(normalized.value());
0365             return;
0366         }
0367 
0368         qCDebug(KICKER_DEBUG) << "Removing result" << resource;
0369 
0370         auto index = m_items.indexOf(normalized);
0371 
0372         if (index == -1)
0373             return;
0374 
0375         beginRemoveRows(QModelIndex(), index, index);
0376         const auto entry = m_itemEntries[resource];
0377         m_items.removeAt(index);
0378 
0379         // Removing the entry from the cache
0380         QMutableHashIterator<QString, std::shared_ptr<AbstractEntry>> i(m_itemEntries);
0381         while (i.hasNext()) {
0382             i.next();
0383             if (i.value() == entry) {
0384                 i.remove();
0385             }
0386         }
0387 
0388         endRemoveRows();
0389 
0390         saveOrdering();
0391     }
0392 
0393     int rowCount(const QModelIndex &parent = QModelIndex()) const override
0394     {
0395         if (parent.isValid())
0396             return 0;
0397 
0398         return m_items.count();
0399     }
0400 
0401     QVariant data(const QModelIndex &item, int role = Qt::DisplayRole) const override
0402     {
0403         if (item.parent().isValid())
0404             return QVariant();
0405 
0406         const auto index = item.row();
0407 
0408         const auto entry = m_itemEntries[m_items[index].value()];
0409         // clang-format off
0410         return entry == nullptr ? QVariant()
0411              : role == Qt::DisplayRole ? entry->name()
0412              : role == Kicker::DisplayWrappedRole ? KStringHandler::preProcessWrap(entry->name())
0413              : role == Qt::DecorationRole ? entry->icon()
0414              : role == Kicker::DescriptionRole ? entry->description()
0415              : role == Kicker::FavoriteIdRole ? entry->id()
0416              : role == Kicker::UrlRole ? entry->url()
0417              : role == Kicker::HasActionListRole ? entry->hasActions()
0418              : role == Kicker::ActionListRole ? entry->actions()
0419              : QVariant();
0420         // clang-format on
0421     }
0422 
0423     bool trigger(int row, const QString &actionId, const QVariant &argument)
0424     {
0425         if (row < 0 || row >= rowCount()) {
0426             return false;
0427         }
0428 
0429         const QString id = data(index(row, 0), Kicker::UrlRole).toString();
0430         if (m_itemEntries.contains(id)) {
0431             return m_itemEntries[id]->run(actionId, argument);
0432         }
0433         // Entries with preferred:// can be changed by the user, BUG: 416161
0434         // then the list of entries could be out of sync
0435         const auto entry = m_itemEntries[m_items[row].value()];
0436         if (QUrl(entry->id()).scheme() == QLatin1String("preferred")) {
0437             return entry->run(actionId, argument);
0438         }
0439         return false;
0440     }
0441 
0442     void move(int from, int to)
0443     {
0444         if (from < 0)
0445             return;
0446         if (from >= m_items.count())
0447             return;
0448         if (to < 0)
0449             return;
0450         if (to >= m_items.count())
0451             return;
0452 
0453         if (from == to)
0454             return;
0455 
0456         const int modelTo = to + (to > from ? 1 : 0);
0457 
0458         if (q->beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo)) {
0459             m_items.move(from, to);
0460             q->endMoveRows();
0461 
0462             qCDebug(KICKER_DEBUG) << "Save ordering (from Private::move) -->";
0463             saveOrdering();
0464         }
0465     }
0466 
0467     void saveOrdering()
0468     {
0469         QStringList ids;
0470 
0471         for (const auto &item : std::as_const(m_items)) {
0472             ids << item.value();
0473         }
0474 
0475         qCDebug(KICKER_DEBUG) << "Save ordering (from Private::saveOrdering) -->";
0476         saveOrdering(ids, m_clientId, m_activities.currentActivity());
0477     }
0478 
0479     static void saveOrdering(const QStringList &ids, const QString &clientId, const QString &currentActivity)
0480     {
0481         const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
0482 
0483         QStringList activities{currentActivity, QStringLiteral("global")};
0484 
0485         qCDebug(KICKER_DEBUG) << "Saving ordering for" << currentActivity << "and global" << ids;
0486 
0487         for (const auto &activity : activities) {
0488             const QString groupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + activity;
0489 
0490             KConfigGroup cfgGroup(cfg, groupName);
0491 
0492             cfgGroup.writeEntry("ordering", ids);
0493         }
0494 
0495         cfg->sync();
0496     }
0497 
0498     KAStatsFavoritesModel *const q;
0499     KActivities::Consumer m_activities;
0500     Query m_query;
0501     ResultWatcher m_watcher;
0502     QString m_clientId;
0503 
0504     QList<NormalizedId> m_items;
0505     QHash<QString, std::shared_ptr<AbstractEntry>> m_itemEntries;
0506     QStringList m_ignoredItems;
0507 };
0508 
0509 KAStatsFavoritesModel::KAStatsFavoritesModel(QObject *parent)
0510     : PlaceholderModel(parent)
0511     , d(nullptr) // we have no client id yet
0512     , m_enabled(true)
0513     , m_maxFavorites(-1)
0514     , m_activities(new KActivities::Consumer(this))
0515 {
0516     connect(m_activities, &KActivities::Consumer::currentActivityChanged, this, [&](const QString &currentActivity) {
0517         qCDebug(KICKER_DEBUG) << "Activity just got changed to" << currentActivity;
0518         Q_UNUSED(currentActivity);
0519         if (d) {
0520             auto clientId = d->m_clientId;
0521             initForClient(clientId);
0522         }
0523     });
0524 }
0525 
0526 KAStatsFavoritesModel::~KAStatsFavoritesModel()
0527 {
0528     delete d;
0529 }
0530 
0531 void KAStatsFavoritesModel::initForClient(const QString &clientId)
0532 {
0533     qCDebug(KICKER_DEBUG) << "initForClient" << clientId;
0534 
0535     setSourceModel(nullptr);
0536     delete d;
0537     d = new Private(this, clientId);
0538 
0539     setSourceModel(d);
0540 }
0541 
0542 QString KAStatsFavoritesModel::description() const
0543 {
0544     return i18n("Favorites");
0545 }
0546 
0547 bool KAStatsFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument)
0548 {
0549     return d && d->trigger(row, actionId, argument);
0550 }
0551 
0552 bool KAStatsFavoritesModel::enabled() const
0553 {
0554     return m_enabled;
0555 }
0556 
0557 int KAStatsFavoritesModel::maxFavorites() const
0558 {
0559     return m_maxFavorites;
0560 }
0561 
0562 void KAStatsFavoritesModel::setMaxFavorites(int max)
0563 {
0564     Q_UNUSED(max);
0565 }
0566 
0567 void KAStatsFavoritesModel::setEnabled(bool enable)
0568 {
0569     if (m_enabled != enable) {
0570         m_enabled = enable;
0571 
0572         Q_EMIT enabledChanged();
0573     }
0574 }
0575 
0576 QStringList KAStatsFavoritesModel::favorites() const
0577 {
0578     qCWarning(KICKER_DEBUG) << "KAStatsFavoritesModel::favorites returns nothing, it is here just to keep the API backwards-compatible";
0579     return QStringList();
0580 }
0581 
0582 void KAStatsFavoritesModel::setFavorites(const QStringList &favorites)
0583 {
0584     Q_UNUSED(favorites);
0585     qCWarning(KICKER_DEBUG) << "KAStatsFavoritesModel::setFavorites is ignored";
0586 }
0587 
0588 bool KAStatsFavoritesModel::isFavorite(const QString &id) const
0589 {
0590     return d && d->m_itemEntries.contains(id);
0591 }
0592 
0593 #if BUILD_TESTING
0594 void KAStatsFavoritesModel::portOldFavorites(const QStringList &_ids)
0595 #else
0596 void KAStatsFavoritesModel::portOldFavorites(const QStringList &ids)
0597 #endif
0598 {
0599     if (!d)
0600         return;
0601 
0602 #if BUILD_TESTING
0603     const QStringList ids = qEnvironmentVariableIsSet("KDECI_BUILD") ? QStringList{
0604         QLatin1String("org.kde.plasma.emojier.desktop"),
0605         QLatin1String("linguist5.desktop"),
0606         QLatin1String("org.qt.linguist6.desktop"),
0607     } : _ids;
0608 #endif
0609 
0610     qCDebug(KICKER_DEBUG) << "portOldFavorites" << ids;
0611 
0612     const QString activityId = QStringLiteral(":global");
0613     std::for_each(ids.begin(), ids.end(), [&](const QString &id) {
0614         addFavoriteTo(id, activityId);
0615     });
0616 
0617     // Resetting the model
0618     auto clientId = d->m_clientId;
0619     setSourceModel(nullptr);
0620     delete d;
0621     d = nullptr;
0622 
0623     qCDebug(KICKER_DEBUG) << "Save ordering (from portOldFavorites) -->";
0624     Private::saveOrdering(ids, clientId, m_activities->currentActivity());
0625 
0626     QTimer::singleShot(500, this, std::bind(&KAStatsFavoritesModel::initForClient, this, clientId));
0627 }
0628 
0629 void KAStatsFavoritesModel::addFavorite(const QString &id, int index)
0630 {
0631     qCDebug(KICKER_DEBUG) << "addFavorite" << id << index << " -->";
0632     addFavoriteTo(id, QStringLiteral(":global"), index);
0633 }
0634 
0635 void KAStatsFavoritesModel::removeFavorite(const QString &id)
0636 {
0637     qCDebug(KICKER_DEBUG) << "removeFavorite" << id << " -->";
0638     removeFavoriteFrom(id, QStringLiteral(":any"));
0639 }
0640 
0641 void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const QString &activityId, int index)
0642 {
0643     qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activityId << index << " -->";
0644     addFavoriteTo(id, Activity(activityId), index);
0645 }
0646 
0647 void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const QString &activityId)
0648 {
0649     qCDebug(KICKER_DEBUG) << "removeFavoriteFrom" << id << activityId << " -->";
0650     removeFavoriteFrom(id, Activity(activityId));
0651 }
0652 
0653 void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const Activity &activity, int index)
0654 {
0655     if (!d || id.isEmpty())
0656         return;
0657 
0658     Q_ASSERT(!activity.values.isEmpty());
0659 
0660     setDropPlaceholderIndex(-1);
0661 
0662     QStringList matchers{d->m_activities.currentActivity(), QStringLiteral(":global"), QStringLiteral(":current")};
0663     if (std::find_first_of(activity.values.cbegin(), activity.values.cend(), matchers.cbegin(), matchers.cend()) != activity.values.cend()) {
0664         d->addResult(id, index);
0665     }
0666 
0667     const auto url = d->normalizedId(id).value();
0668 
0669     qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << index << url << " (actual)";
0670 
0671     if (url.isEmpty())
0672         return;
0673 
0674     d->m_watcher.linkToActivity(QUrl(url), activity, Agent(agentForUrl(url)));
0675 }
0676 
0677 void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const Activity &activity)
0678 {
0679     if (!d || id.isEmpty())
0680         return;
0681 
0682     Q_ASSERT(!activity.values.isEmpty());
0683 
0684     qCDebug(KICKER_DEBUG) << "removeFavoriteFrom" << id << activity;
0685 
0686     if (!isFavorite(id)) {
0687         return;
0688     }
0689 
0690     QUrl url = QUrl(id);
0691 
0692     d->m_watcher.unlinkFromActivity(url, activity, Agent(agentForUrl(id)));
0693 }
0694 
0695 void KAStatsFavoritesModel::setFavoriteOn(const QString &id, const QString &activityId)
0696 {
0697     if (!d || id.isEmpty())
0698         return;
0699 
0700     const auto url = d->normalizedId(id).value();
0701 
0702     qCDebug(KICKER_DEBUG) << "setFavoriteOn" << id << activityId << url << " (actual)";
0703 
0704     qCDebug(KICKER_DEBUG) << "%%%%%%%%%%% Activity is" << activityId;
0705     if (activityId.isEmpty() || activityId == QLatin1String(":any") || activityId == QLatin1String(":global")
0706         || activityId == m_activities->currentActivity()) {
0707         d->m_ignoredItems << url;
0708     }
0709 
0710     d->m_watcher.unlinkFromActivity(QUrl(url), Activity::any(), Agent(agentForUrl(url)));
0711     d->m_watcher.linkToActivity(QUrl(url), activityId, Agent(agentForUrl(url)));
0712 }
0713 
0714 void KAStatsFavoritesModel::moveRow(int from, int to)
0715 {
0716     if (!d)
0717         return;
0718 
0719     d->move(from, to);
0720 }
0721 
0722 AbstractModel *KAStatsFavoritesModel::favoritesModel()
0723 {
0724     return this;
0725 }
0726 
0727 void KAStatsFavoritesModel::refresh()
0728 {
0729 }
0730 
0731 QObject *KAStatsFavoritesModel::activities() const
0732 {
0733     return m_activities;
0734 }
0735 
0736 QString KAStatsFavoritesModel::activityNameForId(const QString &activityId) const
0737 {
0738     // It is safe to use a short-lived object here,
0739     // we are always synced with KAMD in plasma
0740     KActivities::Info info(activityId);
0741     return info.name();
0742 }
0743 
0744 QStringList KAStatsFavoritesModel::linkedActivitiesFor(const QString &id) const
0745 {
0746     if (!d) {
0747         qCDebug(KICKER_DEBUG) << "Linked for" << id << "is empty, no Private instance";
0748         return {};
0749     }
0750 
0751     auto url = d->normalizedId(id).value();
0752 
0753     if (url.startsWith(QLatin1String("file:"))) {
0754         url = QUrl(url).toLocalFile();
0755     }
0756 
0757     if (url.isEmpty()) {
0758         qCDebug(KICKER_DEBUG) << "The url for" << id << "is empty";
0759         return {};
0760     }
0761 
0762     auto query = LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_DOCUMENTS} | Type::any() | Activity::any() | Url(url) | Limit(100);
0763 
0764     ResultSet results(query);
0765 
0766     for (const auto &result : results) {
0767         qCDebug(KICKER_DEBUG) << "Returning" << result.linkedActivities() << "for" << id << url;
0768         return result.linkedActivities();
0769     }
0770 
0771     qCDebug(KICKER_DEBUG) << "Returning empty list of activities for" << id << url;
0772     return {};
0773 }