File indexing completed on 2024-05-19 05:38:22

0001 /*
0002     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "kcm.h"
0008 
0009 #include <QAction>
0010 #include <QCommandLineParser>
0011 #include <QDialog>
0012 #include <QDialogButtonBox>
0013 #include <QFile>
0014 #include <QFileInfo>
0015 #include <QPushButton>
0016 #include <QQuickItem>
0017 #include <QQuickRenderControl>
0018 #include <QQuickWindow>
0019 #include <QStandardPaths>
0020 #include <QVBoxLayout>
0021 #include <QWindow>
0022 
0023 #include <KConfigGroup>
0024 #include <KGlobalAccel>
0025 #include <KLocalizedString>
0026 #include <KPluginFactory>
0027 
0028 #include <algorithm>
0029 
0030 #include "kcm_notifications_debug.h"
0031 #include "notificationsdata.h"
0032 #include "soundthemeconfig.h"
0033 
0034 #include <libnotificationmanager/badgesettings.h>
0035 #include <libnotificationmanager/behaviorsettings.h>
0036 #include <libnotificationmanager/donotdisturbsettings.h>
0037 #include <libnotificationmanager/jobsettings.h>
0038 #include <libnotificationmanager/notificationsettings.h>
0039 
0040 #include <canberra.h>
0041 
0042 K_PLUGIN_FACTORY_WITH_JSON(KCMNotificationsFactory, "kcm_notifications.json", registerPlugin<KCMNotifications>(); registerPlugin<NotificationsData>();)
0043 
0044 KCMNotifications::KCMNotifications(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
0045     : KQuickManagedConfigModule(parent, data)
0046     , m_sourcesModel(new SourcesModel(this))
0047     , m_filteredModel(new FilterProxyModel(this))
0048     , m_eventsModel(new EventsProxyModel(this))
0049     , m_data(new NotificationsData(this))
0050     , m_toggleDoNotDisturbAction(new QAction(this))
0051     , m_soundThemeConfig(new SoundThemeConfig(this))
0052 {
0053     const char uri[] = "org.kde.private.kcms.notifications";
0054     qmlRegisterUncreatableType<SourcesModel>(uri, 1, 0, "SourcesModel", QStringLiteral("Cannot create instances of SourcesModel"));
0055 
0056     qmlRegisterAnonymousType<FilterProxyModel>("FilterProxyModel", 1);
0057     qmlRegisterAnonymousType<EventsProxyModel>("EventsProxyModel", 1);
0058     qmlRegisterAnonymousType<QKeySequence>("QKeySequence", 1);
0059     qmlRegisterAnonymousType<NotificationManager::DoNotDisturbSettings>("DoNotDisturbSettings", 1);
0060     qmlRegisterAnonymousType<NotificationManager::NotificationSettings>("NotificationSettings", 1);
0061     qmlRegisterAnonymousType<NotificationManager::JobSettings>("JobSettings", 1);
0062     qmlRegisterAnonymousType<NotificationManager::BadgeSettings>("BadgeSettings", 1);
0063     qmlRegisterAnonymousType<NotificationManager::BehaviorSettings>("BehaviorSettings", 1);
0064     qmlProtectModule(uri, 1);
0065 
0066     m_filteredModel->setSourceModel(m_sourcesModel);
0067     m_eventsModel->setSourceModel(m_sourcesModel);
0068 
0069     // for KGlobalAccel...
0070     // keep in sync with globalshortcuts.cpp in notification plasmoid!
0071     m_toggleDoNotDisturbAction->setObjectName(QStringLiteral("toggle do not disturb"));
0072     m_toggleDoNotDisturbAction->setProperty("componentName", QStringLiteral("plasmashell"));
0073     m_toggleDoNotDisturbAction->setText(i18n("Toggle do not disturb"));
0074     m_toggleDoNotDisturbAction->setIcon(QIcon::fromTheme(QStringLiteral("notifications-disabled")));
0075 
0076     QStringList stringArgs;
0077     stringArgs.reserve(args.count() + 1);
0078     // need to add a fake argv[0] for QCommandLineParser
0079     stringArgs.append(QStringLiteral("kcm_notifications"));
0080     for (const QVariant &arg : args) {
0081         stringArgs.append(arg.toString());
0082     }
0083 
0084     QCommandLineParser parser;
0085 
0086     QCommandLineOption desktopEntryOption(QStringLiteral("desktop-entry"), QString(), QStringLiteral("desktop-entry"));
0087     parser.addOption(desktopEntryOption);
0088     QCommandLineOption notifyRcNameOption(QStringLiteral("notifyrc"), QString(), QStringLiteral("notifyrcname"));
0089     parser.addOption(notifyRcNameOption);
0090     QCommandLineOption eventIdOption(QStringLiteral("event-id"), QString(), QStringLiteral("event-id"));
0091     parser.addOption(eventIdOption);
0092 
0093     parser.parse(stringArgs);
0094 
0095     setInitialDesktopEntry(parser.value(desktopEntryOption));
0096     setInitialNotifyRcName(parser.value(notifyRcNameOption));
0097     setInitialEventId(parser.value(eventIdOption));
0098 
0099     connect(this, &KCMNotifications::toggleDoNotDisturbShortcutChanged, this, &KCMNotifications::settingsChanged);
0100     connect(m_sourcesModel, &QAbstractItemModel::dataChanged, this, &KCMNotifications::settingsChanged);
0101     connect(this, &KCMNotifications::defaultsIndicatorsVisibleChanged, this, &KCMNotifications::onDefaultsIndicatorsVisibleChanged);
0102 }
0103 
0104 KCMNotifications::~KCMNotifications()
0105 {
0106 }
0107 
0108 SourcesModel *KCMNotifications::sourcesModel() const
0109 {
0110     return m_sourcesModel;
0111 }
0112 
0113 FilterProxyModel *KCMNotifications::filteredModel() const
0114 {
0115     return m_filteredModel;
0116 }
0117 
0118 EventsProxyModel *KCMNotifications::eventsModel() const
0119 {
0120     return m_eventsModel;
0121 }
0122 
0123 NotificationManager::DoNotDisturbSettings *KCMNotifications::dndSettings() const
0124 {
0125     return m_data->dndSettings();
0126 }
0127 
0128 NotificationManager::NotificationSettings *KCMNotifications::notificationSettings() const
0129 {
0130     return m_data->notificationSettings();
0131 }
0132 
0133 NotificationManager::JobSettings *KCMNotifications::jobSettings() const
0134 {
0135     return m_data->jobSettings();
0136 }
0137 
0138 NotificationManager::BadgeSettings *KCMNotifications::badgeSettings() const
0139 {
0140     return m_data->badgeSettings();
0141 }
0142 
0143 QKeySequence KCMNotifications::toggleDoNotDisturbShortcut() const
0144 {
0145     return m_toggleDoNotDisturbShortcut;
0146 }
0147 
0148 void KCMNotifications::setToggleDoNotDisturbShortcut(const QKeySequence &shortcut)
0149 {
0150     if (m_toggleDoNotDisturbShortcut == shortcut) {
0151         return;
0152     }
0153 
0154     m_toggleDoNotDisturbShortcut = shortcut;
0155     m_toggleDoNotDisturbShortcutDirty = true;
0156     Q_EMIT toggleDoNotDisturbShortcutChanged();
0157 }
0158 
0159 QString KCMNotifications::initialDesktopEntry() const
0160 {
0161     return m_initialDesktopEntry;
0162 }
0163 
0164 void KCMNotifications::setInitialDesktopEntry(const QString &desktopEntry)
0165 {
0166     if (m_initialDesktopEntry != desktopEntry) {
0167         m_initialDesktopEntry = desktopEntry;
0168         Q_EMIT initialDesktopEntryChanged();
0169     }
0170 }
0171 
0172 QString KCMNotifications::initialNotifyRcName() const
0173 {
0174     return m_initialNotifyRcName;
0175 }
0176 
0177 void KCMNotifications::setInitialNotifyRcName(const QString &notifyRcName)
0178 {
0179     if (m_initialNotifyRcName != notifyRcName) {
0180         m_initialNotifyRcName = notifyRcName;
0181         Q_EMIT initialNotifyRcNameChanged();
0182     }
0183 }
0184 
0185 QString KCMNotifications::initialEventId() const
0186 {
0187     return m_initialEventId;
0188 }
0189 
0190 void KCMNotifications::setInitialEventId(const QString &eventId)
0191 {
0192     if (m_initialEventId != eventId) {
0193         m_initialEventId = eventId;
0194         Q_EMIT initialEventIdChanged();
0195     }
0196 }
0197 
0198 NotificationManager::BehaviorSettings *KCMNotifications::behaviorSettings(const QModelIndex &index)
0199 {
0200     if (!index.isValid()) {
0201         return nullptr;
0202     }
0203     return m_data->behaviorSettings(index.row());
0204 }
0205 
0206 bool KCMNotifications::isDefaultsBehaviorSettings() const
0207 {
0208     return m_data->isDefaultsBehaviorSettings();
0209 }
0210 
0211 void KCMNotifications::load()
0212 {
0213     KQuickManagedConfigModule::load();
0214 
0215     bool firstLoad = m_firstLoad;
0216     if (m_firstLoad) {
0217         m_firstLoad = false;
0218         m_sourcesModel->load();
0219 
0220         for (int i = 0; i < m_sourcesModel->rowCount(); ++i) {
0221             const QModelIndex index = m_sourcesModel->index(i, 0);
0222             if (!index.isValid()) {
0223                 continue;
0224             }
0225 
0226             QString typeName;
0227             QString groupName;
0228             if (m_sourcesModel->data(index, SourcesModel::SourceTypeRole) == SourcesModel::ApplicationType) {
0229                 typeName = QStringLiteral("Applications");
0230                 groupName = m_sourcesModel->data(index, SourcesModel::DesktopEntryRole).toString();
0231             } else {
0232                 typeName = QStringLiteral("Services");
0233                 groupName = m_sourcesModel->data(index, SourcesModel::NotifyRcNameRole).toString();
0234             }
0235             auto *toAdd = new NotificationManager::BehaviorSettings(typeName, groupName, this);
0236             m_data->insertBehaviorSettings(index.row(), toAdd);
0237             createConnections(toAdd, index);
0238         }
0239     }
0240 
0241     m_sourcesModel->loadEvents();
0242     m_data->loadBehaviorSettings();
0243 
0244     const QKeySequence toggleDoNotDisturbShortcut =
0245         KGlobalAccel::self()
0246             ->globalShortcut(m_toggleDoNotDisturbAction->property("componentName").toString(), m_toggleDoNotDisturbAction->objectName())
0247             .value(0);
0248 
0249     if (m_toggleDoNotDisturbShortcut != toggleDoNotDisturbShortcut) {
0250         m_toggleDoNotDisturbShortcut = toggleDoNotDisturbShortcut;
0251         Q_EMIT toggleDoNotDisturbShortcutChanged();
0252     }
0253 
0254     m_toggleDoNotDisturbShortcutDirty = false;
0255     if (firstLoad) {
0256         Q_EMIT firstLoadDone();
0257     }
0258 }
0259 
0260 void KCMNotifications::save()
0261 {
0262     KQuickManagedConfigModule::save();
0263     m_data->saveBehaviorSettings();
0264     m_sourcesModel->saveEvents();
0265 
0266     if (m_toggleDoNotDisturbShortcutDirty) {
0267         // KeySequenceItem will already have checked whether the shortcut is available
0268         KGlobalAccel::self()->setShortcut(m_toggleDoNotDisturbAction, {m_toggleDoNotDisturbShortcut}, KGlobalAccel::NoAutoloading);
0269     }
0270 }
0271 
0272 void KCMNotifications::defaults()
0273 {
0274     KQuickManagedConfigModule::defaults();
0275     m_data->defaultsBehaviorSettings();
0276     m_sourcesModel->setEventDefaults();
0277 
0278     setToggleDoNotDisturbShortcut(QKeySequence());
0279 }
0280 
0281 void KCMNotifications::onDefaultsIndicatorsVisibleChanged()
0282 {
0283     for (int i = 0; i < m_sourcesModel->rowCount(); ++i) {
0284         const QModelIndex index = m_sourcesModel->index(i, 0);
0285         updateModelIsDefaultStatus(index);
0286     }
0287 }
0288 
0289 void KCMNotifications::updateModelIsDefaultStatus(const QModelIndex &index)
0290 {
0291     if (index.isValid()) {
0292         m_sourcesModel->setData(index, behaviorSettings(index)->isDefaults(), SourcesModel::IsDefaultRole);
0293         Q_EMIT isDefaultsBehaviorSettingsChanged();
0294     }
0295 }
0296 
0297 bool KCMNotifications::isSaveNeeded() const
0298 {
0299     return m_toggleDoNotDisturbShortcutDirty || m_data->isSaveNeededBehaviorSettings() || m_sourcesModel->isEventSaveNeeded();
0300 }
0301 
0302 bool KCMNotifications::isDefaults() const
0303 {
0304     return m_data->isDefaultsBehaviorSettings();
0305 }
0306 
0307 void KCMNotifications::createConnections(NotificationManager::BehaviorSettings *settings, const QModelIndex &index)
0308 {
0309     connect(settings, &NotificationManager::BehaviorSettings::ShowPopupsChanged, this, &KCMNotifications::settingsChanged);
0310     connect(settings, &NotificationManager::BehaviorSettings::ShowPopupsInDndModeChanged, this, &KCMNotifications::settingsChanged);
0311     connect(settings, &NotificationManager::BehaviorSettings::ShowInHistoryChanged, this, &KCMNotifications::settingsChanged);
0312     connect(settings, &NotificationManager::BehaviorSettings::ShowBadgesChanged, this, &KCMNotifications::settingsChanged);
0313 
0314     connect(settings, &NotificationManager::BehaviorSettings::ShowPopupsChanged, this, [this, index] {
0315         updateModelIsDefaultStatus(index);
0316     });
0317     connect(settings, &NotificationManager::BehaviorSettings::ShowPopupsInDndModeChanged, this, [this, index] {
0318         updateModelIsDefaultStatus(index);
0319     });
0320     connect(settings, &NotificationManager::BehaviorSettings::ShowInHistoryChanged, this, [this, index] {
0321         updateModelIsDefaultStatus(index);
0322     });
0323     connect(settings, &NotificationManager::BehaviorSettings::ShowBadgesChanged, this, [this, index] {
0324         updateModelIsDefaultStatus(index);
0325     });
0326 }
0327 
0328 QUrl KCMNotifications::soundsLocation()
0329 {
0330     const QString soundsPath = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("sounds"), QStandardPaths::LocateDirectory).last();
0331     return QUrl::fromLocalFile(soundsPath);
0332 }
0333 
0334 void KCMNotifications::playSound(const QString &soundName)
0335 {
0336     // Legacy implementation. Fallback lookup for a full path within the `$XDG_DATA_LOCATION/sounds` dirs
0337     QUrl fallbackUrl;
0338     const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0339     for (const QString &dataLocation : dataLocations) {
0340         fallbackUrl = QUrl::fromUserInput(soundName, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile);
0341         if (fallbackUrl.isLocalFile() && QFileInfo::exists(fallbackUrl.toLocalFile())) {
0342             break;
0343         } else if (!fallbackUrl.isLocalFile() && fallbackUrl.isValid()) {
0344             break;
0345         }
0346         fallbackUrl.clear();
0347     }
0348 
0349     if (!m_canberraContext) {
0350         int ret = ca_context_create(&m_canberraContext);
0351         if (ret != CA_SUCCESS) {
0352             qCWarning(KCM_NOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret);
0353             m_canberraContext = nullptr;
0354             return;
0355         }
0356 
0357         // clang-format off
0358         ret = ca_context_change_props(m_canberraContext,
0359                                       CA_PROP_APPLICATION_NAME, qUtf8Printable(metaData().name()),
0360                                       CA_PROP_APPLICATION_ID, qUtf8Printable(metaData().pluginId()),
0361                                       CA_PROP_APPLICATION_ICON_NAME, qUtf8Printable(metaData().iconName()),
0362                                       nullptr);
0363         // clang-format on
0364         if (ret != CA_SUCCESS) {
0365             qCWarning(KCM_NOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret);
0366         }
0367     }
0368 
0369     ca_proplist *props = nullptr;
0370     ca_proplist_create(&props);
0371 
0372     ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
0373     ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME, m_soundThemeConfig->soundTheme().toLatin1().constData());
0374     if (!fallbackUrl.isEmpty()) {
0375         ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(fallbackUrl.toLocalFile()).constData());
0376     }
0377 
0378     // We'll also want this cached for a time. volatile makes sure the cache is
0379     // dropped after some time or when the cache is under pressure.
0380     ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
0381 
0382     int ret = ca_context_play_full(m_canberraContext, 0, props, nullptr, nullptr);
0383 
0384     ca_proplist_destroy(props);
0385 
0386     if (ret != CA_SUCCESS) {
0387         qCWarning(KCM_NOTIFICATIONS) << "Failed to play sound" << soundName << "with canberra:" << ca_strerror(ret);
0388         return;
0389     }
0390 }
0391 
0392 #include "kcm.moc"