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 ¬ifyRcName) 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 }