Warning, file /plasma/plasma-workspace/kcms/notifications/sourcesmodel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2007 Jeremy Whiting <jpwhiting@kde.org>
0004     SPDX-FileCopyrightText: 2016 Olivier Churlaud <olivier@churlaud.com>
0005     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
0006 
0007     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0008 */
0009 
0010 #include "sourcesmodel.h"
0011 
0012 #include <QCollator>
0013 #include <QDir>
0014 #include <QRegularExpression>
0015 #include <QStandardPaths>
0016 #include <QStringList>
0017 
0018 #include <KApplicationTrader>
0019 #include <KConfig>
0020 #include <KConfigGroup>
0021 #include <KLocalizedString>
0022 #include <KService>
0023 #include <KSharedConfig>
0024 
0025 #include <algorithm>
0026 
0027 static const QString s_plasmaWorkspaceNotifyRcName = QStringLiteral("plasma_workspace");
0028 
0029 SourcesModel::SourcesModel(QObject *parent)
0030     : QAbstractItemModel(parent)
0031 {
0032 }
0033 
0034 SourcesModel::~SourcesModel() = default;
0035 
0036 QPersistentModelIndex SourcesModel::makePersistentModelIndex(const QModelIndex &idx) const
0037 {
0038     return QPersistentModelIndex(idx);
0039 }
0040 
0041 QPersistentModelIndex SourcesModel::persistentIndexForDesktopEntry(const QString &desktopEntry) const
0042 {
0043     if (desktopEntry.isEmpty()) {
0044         return QPersistentModelIndex();
0045     }
0046     const auto matches = match(index(0, 0), SourcesModel::DesktopEntryRole, desktopEntry, 1, Qt::MatchFixedString);
0047     if (matches.isEmpty()) {
0048         return QPersistentModelIndex();
0049     }
0050     return QPersistentModelIndex(matches.first());
0051 }
0052 
0053 QPersistentModelIndex SourcesModel::persistentIndexForNotifyRcName(const QString &notifyRcName) const
0054 {
0055     if (notifyRcName.isEmpty()) {
0056         return QPersistentModelIndex();
0057     }
0058     const auto matches = match(index(0, 0), SourcesModel::NotifyRcNameRole, notifyRcName, 1, Qt::MatchFixedString);
0059     if (matches.isEmpty()) {
0060         return QPersistentModelIndex();
0061     }
0062     return QPersistentModelIndex(matches.first());
0063 }
0064 
0065 int SourcesModel::columnCount(const QModelIndex &parent) const
0066 {
0067     Q_UNUSED(parent);
0068     return 1;
0069 }
0070 
0071 int SourcesModel::rowCount(const QModelIndex &parent) const
0072 {
0073     if (parent.column() > 0) {
0074         return 0;
0075     }
0076 
0077     if (!parent.isValid()) {
0078         return m_data.count();
0079     }
0080 
0081     if (parent.internalId()) {
0082         return 0;
0083     }
0084 
0085     return m_data.at(parent.row()).events.count();
0086 }
0087 
0088 QVariant SourcesModel::data(const QModelIndex &index, int role) const
0089 {
0090     if (!index.isValid()) {
0091         return QVariant();
0092     }
0093 
0094     if (index.internalId()) { // event
0095         const auto &event = m_data.at(index.internalId() - 1).events.at(index.row());
0096 
0097         switch (role) {
0098         case Qt::DisplayRole:
0099             return event.name;
0100         case Qt::DecorationRole:
0101             return event.iconName;
0102         case EventIdRole:
0103             return event.eventId;
0104         case ActionsRole:
0105             return event.actions;
0106         }
0107 
0108         return QVariant();
0109     }
0110 
0111     const auto &source = m_data.at(index.row());
0112 
0113     switch (role) {
0114     case Qt::DisplayRole:
0115         return source.display();
0116     case Qt::DecorationRole:
0117         return source.iconName;
0118     case SourceTypeRole:
0119         return source.desktopEntry.isEmpty() ? ServiceType : ApplicationType;
0120     case NotifyRcNameRole:
0121         return source.notifyRcName;
0122     case DesktopEntryRole:
0123         return source.desktopEntry;
0124     case IsDefaultRole:
0125         return source.isDefault;
0126     }
0127 
0128     return QVariant();
0129 }
0130 
0131 bool SourcesModel::setData(const QModelIndex &index, const QVariant &value, int role)
0132 {
0133     if (!index.isValid()) {
0134         return false;
0135     }
0136 
0137     bool dirty = false;
0138 
0139     if (index.internalId()) { // event
0140         auto &event = m_data[index.internalId() - 1].events[index.row()];
0141         switch (role) {
0142         case ActionsRole: {
0143             const QStringList newActions = value.toStringList();
0144             if (event.actions != newActions) {
0145                 event.actions = newActions;
0146                 dirty = true;
0147             }
0148             break;
0149         }
0150         }
0151     }
0152 
0153     auto &source = m_data[index.row()];
0154 
0155     switch (role) {
0156     case IsDefaultRole: {
0157         if (source.isDefault != value.toBool()) {
0158             source.isDefault = value.toBool();
0159             dirty = true;
0160         }
0161         break;
0162     }
0163     }
0164 
0165     if (dirty) {
0166         Q_EMIT dataChanged(index, index, {role});
0167     }
0168 
0169     return dirty;
0170 }
0171 
0172 QModelIndex SourcesModel::index(int row, int column, const QModelIndex &parent) const
0173 {
0174     if (row < 0 || column != 0) {
0175         return QModelIndex();
0176     }
0177 
0178     if (parent.isValid()) {
0179         const auto events = m_data.at(parent.row()).events;
0180         if (row < events.count()) {
0181             return createIndex(row, column, parent.row() + 1);
0182         }
0183 
0184         return QModelIndex();
0185     }
0186 
0187     if (row < m_data.count()) {
0188         return createIndex(row, column, nullptr);
0189     }
0190 
0191     return QModelIndex();
0192 }
0193 
0194 QModelIndex SourcesModel::parent(const QModelIndex &child) const
0195 {
0196     if (child.internalId()) {
0197         return createIndex(child.internalId() - 1, 0, nullptr);
0198     }
0199 
0200     return QModelIndex();
0201 }
0202 
0203 QHash<int, QByteArray> SourcesModel::roleNames() const
0204 {
0205     return {{Qt::DisplayRole, QByteArrayLiteral("display")},
0206             {Qt::DecorationRole, QByteArrayLiteral("decoration")},
0207             {SourceTypeRole, QByteArrayLiteral("sourceType")},
0208             {NotifyRcNameRole, QByteArrayLiteral("notifyRcName")},
0209             {DesktopEntryRole, QByteArrayLiteral("desktopEntry")},
0210             {IsDefaultRole, QByteArrayLiteral("isDefault")},
0211             {EventIdRole, QByteArrayLiteral("eventId")},
0212             {ActionsRole, QByteArrayLiteral("actions")}};
0213 }
0214 
0215 void SourcesModel::load()
0216 {
0217     beginResetModel();
0218 
0219     m_data.clear();
0220 
0221     QCollator collator;
0222 
0223     QVector<SourceData> appsData;
0224     QVector<SourceData> servicesData;
0225 
0226     QStringList notifyRcFiles;
0227     QStringList desktopEntries;
0228 
0229     // old code did KGlobal::dirs()->findAllResources("data", QStringLiteral("*/*.notifyrc")) but in KF5
0230     // only notifyrc files in knotifications5/ folder are supported
0231     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5"), QStandardPaths::LocateDirectory);
0232     for (const QString &dir : dirs) {
0233         const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.notifyrc"));
0234         for (const QString &file : fileNames) {
0235             if (notifyRcFiles.contains(file)) {
0236                 continue;
0237             }
0238 
0239             notifyRcFiles.append(file);
0240 
0241             KConfig config(file, KConfig::NoGlobals);
0242             config.addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5/") + file));
0243 
0244             KConfigGroup globalGroup(&config, QLatin1String("Global"));
0245 
0246             const QRegularExpression regExp(QStringLiteral("^Event/([^/]*)$"));
0247             const QStringList groups = config.groupList().filter(regExp);
0248 
0249             const QString notifyRcName = file.section(QLatin1Char('.'), 0, -2);
0250             const QString desktopEntry = globalGroup.readEntry(QStringLiteral("DesktopEntry"));
0251             if (!desktopEntry.isEmpty()) {
0252                 if (desktopEntries.contains(desktopEntry)) {
0253                     continue;
0254                 }
0255 
0256                 desktopEntries.append(desktopEntry);
0257             }
0258 
0259             SourceData source{
0260                 // The old KCM read the Name and Comment from global settings disregarding
0261                 // any user settings and just used user-specific files for actions config
0262                 // I'm pretty sure there's a readEntry equivalent that does that without
0263                 // reading the config stuff twice, assuming we care about this to begin with
0264                 globalGroup.readEntry(QStringLiteral("Name")),
0265                 globalGroup.readEntry(QStringLiteral("Comment")),
0266                 globalGroup.readEntry(QStringLiteral("IconName")),
0267                 true,
0268                 notifyRcName,
0269                 desktopEntry,
0270                 {} // events
0271             };
0272 
0273             QVector<EventData> events;
0274             for (const QString &group : groups) {
0275                 KConfigGroup cg(&config, group);
0276 
0277                 const QString eventId = regExp.match(group).captured(1);
0278                 // TODO context stuff
0279                 // TODO load defaults thing
0280 
0281                 EventData event{cg.readEntry("Name"),
0282                                 cg.readEntry("Comment"),
0283                                 cg.readEntry("IconName"),
0284                                 eventId,
0285                                 // TODO Flags?
0286                                 cg.readEntry("Action").split(QLatin1Char('|'))};
0287                 events.append(event);
0288             }
0289 
0290             std::sort(events.begin(), events.end(), [&collator](const EventData &a, const EventData &b) {
0291                 return collator.compare(a.name, b.name) < 0;
0292             });
0293 
0294             source.events = events;
0295 
0296             if (!source.desktopEntry.isEmpty()) {
0297                 appsData.append(source);
0298             } else {
0299                 servicesData.append(source);
0300             }
0301         }
0302     }
0303 
0304     const auto services = KApplicationTrader::query([desktopEntries](const KService::Ptr &app) {
0305         if (app->noDisplay()) {
0306             return false;
0307         }
0308 
0309         if (desktopEntries.contains(app->desktopEntryName())) {
0310             return false;
0311         }
0312 
0313         if (!app->property(QStringLiteral("X-GNOME-UsesNotifications")).toBool()) {
0314             return false;
0315         }
0316 
0317         return true;
0318     });
0319 
0320     for (const auto &service : services) {
0321         SourceData source{
0322             service->name(),
0323             service->comment(),
0324             service->icon(),
0325             true,
0326             QString(), // notifyRcFile
0327             service->desktopEntryName(),
0328             {} // events
0329         };
0330         appsData.append(source);
0331         desktopEntries.append(service->desktopEntryName());
0332     }
0333 
0334     KSharedConfig::Ptr plasmanotifyrc = KSharedConfig::openConfig(QStringLiteral("plasmanotifyrc"));
0335     KConfigGroup applicationsGroup = plasmanotifyrc->group("Applications");
0336     const QStringList seenApps = applicationsGroup.groupList();
0337     for (const QString &app : seenApps) {
0338         if (desktopEntries.contains(app)) {
0339             continue;
0340         }
0341 
0342         KService::Ptr service = KService::serviceByDesktopName(app);
0343         if (!service || service->noDisplay()) {
0344             continue;
0345         }
0346 
0347         SourceData source{service->name(),
0348                           service->comment(),
0349                           service->icon(),
0350                           true,
0351                           QString(), // notifyRcFile
0352                           service->desktopEntryName(),
0353                           {}};
0354         appsData.append(source);
0355         desktopEntries.append(service->desktopEntryName());
0356     }
0357 
0358     std::sort(appsData.begin(), appsData.end(), [&collator](const SourceData &a, const SourceData &b) {
0359         return collator.compare(a.display(), b.display()) < 0;
0360     });
0361 
0362     // Fake entry for configuring non-identifyable applications
0363     appsData << SourceData{i18n("Other Applications"), {}, QStringLiteral("applications-other"), true, QString(), QStringLiteral("@other"), {}};
0364 
0365     // Sort and make sure plasma_workspace is at the beginning of the list
0366     std::sort(servicesData.begin(), servicesData.end(), [&collator](const SourceData &a, const SourceData &b) {
0367         if (a.notifyRcName == s_plasmaWorkspaceNotifyRcName) {
0368             return true;
0369         }
0370         if (b.notifyRcName == s_plasmaWorkspaceNotifyRcName) {
0371             return false;
0372         }
0373         return collator.compare(a.display(), b.display()) < 0;
0374     });
0375 
0376     m_data << appsData << servicesData;
0377 
0378     endResetModel();
0379 }