forked from Qortal/Brooklyn
382 lines
12 KiB
C++
382 lines
12 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
|
|
SPDX-FileCopyrightText: 2007 Jeremy Whiting <jpwhiting@kde.org>
|
|
SPDX-FileCopyrightText: 2016 Olivier Churlaud <olivier@churlaud.com>
|
|
SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
*/
|
|
|
|
#include "sourcesmodel.h"
|
|
|
|
#include <QCollator>
|
|
#include <QDir>
|
|
#include <QRegularExpression>
|
|
#include <QStandardPaths>
|
|
#include <QStringList>
|
|
|
|
#include <KApplicationTrader>
|
|
#include <KConfig>
|
|
#include <KConfigGroup>
|
|
#include <KLocalizedString>
|
|
#include <KService>
|
|
#include <KSharedConfig>
|
|
|
|
#include <algorithm>
|
|
|
|
static const QString s_plasmaWorkspaceNotifyRcName = QStringLiteral("plasma_workspace");
|
|
|
|
SourcesModel::SourcesModel(QObject *parent)
|
|
: QAbstractItemModel(parent)
|
|
{
|
|
}
|
|
|
|
SourcesModel::~SourcesModel() = default;
|
|
|
|
QPersistentModelIndex SourcesModel::makePersistentModelIndex(const QModelIndex &idx) const
|
|
{
|
|
return QPersistentModelIndex(idx);
|
|
}
|
|
|
|
QPersistentModelIndex SourcesModel::persistentIndexForDesktopEntry(const QString &desktopEntry) const
|
|
{
|
|
if (desktopEntry.isEmpty()) {
|
|
return QPersistentModelIndex();
|
|
}
|
|
const auto matches = match(index(0, 0), SourcesModel::DesktopEntryRole, desktopEntry, 1, Qt::MatchFixedString);
|
|
if (matches.isEmpty()) {
|
|
return QPersistentModelIndex();
|
|
}
|
|
return QPersistentModelIndex(matches.first());
|
|
}
|
|
|
|
QPersistentModelIndex SourcesModel::persistentIndexForNotifyRcName(const QString ¬ifyRcName) const
|
|
{
|
|
if (notifyRcName.isEmpty()) {
|
|
return QPersistentModelIndex();
|
|
}
|
|
const auto matches = match(index(0, 0), SourcesModel::NotifyRcNameRole, notifyRcName, 1, Qt::MatchFixedString);
|
|
if (matches.isEmpty()) {
|
|
return QPersistentModelIndex();
|
|
}
|
|
return QPersistentModelIndex(matches.first());
|
|
}
|
|
|
|
int SourcesModel::columnCount(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent);
|
|
return 1;
|
|
}
|
|
|
|
int SourcesModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
if (parent.column() > 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (!parent.isValid()) {
|
|
return m_data.count();
|
|
}
|
|
|
|
if (parent.internalId()) {
|
|
return 0;
|
|
}
|
|
|
|
return m_data.at(parent.row()).events.count();
|
|
}
|
|
|
|
QVariant SourcesModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return QVariant();
|
|
}
|
|
|
|
if (index.internalId()) { // event
|
|
const auto &event = m_data.at(index.internalId() - 1).events.at(index.row());
|
|
|
|
switch (role) {
|
|
case Qt::DisplayRole:
|
|
return event.name;
|
|
case Qt::DecorationRole:
|
|
return event.iconName;
|
|
case EventIdRole:
|
|
return event.eventId;
|
|
case ActionsRole:
|
|
return event.actions;
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
const auto &source = m_data.at(index.row());
|
|
|
|
switch (role) {
|
|
case Qt::DisplayRole:
|
|
return source.display();
|
|
case Qt::DecorationRole:
|
|
return source.iconName;
|
|
case SourceTypeRole:
|
|
return source.desktopEntry.isEmpty() ? ServiceType : ApplicationType;
|
|
case NotifyRcNameRole:
|
|
return source.notifyRcName;
|
|
case DesktopEntryRole:
|
|
return source.desktopEntry;
|
|
case IsDefaultRole:
|
|
return source.isDefault;
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
bool SourcesModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
{
|
|
if (!index.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
bool dirty = false;
|
|
|
|
if (index.internalId()) { // event
|
|
auto &event = m_data[index.internalId() - 1].events[index.row()];
|
|
switch (role) {
|
|
case ActionsRole: {
|
|
const QStringList newActions = value.toStringList();
|
|
if (event.actions != newActions) {
|
|
event.actions = newActions;
|
|
dirty = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto &source = m_data[index.row()];
|
|
|
|
switch (role) {
|
|
case IsDefaultRole: {
|
|
if (source.isDefault != value.toBool()) {
|
|
source.isDefault = value.toBool();
|
|
dirty = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (dirty) {
|
|
Q_EMIT dataChanged(index, index, {role});
|
|
}
|
|
|
|
return dirty;
|
|
}
|
|
|
|
QModelIndex SourcesModel::index(int row, int column, const QModelIndex &parent) const
|
|
{
|
|
if (row < 0 || column != 0) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
if (parent.isValid()) {
|
|
const auto events = m_data.at(parent.row()).events;
|
|
if (row < events.count()) {
|
|
return createIndex(row, column, parent.row() + 1);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
if (row < m_data.count()) {
|
|
return createIndex(row, column, nullptr);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex SourcesModel::parent(const QModelIndex &child) const
|
|
{
|
|
if (child.internalId()) {
|
|
return createIndex(child.internalId() - 1, 0, nullptr);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QHash<int, QByteArray> SourcesModel::roleNames() const
|
|
{
|
|
return {{Qt::DisplayRole, QByteArrayLiteral("display")},
|
|
{Qt::DecorationRole, QByteArrayLiteral("decoration")},
|
|
{SourceTypeRole, QByteArrayLiteral("sourceType")},
|
|
{NotifyRcNameRole, QByteArrayLiteral("notifyRcName")},
|
|
{DesktopEntryRole, QByteArrayLiteral("desktopEntry")},
|
|
{IsDefaultRole, QByteArrayLiteral("isDefault")},
|
|
{EventIdRole, QByteArrayLiteral("eventId")},
|
|
{ActionsRole, QByteArrayLiteral("actions")}};
|
|
}
|
|
|
|
void SourcesModel::load()
|
|
{
|
|
beginResetModel();
|
|
|
|
m_data.clear();
|
|
|
|
QCollator collator;
|
|
|
|
QVector<SourceData> appsData;
|
|
QVector<SourceData> servicesData;
|
|
|
|
QStringList notifyRcFiles;
|
|
QStringList desktopEntries;
|
|
|
|
// old code did KGlobal::dirs()->findAllResources("data", QStringLiteral("*/*.notifyrc")) but in KF5
|
|
// only notifyrc files in knotifications5/ folder are supported
|
|
const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5"), QStandardPaths::LocateDirectory);
|
|
for (const QString &dir : dirs) {
|
|
const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.notifyrc"));
|
|
for (const QString &file : fileNames) {
|
|
if (notifyRcFiles.contains(file)) {
|
|
continue;
|
|
}
|
|
|
|
notifyRcFiles.append(file);
|
|
|
|
KConfig config(file, KConfig::NoGlobals);
|
|
config.addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/") + file));
|
|
|
|
KConfigGroup globalGroup(&config, QLatin1String("Global"));
|
|
|
|
const QRegularExpression regExp(QStringLiteral("^Event/([^/]*)$"));
|
|
const QStringList groups = config.groupList().filter(regExp);
|
|
|
|
const QString notifyRcName = file.section(QLatin1Char('.'), 0, -2);
|
|
const QString desktopEntry = globalGroup.readEntry(QStringLiteral("DesktopEntry"));
|
|
if (!desktopEntry.isEmpty()) {
|
|
if (desktopEntries.contains(desktopEntry)) {
|
|
continue;
|
|
}
|
|
|
|
desktopEntries.append(desktopEntry);
|
|
}
|
|
|
|
SourceData source{
|
|
// The old KCM read the Name and Comment from global settings disregarding
|
|
// any user settings and just used user-specific files for actions config
|
|
// I'm pretty sure there's a readEntry equivalent that does that without
|
|
// reading the config stuff twice, assuming we care about this to begin with
|
|
globalGroup.readEntry(QStringLiteral("Name")),
|
|
globalGroup.readEntry(QStringLiteral("Comment")),
|
|
globalGroup.readEntry(QStringLiteral("IconName")),
|
|
true,
|
|
notifyRcName,
|
|
desktopEntry,
|
|
{} // events
|
|
};
|
|
|
|
QVector<EventData> events;
|
|
for (const QString &group : groups) {
|
|
KConfigGroup cg(&config, group);
|
|
|
|
const QString eventId = regExp.match(group).captured(1);
|
|
// TODO context stuff
|
|
// TODO load defaults thing
|
|
|
|
EventData event{cg.readEntry("Name"),
|
|
cg.readEntry("Comment"),
|
|
cg.readEntry("IconName"),
|
|
eventId,
|
|
// TODO Flags?
|
|
cg.readEntry("Action").split(QLatin1Char('|'))};
|
|
events.append(event);
|
|
}
|
|
|
|
std::sort(events.begin(), events.end(), [&collator](const EventData &a, const EventData &b) {
|
|
return collator.compare(a.name, b.name) < 0;
|
|
});
|
|
|
|
source.events = events;
|
|
|
|
if (!source.desktopEntry.isEmpty()) {
|
|
appsData.append(source);
|
|
} else {
|
|
servicesData.append(source);
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto services = KApplicationTrader::query([desktopEntries](const KService::Ptr &app) {
|
|
if (app->noDisplay()) {
|
|
return false;
|
|
}
|
|
|
|
if (desktopEntries.contains(app->desktopEntryName())) {
|
|
return false;
|
|
}
|
|
|
|
const QString usesNotis = app->property(QStringLiteral("X-GNOME-UsesNotifications")).toString();
|
|
|
|
if (usesNotis.compare(QLatin1String("true"), Qt::CaseInsensitive) != 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
for (const auto &service : services) {
|
|
SourceData source{
|
|
service->name(),
|
|
service->comment(),
|
|
service->icon(),
|
|
true,
|
|
QString(), // notifyRcFile
|
|
service->desktopEntryName(),
|
|
{} // events
|
|
};
|
|
appsData.append(source);
|
|
desktopEntries.append(service->desktopEntryName());
|
|
}
|
|
|
|
KSharedConfig::Ptr plasmanotifyrc = KSharedConfig::openConfig(QStringLiteral("plasmanotifyrc"));
|
|
KConfigGroup applicationsGroup = plasmanotifyrc->group("Applications");
|
|
const QStringList seenApps = applicationsGroup.groupList();
|
|
for (const QString &app : seenApps) {
|
|
if (desktopEntries.contains(app)) {
|
|
continue;
|
|
}
|
|
|
|
KService::Ptr service = KService::serviceByDesktopName(app);
|
|
if (!service || service->noDisplay()) {
|
|
continue;
|
|
}
|
|
|
|
SourceData source{service->name(),
|
|
service->comment(),
|
|
service->icon(),
|
|
true,
|
|
QString(), // notifyRcFile
|
|
service->desktopEntryName(),
|
|
{}};
|
|
appsData.append(source);
|
|
desktopEntries.append(service->desktopEntryName());
|
|
}
|
|
|
|
std::sort(appsData.begin(), appsData.end(), [&collator](const SourceData &a, const SourceData &b) {
|
|
return collator.compare(a.display(), b.display()) < 0;
|
|
});
|
|
|
|
// Fake entry for configuring non-identifyable applications
|
|
appsData << SourceData{i18n("Other Applications"), {}, QStringLiteral("applications-other"), true, QString(), QStringLiteral("@other"), {}};
|
|
|
|
// Sort and make sure plasma_workspace is at the beginning of the list
|
|
std::sort(servicesData.begin(), servicesData.end(), [&collator](const SourceData &a, const SourceData &b) {
|
|
if (a.notifyRcName == s_plasmaWorkspaceNotifyRcName) {
|
|
return true;
|
|
}
|
|
if (b.notifyRcName == s_plasmaWorkspaceNotifyRcName) {
|
|
return false;
|
|
}
|
|
return collator.compare(a.display(), b.display()) < 0;
|
|
});
|
|
|
|
m_data << appsData << servicesData;
|
|
|
|
endResetModel();
|
|
}
|