diff --git a/applets/notifications/notificationapplet.cpp b/applets/notifications/notificationapplet.cpp index 3b4be9529..dd8fd2770 100644 --- a/applets/notifications/notificationapplet.cpp +++ b/applets/notifications/notificationapplet.cpp @@ -1,140 +1,170 @@ /* * Copyright 2018 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 "notificationapplet.h" #include #include #include #include +#include +#include #include #include #include #include #include #include "filemenu.h" #include "globalshortcuts.h" #include "thumbnailer.h" NotificationApplet::NotificationApplet(QObject *parent, const QVariantList &data) : Plasma::Applet(parent, data) { static bool s_typesRegistered = false; if (!s_typesRegistered) { const char uri[] = "org.kde.plasma.private.notifications"; qmlRegisterType(uri, 2, 0, "FileMenu"); qmlRegisterType(uri, 2, 0, "GlobalShortcuts"); qmlRegisterType(uri, 2, 0, "Thumbnailer"); qmlProtectModule(uri, 2); s_typesRegistered = true; } connect(qApp, &QGuiApplication::focusWindowChanged, this, &NotificationApplet::focussedPlasmaDialogChanged); } NotificationApplet::~NotificationApplet() = default; void NotificationApplet::init() { } void NotificationApplet::configChanged() { } bool NotificationApplet::dragActive() const { return m_dragActive; } +int NotificationApplet::dragPixmapSize() const +{ + return m_dragPixmapSize; +} + +void NotificationApplet::setDragPixmapSize(int dragPixmapSize) +{ + if (m_dragPixmapSize != dragPixmapSize) { + m_dragPixmapSize = dragPixmapSize; + emit dragPixmapSizeChanged(); + } +} + bool NotificationApplet::isDrag(int oldX, int oldY, int newX, int newY) const { return ((QPoint(oldX, oldY) - QPoint(newX, newY)).manhattanLength() >= qApp->styleHints()->startDragDistance()); } +void NotificationApplet::startDrag(QQuickItem *item, const QUrl &url, const QString &iconName) +{ + startDrag(item, url, QIcon::fromTheme(iconName).pixmap(m_dragPixmapSize, m_dragPixmapSize)); +} + void NotificationApplet::startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap) { // This allows the caller to return, making sure we don't crash if // the caller is destroyed mid-drag QMetaObject::invokeMethod(this, "doDrag", Qt::QueuedConnection, Q_ARG(QQuickItem*, item), Q_ARG(QUrl, url), Q_ARG(QPixmap, pixmap)); } void NotificationApplet::doDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap) { if (item && item->window() && item->window()->mouseGrabberItem()) { item->window()->mouseGrabberItem()->ungrabMouse(); } QDrag *drag = new QDrag(item); QMimeData *mimeData = new QMimeData(); if (!url.isEmpty()) { mimeData->setUrls(QList() << url); } drag->setMimeData(mimeData); if (!pixmap.isNull()) { drag->setPixmap(pixmap); } m_dragActive = true; emit dragActiveChanged(); drag->exec(); m_dragActive = false; emit dragActiveChanged(); } QWindow *NotificationApplet::focussedPlasmaDialog() const { return qobject_cast(qApp->focusWindow()); } void NotificationApplet::setSelectionClipboardText(const QString &text) { // FIXME KDeclarative Clipboard item uses QClipboard::Mode for "mode" // which is an enum inaccessible from QML QGuiApplication::clipboard()->setText(text, QClipboard::Selection); } bool NotificationApplet::isPrimaryScreen(const QRect &rect) const { QScreen *screen = QGuiApplication::primaryScreen(); if (!screen) { return false; } // HACK return rect == screen->geometry(); } +QString NotificationApplet::iconNameForUrl(const QUrl &url) const +{ + QMimeType mime = QMimeDatabase().mimeTypeForUrl(url); + if (mime.isDefault()) { + return QString(); + } + + return mime.iconName(); +} + K_EXPORT_PLASMA_APPLET_WITH_JSON(icon, NotificationApplet, "metadata.json") #include "notificationapplet.moc" diff --git a/applets/notifications/notificationapplet.h b/applets/notifications/notificationapplet.h index 5756a7f58..7a78b36fc 100644 --- a/applets/notifications/notificationapplet.h +++ b/applets/notifications/notificationapplet.h @@ -1,65 +1,75 @@ /* * Copyright 2018 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 QQuickItem; class QString; class QRect; class NotificationApplet : public Plasma::Applet { Q_OBJECT Q_PROPERTY(bool dragActive READ dragActive NOTIFY dragActiveChanged) + Q_PROPERTY(int dragPixmapSize READ dragPixmapSize WRITE setDragPixmapSize NOTIFY dragPixmapSizeChanged) Q_PROPERTY(QWindow *focussedPlasmaDialog READ focussedPlasmaDialog NOTIFY focussedPlasmaDialogChanged) public: explicit NotificationApplet(QObject *parent, const QVariantList &data); ~NotificationApplet() override; void init() override; void configChanged() override; bool dragActive() const; + + int dragPixmapSize() const; + void setDragPixmapSize(int dragPixmapSize); + Q_INVOKABLE bool isDrag(int oldX, int oldY, int newX, int newY) const; + Q_INVOKABLE void startDrag(QQuickItem *item, const QUrl &url, const QString &iconName); Q_INVOKABLE void startDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap); QWindow *focussedPlasmaDialog() const; Q_INVOKABLE void setSelectionClipboardText(const QString &text); Q_INVOKABLE bool isPrimaryScreen(const QRect &rect) const; + Q_INVOKABLE QString iconNameForUrl(const QUrl &url) const; + signals: void dragActiveChanged(); + void dragPixmapSizeChanged(); void focussedPlasmaDialogChanged(); private slots: void doDrag(QQuickItem *item, const QUrl &url, const QPixmap &pixmap); private: bool m_dragActive = false; + int m_dragPixmapSize = 48; // Bound to units.iconSizes.large in main.qml }; diff --git a/applets/notifications/package/contents/ui/JobItem.qml b/applets/notifications/package/contents/ui/JobItem.qml index 3d7e87030..e90dd9060 100644 --- a/applets/notifications/package/contents/ui/JobItem.qml +++ b/applets/notifications/package/contents/ui/JobItem.qml @@ -1,201 +1,242 @@ /* * 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.8 import QtQuick.Window 2.2 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.notificationmanager 1.0 as NotificationManager import org.kde.plasma.private.notifications 2.0 as Notifications ColumnLayout { id: jobItem property int jobState property int jobError property alias percentage: progressBar.value property alias suspendable: suspendButton.visible property alias killable: killButton.visible property bool hovered property QtObject jobDetails // TOOD make an alias on visible if we're not doing an animation property bool showDetails + readonly property int totalFiles: jobItem.jobDetails && jobItem.jobDetails.totalFiles || 0 + readonly property var url: { + if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped + || jobItem.jobError + || totalFiles <= 0) { + return null; + } + + // For a single file show actions for it + if (totalFiles === 1) { + return jobItem.jobDetails.descriptionUrl; + // Otherwise the destination folder all of them were copied into + } else { + return jobItem.jobDetails.destUrl; + } + } + + property alias iconContainerItem: jobDragIcon.parent + + readonly property alias dragging: jobDragArea.dragging readonly property alias menuOpen: otherFileActionsMenu.visible signal suspendJobClicked signal resumeJobClicked signal killJobClicked signal openUrl(string url) signal fileActionInvoked spacing: 0 + // This item is parented to the NotificationItem iconContainer + PlasmaCore.IconItem { + id: jobDragIcon + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 + usesPlasmaTheme: false + visible: valid + active: jobDragArea.containsMouse + source: jobItem.totalFiles === 1 && jobItem.url ? plasmoid.nativeInterface.iconNameForUrl(jobItem.url) : "" + + Binding { + target: jobDragIcon.parent + property: "visible" + value: true + when: jobDragIcon.valid + } + + DraggableFileArea { + id: jobDragArea + anchors.fill: parent + + hoverEnabled: true + dragParent: jobDragIcon + dragUrl: jobItem.url || "" + dragPixmap: jobDragIcon.source + + onActivated: jobItem.openUrl(jobItem.url) + onContextMenuRequested: { + // avoid menu button glowing if we didn't actually press it + otherFileActionsButton.checked = false; + + otherFileActionsMenu.visualParent = this; + otherFileActionsMenu.open(x, y); + } + } + } + RowLayout { id: progressRow Layout.fillWidth: true spacing: units.smallSpacing PlasmaComponents.ProgressBar { id: progressBar Layout.fillWidth: true minimumValue: 0 maximumValue: 100 // TODO do we actually need the window visible check? perhaps I do because it can be in popup or expanded plasmoid indeterminate: visible && Window.window && Window.window.visible && percentage < 1 && jobItem.jobState === NotificationManager.Notifications.JobStateRunning // is this too annoying? && (jobItem.jobDetails.processedBytes === 0 || jobItem.jobDetails.totalBytes === 0) && jobItem.jobDetails.processedFiles === 0 //&& jobItem.jobDetails.processedDirectories === 0 } RowLayout { spacing: 0 PlasmaComponents.ToolButton { id: suspendButton tooltip: i18ndc("plasma_applet_org.kde.plasma.notifications", "Pause running job", "Pause") iconSource: "media-playback-pause" onClicked: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended ? jobItem.resumeJobClicked() : jobItem.suspendJobClicked() } PlasmaComponents.ToolButton { id: killButton tooltip: i18ndc("plasma_applet_org.kde.plasma.notifications", "Cancel running job", "Cancel") iconSource: "media-playback-stop" onClicked: jobItem.killJobClicked() } PlasmaComponents.ToolButton { id: expandButton iconSource: checked ? "arrow-down" : (LayoutMirroring.enabled ? "arrow-left" : "arrow-right") tooltip: checked ? i18ndc("plasma_applet_org.kde.plasma.notifications", "A button tooltip; hides item details", "Hide Details") : i18ndc("plasma_applet_org.kde.plasma.notifications", "A button tooltip; expands the item to show details", "Show Details") checkable: true enabled: jobItem.jobDetails && jobItem.jobDetails.hasDetails } } } Loader { Layout.fillWidth: true active: expandButton.checked // Loader doesn't reset its height when unloaded, just hide it altogether visible: active sourceComponent: JobDetails { jobDetails: jobItem.jobDetails } } Flow { // it's a Flow so it can wrap if too long - id: jobDoneActions Layout.fillWidth: true spacing: units.smallSpacing // We want the actions to be right-aligned but Flow also reverses // the order of items, so we put them in reverse order layoutDirection: Qt.RightToLeft visible: url && url.toString() !== "" - property var url: { - if (jobItem.jobState !== NotificationManager.Notifications.JobStateStopped - || jobItem.jobError - || !jobItem.jobDetails - || jobItem.jobDetails.totalFiles <= 0) { - return null; - } - - // For a single file show actions for it - if (jobItem.jobDetails.totalFiles === 1) { - return jobItem.jobDetails.descriptionUrl; - } else { - return jobItem.jobDetails.destUrl; - } - } - PlasmaComponents.Button { id: otherFileActionsButton height: Math.max(implicitHeight, openButton.implicitHeight) iconName: "application-menu" tooltip: i18nd("plasma_applet_org.kde.plasma.notifications", "More Options...") checkable: true onPressedChanged: { if (pressed) { checked = Qt.binding(function() { return otherFileActionsMenu.visible; }); + otherFileActionsMenu.visualParent = this; + // -1 tells it to "align bottom left of visualParent (this)" otherFileActionsMenu.open(-1, -1); } } Notifications.FileMenu { id: otherFileActionsMenu - url: jobDoneActions.url || "" - visualParent: otherFileActionsButton + url: jobItem.url || "" onActionTriggered: jobItem.fileActionInvoked() } } PlasmaComponents.Button { id: openButton height: Math.max(implicitHeight, otherFileActionsButton.implicitHeight) // would be nice to have the file icon here? text: jobItem.jobDetails && jobItem.jobDetails.totalFiles > 1 ? i18nd("plasma_applet_org.kde.plasma.notifications", "Open Containing Folder") : i18nd("plasma_applet_org.kde.plasma.notifications", "Open") - onClicked: jobItem.openUrl(jobDoneActions.url) + onClicked: jobItem.openUrl(jobItem.url) width: minimumWidth } } states: [ State { when: jobItem.jobState === NotificationManager.Notifications.JobStateSuspended PropertyChanges { target: suspendButton tooltip: i18ndc("plasma_applet_org.kde.plasma.notifications", "Resume paused job", "Resume") iconSource: "media-playback-start" } PropertyChanges { target: progressBar enabled: false } }, State { when: jobItem.jobState === NotificationManager.Notifications.JobStateStopped PropertyChanges { target: progressRow visible: false } PropertyChanges { target: expandButton checked: false } } ] } diff --git a/applets/notifications/package/contents/ui/NotificationItem.qml b/applets/notifications/package/contents/ui/NotificationItem.qml index ac262b0a3..6d36e3d60 100644 --- a/applets/notifications/package/contents/ui/NotificationItem.qml +++ b/applets/notifications/package/contents/ui/NotificationItem.qml @@ -1,368 +1,373 @@ /* * Copyright 2018-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.8 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.kquickcontrolsaddons 2.0 as KQCAddons import org.kde.notificationmanager 1.0 as NotificationManager ColumnLayout { id: notificationItem property bool hovered: false property int maximumLineCount: 0 property alias bodyCursorShape: bodyLabel.cursorShape property int notificationType property bool inGroup: false property alias applicationIconSource: notificationHeading.applicationIconSource property alias applicationName: notificationHeading.applicationName property alias originName: notificationHeading.originName property string summary property alias time: notificationHeading.time property alias configurable: notificationHeading.configurable property alias dismissable: notificationHeading.dismissable property alias dismissed: notificationHeading.dismissed property alias closable: notificationHeading.closable // This isn't an alias because TextEdit RichText adds some HTML tags to it property string body property var icon property var urls: [] property int jobState property int percentage property int jobError: 0 property bool suspendable property bool killable property QtObject jobDetails property bool showDetails property alias configureActionLabel: notificationHeading.configureActionLabel property var actionNames: [] property var actionLabels: [] property int headingLeftPadding: 0 property int headingRightPadding: 0 property int thumbnailLeftPadding: 0 property int thumbnailRightPadding: 0 property int thumbnailTopPadding: 0 property int thumbnailBottomPadding: 0 property alias timeout: notificationHeading.timeout property alias remainingTime: notificationHeading.remainingTime readonly property bool menuOpen: bodyLabel.contextMenu !== null || (thumbnailStripLoader.item && thumbnailStripLoader.item.menuOpen) || (jobLoader.item && jobLoader.item.menuOpen) - readonly property bool dragging: thumbnailStripLoader.item && thumbnailStripLoader.item.dragging + readonly property bool dragging: (thumbnailStripLoader.item && thumbnailStripLoader.item.dragging) + || (jobLoader.item && jobLoader.item.dragging) signal bodyClicked(var mouse) signal closeClicked signal configureClicked signal dismissClicked signal actionInvoked(string actionName) signal openUrl(string url) signal fileActionInvoked signal suspendJobClicked signal resumeJobClicked signal killJobClicked spacing: units.smallSpacing NotificationHeader { id: notificationHeading Layout.fillWidth: true Layout.leftMargin: notificationItem.headingLeftPadding Layout.rightMargin: notificationItem.headingRightPadding inGroup: notificationItem.inGroup notificationType: notificationItem.notificationType jobState: notificationItem.jobState jobDetails: notificationItem.jobDetails onConfigureClicked: notificationItem.configureClicked() onDismissClicked: notificationItem.dismissClicked() onCloseClicked: notificationItem.closeClicked() } RowLayout { id: defaultHeaderContainer Layout.fillWidth: true } // Notification body RowLayout { id: bodyRow Layout.fillWidth: true spacing: units.smallSpacing ColumnLayout { Layout.fillWidth: true spacing: 0 RowLayout { id: summaryRow Layout.fillWidth: true visible: summaryLabel.text !== "" PlasmaExtras.Heading { id: summaryLabel Layout.fillWidth: true Layout.preferredHeight: implicitHeight textFormat: Text.PlainText maximumLineCount: 3 wrapMode: Text.WordWrap elide: Text.ElideRight level: 4 text: { if (notificationItem.notificationType === NotificationManager.Notifications.JobType) { if (notificationItem.jobState === NotificationManager.Notifications.JobStateSuspended) { if (notificationItem.summary) { return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying is paused", "%1 (Paused)", notificationItem.summary); } } else if (notificationItem.jobState === NotificationManager.Notifications.JobStateStopped) { if (notificationItem.jobError) { if (notificationItem.summary) { return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has failed", "%1 (Failed)", notificationItem.summary); } else { return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Failed"); } } else { if (notificationItem.summary) { return i18ndc("plasma_applet_org.kde.plasma.notifications", "Job name, e.g. Copying has finished", "%1 (Finished)", notificationItem.summary); } else { return i18nd("plasma_applet_org.kde.plasma.notifications", "Job Finished"); } } } } // some apps use their app name as summary, avoid showing the same text twice // try very hard to match the two if (notificationItem.summary && notificationItem.summary.toLocaleLowerCase().trim() != notificationItem.applicationName.toLocaleLowerCase().trim()) { return notificationItem.summary; } return ""; } visible: text !== "" } // inGroup headerItem is reparented here } RowLayout { id: bodyTextRow Layout.fillWidth: true spacing: units.smallSpacing SelectableLabel { id: bodyLabel // FIXME how to assign this via State? target: bodyLabel.Layout doesn't work and just assigning the property doesn't either Layout.alignment: notificationItem.inGroup ? Qt.AlignTop : Qt.AlignVCenter Layout.fillWidth: true Layout.maximumHeight: notificationItem.maximumLineCount > 0 ? (theme.mSize(font).height * notificationItem.maximumLineCount) : -1 text: notificationItem.body // Cannot do text !== "" because RichText adds some HTML tags even when empty visible: notificationItem.body !== "" onClicked: notificationItem.bodyClicked(mouse) onLinkActivated: Qt.openUrlExternally(link) } // inGroup iconContainer is reparented here } } Item { id: iconContainer Layout.preferredWidth: units.iconSizes.large Layout.preferredHeight: units.iconSizes.large visible: iconItem.active || imageItem.active PlasmaCore.IconItem { id: iconItem // don't show two identical icons readonly property bool active: valid && source != notificationItem.applicationIconSource anchors.fill: parent usesPlasmaTheme: false smooth: true source: { var icon = notificationItem.icon; if (typeof icon !== "string") { // displayed by QImageItem below return ""; } // don't show a generic "info" icon since this is a notification already if (icon === "dialog-information") { return ""; } return icon; } visible: active } KQCAddons.QImageItem { id: imageItem readonly property bool active: !null && nativeWidth > 0 anchors.fill: parent smooth: true fillMode: KQCAddons.QImageItem.PreserveAspectFit visible: active image: typeof notificationItem.icon === "object" ? notificationItem.icon : undefined } + + // JobItem reparents a file icon here for finished jobs with one total file } } // Job progress reporting Loader { id: jobLoader Layout.fillWidth: true active: notificationItem.notificationType === NotificationManager.Notifications.JobType visible: active sourceComponent: JobItem { + iconContainerItem: iconContainer + jobState: notificationItem.jobState jobError: notificationItem.jobError percentage: notificationItem.percentage suspendable: notificationItem.suspendable killable: notificationItem.killable jobDetails: notificationItem.jobDetails showDetails: notificationItem.showDetails onSuspendJobClicked: notificationItem.suspendJobClicked() onResumeJobClicked: notificationItem.resumeJobClicked() onKillJobClicked: notificationItem.killJobClicked() onOpenUrl: notificationItem.openUrl(url) onFileActionInvoked: notificationItem.fileActionInvoked() hovered: notificationItem.hovered } } RowLayout { Layout.fillWidth: true visible: actionRepeater.count > 0 // Notification actions Flow { // it's a Flow so it can wrap if too long Layout.fillWidth: true spacing: units.smallSpacing layoutDirection: Qt.RightToLeft Repeater { id: actionRepeater model: { var buttons = []; // HACK We want the actions to be right-aligned but Flow also reverses var actionNames = (notificationItem.actionNames || []).reverse(); var actionLabels = (notificationItem.actionLabels || []).reverse(); for (var i = 0; i < actionNames.length; ++i) { buttons.push({ actionName: actionNames[i], label: actionLabels[i] }); } return buttons; } PlasmaComponents.ToolButton { flat: false // why does it spit "cannot assign undefined to string" when a notification becomes expired? text: modelData.label || "" Layout.preferredWidth: minimumWidth onClicked: notificationItem.actionInvoked(modelData.actionName) } } } } // thumbnails Loader { id: thumbnailStripLoader Layout.leftMargin: notificationItem.thumbnailLeftPadding Layout.rightMargin: notificationItem.thumbnailRightPadding // no change in Layout.topMargin to keep spacing to notification text consistent Layout.topMargin: 0 Layout.bottomMargin: notificationItem.thumbnailBottomPadding Layout.fillWidth: true active: notificationItem.urls.length > 0 visible: active sourceComponent: ThumbnailStrip { leftPadding: -thumbnailStripLoader.Layout.leftMargin rightPadding: -thumbnailStripLoader.Layout.rightMargin topPadding: -notificationItem.thumbnailTopPadding bottomPadding: -thumbnailStripLoader.Layout.bottomMargin urls: notificationItem.urls onOpenUrl: notificationItem.openUrl(url) onFileActionInvoked: notificationItem.fileActionInvoked() } } states: [ State { when: notificationItem.inGroup PropertyChanges { target: notificationHeading parent: summaryRow } PropertyChanges { target: summaryRow visible: true } PropertyChanges { target: summaryLabel visible: true } /*PropertyChanges { target: bodyLabel.Label alignment: Qt.AlignTop }*/ PropertyChanges { target: iconContainer parent: bodyTextRow } } ] } diff --git a/applets/notifications/package/contents/ui/main.qml b/applets/notifications/package/contents/ui/main.qml index 5b93e19d0..5b31ad787 100644 --- a/applets/notifications/package/contents/ui/main.qml +++ b/applets/notifications/package/contents/ui/main.qml @@ -1,159 +1,165 @@ /* * Copyright 2018-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.8 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.kquickcontrolsaddons 2.0 import org.kde.kcoreaddons 1.0 as KCoreAddons import org.kde.kquickcontrolsaddons 2.0 as KQCAddons import org.kde.notificationmanager 1.0 as NotificationManager import "global" Item { id: root Plasmoid.status: historyModel.activeJobsCount > 0 || historyModel.unreadNotificationsCount > 0 || Globals.inhibited ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus Plasmoid.toolTipSubText: { var lines = []; if (historyModel.activeJobsCount > 0) { lines.push(i18np("%1 running job", "%1 running jobs", historyModel.activeJobsCount)); } if (!NotificationManager.Server.valid) { lines.push(i18n("Notification service not available")); } else { // Any notification that is newer than "lastRead" is "unread" // since it doesn't know the popup is on screen which makes the user see it var actualUnread = historyModel.unreadNotificationsCount - Globals.popupNotificationsModel.activeNotificationsCount; if (actualUnread > 0) { lines.push(i18np("%1 unread notification", "%1 unread notifications", actualUnread)); } if (Globals.inhibited) { var inhibitedUntil = notificationSettings.notificationsInhibitedUntil var inhibitedUntilValid = !isNaN(inhibitedUntil.getTime()); // Show until time if valid but not if too far in the future // TODO check app inhibition, too if (inhibitedUntilValid && inhibitedUntil.getTime() - new Date().getTime() < 365 * 24 * 60 * 60 * 1000 /* 1 year*/) { lines.push(i18n("Do not disturb until %1", KCoreAddons.Format.formatRelativeDateTime(inhibitedUntil, Locale.ShortFormat))); } else { lines.push(i18n("Do not disturb")); } } else if (lines.length === 0) { lines.push(i18n("No unread notifications")); } } return lines.join("\n"); } Plasmoid.switchWidth: units.gridUnit * 14 // This is to let the plasmoid expand in a vertical panel for a "sidebar" notification panel // The CompactRepresentation size is limited to not have the notification icon grow gigantic // but it should still switch over to full rep once there's enough width (disregarding the limited height) Plasmoid.switchHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? 1 : units.gridUnit * 10 Plasmoid.onExpandedChanged: { if (!plasmoid.expanded) { // FIXME Qt.callLater because system tray gets confused when an applet becomes passive when clicking to hide it Qt.callLater(function() { historyModel.lastRead = undefined; // reset to now historyModel.collapseAllGroups(); }); } } Plasmoid.compactRepresentation: CompactRepresentation { activeCount: Globals.popupNotificationsModel.activeNotificationsCount unreadCount: Math.min(99, historyModel.unreadNotificationsCount) jobsCount: historyModel.activeJobsCount jobsPercentage: historyModel.jobsPercentage inhibited: Globals.inhibited || !NotificationManager.Server.valid } Plasmoid.fullRepresentation: FullRepresentation { } NotificationManager.Settings { id: notificationSettings } NotificationManager.Notifications { id: historyModel showExpired: true showDismissed: true showJobs: notificationSettings.jobsInNotifications sortMode: NotificationManager.Notifications.SortByTypeAndUrgency groupMode: NotificationManager.Notifications.GroupApplicationsFlat groupLimit: 2 expandUnread: true blacklistedDesktopEntries: notificationSettings.historyBlacklistedApplications blacklistedNotifyRcNames: notificationSettings.historyBlacklistedServices urgencies: { var urgencies = NotificationManager.Notifications.CriticalUrgency | NotificationManager.Notifications.NormalUrgency; if (notificationSettings.lowPriorityHistory) { urgencies |= NotificationManager.Notifications.LowUrgency; } return urgencies; } } + Binding { + target: plasmoid.nativeInterface + property: "dragPixmapSize" + value: units.iconSizes.large + } + function action_clearHistory() { historyModel.clear(NotificationManager.Notifications.ClearExpired); if (historyModel.count === 0) { plasmoid.expanded = false; } } function action_openKcm() { KQCAddons.KCMShell.open("kcm_notifications"); } Component.onCompleted: { Globals.adopt(plasmoid); plasmoid.setAction("clearHistory", i18n("Clear History"), "edit-clear-history"); var clearAction = plasmoid.action("clearHistory"); clearAction.visible = Qt.binding(function() { return historyModel.expiredNotificationsCount > 0; }); // FIXME only while Multi-page KCMs are broken when embedded in plasmoid config plasmoid.setAction("openKcm", i18n("&Configure Event Notifications and Actions..."), "preferences-desktop-notification-bell"); plasmoid.action("openKcm").visible = (KQCAddons.KCMShell.authorize("kcm_notifications.desktop").length > 0); } }