diff --git a/kcms/notifications/CMakeLists.txt b/kcms/notifications/CMakeLists.txt index 9b4d2e66f..a46825e2b 100644 --- a/kcms/notifications/CMakeLists.txt +++ b/kcms/notifications/CMakeLists.txt @@ -1,28 +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/filterproxymodel.cpp b/kcms/notifications/filterproxymodel.cpp index 07df3e5b8..50015f3c1 100644 --- a/kcms/notifications/filterproxymodel.cpp +++ b/kcms/notifications/filterproxymodel.cpp @@ -1,83 +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(); - emit currentIndexChanged(); } } -int FilterProxyModel::currentIndex() const -{ - if (m_currentIndex.isValid()) { - return m_currentIndex.row(); - } - return -1; -} - -void FilterProxyModel::setCurrentIndex(const QPersistentModelIndex &idx) -{ - const int oldIndex = currentIndex(); - m_currentIndex = idx; - if (oldIndex != currentIndex()) { - emit currentIndexChanged(); - } -} - -QPersistentModelIndex FilterProxyModel::makePersistentModelIndex(int row) const -{ - return QPersistentModelIndex(index(row, 0)); -} - bool FilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if (m_query.isEmpty()) { return true; } - const QModelIndex idx = source_parent.child(source_row, 0); + 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/filterproxymodel.h b/kcms/notifications/filterproxymodel.h index 2d6636166..c08d55ddd 100644 --- a/kcms/notifications/filterproxymodel.h +++ b/kcms/notifications/filterproxymodel.h @@ -1,56 +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) - Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged) - public: FilterProxyModel(QObject *parent = nullptr); ~FilterProxyModel() override; QString query() const; void setQuery(const QString &query); - int currentIndex() const; - - Q_INVOKABLE void setCurrentIndex(const QPersistentModelIndex &idx); - - Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(int row) const; - bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; Q_SIGNALS: void queryChanged(); - void currentIndexChanged(); private: QString m_query; - QPersistentModelIndex m_currentIndex; }; diff --git a/kcms/notifications/kcm.cpp b/kcms/notifications/kcm.cpp index 20d5b0025..459bab0f1 100644 --- a/kcms/notifications/kcm.cpp +++ b/kcms/notifications/kcm.cpp @@ -1,118 +1,225 @@ /* * 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); connect(m_sourcesModel, &SourcesModel::pendingDeletionsChanged, this, [this] { setNeedsSave(true); }); + + 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(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(); m_sourcesModel->load(); - - //m_config->markAsClean(); - //m_config->reparseConfiguration(); } void KCMNotifications::save() { processPendingDeletions(); m_settings->save(); - //setNeedsSave(false); } void KCMNotifications::defaults() { m_settings->defaults(); - //setNeedsSave(true); } void KCMNotifications::processPendingDeletions() { const QStringList pendingDeletions = m_sourcesModel->pendingDeletions(); for (const QString &desktopEntry : pendingDeletions) { m_settings->forgetKnownApplication(desktopEntry); } m_sourcesModel->removeItemsPendingDeletion(); } #include "kcm.moc" diff --git a/kcms/notifications/kcm.h b/kcms/notifications/kcm.h index e4feb9804..a76b085e6 100644 --- a/kcms/notifications/kcm.h +++ b/kcms/notifications/kcm.h @@ -1,73 +1,95 @@ /* * 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 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; enum Roles { SchemeNameRole = Qt::UserRole + 1, PaletteRole, RemovableRole, PendingDeletionRole }; 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/package/contents/ui/ApplicationConfiguration.qml b/kcms/notifications/package/contents/ui/ApplicationConfiguration.qml new file mode 100644 index 000000000..1fc298eea --- /dev/null +++ b/kcms/notifications/package/contents/ui/ApplicationConfiguration.qml @@ -0,0 +1,152 @@ +/* + * 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 { + Item { + width: Kirigami.Units.gridUnit + } + + QtControls.CheckBox { + 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/EventDetails.qml b/kcms/notifications/package/contents/ui/EventDetails.qml deleted file mode 100644 index 9675f421b..000000000 --- a/kcms/notifications/package/contents/ui/EventDetails.qml +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 - -ColumnLayout { - id: detailsLayout - - property alias model: actionsRepeater.model - - property Component actionSettingsSound: RowLayout { - QtControls.Button { - icon.name: "media-playback-start" - } - QtControls.TextField { - //text: "Oxygen-Sys-Trash-Emptied"//model.sound - //textFormat: Text.PlainText - //elide: Text.ElideMiddle - //enabled: false - } - QtControls.Button { - icon.name: "folder-open" - } - } - - property Component actionSettingsLogfile: RowLayout { - QtControls.TextField { - - } - QtControls.Button { - icon.name: "folder-open" - } - } - - property Component actionSettingsExecute: RowLayout { - QtControls.TextField { - - } - QtControls.Button { - icon.name: "folder-open" - } - } - - Repeater { - id: actionsRepeater - model: eventsColumn.actions - - RowLayout { - Layout.fillWidth: true - - QtControls.CheckBox { - id: actionCheck - Layout.fillWidth: true - text: modelData.label - icon.name: modelData.icon - checked: eventDelegate.isActionEnabled(modelData.key) - onClicked: eventDelegate.setActionEnabled(modelData.key, checked) - enabled: modelData.key !== "Popup" || showPopupsCheck.checked - - contentItem: RowLayout { - Item { - width: actionCheck.indicator.width - } - - Kirigami.Icon { - source: actionCheck.icon.name - Layout.preferredWidth: Kirigami.Units.iconSizes.small - Layout.preferredHeight: Kirigami.Units.iconSizes.small - } - - QtControls.Label { - Layout.fillWidth: true - text: actionCheck.text - elide: Text.ElideRight - } - } - } - - Loader { - sourceComponent: detailsLayout["actionSettings" + modelData.key] - } - } - } -} diff --git a/kcms/notifications/package/contents/ui/EventsPage.qml b/kcms/notifications/package/contents/ui/EventsPage.qml deleted file mode 100644 index f5fe03838..000000000 --- a/kcms/notifications/package/contents/ui/EventsPage.qml +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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 - -ColumnLayout { - id: eventsColumn - - property var rootIndex - property var appData - - property int behavior: { - if (appData.desktopEntry) { - return kcm.settings.applicationBehavior(appData.desktopEntry); - } else if (appData.notifyRcName) { - return kcm.settings.serviceBehavior(appData.notifyRcName); - } - return -1; - } - - function setBehavior(flag, enable) { - var newBehavior = behavior; - if (enable) { - newBehavior |= flag; - } else { - newBehavior &= ~flag; - } - - if (appData.desktopEntry) { - return kcm.settings.setApplicationBehavior(appData.desktopEntry, newBehavior); - } else if (appData.notifyRcName) { - return kcm.settings.setServiceBehavior(appData.notifyRcName, newBehavior); - } - } - - readonly property var actions: [ - {key: "Popup", label: i18n("Show popup"), icon: "dialog-information"}, - {key: "Sound", label: i18n("Play sound"), icon: "folder-sound"},// "media-playback-start"}, - {key: "Logfile", label: i18n("Log to a file"), icon: "text-x-generic"}, - {key: "Taskbar", label: i18n("Mark taskbar entry"), icon: "services"}, - {key: "Execute", label: i18n("Run command"), icon: "system-run"}, - {key: "TTS", label: i18n("Speech"), icon: "text-speak"} // FIXME only when available - ] - - spacing: Kirigami.Units.smallSpacing - - Kirigami.FormLayout { - Layout.fillWidth: true - - QtControls.CheckBox { - id: showPopupsCheck - text: i18n("Show popups") - checked: eventsColumn.behavior & NotificationManager.Settings.ShowPopups - onClicked: eventsColumn.setBehavior(NotificationManager.Settings.ShowPopups, checked) - } - - RowLayout { - Item { - width: Kirigami.Units.gridUnit - } - - QtControls.CheckBox { - text: i18n("Show in do not disturb mode") - enabled: showPopupsCheck.checked - checked: eventsColumn.behavior & NotificationManager.Settings.ShowPopupsInDoNotDisturbMode - onClicked: eventsColumn.setBehavior(NotificationManager.Settings.ShowPopupsInDoNotDisturbMode, checked) - } - } - - QtControls.CheckBox { - text: i18n("Show in history") - checked: eventsColumn.behavior & NotificationManager.Settings.ShowInHistory - onClicked: eventsColumn.setBehavior(NotificationManager.Settings.ShowInHistory, checked) - } - - QtControls.CheckBox { - text: i18n("Show notification badges") - enabled: !!eventsColumn.appData.desktopEntry - checked: eventsColumn.behavior & NotificationManager.Settings.ShowBadges - onClicked: eventsColumn.setBehavior(NotificationManager.Settings.ShowBadges, checked) - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - visible: eventsList.count > 0 - - QtControls.ScrollView { - id: eventsScroll - - anchors.fill: parent - activeFocusOnTab: false - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: false - - Component.onCompleted: background.visible = true - - QtControls.ScrollBar.horizontal.visible: false - - ListView { - id: eventsList - anchors { - fill: parent - margins: 2 - //leftMargin: sourcesScroll.QtControls.ScrollBar.vertical.visible ? 2 : internal.scrollBarSpace/2 + 2 - } - clip: true - activeFocusOnTab: true - - keyNavigationEnabled: true - keyNavigationWraps: true - highlightMoveDuration: 0 - - model: DelegateModel { - id: eventsModel - model: eventsColumn.rootIndex ? kcm.filteredModel : null - rootIndex: eventsColumn.rootIndex - onRootIndexChanged: eventsList.currentIndex = -1 - delegate: QtControls.ItemDelegate { - id: eventDelegate - - function isActionEnabled(action) { - return model.actions.indexOf(action) > -1; - } - - function setActionEnabled(action, enable) { - var actions = model.actions; - var idx = actions.indexOf(action); - if (enable && idx === -1) { - actions.push(action); - } else if (!enable && idx > -1) { - actions.splice(idx, 1); - } - model.actions = actions; - } - - width: eventsList.width - text: model.display - onClicked: eventsList.currentIndex = (eventsList.currentIndex === index ? -1 : index) - - contentItem: RowLayout { - //Kirigami.Theme.textColor: eventDelegate.highlighted || eventDelegate.checked || (eventDelegate.pressed && !eventDelegate.checked && !eventDelegate.sectionDelegate) ? Kirigami.Theme.highlightedTextColor : (eventDelegate.enabled ? Kirigami.Theme.textColor : Kirigami.Theme.disabledTextColor) - - /*QtControls.ToolTip { - visible: eventDelegate.hovered - text: model.comment - }*/ - - Kirigami.Icon { - Layout.alignment: Qt.AlignTop - Layout.preferredWidth: Kirigami.Units.iconSizes.small - Layout.preferredHeight: Kirigami.Units.iconSizes.small - source: model.decoration - } - - ColumnLayout { - Layout.fillWidth: true - - RowLayout { - Layout.fillWidth: true - - QtControls.Label { - Layout.fillWidth: true - text: eventDelegate.text - font: eventDelegate.font - //color: - elide: Text.ElideRight - } - - Repeater { - model: eventsColumn.actions - - // TODO use abstract button? - QtControls.AbstractButton { - id: actionStripButton - Layout.preferredWidth: Kirigami.Units.iconSizes.small - Layout.preferredHeight: Kirigami.Units.iconSizes.small - icon.name: modelData.icon - checkable: true - checked: eventDelegate.isActionEnabled(modelData.key) - onClicked: { - eventDelegate.setActionEnabled(modelData.key, checked) - if (checked) { - // Some actons might need configuration (e.g. sound needs a filename) - // FIXME check this and expand and focus if needed - } - } - - contentItem: Kirigami.Icon { - anchors.fill: parent - source: modelData.icon - opacity: actionStripButton.checked ? 1 : (actionStripButton.hovered ? 0.5 : 0.1) - } - - QtControls.ToolTip { - text: modelData.label - visible: parent.hovered - } - } - } - } - - Loader { - Layout.fillWidth: true - active: eventDelegate.ListView.isCurrentItem - visible: active - sourceComponent: EventDetails { - Layout.fillWidth: true - model: eventsColumn.actions - } - } - } - } - } - } - } - } - } - - // compact layout when event list isnt't shown - Item { - Layout.fillHeight: true - visible: eventsList.count === 0 - } -} diff --git a/kcms/notifications/package/contents/ui/SourcesPage.qml b/kcms/notifications/package/contents/ui/SourcesPage.qml index 69d172c8b..22d7a72fc 100644 --- a/kcms/notifications/package/contents/ui/SourcesPage.qml +++ b/kcms/notifications/package/contents/ui/SourcesPage.qml @@ -1,190 +1,238 @@ /* * 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 +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: { + // TODO the page is push'd in Qt.callLater, why is the model still not loaded when we get here? + Qt.callLater(function() { + if (kcm.initialDesktopEntry) { + appConfiguration.rootIndex = kcm.sourcesModel.persistentIndexForDesktopEntry(kcm.initialDesktopEntry); + } else if (kcm.initialNotifyRcName) { + appConfiguration.rootIndex = kcm.sourcesModel.persistentIndexForNotifyRcName(kcm.initialNotifyRcName); + if (kcm.initialEventId) { + 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 { - Layout.fillWidth: true - }*/ - QtControls.TextField { // FIXME search field + Kirigami.SearchField { id: searchField Layout.fillWidth: true - placeholderText: i18n("Search...") - // TODO autofocus this? - - Shortcut { - sequence: StandardKey.Find - onActivated: searchField.forceActiveFocus() - } } 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 anchors { fill: parent margins: 2 - //leftMargin: sourcesScroll.QtControls.ScrollBar.vertical.visible ? 2 : internal.scrollBarSpace/2 + 2 } clip: true activeFocusOnTab: true - currentIndex: kcm.filteredModel.currentIndex 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"); - case Private.SourcesModel.KNotifyAppType: return i18n("Applications"); - case Private.SourcesModel.FdoAppType: return i18n("Other Applications"); } } // 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 } } - model: kcm.filteredModel + // 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 opacity: model.pendingDeletion ? 0.6 : 1 onClicked: { - var idx = kcm.filteredModel.makePersistentModelIndex(index, 0); - kcm.filteredModel.setCurrentIndex(idx); - eventsConfiguration.rootIndex = idx; - eventsConfiguration.appData = model + 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 enabled: !model.pendingDeletion } 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 } // FIXME alignment QtControls.ToolButton { Layout.preferredWidth: Kirigami.Units.iconSizes.small + leftPadding + rightPadding Layout.preferredHeight: Kirigami.Units.iconSizes.small + topPadding + bottomPadding icon.name: model.pendingDeletion ? "edit-undo" : "edit-delete" visible: model.removable onClicked: model.pendingDeletion = !model.pendingDeletion QtControls.ToolTip { text: model.pendingDeletion ? i18n("Undo Remove") : i18n("Remove") } } } } + + 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) - EventsPage { - id: eventsConfiguration + ApplicationConfiguration { + id: appConfiguration anchors.fill: parent - visible: !!rootIndex + 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("No application or event matches your search term.") - visible: sourcesList.count === 0 && searchField.length > 0 + 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 index 020f5dcbf..3a875af38 100644 --- a/kcms/notifications/package/contents/ui/main.qml +++ b/kcms/notifications/package/contents/ui/main.qml @@ -1,165 +1,183 @@ /* * 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.Default | KCM.ConfigModule.Apply + KCM.ConfigModule.buttons: KCM.ConfigModule.Help | KCM.ConfigModule.Apply - implicitHeight: 550 // HACK FIXME + 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: [positionNearWidget, positionCustomPosition] + buttons: [positionCloseToWidget, positionCustomPosition] } QtControls.RadioButton { - id: positionNearWidget + 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.NearWidget - onClicked: kcm.settings.popupPosition = NotificationManager.Settings.NearWidget + 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.NearWidget + 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: indicator.width 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: kcm.push("SourcesPage.qml") + 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/sourcesmodel.cpp b/kcms/notifications/sourcesmodel.cpp index c2f07ada7..64467bb12 100644 --- a/kcms/notifications/sourcesmodel.cpp +++ b/kcms/notifications/sourcesmodel.cpp @@ -1,391 +1,400 @@ /* * 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 #include SourcesModel::SourcesModel(QObject *parent) : QAbstractItemModel(parent) { } SourcesModel::~SourcesModel() = default; -QPersistentModelIndex SourcesModel::makePersistentModelIndex(int row) const +QPersistentModelIndex SourcesModel::makePersistentModelIndex(const QModelIndex &idx) const { - return QPersistentModelIndex(index(row, 0)); + 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: - if (!source.notifyRcName.isEmpty()) { - if (!source.desktopEntry.isEmpty()) { - return KNotifyAppType; - } - return ServiceType; - } - return FdoAppType; + case SourceTypeRole: return source.desktopEntry.isEmpty() ? ServiceType : ApplicationType; case NotifyRcNameRole: return source.notifyRcName; case DesktopEntryRole: return source.desktopEntry; case RemovableRole: return source.removable; case PendingDeletionRole: return source.pendingDeletion; } 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; } } } else { // source auto &source = m_data[index.row()]; switch (role) { case PendingDeletionRole: { const bool newPending = value.toBool(); if (source.pendingDeletion != newPending) { source.pendingDeletion = newPending; dirty = true; } emit pendingDeletionsChanged(); } } } 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")}, {RemovableRole, QByteArrayLiteral("removable")}, {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")} }; } void SourcesModel::load() { beginResetModel(); m_data.clear(); QCollator collator; + QVector appsData; QVector servicesData; - QVector knotifyAppsData; - // apps that have the X-GNOME-UsesNotifications property or that have been observed sending notifications - QVector fdoAppsData; 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); - //const QString path = dir + QLatin1Char('/') + file; - - //KConfig config(path, KConfig::NoGlobals, QStandardPaths::DataLocation); - KConfig *config = new KConfig(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 config, false, // removable false // pendingDeletion }; 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()) { - knotifyAppsData.append(source); + 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(), {}, nullptr, false, // removable false // pendingDeletion }; - fdoAppsData.append(source); - + appsData.append(source); desktopEntries.append(service->desktopEntryName()); } - const QStringList seenApps = KSharedConfig::openConfig(QStringLiteral("plasmanotifyrc"))->group("Applications").groupList(); + 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(), {}, nullptr, - true, // removable + applicationsGroup.group(app).readEntry("Seen", false), // removable false // pendingDeletion }; - fdoAppsData.append(source); + 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); - std::sort(knotifyAppsData.begin(), knotifyAppsData.end(), sortData); - // thse are both "Other apps", sort them together or come up with anothe heading "Further other apps".. - std::sort(fdoAppsData.begin(), fdoAppsData.end(), sortData); - m_data << servicesData << knotifyAppsData << fdoAppsData; + m_data << appsData << servicesData; endResetModel(); } QStringList SourcesModel::pendingDeletions() const { QStringList pendingDeletions; for (const auto &item : m_data) { if (item.pendingDeletion) { // Only apps can be deleted so we can assume it has a desktopEntry pendingDeletions.append(item.desktopEntry); } } return pendingDeletions; } void SourcesModel::removeItemsPendingDeletion() { for (int i = m_data.count() - 1; i >= 0; --i) { if (m_data.at(i).pendingDeletion) { beginRemoveRows(QModelIndex(), i, i); m_data.remove(i); endRemoveRows(); } } } diff --git a/kcms/notifications/sourcesmodel.h b/kcms/notifications/sourcesmodel.h index 680dcf56f..6d9410ddf 100644 --- a/kcms/notifications/sourcesmodel.h +++ b/kcms/notifications/sourcesmodel.h @@ -1,112 +1,115 @@ /* * 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 class KConfig; 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; KConfig *config; // KSharedConfig::Ptr? bool removable; // for "observed" apps bool pendingDeletion; 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, RemovableRole, // for "observed" apps PendingDeletionRole }; + Q_ENUM(Roles) enum Type { - ServiceType, - KNotifyAppType, - FdoAppType + ApplicationType, + ServiceType }; Q_ENUM(Type) - Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(int row) const; + 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; QStringList pendingDeletions() const; void removeItemsPendingDeletion(); void load(); Q_SIGNALS: void pendingDeletionsChanged(); private: QVector m_data; };