File indexing completed on 2024-05-19 05:38:23
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 SPDX-FileCopyrightText: 2023 Ismael Asensio <isma.af@gmail.com> 0007 0008 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0009 */ 0010 0011 #include "sourcesmodel.h" 0012 0013 #include <QCollator> 0014 #include <QDir> 0015 #include <QRegularExpression> 0016 #include <QStandardPaths> 0017 #include <QStringList> 0018 0019 #include <KApplicationTrader> 0020 #include <KConfig> 0021 #include <KConfigGroup> 0022 #include <KLocalizedString> 0023 #include <KService> 0024 #include <KSharedConfig> 0025 0026 #include <algorithm> 0027 0028 using namespace Qt::StringLiterals; 0029 0030 static const QString s_plasmaWorkspaceNotifyRcName = QStringLiteral("plasma_workspace"); 0031 static const QRegularExpression s_eventGroupRegExp(QStringLiteral("^Event/([^/]*)$")); 0032 0033 SourcesModel::SourcesModel(QObject *parent) 0034 : QAbstractItemModel(parent) 0035 { 0036 } 0037 0038 SourcesModel::~SourcesModel() = default; 0039 0040 QPersistentModelIndex SourcesModel::makePersistentModelIndex(const QModelIndex &idx) const 0041 { 0042 return QPersistentModelIndex(idx); 0043 } 0044 0045 QPersistentModelIndex SourcesModel::persistentIndexForDesktopEntry(const QString &desktopEntry) const 0046 { 0047 if (desktopEntry.isEmpty()) { 0048 return QPersistentModelIndex(); 0049 } 0050 const auto matches = match(index(0, 0), SourcesModel::DesktopEntryRole, desktopEntry, 1, Qt::MatchFixedString); 0051 if (matches.isEmpty()) { 0052 return QPersistentModelIndex(); 0053 } 0054 return QPersistentModelIndex(matches.first()); 0055 } 0056 0057 QPersistentModelIndex SourcesModel::persistentIndexForNotifyRcName(const QString ¬ifyRcName) const 0058 { 0059 if (notifyRcName.isEmpty()) { 0060 return QPersistentModelIndex(); 0061 } 0062 const auto matches = match(index(0, 0), SourcesModel::NotifyRcNameRole, notifyRcName, 1, Qt::MatchFixedString); 0063 if (matches.isEmpty()) { 0064 return QPersistentModelIndex(); 0065 } 0066 return QPersistentModelIndex(matches.first()); 0067 } 0068 0069 int SourcesModel::columnCount(const QModelIndex &parent) const 0070 { 0071 Q_UNUSED(parent); 0072 return 1; 0073 } 0074 0075 int SourcesModel::rowCount(const QModelIndex &parent) const 0076 { 0077 if (parent.column() > 0) { 0078 return 0; 0079 } 0080 0081 if (!parent.isValid()) { 0082 return m_data.count(); 0083 } 0084 0085 if (parent.internalId()) { 0086 return 0; 0087 } 0088 0089 return m_data.at(parent.row()).events.count(); 0090 } 0091 0092 QVariant SourcesModel::data(const QModelIndex &index, int role) const 0093 { 0094 if (!index.isValid()) { 0095 return QVariant(); 0096 } 0097 0098 if (index.internalId()) { // event 0099 const auto &events = m_data.at(index.internalId() - 1).events; 0100 const auto event = events.at(index.row()); 0101 0102 switch (role) { 0103 case Qt::DisplayRole: 0104 return event->name(); 0105 case Qt::DecorationRole: 0106 return event->iconName(); 0107 case CommentRole: 0108 return event->comment(); 0109 case ActionsRole: 0110 return event->action().split(QLatin1Char('|'), Qt::SkipEmptyParts); 0111 case SoundRole: 0112 return event->sound(); 0113 case DefaultActionsRole: { 0114 // Weird KConfigSkeleton API to get the cascaded default values 0115 event->useDefaults(true); 0116 const QStringList defaultActions = event->action().split(QLatin1Char('|'), Qt::SkipEmptyParts); 0117 event->useDefaults(false); 0118 return defaultActions; 0119 } 0120 case DefaultSoundRole: { 0121 // Weird KConfigSkeleton API to get the cascaded default values 0122 event->useDefaults(true); 0123 const QString defaultSound = event->sound(); 0124 event->useDefaults(false); 0125 return defaultSound; 0126 } 0127 case IsDefaultRole: 0128 return event->isDefaults(); 0129 case ShowIconsRole: 0130 // We show the icons when at least one of the events specifies an icon name 0131 return std::any_of(events.cbegin(), events.cend(), [](auto *event) { 0132 return !event->iconName().isEmpty(); 0133 }); 0134 } 0135 0136 return QVariant(); 0137 } 0138 0139 const auto &source = m_data.at(index.row()); 0140 0141 switch (role) { 0142 case Qt::DisplayRole: 0143 return source.display(); 0144 case Qt::DecorationRole: 0145 return source.iconName; 0146 case SourceTypeRole: 0147 return source.desktopEntry.isEmpty() ? ServiceType : ApplicationType; 0148 case NotifyRcNameRole: 0149 return source.notifyRcName; 0150 case DesktopEntryRole: 0151 return source.desktopEntry; 0152 case IsDefaultRole: 0153 return source.isDefault && std::all_of(source.events.cbegin(), source.events.cend(), [](auto event) { 0154 return event->isDefaults(); 0155 }); 0156 } 0157 0158 return QVariant(); 0159 } 0160 0161 bool SourcesModel::setData(const QModelIndex &index, const QVariant &value, int role) 0162 { 0163 if (!index.isValid()) { 0164 return false; 0165 } 0166 0167 if (!index.internalId()) { // source 0168 auto &source = m_data[index.row()]; 0169 0170 switch (role) { 0171 case IsDefaultRole: { 0172 if (source.isDefault != value.toBool()) { 0173 source.isDefault = value.toBool(); 0174 Q_EMIT dataChanged(index, index, {role}); 0175 return true; 0176 } 0177 break; 0178 } 0179 } 0180 return false; 0181 } 0182 0183 NotificationManager::EventSettings *event = m_data[index.internalId() - 1].events[index.row()]; 0184 0185 const bool wasDefault = event->isDefaults(); 0186 QList<int> changedRoles; 0187 0188 switch (role) { 0189 case ActionsRole: { 0190 const QString newAction = value.toStringList().join(QLatin1Char('|')); 0191 if (event->action() != newAction) { 0192 event->setAction(newAction); 0193 changedRoles << role; 0194 } 0195 break; 0196 } 0197 case SoundRole: { 0198 const QString newSound = value.toString(); 0199 if (event->sound() != newSound) { 0200 event->setSound(newSound); 0201 changedRoles << role; 0202 } 0203 break; 0204 } 0205 } 0206 0207 if (event->isDefaults() != wasDefault) { 0208 changedRoles << IsDefaultRole; 0209 } 0210 0211 if (changedRoles.isEmpty()) { 0212 return false; 0213 } 0214 0215 Q_EMIT dataChanged(index, index, changedRoles); 0216 // Also notify the possible defaults change in the parent source index 0217 if (changedRoles.contains(IsDefaultRole)) { 0218 const QModelIndex sourceIndex = this->index(index.internalId() - 1, 0, QModelIndex()); 0219 Q_EMIT dataChanged(sourceIndex, sourceIndex, {IsDefaultRole}); 0220 } 0221 return true; 0222 } 0223 0224 QModelIndex SourcesModel::index(int row, int column, const QModelIndex &parent) const 0225 { 0226 if (row < 0 || column != 0) { 0227 return QModelIndex(); 0228 } 0229 0230 if (parent.isValid()) { 0231 const auto events = m_data.at(parent.row()).events; 0232 if (row < events.count()) { 0233 return createIndex(row, column, parent.row() + 1); 0234 } 0235 0236 return QModelIndex(); 0237 } 0238 0239 if (row < m_data.count()) { 0240 return createIndex(row, column, nullptr); 0241 } 0242 0243 return QModelIndex(); 0244 } 0245 0246 QModelIndex SourcesModel::parent(const QModelIndex &child) const 0247 { 0248 if (child.internalId()) { 0249 return createIndex(child.internalId() - 1, 0, nullptr); 0250 } 0251 0252 return QModelIndex(); 0253 } 0254 0255 QHash<int, QByteArray> SourcesModel::roleNames() const 0256 { 0257 return { 0258 {Qt::DisplayRole, QByteArrayLiteral("display")}, 0259 {Qt::DecorationRole, QByteArrayLiteral("decoration")}, 0260 {SourceTypeRole, QByteArrayLiteral("sourceType")}, 0261 {NotifyRcNameRole, QByteArrayLiteral("notifyRcName")}, 0262 {DesktopEntryRole, QByteArrayLiteral("desktopEntry")}, 0263 {IsDefaultRole, QByteArrayLiteral("isDefault")}, 0264 {CommentRole, QByteArrayLiteral("comment")}, 0265 {ShowIconsRole, QByteArrayLiteral("showIcons")}, 0266 {ActionsRole, QByteArrayLiteral("actions")}, 0267 {SoundRole, QByteArrayLiteral("sound")}, 0268 {DefaultActionsRole, QByteArrayLiteral("defaultActions")}, 0269 {DefaultSoundRole, QByteArrayLiteral("defaultSound")}, 0270 }; 0271 } 0272 0273 void SourcesModel::load() 0274 { 0275 beginResetModel(); 0276 0277 m_data.clear(); 0278 0279 QCollator collator; 0280 0281 QList<SourceData> appsData; 0282 QList<SourceData> servicesData; 0283 0284 QStringList notifyRcFiles; 0285 QStringList desktopEntries; 0286 0287 // Search for notifyrc files in `/knotifications6` folders first, but also in `/knotifications5` for compatibility with KF5 applications 0288 const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications6"), QStandardPaths::LocateDirectory) 0289 + QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knotifications5"), QStandardPaths::LocateDirectory); 0290 for (const QString &dir : dirs) { 0291 const QDir dirInfo(dir); 0292 const QStringList fileNames = dirInfo.entryList(QStringList() << QStringLiteral("*.notifyrc")); 0293 for (const QString &file : fileNames) { 0294 if (notifyRcFiles.contains(file)) { 0295 continue; 0296 } 0297 notifyRcFiles.append(file); 0298 0299 KSharedConfig::Ptr config = KSharedConfig::openConfig(file, KConfig::NoGlobals); 0300 config->addConfigSources(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("%2/%1").arg(file).arg(dirInfo.dirName()))); 0301 0302 KConfigGroup globalGroup(config, QLatin1String("Global")); 0303 0304 const QString notifyRcName = file.section(QLatin1Char('.'), 0, -2); 0305 const QString desktopEntry = globalGroup.readEntry(QStringLiteral("DesktopEntry")); 0306 if (!desktopEntry.isEmpty()) { 0307 if (desktopEntries.contains(desktopEntry)) { 0308 continue; 0309 } 0310 desktopEntries.append(desktopEntry); 0311 } 0312 0313 SourceData source{ 0314 // The old KCM read the Name and Comment from global settings disregarding 0315 // any user settings and just used user-specific files for actions config 0316 // I'm pretty sure there's a readEntry equivalent that does that without 0317 // reading the config stuff twice, assuming we care about this to begin with 0318 .name = globalGroup.readEntry(QStringLiteral("Name")), 0319 .comment = globalGroup.readEntry(QStringLiteral("Comment")), 0320 .iconName = globalGroup.readEntry(QStringLiteral("IconName")), 0321 .isDefault = true, 0322 .notifyRcName = notifyRcName, 0323 .desktopEntry = desktopEntry, 0324 .events = {}, 0325 }; 0326 0327 // Add events 0328 const QStringList groups = config->groupList().filter(s_eventGroupRegExp); 0329 0330 QList<NotificationManager::EventSettings *> events; 0331 events.reserve(groups.size()); 0332 for (const QString &group : groups) { 0333 const QString eventId = s_eventGroupRegExp.match(group).captured(1); 0334 events.append(new NotificationManager::EventSettings(config, eventId, this)); 0335 } 0336 std::sort(events.begin(), events.end(), [&collator](NotificationManager::EventSettings *a, NotificationManager::EventSettings *b) { 0337 return collator.compare(a->name(), b->name()) < 0; 0338 }); 0339 source.events = events; 0340 0341 if (!source.desktopEntry.isEmpty()) { 0342 appsData.append(source); 0343 } else { 0344 servicesData.append(source); 0345 } 0346 } 0347 } 0348 0349 const auto services = KApplicationTrader::query([desktopEntries](const KService::Ptr &app) { 0350 if (app->noDisplay()) { 0351 return false; 0352 } 0353 0354 if (desktopEntries.contains(app->desktopEntryName())) { 0355 return false; 0356 } 0357 0358 if (!app->property<bool>(QStringLiteral("X-GNOME-UsesNotifications"))) { 0359 return false; 0360 } 0361 0362 return true; 0363 }); 0364 0365 for (const auto &service : services) { 0366 appsData.append(SourceData::fromService(service)); 0367 desktopEntries.append(service->desktopEntryName()); 0368 } 0369 0370 KSharedConfig::Ptr plasmanotifyrc = KSharedConfig::openConfig(u"plasmanotifyrc"_s); 0371 KConfigGroup applicationsGroup = plasmanotifyrc->group(u"Applications"_s); 0372 const QStringList seenApps = applicationsGroup.groupList(); 0373 for (const QString &app : seenApps) { 0374 if (desktopEntries.contains(app)) { 0375 continue; 0376 } 0377 0378 KService::Ptr service = KService::serviceByDesktopName(app); 0379 if (!service || service->noDisplay()) { 0380 continue; 0381 } 0382 0383 appsData.append(SourceData::fromService(service)); 0384 desktopEntries.append(service->desktopEntryName()); 0385 } 0386 0387 std::sort(appsData.begin(), appsData.end(), [&collator](const SourceData &a, const SourceData &b) { 0388 return collator.compare(a.display(), b.display()) < 0; 0389 }); 0390 0391 // Fake entry for configuring non-identifyable applications 0392 appsData << SourceData{ 0393 .name = i18n("Other Applications"), 0394 .comment = {}, 0395 .iconName = QStringLiteral("applications-other"), 0396 .isDefault = true, 0397 .notifyRcName = {}, 0398 .desktopEntry = QStringLiteral("@other"), 0399 .events = {}, 0400 }; 0401 0402 // Sort and make sure plasma_workspace is at the beginning of the list 0403 std::sort(servicesData.begin(), servicesData.end(), [&collator](const SourceData &a, const SourceData &b) { 0404 if (a.notifyRcName == s_plasmaWorkspaceNotifyRcName) { 0405 return true; 0406 } 0407 if (b.notifyRcName == s_plasmaWorkspaceNotifyRcName) { 0408 return false; 0409 } 0410 return collator.compare(a.display(), b.display()) < 0; 0411 }); 0412 0413 m_data << appsData << servicesData; 0414 0415 endResetModel(); 0416 } 0417 0418 void SourcesModel::loadEvents() 0419 { 0420 beginResetModel(); 0421 0422 for (const SourceData &source : std::as_const(m_data)) { 0423 for (auto &event : source.events) { 0424 event->load(); 0425 } 0426 } 0427 0428 endResetModel(); 0429 } 0430 0431 void SourcesModel::saveEvents() 0432 { 0433 for (const SourceData &source : std::as_const(m_data)) { 0434 for (auto &event : source.events) { 0435 event->save(); 0436 } 0437 } 0438 } 0439 0440 bool SourcesModel::isEventDefaults() const 0441 { 0442 for (const SourceData &source : std::as_const(m_data)) { 0443 for (const auto &event : source.events) { 0444 if (!event->isDefaults()) { 0445 return false; 0446 } 0447 } 0448 } 0449 return true; 0450 } 0451 0452 bool SourcesModel::isEventSaveNeeded() const 0453 { 0454 for (const SourceData &source : std::as_const(m_data)) { 0455 for (const auto &event : source.events) { 0456 if (event->isSaveNeeded()) { 0457 return true; 0458 } 0459 } 0460 } 0461 return false; 0462 } 0463 0464 void SourcesModel::setEventDefaults() 0465 { 0466 beginResetModel(); 0467 0468 for (const SourceData &source : std::as_const(m_data)) { 0469 for (auto &event : source.events) { 0470 event->setDefaults(); 0471 } 0472 } 0473 0474 endResetModel(); 0475 } 0476 0477 SourceData SourceData::fromService(KService::Ptr service) 0478 { 0479 return SourceData{ 0480 .name = service->name(), 0481 .comment = service->comment(), 0482 .iconName = service->icon(), 0483 .isDefault = true, 0484 .notifyRcName = {}, 0485 .desktopEntry = service->desktopEntryName(), 0486 .events = {}, 0487 }; 0488 }