diff --git a/CMakeLists.txt b/CMakeLists.txt index 0846ea57f..f7d4da7e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,187 +1,188 @@ cmake_minimum_required(VERSION 3.0) project(plasma-desktop) set(PROJECT_VERSION "5.15.80") set(PROJECT_VERSION_MAJOR 5) set(QT_MIN_VERSION "5.11.0") set(KF5_MIN_VERSION "5.56.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMMarkAsTest) include(ECMMarkNonGuiExecutable) include(ECMOptionalAddSubdirectory) include(ECMQtDeclareLoggingCategory) include(FeatureSummary) include(CheckIncludeFiles) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Quick QuickWidgets DBus Widgets X11Extras Svg Concurrent ) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Auth Plasma PlasmaQuick DocTools I18n KCMUtils NewStuff KDELibs4Support Notifications NotifyConfig Attica Wallet Runner GlobalAccel Declarative People DBusAddons Activities ActivitiesStats Config ) find_package(KF5Kirigami2 ${KF5_MIN_VERSION} CONFIG) set_package_properties(KF5Kirigami2 PROPERTIES DESCRIPTION "A QtQuick based components set" PURPOSE "Required at runtime by many KCMs" TYPE RUNTIME ) find_package(KF5QQC2DeskopStyle ${KF5_MIN_VERSION} CONFIG) set_package_properties(KF5QQC2DeskopStyle PROPERTIES DESCRIPTION "QtQuickControls 2 style that uses QWidget's QStyle for painting" PURPOSE "Required at runtime by many KCMs" TYPE RUNTIME ) 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) find_package(KRunnerAppDBusInterface CONFIG REQUIRED) find_package(KSMServerDBusInterface CONFIG REQUIRED) find_package(KF5ItemModels CONFIG REQUIRED) find_package(KF5Emoticons CONFIG REQUIRED) find_package(AppStreamQt 0.10.4) set_package_properties(AppStreamQt PROPERTIES DESCRIPTION "Appstream integration" TYPE RECOMMENDED PURPOSE "Needed to allow appstream integration from application menus" ) find_package(KF5Baloo 5.15) set_package_properties(KF5Baloo PROPERTIES DESCRIPTION "File Searching" TYPE RECOMMENDED PURPOSE "Needed to build the File Search KCM" ) find_package(Fontconfig) set_package_properties(Fontconfig PROPERTIES DESCRIPTION "Font access configuration library" URL "http://www.freedesktop.org/wiki/Software/fontconfig" TYPE OPTIONAL PURPOSE "Needed to build font configuration and installation tools" ) find_package(X11) set_package_properties(X11 PROPERTIES DESCRIPTION "X11 libraries" URL "http://www.x.org" TYPE REQUIRED PURPOSE "Required for building the X11 based workspace" ) find_package(UDev) set_package_properties(UDev PROPERTIES DESCRIPTION "UDev library" URL "http://www.kernel.org/pub/linux/utils/kernel/hotplug/udev.html" TYPE OPTIONAL PURPOSE "Required for device discovery in keyboard daemon" ) find_package(XCB REQUIRED COMPONENTS XCB SHM IMAGE OPTIONAL_COMPONENTS XKB XINPUT ) set_package_properties(XCB PROPERTIES TYPE REQUIRED) add_feature_info("XCB-XKB" XCB_XKB_FOUND "Required for building kcm/keyboard") add_feature_info("libxft" X11_Xft_FOUND "X FreeType interface library required for font installation") find_package(Evdev) set_package_properties(Evdev PROPERTIES TYPE OPTIONAL) add_feature_info("Evdev" EVDEV_FOUND "Evdev driver headers needed for mouse KCM") find_package(Synaptics) set_package_properties(Synaptics PROPERTIES TYPE OPTIONAL) add_feature_info("Synaptics" SYNAPTICS_FOUND "Synaptics libraries needed for touchpad KCM") find_package(XorgLibinput) set_package_properties(XorgLibinput PROPERTIES TYPE OPTIONAL) add_feature_info("XorgLibinput" XORGLIBINPUT_FOUND "Libinput driver headers needed for mouse KCM") include(ConfigureChecks.cmake) find_package(Breeze ${PROJECT_VERSION} CONFIG) set_package_properties(Breeze PROPERTIES TYPE OPTIONAL PURPOSE "For setting the default window decoration plugin") if(${Breeze_FOUND}) if(${BREEZE_WITH_KDECORATION}) set(HAVE_BREEZE_DECO true) else() set(HAVE_BREEZE_DECO FALSE) endif() else() set(HAVE_BREEZE_DECO FALSE) endif() if(${AppStreamQt_FOUND}) set(HAVE_APPSTREAMQT true) endif() include_directories("${CMAKE_CURRENT_BINARY_DIR}") configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h) configure_file(config-unix.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-unix.h ) configure_file(config-appstream.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-appstream.h ) configure_file(config-X11.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-X11.h) configure_file(config-runtime.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-runtime.h) plasma_install_package(desktoppackage org.kde.plasma.desktop shells shell) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) add_subdirectory(layout-templates) add_subdirectory(doc) add_subdirectory(runners) add_subdirectory(containments) add_subdirectory(toolboxes) add_subdirectory(applets) add_subdirectory(dataengines) add_subdirectory(kcms) add_subdirectory(knetattach) add_subdirectory(attica-kde) add_subdirectory(imports/activitymanager/) add_subdirectory(solid-device-automounter) if(X11_Xkb_FOUND AND XCB_XKB_FOUND) add_subdirectory(kaccess) endif() install(FILES org.kde.plasmashell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES plasma-desktop.categories DESTINATION ${KDE_INSTALL_CONFDIR}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/kcms/notifications/CMakeLists.txt b/kcms/notifications/CMakeLists.txt index 56c14ce7d..728971868 100644 --- a/kcms/notifications/CMakeLists.txt +++ b/kcms/notifications/CMakeLists.txt @@ -1,29 +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 - #Qt5::DBus KF5::KCMUtils KF5::Activities KF5::CoreAddons KF5::Declarative KF5::GuiAddons KF5::I18n KF5::QuickAddons 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 c2a7af3d6..07df3e5b8 100644 --- a/kcms/notifications/filterproxymodel.cpp +++ b/kcms/notifications/filterproxymodel.cpp @@ -1,66 +1,83 @@ /* * 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 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 e786fb911..2d6636166 100644 --- a/kcms/notifications/filterproxymodel.h +++ b/kcms/notifications/filterproxymodel.h @@ -1,48 +1,56 @@ /* * 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 d3c44e8cd..682afabf0 100644 --- a/kcms/notifications/kcm.cpp +++ b/kcms/notifications/kcm.cpp @@ -1,101 +1,111 @@ /* * 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 "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)) , m_activitiesModel(new KActivities::ActivitiesModel(this)) { const char uri[] = "org.kde.private.kcms.notifications"; qmlRegisterUncreatableType(uri, 1, 0, "SourcesModel", QStringLiteral("Cannot create instances of SourcesModel")); qmlRegisterType(); qmlProtectModule(uri, 1); qmlRegisterType(); 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); } KCMNotifications::~KCMNotifications() { } SourcesModel *KCMNotifications::sourcesModel() const { return m_sourcesModel; } FilterProxyModel *KCMNotifications::filteredModel() const { return m_filteredModel; } +NotificationManager::Settings *KCMNotifications::settings() const +{ + return m_settings; +} + KActivities::ActivitiesModel *KCMNotifications::activitiesModel() const { return m_activitiesModel; } void KCMNotifications::load() { + m_settings->load(); m_sourcesModel->load(); //m_config->markAsClean(); //m_config->reparseConfiguration(); } void KCMNotifications::save() { - - setNeedsSave(false); + m_settings->save(); + //setNeedsSave(false); } void KCMNotifications::defaults() { - setNeedsSave(true); + m_settings->defaults(); + //setNeedsSave(true); } #include "kcm.moc" diff --git a/kcms/notifications/kcm.h b/kcms/notifications/kcm.h index 9a784b58a..e19ccfbdc 100644 --- a/kcms/notifications/kcm.h +++ b/kcms/notifications/kcm.h @@ -1,70 +1,81 @@ /* * 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; +} + namespace KActivities { class ActivitiesModel; } 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) + Q_PROPERTY(KActivities::ActivitiesModel *activitiesModel READ activitiesModel CONSTANT) 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; + KActivities::ActivitiesModel *activitiesModel() const; public Q_SLOTS: void load() override; void save() override; void defaults() override; private: SourcesModel *m_sourcesModel; FilterProxyModel *m_filteredModel; + NotificationManager::Settings *m_settings; + KActivities::ActivitiesModel *m_activitiesModel; }; diff --git a/kcms/notifications/package/contents/ui/EventsPage.qml b/kcms/notifications/package/contents/ui/EventsPage.qml index 1bba990c8..facab21be 100644 --- a/kcms/notifications/package/contents/ui/EventsPage.qml +++ b/kcms/notifications/package/contents/ui/EventsPage.qml @@ -1,230 +1,245 @@ /* * 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; + } - property bool customActivitySettings: false + 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: true + 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("Notification badges") - enabled: !!eventsColumn.desktopEntry + 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 - enabled: !eventsColumn.customActivitySettings - 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 } } } } } } } } - - QtControls.Label { - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - right: parent.right - margins: Kirigami.Units.smallSpacing - } - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - text: i18n("Application events cannot be configured on a per-activity basis.") - visible: eventsColumn.customActivitySettings - } } // compact layout when event list isnt't shown Item { Layout.fillHeight: true visible: eventsList.count === 0 } } diff --git a/kcms/notifications/package/contents/ui/PopupPositionPage.qml b/kcms/notifications/package/contents/ui/PopupPositionPage.qml index cf51bba10..84d56c4b5 100644 --- a/kcms/notifications/package/contents/ui/PopupPositionPage.qml +++ b/kcms/notifications/package/contents/ui/PopupPositionPage.qml @@ -1,32 +1,38 @@ /* * 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 + + property QtObject settings + title: i18n("Popup Position") ScreenPositionSelector { - anchors.fill: parent + anchors.horizontalCenter: parent.horizontalCenter + selectedPosition: settings.popupPosition + onSelectedPositionChanged: settings.popupPosition = selectedPosition } } diff --git a/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml b/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml index 4f238b594..164c57eed 100644 --- a/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml +++ b/kcms/notifications/package/contents/ui/ScreenPositionSelector.qml @@ -1,276 +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.plasma.private.notifications 1.0 +import org.kde.notificationmanager 1.0 as NotificationManager Item { id: monitorPanel - property int baseUnit: Kirigami.Units.gridUnit// Math.round(Kirigami.Units.gridUnit / 1.5) + property int baseUnit: Kirigami.Units.gridUnit implicitWidth: baseUnit * 13 + baseUnit * 2 implicitHeight: (screenRatio * baseUnit * 13) + (baseUnit * 2) + basePart.height - //flat: true - property int selectedPosition property var disabledPositions: [] property real screenRatio: Screen.height / Screen.width - onEnabledChanged: { - if (!enabled) { - positionRadios.current = null + 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; + } } - - selectedPosition = NotificationsHelper.Default } 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 } - /*QtControls.ExclusiveGroup { - id: positionRadios - - onCurrentChanged: { - monitorPanel.selectedPosition = current.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: NotificationsHelper.TopLeft + 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: NotificationsHelper.TopCenter + 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: NotificationsHelper.TopRight - checked: monitorPanel.selectedPosition == position - visible: monitorPanel.disabledPositions.indexOf(position) == -1 - QtControls.ButtonGroup.group: positionRadios - } - QtControls.RadioButton { - anchors { - left: leftPart.right - verticalCenter: leftPart.verticalCenter - margins: Kirigami.Units.smallSpacing - } - readonly property int position: NotificationsHelper.Left - checked: monitorPanel.selectedPosition == position - visible: monitorPanel.disabledPositions.indexOf(position) == -1 - QtControls.ButtonGroup.group: positionRadios - } - QtControls.RadioButton { - anchors { - horizontalCenter: topPart.horizontalCenter - verticalCenter: leftPart.verticalCenter - margins: Kirigami.Units.smallSpacing - } - readonly property int position: NotificationsHelper.Center - checked: monitorPanel.selectedPosition == position - visible: monitorPanel.disabledPositions.indexOf(position) == -1 - QtControls.ButtonGroup.group: positionRadios - } - QtControls.RadioButton { - anchors { - right: rightPart.left - verticalCenter: rightPart.verticalCenter - margins: Kirigami.Units.smallSpacing - } - readonly property int position: NotificationsHelper.Right + 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: NotificationsHelper.BottomLeft + 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: NotificationsHelper.BottomCenter + 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: NotificationsHelper.BottomRight + 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 index 7dd7c703f..cc9359ed0 100644 --- a/kcms/notifications/package/contents/ui/SourcesPage.qml +++ b/kcms/notifications/package/contents/ui/SourcesPage.qml @@ -1,243 +1,186 @@ /* * 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.kcm 1.2 as KCM import org.kde.private.kcms.notifications 1.0 as Private Kirigami.Page { id: sourcesPage title: i18n("Application Settings") - readonly property bool hasActivities: activityTabsRepeater.count > 1 - Binding { target: kcm.filteredModel property: "query" value: searchField.text } - ColumnLayout { - id: rootColumn + RowLayout { + id: rootRow anchors.fill: parent - spacing: 0 - QtControls.TabBar { - id: activitiesTabBar - Layout.fillWidth: true - visible: sourcesPage.hasActivities - - Repeater { - id: activityTabsRepeater - model: kcm.activitiesModel - - QtControls.TabButton { - readonly property bool isCurrent: model.isCurrent - text: model.name - // FIXME support icon in tab button - //icon.name: model.iconSource - - // Switch to current activity initially - Component.onCompleted: { - if (model.isCurrent) { - activitiesTabBar.currentIndex = index; - } - } + ColumnLayout { + Layout.minimumWidth: Kirigami.Units.gridUnit * 12 + Layout.preferredWidth: Math.round(rootRow.width / 3) + + /*Kirigami.SearchField { + Layout.fillWidth: true + }*/ + QtControls.TextField { // FIXME search field + id: searchField + Layout.fillWidth: true + placeholderText: i18n("Search...") + // TODO autofocus this? + + Shortcut { + sequence: StandardKey.Find + onActivated: searchField.forceActiveFocus() } } - } - QtControls.Frame { - Layout.fillWidth: true - Layout.fillHeight: true - - padding: sourcesPage.hasActivities ? 6 : 0 // magic number comes from qqc2-desktop-style - - Component.onCompleted: { - background.visible = Qt.binding(function() { - return sourcesPage.hasActivities; - }); - } - - ColumnLayout { - anchors.fill: parent - - Kirigami.FormLayout { - Layout.fillWidth: false // keep left-aligned - visible: sourcesPage.hasActivities - - QtControls.RadioButton { - id: activityDefaultCheck - Kirigami.FormData.label: i18n("In this activity:") - text: i18n("Use default settings") - checked: 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 + anchors { + fill: parent + margins: 2 + //leftMargin: sourcesScroll.QtControls.ScrollBar.vertical.visible ? 2 : internal.scrollBarSpace/2 + 2 } - - QtControls.RadioButton { - id: activityCustomCheck - text: i18n("Custom settings") - } - } - - RowLayout { - Layout.fillWidth: true - - ColumnLayout { - Layout.minimumWidth: Kirigami.Units.gridUnit * 12 - Layout.preferredWidth: Math.round(rootColumn.width / 3) - - /*Kirigami.SearchField { - Layout.fillWidth: true - }*/ - QtControls.TextField { // FIXME search field - 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 - // FIXME fix current index when we filter - //currentIndex: -1 - - keyNavigationEnabled: true - keyNavigationWraps: true - highlightMoveDuration: 0 - - section { - criteria: ViewSection.FullString - property: "sourceType" - delegate: QtControls.ItemDelegate { - id: sourceSection - width: sourcesList.width - text: { - switch (Number(section)) { - 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: rootColumn.Kirigami.Theme.textColor - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - enabled: false - } + clip: true + activeFocusOnTab: true + currentIndex: kcm.filteredModel.currentIndex + + keyNavigationEnabled: true + keyNavigationWraps: true + highlightMoveDuration: 0 + + section { + criteria: ViewSection.FullString + property: "sourceType" + delegate: QtControls.ItemDelegate { + id: sourceSection + width: sourcesList.width + text: { + switch (Number(section)) { + case Private.SourcesModel.ServiceType: return i18n("System Services"); + case Private.SourcesModel.KNotifyAppType: return i18n("Applications"); + case Private.SourcesModel.FdoAppType: return i18n("Other Applications"); } + } - model: kcm.filteredModel - - delegate: QtControls.ItemDelegate { - id: sourceDelegate - width: sourcesList.width - text: model.display - highlighted: ListView.isCurrentItem - onClicked: { - eventsConfiguration.rootIndex = kcm.filteredModel.makePersistentModelIndex(index, 0) - // FIXME move as we filter - sourcesList.currentIndex = index - } - - 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 - } - } - } + // 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 } } - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: Math.round(rootColumn.width / 3 * 2) - - EventsPage { - id: eventsConfiguration - anchors.fill: parent - //rootIndex: - customActivitySettings: activityCustomCheck.checked - visible: !!rootIndex + model: kcm.filteredModel + + delegate: QtControls.ItemDelegate { + id: sourceDelegate + width: sourcesList.width + text: model.display + highlighted: ListView.isCurrentItem + onClicked: { + var idx = kcm.filteredModel.makePersistentModelIndex(index, 0); + kcm.filteredModel.setCurrentIndex(idx); + eventsConfiguration.rootIndex = idx; + eventsConfiguration.appData = model } - QtControls.Label { - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - right: parent.right - margins: Kirigami.Units.smallSpacing + 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 + } + + // FIXME alignment + QtControls.ToolButton { + Layout.topMargin: -sourceDelegate.topPadding + Layout.bottomMargin: -sourceDelegate.bottomPadding + Layout.preferredWidth: height + Layout.preferredHeight: Kirigami.Units.iconSizes.small + icon.name: "edit-delete" + opacity: 0.6 + visible: model.removable } - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - text: i18n("No application or event matches your search term.") - visible: sourcesList.count === 0 && searchField.length > 0 } } } } } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: Math.round(rootRow.width / 3 * 2) + + EventsPage { + id: eventsConfiguration + anchors.fill: parent + visible: !!rootIndex + } + + 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 + } + } } } diff --git a/kcms/notifications/package/contents/ui/main.qml b/kcms/notifications/package/contents/ui/main.qml index eaf39f5a6..7486b816c 100644 --- a/kcms/notifications/package/contents/ui/main.qml +++ b/kcms/notifications/package/contents/ui/main.qml @@ -1,208 +1,179 @@ /* * 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.Window 2.2 -//import QtQuick.Dialogs 1.0 as QtDialogs import QtQuick.Controls 2.3 as QtControls import org.kde.kirigami 2.4 as Kirigami -//import org.kde.kconfig 1.0 // for KAuthorized import org.kde.kcm 1.2 as KCM +import org.kde.notificationmanager 1.0 as NotificationManager + KCM.SimpleKCM { 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.Default | KCM.ConfigModule.Apply implicitHeight: 550 // HACK FIXME + Binding { + target: kcm + property: "needsSave" + value: kcm.settings.dirty // TODO or other stuff + } + Kirigami.FormLayout { RowLayout { Kirigami.FormData.label: i18n("Do not disturb mode:") QtControls.CheckBox { id: dndTimeCheck - text: i18nc("Enable between hh:mm and hh:mm", "Automatically enable") + text: i18nc("Enable do not disturb during following times", "During following times:") } QtControls.Button { - text: i18nc("Set times for automatic do not disturb mode", "Set Times...") + text: i18nc("Choose times for do not disturb mode", "Choose...") icon.name: "preferences-system-time" onClicked: kcm.push("DndTimePage.qml") enabled: dndTimeCheck.checked } } - QtControls.CheckBox { - text: i18nc("Apps can enable do not disturb mode", "Applications can enable") - checked: true - } - - RowLayout { - QtControls.Label { - text: i18n("Keyboard Shortcut:") - } - - // TODO keysequence thing - QtControls.Button { - icon.name: "configure" - text: i18n("Meta+N") - } - - QtControls.Button { - icon.name: "edit-clear" - } - } - - ColumnLayout { - visible: activitiesDndRepeater.count > 1 - - QtControls.Label { - text: i18n("Automatically enable in the following activities:") - } - - Repeater { - id: activitiesDndRepeater - model: kcm.activitiesModel - - QtControls.CheckBox { - id: activityCheck - - text: model.name - icon.name: model.iconSource || "preferences-activities" - - // FIXME make icons work in QQC2 desktop style CheckBox - contentItem: RowLayout { - Kirigami.Icon { - Layout.leftMargin: indicator.width + Kirigami.Units.smallSpacing - source: activityCheck.icon.name - Layout.preferredWidth: Kirigami.Units.iconSizes.small - Layout.preferredHeight: Kirigami.Units.iconSizes.small - } - - QtControls.Label { - text: activityCheck.text - } - } - } - } - } - Kirigami.Separator { Kirigami.FormData.isSection: true } QtControls.CheckBox { Kirigami.FormData.label: i18n("Critical notifications:") text: i18n("Show in do not disturb mode") - checked: true + checked: kcm.settings.criticalPopupsInDoNotDisturbMode + onClicked: kcm.settings.criticalPopupsInDoNotDisturbMode = checked } QtControls.CheckBox { text: i18n("Keep always on top") - checked: true + 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.ButtonGroup { id: positionGroup - buttons: [positionCloseToPanel, positionCustomPosition] + buttons: [positionNearWidget, positionCustomPosition] } QtControls.RadioButton { - id: positionCloseToPanel + id: positionNearWidget Kirigami.FormData.label: i18n("Popup position:") - text: i18n("Near the notification icon") // "widget" - checked: true + 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 } RowLayout { QtControls.RadioButton { id: positionCustomPosition text: i18n("Custom Position") + checked: kcm.settings.popupPosition !== NotificationManager.Settings.NearWidget } QtControls.Button { text: i18n("Choose...") + icon.name: "preferences-desktop-display" onClicked: kcm.push("PopupPositionPage.qml") enabled: positionCustomPosition.checked } } QtControls.ComboBox { Layout.fillWidth: false Kirigami.FormData.label: i18n("Hide popup after:") textRole: "label" + currentIndex: { + var idx = model.findIndex(function (item) { + return item.value === kcm.settings.popupTimeout; + }); + // would be neat if we could add a custom timeout if setting isn't listed + return idx !== -1 ? idx : 0; + } + model: [ - {label: i18n("5 seconds"), value: 5}, - {label: i18n("7 seconds"), value: 7}, - {label: i18n("10 seconds"), value: 10}, - {label: i18n("15 seconds"), value: 15}, - {label: i18n("30 seconds"), value: 30}, - {label: i18n("1 minute"), value: 60} + {label: i18n("5 seconds"), value: 5 * 1000}, + {label: i18n("7 seconds"), value: 7 * 1000}, + {label: i18n("10 seconds"), value: 10 * 1000}, + {label: i18n("15 seconds"), value: 15 * 1000}, + {label: i18n("30 seconds"), value: 30 * 1000}, + {label: i18n("1 minute"), value: 60 * 1000} ] + onActivated: kcm.settings.popupTimeout = model[index].value // FIXME proper sizing, especially for the popup Layout.maximumWidth: Kirigami.Units.gridUnit * 6 } Kirigami.Separator { Kirigami.FormData.isSection: true } QtControls.CheckBox { Kirigami.FormData.label: i18n("Application progress:") text: i18n("Show in task manager") - checked: true + checked: kcm.settings.jobsInTaskManager + onClicked: kcm.settings.jobsInTaskManager = checked } QtControls.CheckBox { id: applicationJobsEnabledCheck - text: i18n("Show popup") - checked: true + text: i18nc("Show application jobs in notification widget", "Show in notifications") + checked: kcm.settings.jobsInNotifications + onClicked: kcm.settings.jobsInNotifications = checked } - RowLayout { // HACK just for indentation + RowLayout { // just for indentation QtControls.CheckBox { Layout.leftMargin: indicator.width - text: i18n("Hide when running") + 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: true + checked: kcm.settings.badgesInTaskManager + onClicked: kcm.settings.badgesInTaskManager = checked } Kirigami.Separator { Kirigami.FormData.isSection: true } QtControls.Button { text: i18n("Application Settings") icon.name: "preferences-desktop-notification" onClicked: kcm.push("SourcesPage.qml") } } } diff --git a/kcms/notifications/sourcesmodel.cpp b/kcms/notifications/sourcesmodel.cpp index a7020f680..d5d7a6daa 100644 --- a/kcms/notifications/sourcesmodel.cpp +++ b/kcms/notifications/sourcesmodel.cpp @@ -1,307 +1,345 @@ /* * 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 { return QPersistentModelIndex(index(row, 0)); } 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()) { 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 NotifyRcNameRole: return source.notifyRcName; case DesktopEntryRole: return source.desktopEntry; + case RemovableRole: return source.removable; } return QVariant(); } bool SourcesModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { return false; } if (!index.internalId()) { return false; } bool dirty = false; auto &event = m_data[index.internalId() - 1].events[index.row()]; switch (role) { case ActionsRole: event.actions = value.toStringList(); 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")} + {ActionsRole, QByteArrayLiteral("actions")}, + {RemovableRole, QByteArrayLiteral("removable")} }; } void SourcesModel::load() { beginResetModel(); m_data.clear(); + QCollator collator; + 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; 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()) { 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 + config, + false // removable }; 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); } 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 + nullptr, + false // removable }; fdoAppsData.append(source); } - QCollator collator; + const QStringList seenApps = KSharedConfig::openConfig(QStringLiteral("plasmanotifyrc"))->group("Applications").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 + }; + fdoAppsData.append(source); + } auto sortData = [&collator](const SourceData &a, const SourceData &b) { return collator.compare(a.display(), b.display()) < 0; }; 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; endResetModel(); } diff --git a/kcms/notifications/sourcesmodel.h b/kcms/notifications/sourcesmodel.h index 2ca11318d..ccdb8b7e9 100644 --- a/kcms/notifications/sourcesmodel.h +++ b/kcms/notifications/sourcesmodel.h @@ -1,103 +1,106 @@ /* * 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 + 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 + ActionsRole, + RemovableRole // for "observed" apps }; enum Type { ServiceType, KNotifyAppType, FdoAppType }; Q_ENUM(Type) Q_INVOKABLE QPersistentModelIndex makePersistentModelIndex(int row) 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; void load(); Q_SIGNALS: private: QVector m_data; };