diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ find_package(LibKWorkspace 5.14.90 CONFIG REQUIRED) find_package(LibTaskManager 5.14.90 CONFIG REQUIRED) +find_package(LibNotificationManager 5.14.90 CONFIG REQUIRED) find_package(LibColorCorrect 5.14.90 CONFIG REQUIRED) find_package(KWinDBusInterface CONFIG REQUIRED) find_package(ScreenSaverDBusInterface CONFIG REQUIRED) diff --git a/kcms/CMakeLists.txt b/kcms/CMakeLists.txt --- a/kcms/CMakeLists.txt +++ b/kcms/CMakeLists.txt @@ -44,9 +44,10 @@ add_subdirectory(kded) add_subdirectory(knotify) add_subdirectory(formats) -add_subdirectory(spellchecking) +add_subdirectory(notifications) add_subdirectory(phonon) add_subdirectory(runners) +add_subdirectory(spellchecking) add_subdirectory(qtquicksettings) add_subdirectory(workspaceoptions) diff --git a/kcms/notifications/CMakeLists.txt b/kcms/notifications/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/kcms/notifications/CMakeLists.txt @@ -0,0 +1,29 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_notifications\") + +set(kcm_notifications_SRCS + kcm.cpp + sourcesmodel.cpp + filterproxymodel.cpp +) + +add_library(kcm_notifications MODULE ${kcm_notifications_SRCS}) +target_link_libraries(kcm_notifications + KF5::KCMUtils + KF5::CoreAddons + KF5::Declarative + KF5::GuiAddons + KF5::I18n + KF5::QuickAddons + KF5::NotifyConfig + KF5::Service + PW::LibNotificationManager +) + +kcoreaddons_desktop_to_json(kcm_notifications "kcm_notifications.desktop") + +install(FILES kcm_notifications.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +install(TARGETS kcm_notifications DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) + +kpackage_install_package(package kcm_notifications kcms) + diff --git a/kcms/notifications/Messages.sh b/kcms/notifications/Messages.sh new file mode 100644 --- /dev/null +++ b/kcms/notifications/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_notifications.pot diff --git a/kcms/notifications/filterproxymodel.h b/kcms/notifications/filterproxymodel.h new file mode 100644 --- /dev/null +++ b/kcms/notifications/filterproxymodel.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + +public: + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + QString query() const; + void setQuery(const QString &query); + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +Q_SIGNALS: + void queryChanged(); + +private: + QString m_query; + +}; diff --git a/kcms/notifications/filterproxymodel.cpp b/kcms/notifications/filterproxymodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/notifications/filterproxymodel.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "filterproxymodel.h" + +#include "sourcesmodel.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + setRecursiveFilteringEnabled(true); +} + +FilterProxyModel::~FilterProxyModel() = default; + +QString FilterProxyModel::query() const +{ + return m_query; +} + +void FilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + m_query = query; + invalidateFilter(); + emit queryChanged(); + } +} + +bool FilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (m_query.isEmpty()) { + return true; + } + + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + const QString display = idx.data(Qt::DisplayRole).toString(); + if (display.contains(m_query, Qt::CaseInsensitive)) { + return true; + } + + return false; +} diff --git a/kcms/notifications/kcm.h b/kcms/notifications/kcm.h new file mode 100644 --- /dev/null +++ b/kcms/notifications/kcm.h @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +class SourcesModel; +class FilterProxyModel; + +namespace NotificationManager { +class Settings; +} + +class KCMNotifications : public KQuickAddons::ConfigModule +{ + Q_OBJECT + + Q_PROPERTY(SourcesModel *sourcesModel READ sourcesModel CONSTANT) + Q_PROPERTY(FilterProxyModel *filteredModel READ filteredModel CONSTANT) + + Q_PROPERTY(NotificationManager::Settings *settings READ settings CONSTANT) + + // So it can show the respective settings module right away + Q_PROPERTY(QString initialDesktopEntry READ initialDesktopEntry WRITE setInitialDesktopEntry NOTIFY initialDesktopEntryChanged) + Q_PROPERTY(QString initialNotifyRcName READ initialNotifyRcName WRITE setInitialNotifyRcName NOTIFY initialNotifyRcNameChanged) + Q_PROPERTY(QString initialEventId READ initialEventId WRITE setInitialEventId NOTIFY initialEventIdChanged) + +public: + KCMNotifications(QObject *parent, const QVariantList &args); + ~KCMNotifications() override; + + SourcesModel *sourcesModel() const; + FilterProxyModel *filteredModel() const; + + NotificationManager::Settings *settings() const; + + QString initialDesktopEntry() const; + void setInitialDesktopEntry(const QString &desktopEntry); + + QString initialNotifyRcName() const; + void setInitialNotifyRcName(const QString ¬ifyRcName); + + QString initialEventId() const; + void setInitialEventId(const QString &eventId); + + Q_INVOKABLE void configureEvents(const QString ¬ifyRcName, const QString &eventId, QQuickItem *ctx = nullptr); + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + +signals: + void initialDesktopEntryChanged(); + void initialNotifyRcNameChanged(); + void initialEventIdChanged(); + +private: + void processPendingDeletions(); + + SourcesModel *m_sourcesModel; + FilterProxyModel *m_filteredModel; + + NotificationManager::Settings *m_settings; + + QString m_initialDesktopEntry; + QString m_initialNotifyRcName; + QString m_initialEventId; + +}; diff --git a/kcms/notifications/kcm.cpp b/kcms/notifications/kcm.cpp new file mode 100644 --- /dev/null +++ b/kcms/notifications/kcm.cpp @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "kcm.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "sourcesmodel.h" +#include "filterproxymodel.h" + +#include + +K_PLUGIN_FACTORY_WITH_JSON(KCMNotificationsFactory, "kcm_notifications.json", registerPlugin();) + +KCMNotifications::KCMNotifications(QObject *parent, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, args) + , m_sourcesModel(new SourcesModel(this)) + , m_filteredModel(new FilterProxyModel(this)) + , m_settings(new NotificationManager::Settings(this)) +{ + + const char uri[] = "org.kde.private.kcms.notifications"; + qmlRegisterUncreatableType(uri, 1, 0, "SourcesModel", + QStringLiteral("Cannot create instances of SourcesModel")); + qmlRegisterType(); + qmlProtectModule(uri, 1); + + KAboutData *about = new KAboutData(QStringLiteral("kcm_notifications"), i18n("Notifications"), + QStringLiteral("5.0"), QString(), KAboutLicense::GPL); + about->addAuthor(i18n("Kai Uwe Broulik"), QString(), QStringLiteral("kde@privat.broulik.de")); + setAboutData(about); + + m_filteredModel->setSourceModel(m_sourcesModel); + + QStringList stringArgs; + stringArgs.reserve(args.count() + 1); + // need to add a fake argv[0] for QCommandLineParser + stringArgs.append(QStringLiteral("kcm_notifications")); + for (const QVariant &arg : args) { + stringArgs.append(arg.toString()); + } + + QCommandLineParser parser; + + QCommandLineOption desktopEntryOption(QStringLiteral("desktop-entry"), QString(), QStringLiteral("desktop-entry")); + parser.addOption(desktopEntryOption); + QCommandLineOption notifyRcNameOption(QStringLiteral("notifyrc"), QString(), QStringLiteral("notifyrcname")); + parser.addOption(notifyRcNameOption); + QCommandLineOption eventIdOption(QStringLiteral("event-id"), QString(), QStringLiteral("event-id")); + parser.addOption(eventIdOption); + + parser.parse(stringArgs); + + setInitialDesktopEntry(parser.value(desktopEntryOption)); + setInitialNotifyRcName(parser.value(notifyRcNameOption)); + setInitialEventId(parser.value(eventIdOption)); +} + +KCMNotifications::~KCMNotifications() +{ + +} + +SourcesModel *KCMNotifications::sourcesModel() const +{ + return m_sourcesModel; +} + +FilterProxyModel *KCMNotifications::filteredModel() const +{ + return m_filteredModel; +} + +NotificationManager::Settings *KCMNotifications::settings() const +{ + return m_settings; +} + +QString KCMNotifications::initialDesktopEntry() const +{ + return m_initialDesktopEntry; +} + +void KCMNotifications::setInitialDesktopEntry(const QString &desktopEntry) +{ + if (m_initialDesktopEntry != desktopEntry) { + m_initialDesktopEntry = desktopEntry; + emit initialDesktopEntryChanged(); + } +} + +QString KCMNotifications::initialNotifyRcName() const +{ + return m_initialNotifyRcName; +} + +void KCMNotifications::setInitialNotifyRcName(const QString ¬ifyRcName) +{ + if (m_initialNotifyRcName != notifyRcName) { + m_initialNotifyRcName = notifyRcName; + emit initialNotifyRcNameChanged(); + } +} + +QString KCMNotifications::initialEventId() const +{ + return m_initialEventId; +} + +void KCMNotifications::setInitialEventId(const QString &eventId) +{ + if (m_initialEventId != eventId) { + m_initialEventId = eventId; + emit initialEventIdChanged(); + } +} + +void KCMNotifications::configureEvents(const QString ¬ifyRcName, const QString &eventId, QQuickItem *ctx) +{ + // We're not using KNotifyConfigWidget::configure here as we want to handle the + // saving ourself (so we Apply with all other KCM settings) but there's no way + // to access the config object :( + // We also need access to the QDialog so we can set the KCM as transient parent. + + QDialog *dialog = new QDialog(nullptr); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle(i18n("Configure Notifications")); + + if (ctx && ctx->window()) { + dialog->winId(); // so it creates windowHandle + dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(ctx->window())); + dialog->setModal(true); + } + + KNotifyConfigWidget *w = new KNotifyConfigWidget(dialog); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); + buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel); + buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget(w); + layout->addWidget(buttonBox); + dialog->setLayout(layout); + + // TODO we should only save settings when clicking Apply in the main UI + connect(buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, w, &KNotifyConfigWidget::save); + connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, w, &KNotifyConfigWidget::save); + connect(w, &KNotifyConfigWidget::changed, buttonBox->button(QDialogButtonBox::Apply), &QPushButton::setEnabled); + + connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + + w->setApplication(notifyRcName); + w->selectEvent(eventId); + + dialog->show(); +} + +void KCMNotifications::load() +{ + m_settings->load(); +} + +void KCMNotifications::save() +{ + m_settings->save(); +} + +void KCMNotifications::defaults() +{ + m_settings->defaults(); +} + +#include "kcm.moc" diff --git a/kcms/notifications/kcm_notifications.desktop b/kcms/notifications/kcm_notifications.desktop new file mode 100644 --- /dev/null +++ b/kcms/notifications/kcm_notifications.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Exec=kcmshell5 kcm_notifications +Icon=preferences-desktop-notification-bell +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/notifications/index.html + +X-KDE-Library=kcm_notifications +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=personalization +X-KDE-Weight=40 + +Name=Notifications +Comment=Event Notifications and Actions + +X-KDE_Keywords=System sounds,Audio,Sound,Notify,Alerts,Notification,popups,disturb,do not disturb,quiet,concentrate,concentration + +Categories=Qt;KDE;X-KDE-settings-sound; diff --git a/kcms/notifications/package/contents/ui/ApplicationConfiguration.qml b/kcms/notifications/package/contents/ui/ApplicationConfiguration.qml new file mode 100644 --- /dev/null +++ b/kcms/notifications/package/contents/ui/ApplicationConfiguration.qml @@ -0,0 +1,150 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtQml.Models 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls + +import org.kde.kirigami 2.7 as Kirigami +import org.kde.kcm 1.2 as KCM + +import org.kde.notificationmanager 1.0 as NotificationManager + +import org.kde.private.kcms.notifications 1.0 as Private + +ColumnLayout { + id: configColumn + + property var rootIndex + + readonly property string appDisplayName: kcm.sourcesModel.data(rootIndex, Qt.DisplayRole) || "" + readonly property string appIconName: kcm.sourcesModel.data(rootIndex, Qt.DecorationRole) || "" + readonly property string desktopEntry: kcm.sourcesModel.data(rootIndex, Private.SourcesModel.DesktopEntryRole) || "" + readonly property string notifyRcName: kcm.sourcesModel.data(rootIndex, Private.SourcesModel.NotifyRcNameRole) || "" + + property int behavior: { + if (configColumn.desktopEntry) { + return kcm.settings.applicationBehavior(configColumn.desktopEntry); + } else if (configColumn.notifyRcName) { + return kcm.settings.serviceBehavior(configColumn.notifyRcName); + } + return -1; + } + + function setBehavior(flag, enable) { + var newBehavior = behavior; + if (enable) { + newBehavior |= flag; + } else { + newBehavior &= ~flag; + } + + if (configColumn.desktopEntry) { + return kcm.settings.setApplicationBehavior(configColumn.desktopEntry, newBehavior); + } else if (configColumn.notifyRcName) { + return kcm.settings.setServiceBehavior(configColumn.notifyRcName, newBehavior); + } + } + + function configureEvents(eventId) { + kcm.configureEvents(configColumn.notifyRcName, eventId, this) + } + + spacing: Kirigami.Units.smallSpacing + + Kirigami.FormLayout { + id: form + + RowLayout { + Kirigami.FormData.isSection: true + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + source: configColumn.appIconName + } + + Kirigami.Heading { + level: 2 + text: configColumn.appDisplayName + elide: Text.ElideRight + textFormat: Text.PlainText + } + } + + QtControls.CheckBox { + id: showPopupsCheck + text: i18n("Show popups") + checked: configColumn.behavior & NotificationManager.Settings.ShowPopups + onClicked: configColumn.setBehavior(NotificationManager.Settings.ShowPopups, checked) + } + + RowLayout { // just for indentation + QtControls.CheckBox { + Layout.leftMargin: mirrored ? 0 : indicator.width + Layout.rightMargin: mirrored ? indicator.width : 0 + text: i18n("Show in do not disturb mode") + enabled: showPopupsCheck.checked + checked: configColumn.behavior & NotificationManager.Settings.ShowPopupsInDoNotDisturbMode + onClicked: configColumn.setBehavior(NotificationManager.Settings.ShowPopupsInDoNotDisturbMode, checked) + } + } + + QtControls.CheckBox { + text: i18n("Show in history") + checked: configColumn.behavior & NotificationManager.Settings.ShowInHistory + onClicked: configColumn.setBehavior(NotificationManager.Settings.ShowInHistory, checked) + } + + QtControls.CheckBox { + text: i18n("Show notification badges") + enabled: !!configColumn.desktopEntry + checked: configColumn.behavior & NotificationManager.Settings.ShowBadges + onClicked: configColumn.setBehavior(NotificationManager.Settings.ShowBadges, checked) + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + + QtControls.Button { + id: configureEventsButton + text: i18n("Configure Events...") + icon.name: "preferences-desktop-notification" + visible: !!configColumn.notifyRcName + onClicked: configColumn.configureEvents() + } + } + + QtControls.Label { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: form.implicitWidth + text: i18n("This application does not support configuring notifications on a per-event basis."); + wrapMode: Text.WordWrap + visible: !configColumn.notifyRcName + } + + // compact layout + Item { + Layout.fillHeight: true + } +} diff --git a/kcms/notifications/package/contents/ui/PopupPositionPage.qml b/kcms/notifications/package/contents/ui/PopupPositionPage.qml new file mode 100644 --- /dev/null +++ b/kcms/notifications/package/contents/ui/PopupPositionPage.qml @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls +import org.kde.kirigami 2.7 as Kirigami + +Kirigami.Page { + id: positionPage + + title: i18n("Popup Position") + + ScreenPositionSelector { + anchors.horizontalCenter: parent.horizontalCenter + selectedPosition: kcm.settings.popupPosition + onSelectedPositionChanged: kcm.settings.popupPosition = selectedPosition + } +} diff --git a/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml b/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml new file mode 100644 --- /dev/null +++ b/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml @@ -0,0 +1,239 @@ +/* + * Copyright 2015 (C) Martin Klapetek + * Copyright 2019 (C) Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import QtQuick 2.0 +import QtQuick.Window 2.1 +import QtQuick.Controls 2.2 as QtControls +import org.kde.kirigami 2.4 as Kirigami +import org.kde.plasma.core 2.0 as PlasmaCore + +import org.kde.notificationmanager 1.0 as NotificationManager + +Item { + id: monitorPanel + + property int baseUnit: Kirigami.Units.gridUnit + + implicitWidth: baseUnit * 13 + baseUnit * 2 + implicitHeight: (screenRatio * baseUnit * 13) + (baseUnit * 2) + basePart.height + + property int selectedPosition + property var disabledPositions: [] + property real screenRatio: Screen.height / Screen.width + + onSelectedPositionChanged: { + var buttons = positionRadios.buttons.length; + for (var i = 0; i < buttons.length; ++i) { + var button = buttons[i]; + if (button.position === selectedPosition) { + button.checked = true; + break; + } + } + } + + PlasmaCore.Svg { + id: monitorSvg + imagePath: "widgets/monitor" + } + + PlasmaCore.SvgItem { + id: topleftPart + anchors { + left: parent.left + top: parent.top + } + svg: monitorSvg + elementId: "topleft" + width: baseUnit + height: baseUnit + } + + PlasmaCore.SvgItem { + id: topPart + anchors { + top: parent.top + left: topleftPart.right + right: toprightPart.left + } + svg: monitorSvg + elementId: "top" + height: baseUnit + } + + PlasmaCore.SvgItem { + id: toprightPart + anchors { + right: parent.right + top: parent.top + } + svg: monitorSvg + elementId: "topright" + width: baseUnit + height: baseUnit + } + + PlasmaCore.SvgItem { + id: leftPart + anchors { + left: parent.left + top: topleftPart.bottom + bottom: bottomleftPart.top + } + svg: monitorSvg + elementId: "left" + width: baseUnit + } + + PlasmaCore.SvgItem { + id: rightPart + anchors { + right: parent.right + top: toprightPart.bottom + bottom: bottomrightPart.top + } + svg: monitorSvg + elementId: "right" + width: baseUnit + } + + PlasmaCore.SvgItem { + id: bottomleftPart + anchors { + left: parent.left + bottom: basePart.top + } + svg: monitorSvg + elementId: "bottomleft" + width: baseUnit + height: baseUnit + } + + PlasmaCore.SvgItem { + id: bottomPart + anchors { + bottom: basePart.top + left: bottomleftPart.right + right: bottomrightPart.left + } + svg: monitorSvg + elementId: "bottom" + height: baseUnit + } + + PlasmaCore.SvgItem { + id: bottomrightPart + anchors { + right: parent.right + bottom: basePart.top + } + svg: monitorSvg + elementId: "bottomright" + width: baseUnit + height: baseUnit + } + + PlasmaCore.SvgItem { + id: basePart + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + width: 120 + height: 60 + svg: monitorSvg + elementId: "base" + } + + QtControls.ButtonGroup { + id: positionRadios + onCheckedButtonChanged: monitorPanel.selectedPosition = checkedButton.position + } + + // TODO increase hit area for radio buttons + + QtControls.RadioButton { + anchors { + top: topPart.bottom + left: leftPart.right + margins: Kirigami.Units.smallSpacing + } + readonly property int position: NotificationManager.Settings.TopLeft + checked: monitorPanel.selectedPosition == position + visible: monitorPanel.disabledPositions.indexOf(position) == -1 + QtControls.ButtonGroup.group: positionRadios + } + QtControls.RadioButton { + anchors { + top: topPart.bottom + horizontalCenter: topPart.horizontalCenter + margins: Kirigami.Units.smallSpacing + } + readonly property int position: NotificationManager.Settings.TopCenter + checked: monitorPanel.selectedPosition == position + visible: monitorPanel.disabledPositions.indexOf(position) == -1 + QtControls.ButtonGroup.group: positionRadios + } + QtControls.RadioButton { + anchors { + top: topPart.bottom + right: rightPart.left + margins: Kirigami.Units.smallSpacing + } + readonly property int position: NotificationManager.Settings.TopRight + checked: monitorPanel.selectedPosition == position + visible: monitorPanel.disabledPositions.indexOf(position) == -1 + QtControls.ButtonGroup.group: positionRadios + } + QtControls.RadioButton { + anchors { + bottom: bottomPart.top + left: leftPart.right + margins: Kirigami.Units.smallSpacing + } + readonly property int position: NotificationManager.Settings.BottomLeft + checked: monitorPanel.selectedPosition == position + visible: monitorPanel.disabledPositions.indexOf(position) == -1 + QtControls.ButtonGroup.group: positionRadios + } + QtControls.RadioButton { + anchors { + bottom: bottomPart.top + horizontalCenter: bottomPart.horizontalCenter + margins: Kirigami.Units.smallSpacing + } + readonly property int position: NotificationManager.Settings.BottomCenter + checked: monitorPanel.selectedPosition == position + visible: monitorPanel.disabledPositions.indexOf(position) == -1 + QtControls.ButtonGroup.group: positionRadios + } + QtControls.RadioButton { + anchors { + bottom: bottomPart.top + right: rightPart.left + margins: Kirigami.Units.smallSpacing + } + readonly property int position: NotificationManager.Settings.BottomRight + checked: monitorPanel.selectedPosition == position + visible: monitorPanel.disabledPositions.indexOf(position) == -1 + QtControls.ButtonGroup.group: positionRadios + } +} diff --git a/kcms/notifications/package/contents/ui/SourcesPage.qml b/kcms/notifications/package/contents/ui/SourcesPage.qml new file mode 100644 --- /dev/null +++ b/kcms/notifications/package/contents/ui/SourcesPage.qml @@ -0,0 +1,219 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls + +import org.kde.kirigami 2.8 as Kirigami +import org.kde.kcm 1.2 as KCM + +import org.kde.private.kcms.notifications 1.0 as Private + +Kirigami.Page { + id: sourcesPage + title: i18n("Application Settings") + + Component.onCompleted: { + kcm.sourcesModel.load(); + + if (kcm.initialDesktopEntry) { + appConfiguration.rootIndex = kcm.sourcesModel.persistentIndexForDesktopEntry(kcm.initialDesktopEntry); + } else if (kcm.initialNotifyRcName) { + appConfiguration.rootIndex = kcm.sourcesModel.persistentIndexForNotifyRcName(kcm.initialNotifyRcName); + } + + if (kcm.initialEventId && kcm.initialNotifyRcName) { + appConfiguration.configureEvents(kcm.initialEventId); + } + + kcm.initialDesktopEntry = ""; + kcm.initialNotifyRcName = ""; + kcm.initialEventId = ""; + } + + Binding { + target: kcm.filteredModel + property: "query" + value: searchField.text + } + + // We need to manually keep track of the index as we store the sourceModel index + // and then use a proxy model to filter it. We don't get any QML change signals anywhere + // and ListView needs a currentIndex number + Connections { + target: kcm.filteredModel + onRowsRemoved: sourcesList.updateCurrentIndex() + onRowsInserted: sourcesList.updateCurrentIndex() + // TODO re-create model index if possible + onModelReset: appConfiguration.rootIndex = undefined + } + + RowLayout { + id: rootRow + anchors.fill: parent + + ColumnLayout { + Layout.minimumWidth: Kirigami.Units.gridUnit * 12 + Layout.preferredWidth: Math.round(rootRow.width / 3) + + Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + } + + QtControls.ScrollView { + id: sourcesScroll + Layout.fillWidth: true + Layout.fillHeight: true + activeFocusOnTab: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + + Component.onCompleted: background.visible = true + + ListView { + id: sourcesList + clip: true + activeFocusOnTab: true + + keyNavigationEnabled: true + keyNavigationWraps: true + highlightMoveDuration: 0 + + model: kcm.filteredModel + currentIndex: -1 + + section { + criteria: ViewSection.FullString + property: "sourceType" + delegate: QtControls.ItemDelegate { + id: sourceSection + width: sourcesList.width + text: { + switch (Number(section)) { + case Private.SourcesModel.ApplicationType: return i18n("Applications"); + case Private.SourcesModel.ServiceType: return i18n("System Services"); + } + } + + // unset "disabled" text color... + contentItem: QtControls.Label { + text: sourceSection.text + // FIXME why does none of this work :( + //Kirigami.Theme.colorGroup: Kirigami.Theme.Active + //color: Kirigami.Theme.textColor + color: rootRow.Kirigami.Theme.textColor + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + enabled: false + } + } + + // We need to manually keep track of the index when we filter + function updateCurrentIndex() { + if (!appConfiguration.rootIndex || !appConfiguration.rootIndex.valid) { + currentIndex = -1; + return; + } + + var filteredIdx = kcm.filteredModel.mapFromSource(appConfiguration.rootIndex); + if (!filteredIdx.valid) { + currentIndex = -1; + return; + } + + currentIndex = filteredIdx.row; + } + + delegate: QtControls.ItemDelegate { + id: sourceDelegate + width: sourcesList.width + text: model.display + highlighted: ListView.isCurrentItem + onClicked: { + var sourceIdx = kcm.filteredModel.mapToSource(kcm.filteredModel.index(index, 0)); + appConfiguration.rootIndex = kcm.sourcesModel.makePersistentModelIndex(sourceIdx); + sourcesList.updateCurrentIndex(); + } + + contentItem: RowLayout { + Kirigami.Icon { + Layout.preferredWidth: Kirigami.Units.iconSizes.small + Layout.preferredHeight: Kirigami.Units.iconSizes.small + source: model.decoration + } + + QtControls.Label { + Layout.fillWidth: true + text: sourceDelegate.text + font: sourceDelegate.font + color: sourceDelegate.highlighted || sourceDelegate.checked || (sourceDelegate.pressed && !sourceDelegate.checked && !sourceDelegate.sectionDelegate) ? Kirigami.Theme.highlightedTextColor : (sourceDelegate.enabled ? Kirigami.Theme.textColor : Kirigami.Theme.disabledTextColor) + elide: Text.ElideRight + textFormat: Text.PlainText + } + } + } + + QtControls.Label { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + margins: Kirigami.Units.smallSpacing + } + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: i18n("No application or event matches your search term.") + visible: sourcesList.count === 0 && searchField.length > 0 + enabled: false + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: Math.round(rootRow.width / 3 * 2) + + ApplicationConfiguration { + id: appConfiguration + anchors.fill: parent + onRootIndexChanged: sourcesList.updateCurrentIndex() + visible: typeof appConfiguration.rootIndex !== "undefined" && appConfiguration.rootIndex.valid + } + + QtControls.Label { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + margins: Kirigami.Units.smallSpacing + } + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: i18n("Select an application from the list to configure its notification settings and behavior.") + visible: !appConfiguration.rootIndex || !appConfiguration.rootIndex.valid + } + } + } +} diff --git a/kcms/notifications/package/contents/ui/main.qml b/kcms/notifications/package/contents/ui/main.qml new file mode 100644 --- /dev/null +++ b/kcms/notifications/package/contents/ui/main.qml @@ -0,0 +1,184 @@ +/* + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls +import org.kde.kirigami 2.4 as Kirigami +import org.kde.kcm 1.2 as KCM + +import org.kde.notificationmanager 1.0 as NotificationManager + +KCM.SimpleKCM { + id: root + KCM.ConfigModule.quickHelp: i18n("This module lets you manage application and system notifications.") + KCM.ConfigModule.buttons: KCM.ConfigModule.Help | KCM.ConfigModule.Apply + + function openSourcesSettings() { + // TODO would be nice to re-use the current SourcesPage instead of pushing a new one that lost all state + // but there's no pageAt(index) method in KConfigModuleQml + kcm.push("SourcesPage.qml"); + } + + Binding { + target: kcm + property: "needsSave" + value: kcm.settings.dirty // TODO or other stuff + } + + Kirigami.FormLayout { + QtControls.CheckBox { + Kirigami.FormData.label: i18n("Critical notifications:") + text: i18n("Show in do not disturb mode") + checked: kcm.settings.criticalPopupsInDoNotDisturbMode + onClicked: kcm.settings.criticalPopupsInDoNotDisturbMode = checked + } + + QtControls.CheckBox { + text: i18n("Always keep on top") + checked: kcm.settings.keepCriticalAlwaysOnTop + onClicked: kcm.settings.keepCriticalAlwaysOnTop = checked + } + + QtControls.CheckBox { + Kirigami.FormData.label: i18n("Low priority notifications:") + text: i18n("Show popup") + checked: kcm.settings.lowPriorityPopups + onClicked: kcm.settings.lowPriorityPopups = checked + } + + QtControls.CheckBox { + text: i18n("Show in history") + checked: kcm.settings.lowPriorityHistory + onClicked: kcm.settings.lowPriorityHistory = checked + } + + QtControls.ButtonGroup { + id: positionGroup + buttons: [positionCloseToWidget, positionCustomPosition] + } + + QtControls.RadioButton { + id: positionCloseToWidget + Kirigami.FormData.label: i18n("Popup position:") + text: i18nc("Popup position near notification plasmoid", "Near the notification icon") // "widget" + checked: kcm.settings.popupPosition === NotificationManager.Settings.CloseToWidget + onClicked: kcm.settings.popupPosition = NotificationManager.Settings.CloseToWidget + } + + RowLayout { + spacing: 0 + QtControls.RadioButton { + id: positionCustomPosition + checked: kcm.settings.popupPosition !== NotificationManager.Settings.CloseToWidget + activeFocusOnTab: false + + MouseArea { + anchors.fill: parent + onClicked: positionCustomButton.clicked() + } + } + QtControls.Button { + id: positionCustomButton + text: i18n("Choose Custom Position...") + icon.name: "preferences-desktop-display" + onClicked: kcm.push("PopupPositionPage.qml") + } + } + + TextMetrics { + id: timeoutSpinnerMetrics + font: timeoutSpinner.font + text: i18np("%1 second", "%1 seconds", 888) + } + + QtControls.SpinBox { + id: timeoutSpinner + Kirigami.FormData.label: i18n("Hide popup after:") + Layout.preferredWidth: timeoutSpinnerMetrics.width + leftPadding + rightPadding + from: 1000 // 1 second + to: 120000 // 2 minutes + stepSize: 1000 + value: kcm.settings.popupTimeout + editable: true + valueFromText: function(text, locale) { + return parseInt(text) * 1000; + } + textFromValue: function(value, locale) { + return i18np("%1 second", "%1 seconds", Math.round(value / 1000)); + } + onValueModified: kcm.settings.popupTimeout = value + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + + QtControls.CheckBox { + Kirigami.FormData.label: i18n("Application progress:") + text: i18n("Show in task manager") + checked: kcm.settings.jobsInTaskManager + onClicked: kcm.settings.jobsInTaskManager = checked + } + + QtControls.CheckBox { + id: applicationJobsEnabledCheck + text: i18nc("Show application jobs in notification widget", "Show in notifications") + checked: kcm.settings.jobsInNotifications + onClicked: kcm.settings.jobsInNotifications = checked + } + + RowLayout { // just for indentation + QtControls.CheckBox { + Layout.leftMargin: mirrored ? 0 : indicator.width + Layout.rightMargin: mirrored ? indicator.width : 0 + text: i18nc("Keep application job popup open for entire duration of job", "Keep popup open during progress") + enabled: applicationJobsEnabledCheck.checked + checked: kcm.settings.permanentJobPopups + onClicked: kcm.settings.permanentJobPopups = checked + } + } + + QtControls.CheckBox { + Kirigami.FormData.label: i18n("Notification badges:") + text: i18n("Show in task manager") + checked: kcm.settings.badgesInTaskManager + onClicked: kcm.settings.badgesInTaskManager = checked + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + } + + QtControls.Button { + Kirigami.FormData.label: i18n("Applications:") + text: i18n("Configure...") + icon.name: "configure" + onClicked: root.openSourcesSettings() + } + } + + Component.onCompleted: { + if (kcm.initialDesktopEntry || kcm.initialNotifyRcName) { + // FIXME doing that right in onCompleted doesn't work + Qt.callLater(root.openSourcesSettings); + } + } +} diff --git a/kcms/notifications/package/metadata.desktop b/kcms/notifications/package/metadata.desktop new file mode 100644 --- /dev/null +++ b/kcms/notifications/package/metadata.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Name=Notifications +Comment=Event Notifications and Actions + +Icon=preferences-desktop-notification-bell +Type=Service +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kde@privat.broulik.de +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kcm_notifications +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript + +X-Plasma-MainScript=ui/main.qml diff --git a/kcms/notifications/sourcesmodel.h b/kcms/notifications/sourcesmodel.h new file mode 100644 --- /dev/null +++ b/kcms/notifications/sourcesmodel.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +struct EventData +{ + QString name; + QString comment; + QString iconName; + QString eventId; + QStringList actions; +}; + +// FIXME add constructors for KService and KConfigGroup +struct SourceData +{ + QString name; + QString comment; + QString iconName; + + QString notifyRcName; + QString desktopEntry; + + QVector events; + + QString display() const + { + return !name.isEmpty() ? name : comment; + } +}; + +class SourcesModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + SourcesModel(QObject *parent = nullptr); + ~SourcesModel() override; + + enum Roles { + SourceTypeRole = Qt::UserRole + 1, + NotifyRcNameRole, + DesktopEntryRole, + + EventIdRole, + ActionsRole + }; + Q_ENUM(Roles) + + enum Type { + ApplicationType, + ServiceType + }; + Q_ENUM(Type) + + Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(const QModelIndex &idx) const; + + Q_INVOKABLE QPersistentModelIndex persistentIndexForDesktopEntry(const QString &desktopEntry) const; + Q_INVOKABLE QPersistentModelIndex persistentIndexForNotifyRcName(const QString ¬ifyRcName) const; + + int columnCount(const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + QHash roleNames() const override; + + Q_INVOKABLE void load(); + +private: + QVector m_data; + +}; diff --git a/kcms/notifications/sourcesmodel.cpp b/kcms/notifications/sourcesmodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/notifications/sourcesmodel.cpp @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2007 Matthew Woehlke + * Copyright (C) 2007 Jeremy Whiting + * Copyright (C) 2016 Olivier Churlaud + * Copyright (C) 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "sourcesmodel.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +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 +{ + 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 +{ + 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; + } + + 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; + } + } + } + + if (dirty) { + 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 SourcesModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::DecorationRole, QByteArrayLiteral("decoration")}, + {SourceTypeRole, QByteArrayLiteral("sourceType")}, + {NotifyRcNameRole, QByteArrayLiteral("notifyRcName")}, + {DesktopEntryRole, QByteArrayLiteral("desktopEntry")}, + {EventIdRole, QByteArrayLiteral("eventId")}, + {ActionsRole, QByteArrayLiteral("actions")} + }; +} + +void SourcesModel::load() +{ + beginResetModel(); + + m_data.clear(); + + QCollator collator; + + QVector appsData; + QVector 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")), + notifyRcName, + desktopEntry, + {} // events + }; + + QVector 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 = KServiceTypeTrader::self()->query(QStringLiteral("Application"), + QStringLiteral("exist Exec and TRUE == [X-GNOME-UsesNotifications]")); + for (const auto &service : services) { + if (service->noDisplay()) { + continue; + } + + if (desktopEntries.contains(service->desktopEntryName())) { + continue; + } + + SourceData source{ + service->name(), + service->comment(), + service->icon(), + 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(), + QString(), //notifyRcFile + service->desktopEntryName(), + {} + }; + appsData.append(source); + desktopEntries.append(service->desktopEntryName()); + } + + auto sortData = [&collator](const SourceData &a, const SourceData &b) { + return collator.compare(a.display(), b.display()) < 0; + }; + + std::sort(appsData.begin(), appsData.end(), sortData); + std::sort(servicesData.begin(), servicesData.end(), sortData); + + m_data << appsData << servicesData; + + endResetModel(); +}