You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

676 lines
20 KiB

/*
SPDX-FileCopyrightText: 2014-2015 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2016-2017 Ivan Cukic <ivan.cukic@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kastatsfavoritesmodel.h"
#include "actionlist.h"
#include "appentry.h"
#include "contactentry.h"
#include "debug.h"
#include "fileentry.h"
#include <QFileInfo>
#include <QSortFilterProxyModel>
#include <QTimer>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <KActivities/Consumer>
#include <KActivities/Stats/Query>
#include <KActivities/Stats/ResultSet>
#include <KActivities/Stats/ResultWatcher>
#include <KActivities/Stats/Terms>
namespace KAStats = KActivities::Stats;
using namespace KAStats;
using namespace KAStats::Terms;
#define AGENT_APPLICATIONS QStringLiteral("org.kde.plasma.favorites.applications")
#define AGENT_CONTACTS QStringLiteral("org.kde.plasma.favorites.contacts")
#define AGENT_DOCUMENTS QStringLiteral("org.kde.plasma.favorites.documents")
QString agentForUrl(const QString &url)
{
// clang-format off
return url.startsWith(QLatin1String("ktp:"))
? AGENT_CONTACTS
: url.startsWith(QLatin1String("preferred:"))
? AGENT_APPLICATIONS
: url.startsWith(QLatin1String("applications:"))
? AGENT_APPLICATIONS
: (url.startsWith(QLatin1Char('/')) && !url.endsWith(QLatin1String(".desktop")))
? AGENT_DOCUMENTS
: (url.startsWith(QLatin1String("file:/")) && !url.endsWith(QLatin1String(".desktop")))
? AGENT_DOCUMENTS
// use applications as the default
: AGENT_APPLICATIONS;
// clang-format on
}
class KAStatsFavoritesModel::Private : public QAbstractListModel
{
public:
class NormalizedId
{
public:
NormalizedId()
{
}
NormalizedId(const Private *parent, const QString &id)
{
if (id.isEmpty())
return;
QSharedPointer<AbstractEntry> entry = nullptr;
if (parent->m_itemEntries.contains(id)) {
entry = parent->m_itemEntries[id];
} else {
// This entry is not cached - it is temporary,
// so let's clean up when we exit this function
entry = parent->entryForResource(id);
}
if (!entry || !entry->isValid()) {
qCWarning(KICKER_DEBUG) << "Entry is not valid" << id << entry;
m_id = id;
return;
}
const auto url = entry->url();
qCDebug(KICKER_DEBUG) << "Original id is: " << id << ", and the url is" << url;
// Preferred applications need special handling
if (entry->id().startsWith(QLatin1String("preferred:"))) {
m_id = entry->id();
return;
}
// If this is an application, use the applications:-format url
auto appEntry = dynamic_cast<AppEntry *>(entry.data());
if (appEntry && !appEntry->menuId().isEmpty()) {
m_id = QLatin1String("applications:") + appEntry->menuId();
return;
}
// We want to resolve symbolic links not to have two paths
// refer to the same .desktop file
if (url.isLocalFile()) {
QFileInfo file(url.toLocalFile());
if (file.exists()) {
m_id = QUrl::fromLocalFile(file.canonicalFilePath()).toString();
return;
}
}
// If this is a file, we should have already covered it
if (url.scheme() == QLatin1String("file")) {
return;
}
m_id = url.toString();
}
const QString &value() const
{
return m_id;
}
bool operator==(const NormalizedId &other) const
{
return m_id == other.m_id;
}
private:
QString m_id;
};
NormalizedId normalizedId(const QString &id) const
{
return NormalizedId(this, id);
}
QSharedPointer<AbstractEntry> entryForResource(const QString &resource) const
{
using SP = QSharedPointer<AbstractEntry>;
const auto agent = agentForUrl(resource);
if (agent == AGENT_CONTACTS) {
return SP(new ContactEntry(q, resource));
} else if (agent == AGENT_DOCUMENTS) {
if (resource.startsWith(QLatin1String("/"))) {
return SP(new FileEntry(q, QUrl::fromLocalFile(resource)));
} else {
return SP(new FileEntry(q, QUrl(resource)));
}
} else if (agent == AGENT_APPLICATIONS) {
if (resource.startsWith(QLatin1String("applications:"))) {
return SP(new AppEntry(q, resource.mid(13)));
} else {
return SP(new AppEntry(q, resource));
}
} else {
return {};
}
}
Private(KAStatsFavoritesModel *parent, const QString &clientId)
: q(parent)
, m_query(LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_CONTACTS, AGENT_DOCUMENTS} | Type::any() | Activity::current() | Activity::global()
| Limit::all())
, m_watcher(m_query)
, m_clientId(clientId)
{
// Connecting the watcher
connect(&m_watcher, &ResultWatcher::resultLinked, [this](const QString &resource) {
addResult(resource, -1);
});
connect(&m_watcher, &ResultWatcher::resultUnlinked, [this](const QString &resource) {
removeResult(resource);
});
// Loading the items order
const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
// We want first to check whether we have an ordering for this activity.
// If not, we will try to get a global one for this applet
const QString thisGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + m_activities.currentActivity();
const QString globalGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-global");
KConfigGroup thisCfgGroup(cfg, thisGroupName);
KConfigGroup globalCfgGroup(cfg, globalGroupName);
QStringList ordering = thisCfgGroup.readEntry("ordering", QStringList()) + globalCfgGroup.readEntry("ordering", QStringList());
qCDebug(KICKER_DEBUG) << "Loading the ordering " << ordering;
// Loading the results without emitting any model signals
qCDebug(KICKER_DEBUG) << "Query is" << m_query;
ResultSet results(m_query);
for (const auto &result : results) {
qCDebug(KICKER_DEBUG) << "Got " << result.resource() << " -->";
addResult(result.resource(), -1, false);
}
// Normalizing all the ids
std::transform(ordering.begin(), ordering.end(), ordering.begin(), [&](const QString &item) {
return normalizedId(item).value();
});
// Sorting the items in the cache
std::sort(m_items.begin(), m_items.end(), [&](const NormalizedId &left, const NormalizedId &right) {
auto leftIndex = ordering.indexOf(left.value());
auto rightIndex = ordering.indexOf(right.value());
// clang-format off
return (leftIndex == -1 && rightIndex == -1) ?
left.value() < right.value() :
(leftIndex == -1) ?
false :
(rightIndex == -1) ?
true :
// otherwise
leftIndex < rightIndex;
// clang-format on
});
// Debugging:
QVector<QString> itemStrings(m_items.size());
std::transform(m_items.cbegin(), m_items.cend(), itemStrings.begin(), [](const NormalizedId &item) {
return item.value();
});
qCDebug(KICKER_DEBUG) << "After ordering: " << itemStrings;
}
void addResult(const QString &_resource, int index, bool notifyModel = true)
{
// We want even files to have a proper URL
const auto resource = _resource.startsWith(QLatin1Char('/')) ? QUrl::fromLocalFile(_resource).toString() : _resource;
qCDebug(KICKER_DEBUG) << "Adding result" << resource << "already present?" << m_itemEntries.contains(resource);
if (m_itemEntries.contains(resource))
return;
auto entry = entryForResource(resource);
if (!entry || !entry->isValid()) {
qCDebug(KICKER_DEBUG) << "Entry is not valid!";
return;
}
if (index == -1) {
index = m_items.count();
}
if (notifyModel) {
beginInsertRows(QModelIndex(), index, index);
}
auto url = entry->url();
m_itemEntries[resource] = m_itemEntries[entry->id()] = m_itemEntries[url.toString()] = m_itemEntries[url.toLocalFile()] = entry;
auto normalized = normalizedId(resource);
m_items.insert(index, normalized);
m_itemEntries[normalized.value()] = entry;
if (notifyModel) {
endInsertRows();
saveOrdering();
}
}
void removeResult(const QString &resource)
{
auto normalized = normalizedId(resource);
// If we know this item will not really be removed,
// but only that activities it is on have changed,
// lets leave it
if (m_ignoredItems.contains(normalized.value())) {
m_ignoredItems.removeAll(normalized.value());
return;
}
qCDebug(KICKER_DEBUG) << "Removing result" << resource;
auto index = m_items.indexOf(normalizedId(resource));
if (index == -1)
return;
beginRemoveRows(QModelIndex(), index, index);
auto entry = m_itemEntries[resource];
m_items.removeAt(index);
// Removing the entry from the cache
std::remove(m_itemEntries.begin(), m_itemEntries.end(), entry);
endRemoveRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid())
return 0;
return m_items.count();
}
QVariant data(const QModelIndex &item, int role = Qt::DisplayRole) const override
{
if (item.parent().isValid())
return QVariant();
const auto index = item.row();
const auto entry = m_itemEntries[m_items[index].value()];
// clang-format off
return entry == nullptr ? QVariant()
: role == Qt::DisplayRole ? entry->name()
: role == Qt::DecorationRole ? entry->icon()
: role == Kicker::DescriptionRole ? entry->description()
: role == Kicker::FavoriteIdRole ? entry->id()
: role == Kicker::UrlRole ? entry->url()
: role == Kicker::HasActionListRole ? entry->hasActions()
: role == Kicker::ActionListRole ? entry->actions()
: QVariant();
// clang-format on
}
bool trigger(int row, const QString &actionId, const QVariant &argument)
{
if (row < 0 || row >= rowCount()) {
return false;
}
const QString id = data(index(row, 0), Kicker::UrlRole).toString();
if (m_itemEntries.contains(id)) {
return m_itemEntries[id]->run(actionId, argument);
}
// Entries with preferred:// can be changed by the user, BUG: 416161
// then the list of entries could be out of sync
const auto entry = m_itemEntries[m_items[row].value()];
if (QUrl(entry->id()).scheme() == QLatin1String("preferred")) {
return entry->run(actionId, argument);
}
return false;
}
void move(int from, int to)
{
if (from < 0)
return;
if (from >= m_items.count())
return;
if (to < 0)
return;
if (to >= m_items.count())
return;
if (from == to)
return;
const int modelTo = to + (to > from ? 1 : 0);
if (q->beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo)) {
m_items.move(from, to);
q->endMoveRows();
qCDebug(KICKER_DEBUG) << "Save ordering (from Private::move) -->";
saveOrdering();
}
}
void saveOrdering()
{
QStringList ids;
for (const auto &item : qAsConst(m_items)) {
ids << item.value();
}
qCDebug(KICKER_DEBUG) << "Save ordering (from Private::saveOrdering) -->";
saveOrdering(ids, m_clientId, m_activities.currentActivity());
}
static void saveOrdering(const QStringList &ids, const QString &clientId, const QString &currentActivity)
{
const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
QStringList activities{currentActivity, QStringLiteral("global")};
qCDebug(KICKER_DEBUG) << "Saving ordering for" << currentActivity << "and global" << ids;
for (const auto &activity : activities) {
const QString groupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + activity;
KConfigGroup cfgGroup(cfg, groupName);
cfgGroup.writeEntry("ordering", ids);
}
cfg->sync();
}
KAStatsFavoritesModel *const q;
KActivities::Consumer m_activities;
Query m_query;
ResultWatcher m_watcher;
QString m_clientId;
QVector<NormalizedId> m_items;
QHash<QString, QSharedPointer<AbstractEntry>> m_itemEntries;
QStringList m_ignoredItems;
};
KAStatsFavoritesModel::KAStatsFavoritesModel(QObject *parent)
: PlaceholderModel(parent)
, d(nullptr) // we have no client id yet
, m_enabled(true)
, m_maxFavorites(-1)
, m_activities(new KActivities::Consumer(this))
{
connect(m_activities, &KActivities::Consumer::currentActivityChanged, this, [&](const QString &currentActivity) {
qCDebug(KICKER_DEBUG) << "Activity just got changed to" << currentActivity;
Q_UNUSED(currentActivity);
if (d) {
auto clientId = d->m_clientId;
initForClient(clientId);
}
});
}
KAStatsFavoritesModel::~KAStatsFavoritesModel()
{
delete d;
}
void KAStatsFavoritesModel::initForClient(const QString &clientId)
{
qCDebug(KICKER_DEBUG) << "initForClient" << clientId;
setSourceModel(nullptr);
delete d;
d = new Private(this, clientId);
setSourceModel(d);
}
QString KAStatsFavoritesModel::description() const
{
return i18n("Favorites");
}
bool KAStatsFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument)
{
return d && d->trigger(row, actionId, argument);
}
bool KAStatsFavoritesModel::enabled() const
{
return m_enabled;
}
int KAStatsFavoritesModel::maxFavorites() const
{
return m_maxFavorites;
}
void KAStatsFavoritesModel::setMaxFavorites(int max)
{
Q_UNUSED(max);
}
void KAStatsFavoritesModel::setEnabled(bool enable)
{
if (m_enabled != enable) {
m_enabled = enable;
Q_EMIT enabledChanged();
}
}
QStringList KAStatsFavoritesModel::favorites() const
{
qCWarning(KICKER_DEBUG) << "KAStatsFavoritesModel::favorites returns nothing, it is here just to keep the API backwards-compatible";
return QStringList();
}
void KAStatsFavoritesModel::setFavorites(const QStringList &favorites)
{
Q_UNUSED(favorites);
qCWarning(KICKER_DEBUG) << "KAStatsFavoritesModel::setFavorites is ignored";
}
bool KAStatsFavoritesModel::isFavorite(const QString &id) const
{
return d && d->m_itemEntries.contains(id);
}
void KAStatsFavoritesModel::portOldFavorites(const QStringList &ids)
{
if (!d)
return;
qCDebug(KICKER_DEBUG) << "portOldFavorites" << ids;
const QString activityId = QStringLiteral(":global");
std::for_each(ids.begin(), ids.end(), [&](const QString &id) {
addFavoriteTo(id, activityId);
});
// Resetting the model
auto clientId = d->m_clientId;
setSourceModel(nullptr);
delete d;
d = nullptr;
qCDebug(KICKER_DEBUG) << "Save ordering (from portOldFavorites) -->";
Private::saveOrdering(ids, clientId, m_activities->currentActivity());
QTimer::singleShot(500, this, std::bind(&KAStatsFavoritesModel::initForClient, this, clientId));
}
void KAStatsFavoritesModel::addFavorite(const QString &id, int index)
{
qCDebug(KICKER_DEBUG) << "addFavorite" << id << index << " -->";
addFavoriteTo(id, QStringLiteral(":global"), index);
}
void KAStatsFavoritesModel::removeFavorite(const QString &id)
{
qCDebug(KICKER_DEBUG) << "removeFavorite" << id << " -->";
removeFavoriteFrom(id, QStringLiteral(":any"));
}
void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const QString &activityId, int index)
{
qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activityId << index << " -->";
addFavoriteTo(id, Activity(activityId), index);
}
void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const QString &activityId)
{
qCDebug(KICKER_DEBUG) << "removeFavoriteFrom" << id << activityId << " -->";
removeFavoriteFrom(id, Activity(activityId));
}
void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const Activity &activity, int index)
{
if (!d || id.isEmpty())
return;
Q_ASSERT(!activity.values.isEmpty());
setDropPlaceholderIndex(-1);
QStringList matchers{d->m_activities.currentActivity(), QStringLiteral(":global"), QStringLiteral(":current")};
if (std::find_first_of(activity.values.cbegin(), activity.values.cend(), matchers.cbegin(), matchers.cend()) != activity.values.cend()) {
d->addResult(id, index);
}
const auto url = d->normalizedId(id).value();
qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << index << url << " (actual)";
if (url.isEmpty())
return;
d->m_watcher.linkToActivity(QUrl(url), activity, Agent(agentForUrl(url)));
}
void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const Activity &activity)
{
if (!d || id.isEmpty())
return;
const auto url = d->normalizedId(id).value();
Q_ASSERT(!activity.values.isEmpty());
qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << url << " (actual)";
if (url.isEmpty())
return;
d->m_watcher.unlinkFromActivity(QUrl(url), activity, Agent(agentForUrl(url)));
}
void KAStatsFavoritesModel::setFavoriteOn(const QString &id, const QString &activityId)
{
if (!d || id.isEmpty())
return;
const auto url = d->normalizedId(id).value();
qCDebug(KICKER_DEBUG) << "setFavoriteOn" << id << activityId << url << " (actual)";
qCDebug(KICKER_DEBUG) << "%%%%%%%%%%% Activity is" << activityId;
if (activityId.isEmpty() || activityId == QLatin1String(":any") || activityId == QLatin1String(":global")
|| activityId == m_activities->currentActivity()) {
d->m_ignoredItems << url;
}
d->m_watcher.unlinkFromActivity(QUrl(url), Activity::any(), Agent(agentForUrl(url)));
d->m_watcher.linkToActivity(QUrl(url), activityId, Agent(agentForUrl(url)));
}
void KAStatsFavoritesModel::moveRow(int from, int to)
{
if (!d)
return;
d->move(from, to);
}
AbstractModel *KAStatsFavoritesModel::favoritesModel()
{
return this;
}
void KAStatsFavoritesModel::refresh()
{
}
QObject *KAStatsFavoritesModel::activities() const
{
return m_activities;
}
QString KAStatsFavoritesModel::activityNameForId(const QString &activityId) const
{
// It is safe to use a short-lived object here,
// we are always synced with KAMD in plasma
KActivities::Info info(activityId);
return info.name();
}
QStringList KAStatsFavoritesModel::linkedActivitiesFor(const QString &id) const
{
if (!d) {
qCDebug(KICKER_DEBUG) << "Linked for" << id << "is empty, no Private instance";
return {};
}
auto url = d->normalizedId(id).value();
if (url.startsWith(QLatin1String("file:"))) {
url = QUrl(url).toLocalFile();
}
if (url.isEmpty()) {
qCDebug(KICKER_DEBUG) << "The url for" << id << "is empty";
return {};
}
auto query = LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_CONTACTS, AGENT_DOCUMENTS} | Type::any() | Activity::any() | Url(url) | Limit::all();
ResultSet results(query);
for (const auto &result : results) {
qCDebug(KICKER_DEBUG) << "Returning" << result.linkedActivities() << "for" << id << url;
return result.linkedActivities();
}
qCDebug(KICKER_DEBUG) << "Returning empty list of activities for" << id << url;
return {};
}