diff --git a/applets/notifications/package/contents/ui/DraggableDelegate.qml b/applets/notifications/package/contents/ui/DraggableDelegate.qml new file mode 100644 --- /dev/null +++ b/applets/notifications/package/contents/ui/DraggableDelegate.qml @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Marco Martin + * + * 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.10 +import org.kde.kirigami 2.11 as Kirigami + +MouseArea { + id: delegate + + property Item contentItem + property bool draggable: false + signal dismissRequested + + implicitWidth: contentItem ? contentItem.implicitWidth : 0 + implicitHeight: contentItem ? contentItem.implicitHeight : 0 + opacity: 1 - Math.min(1, 1.5 * Math.abs(x) / width) + + drag { + filterChildren: draggable + axis: Drag.XAxis + target: draggable && Kirigami.Settings.tabletMode ? this : null + } + + onReleased: { + if (Math.abs(x) > width / 2) { + delegate.dismissRequested(); + } else { + slideAnim.restart(); + } + } + + NumberAnimation { + id: slideAnim + target: delegate + property:"x" + to: 0 + duration: units.longDuration + } +} diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml --- a/applets/notifications/package/contents/ui/FullRepresentation.qml +++ b/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -18,14 +18,15 @@ * along with this program. If not, see */ -import QtQuick 2.8 +import QtQuick 2.10 import QtQuick.Layouts 1.1 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras +import org.kde.kirigami 2.11 as Kirigami import org.kde.kcoreaddons 1.0 as KCoreAddons @@ -66,6 +67,8 @@ // header ColumnLayout { + id: header + visible: !Kirigami.Settings.isMobile Layout.fillWidth: true spacing: 0 @@ -262,6 +265,7 @@ } PlasmaCore.SvgItem { + visible: header.visible elementId: "horizontal-line" Layout.fillWidth: true // why is this needed here but not in the delegate? @@ -371,9 +375,14 @@ } remove: Transition { + id: removeTransition ParallelAnimation { NumberAnimation { property: "opacity"; to: 0; duration: units.longDuration } - NumberAnimation { property: "x"; to: list.width; duration: units.longDuration } + NumberAnimation { + property: "x" + to: removeTransition.ViewTransition.item.x >= 0 ? list.width : -list.width + duration: units.longDuration + } } } removeDisplaced: Transition { @@ -389,170 +398,179 @@ criteria: ViewSection.FullString } - delegate: Loader { - id: delegateLoader + delegate: DraggableDelegate { width: list.width - sourceComponent: model.isGroup ? groupDelegate : notificationDelegate + contentItem: delegateLoader - Component { - id: groupDelegate - NotificationHeader { - applicationName: model.applicationName - applicationIconSource: model.applicationIconName - originName: model.originName || "" + draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType - // don't show timestamp for group + onDismissRequested: historyModel.close(historyModel.index(index, 0)); - configurable: model.configurable - closable: model.closable - closeButtonTooltip: i18n("Close Group") + Loader { + id: delegateLoader + width: list.width + sourceComponent: model.isGroup ? groupDelegate : notificationDelegate - onCloseClicked: { - historyModel.close(historyModel.index(index, 0)) - if (list.count === 0) { - plasmoid.expanded = false; - } - } + Component { + id: groupDelegate + NotificationHeader { + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" - onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) - } - } + // don't show timestamp for group - Component { - id: notificationDelegate - ColumnLayout { - spacing: units.smallSpacing - - RowLayout { - Item { - id: groupLineContainer - Layout.fillHeight: true - Layout.topMargin: units.smallSpacing - width: units.iconSizes.small - visible: model.isInGroup - - PlasmaCore.SvgItem { - elementId: "vertical-line" - svg: lineSvg - anchors.horizontalCenter: parent.horizontalCenter - width: units.iconSizes.small - height: parent.height + configurable: model.configurable + closable: model.closable + closeButtonTooltip: i18n("Close Group") + + onCloseClicked: { + historyModel.close(historyModel.index(index, 0)) + if (list.count === 0) { + plasmoid.expanded = false; } } - NotificationItem { - Layout.fillWidth: true + onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) + } + } + + Component { + id: notificationDelegate + ColumnLayout { + spacing: units.smallSpacing - notificationType: model.type - - inGroup: model.isInGroup - - applicationName: model.applicationName - applicationIconSource: model.applicationIconName - originName: model.originName || "" - - time: model.updated || model.created - - // configure button on every single notifications is bit overwhelming - configurable: !inGroup && model.configurable - - dismissable: model.type === NotificationManager.Notifications.JobType - && model.jobState !== NotificationManager.Notifications.JobStateStopped - && model.dismissed - // TODO would be nice to be able to undismiss jobs even when they autohide - && notificationSettings.permanentJobPopups - dismissed: model.dismissed || false - closable: model.closable - - summary: model.summary - body: model.body || "" - icon: model.image || model.iconName - - urls: model.urls || [] - - jobState: model.jobState || 0 - percentage: model.percentage || 0 - jobError: model.jobError || 0 - suspendable: !!model.suspendable - killable: !!model.killable - jobDetails: model.jobDetails || null - - configureActionLabel: model.configureActionLabel || "" - // In the popup the default action is triggered by clicking on the popup - // however in the list this is undesirable, so instead show a clickable button - // in case you have a non-expired notification in history (do not disturb mode) - // unless it has the same label as an action - readonly property bool addDefaultAction: (model.hasDefaultAction - && model.defaultActionLabel - && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false - actionNames: { - var actions = (model.actionNames || []); - if (addDefaultAction) { - actions.unshift("default"); // prepend + RowLayout { + Item { + id: groupLineContainer + Layout.fillHeight: true + Layout.topMargin: units.smallSpacing + width: units.iconSizes.small + visible: model.isInGroup + + PlasmaCore.SvgItem { + elementId: "vertical-line" + svg: lineSvg + anchors.horizontalCenter: parent.horizontalCenter + width: units.iconSizes.small + height: parent.height } - return actions; } - actionLabels: { - var labels = (model.actionLabels || []); - if (addDefaultAction) { - labels.unshift(model.defaultActionLabel); + + NotificationItem { + Layout.fillWidth: true + + notificationType: model.type + + inGroup: model.isInGroup + + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" + + time: model.updated || model.created + + // configure button on every single notifications is bit overwhelming + configurable: !inGroup && model.configurable + + dismissable: model.type === NotificationManager.Notifications.JobType + && model.jobState !== NotificationManager.Notifications.JobStateStopped + && model.dismissed + // TODO would be nice to be able to undismiss jobs even when they autohide + && notificationSettings.permanentJobPopups + dismissed: model.dismissed || false + closable: model.closable + + summary: model.summary + body: model.body || "" + icon: model.image || model.iconName + + urls: model.urls || [] + + jobState: model.jobState || 0 + percentage: model.percentage || 0 + jobError: model.jobError || 0 + suspendable: !!model.suspendable + killable: !!model.killable + jobDetails: model.jobDetails || null + + configureActionLabel: model.configureActionLabel || "" + // In the popup the default action is triggered by clicking on the popup + // however in the list this is undesirable, so instead show a clickable button + // in case you have a non-expired notification in history (do not disturb mode) + // unless it has the same label as an action + readonly property bool addDefaultAction: (model.hasDefaultAction + && model.defaultActionLabel + && (model.actionLabels || []).indexOf(model.defaultActionLabel) === -1) ? true : false + actionNames: { + var actions = (model.actionNames || []); + if (addDefaultAction) { + actions.unshift("default"); // prepend + } + return actions; + } + actionLabels: { + var labels = (model.actionLabels || []); + if (addDefaultAction) { + labels.unshift(model.defaultActionLabel); + } + return labels; } - return labels; - } - onCloseClicked: { - historyModel.close(historyModel.index(index, 0)); - if (list.count === 0) { + onCloseClicked: { + historyModel.close(historyModel.index(index, 0)); + if (list.count === 0) { + plasmoid.expanded = false; + } + } + onDismissClicked: { + model.dismissed = false; plasmoid.expanded = false; } - } - onDismissClicked: { - model.dismissed = false; - plasmoid.expanded = false; - } - onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) - - onActionInvoked: { - if (actionName === "default") { - historyModel.invokeDefaultAction(historyModel.index(index, 0)); - } else { - historyModel.invokeAction(historyModel.index(index, 0), actionName); + onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) + + onActionInvoked: { + if (actionName === "default") { + historyModel.invokeDefaultAction(historyModel.index(index, 0)); + } else { + historyModel.invokeAction(historyModel.index(index, 0), actionName); + } + // Keep it in the history + historyModel.expire(historyModel.index(index, 0)); } - // Keep it in the history - historyModel.expire(historyModel.index(index, 0)); - } - onOpenUrl: { - Qt.openUrlExternally(url); - historyModel.expire(historyModel.index(index, 0)); - } - onFileActionInvoked: historyModel.expire(historyModel.index(index, 0)) + onOpenUrl: { + Qt.openUrlExternally(url); + historyModel.expire(historyModel.index(index, 0)); + } + onFileActionInvoked: historyModel.expire(historyModel.index(index, 0)) - onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0)) - onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0)) - onKillJobClicked: historyModel.killJob(historyModel.index(index, 0)) + onSuspendJobClicked: historyModel.suspendJob(historyModel.index(index, 0)) + onResumeJobClicked: historyModel.resumeJob(historyModel.index(index, 0)) + onKillJobClicked: historyModel.killJob(historyModel.index(index, 0)) + } } - } - PlasmaComponents.ToolButton { - Layout.preferredWidth: minimumWidth - iconName: model.isGroupExpanded ? "arrow-up" : "arrow-down" - text: model.isGroupExpanded ? i18n("Show Fewer") - : i18nc("Expand to show n more notifications", - "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) - visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) - && delegateLoader.ListView.nextSection !== delegateLoader.ListView.section - onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) - } + PlasmaComponents.ToolButton { + Layout.preferredWidth: minimumWidth + iconName: model.isGroupExpanded ? "arrow-up" : "arrow-down" + text: model.isGroupExpanded ? i18n("Show Fewer") + : i18nc("Expand to show n more notifications", + "Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) + visible: (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded) + && delegateLoader.ListView.nextSection !== delegateLoader.ListView.section + onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) + } - PlasmaCore.SvgItem { - Layout.fillWidth: true - Layout.bottomMargin: units.smallSpacing - elementId: "horizontal-line" - svg: lineSvg + PlasmaCore.SvgItem { + Layout.fillWidth: true + Layout.bottomMargin: units.smallSpacing + elementId: "horizontal-line" + svg: lineSvg - // property is only atached to the delegate itself (the Loader in our case) - visible: (!model.isInGroup || delegateLoader.ListView.nextSection !== delegateLoader.ListView.section) - && delegateLoader.ListView.nextSection !== "" // don't show after last item + // property is only atached to the delegate itself (the Loader in our case) + visible: (!model.isInGroup || delegateLoader.ListView.nextSection !== delegateLoader.ListView.section) + && delegateLoader.ListView.nextSection !== "" // don't show after last item + } } } } diff --git a/applets/notifications/package/contents/ui/NotificationPopup.qml b/applets/notifications/package/contents/ui/NotificationPopup.qml --- a/applets/notifications/package/contents/ui/NotificationPopup.qml +++ b/applets/notifications/package/contents/ui/NotificationPopup.qml @@ -107,104 +107,114 @@ } } - mainItem: MouseArea { - id: area + mainItem: Item { width: notificationPopup.popupWidth height: notificationItem.implicitHeight + notificationItem.y - hoverEnabled: true - - cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor - acceptedButtons: hasDefaultAction ? Qt.LeftButton : Qt.NoButton - - onClicked: notificationPopup.defaultActionInvoked() - onEntered: notificationPopup.hoverEntered() - onExited: notificationPopup.hoverExited() - - LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft - LayoutMirroring.childrenInherit: true - - Timer { - id: timer - interval: notificationPopup.effectiveTimeout - running: notificationPopup.visible && !area.containsMouse && interval > 0 - && !notificationItem.dragging && !notificationItem.menuOpen - onTriggered: { - if (notificationPopup.dismissTimeout) { - notificationPopup.dismissClicked(); - } else { - notificationPopup.expired(); - } - } - } + DraggableDelegate { + id: area + width: parent.width + height: parent.height + hoverEnabled: true + draggable: notificationItem.notificationType != NotificationManager.Notifications.JobType + onDismissRequested: popupNotificationsModel.close(popupNotificationsModel.index(index, 0)) - Timer { - id: timeoutIndicatorDelayTimer - // only show indicator for the last ten seconds of timeout - readonly property int remainingTimeout: 10000 - interval: Math.max(0, timer.interval - remainingTimeout) - running: interval > 0 && timer.running - } + cursorShape: hasDefaultAction ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: hasDefaultAction || draggable ? Qt.LeftButton : Qt.NoButton - Rectangle { - id: timeoutIndicatorRect - anchors { - right: parent.right - rightMargin: -notificationPopup.margins.right - bottom: parent.bottom - bottomMargin: -notificationPopup.margins.bottom + onClicked: { + if (hasDefaultAction) { + notificationPopup.defaultActionInvoked(); + } } - width: units.devicePixelRatio * 3 - color: theme.highlightColor - opacity: timeoutIndicatorAnimation.running ? 0.6 : 0 - visible: units.longDuration > 1 - Behavior on opacity { - NumberAnimation { - duration: units.longDuration + onEntered: notificationPopup.hoverEntered() + onExited: notificationPopup.hoverExited() + + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + Timer { + id: timer + interval: notificationPopup.effectiveTimeout + running: notificationPopup.visible && !area.containsMouse && interval > 0 + && !notificationItem.dragging && !notificationItem.menuOpen + onTriggered: { + if (notificationPopup.dismissTimeout) { + notificationPopup.dismissClicked(); + } else { + notificationPopup.expired(); + } } } - NumberAnimation { - id: timeoutIndicatorAnimation - target: timeoutIndicatorRect - property: "height" - from: area.height + notificationPopup.margins.top + notificationPopup.margins.bottom - to: 0 - duration: Math.min(timer.interval, timeoutIndicatorDelayTimer.remainingTimeout) - running: timer.running && !timeoutIndicatorDelayTimer.running && units.longDuration > 1 + Timer { + id: timeoutIndicatorDelayTimer + // only show indicator for the last ten seconds of timeout + readonly property int remainingTimeout: 10000 + interval: Math.max(0, timer.interval - remainingTimeout) + running: interval > 0 && timer.running } - } - NotificationItem { - id: notificationItem - // let the item bleed into the dialog margins so the close button margins cancel out - y: closable || dismissable || configurable ? -notificationPopup.margins.top : 0 - headingRightPadding: -notificationPopup.margins.right - width: parent.width - hovered: area.containsMouse - maximumLineCount: 8 - bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0 - - thumbnailLeftPadding: -notificationPopup.margins.left - thumbnailRightPadding: -notificationPopup.margins.right - thumbnailTopPadding: -notificationPopup.margins.top - thumbnailBottomPadding: -notificationPopup.margins.bottom - - closable: true - onBodyClicked: { - if (area.acceptedButtons & mouse.button) { - area.clicked(null /*mouse*/); + Rectangle { + id: timeoutIndicatorRect + anchors { + right: parent.right + rightMargin: -notificationPopup.margins.right + bottom: parent.bottom + bottomMargin: -notificationPopup.margins.bottom + } + width: units.devicePixelRatio * 3 + color: theme.highlightColor + opacity: timeoutIndicatorAnimation.running ? 0.6 : 0 + visible: units.longDuration > 1 + Behavior on opacity { + NumberAnimation { + duration: units.longDuration + } + } + + NumberAnimation { + id: timeoutIndicatorAnimation + target: timeoutIndicatorRect + property: "height" + from: area.height + notificationPopup.margins.top + notificationPopup.margins.bottom + to: 0 + duration: Math.min(timer.interval, timeoutIndicatorDelayTimer.remainingTimeout) + running: timer.running && !timeoutIndicatorDelayTimer.running && units.longDuration > 1 + } + } + + NotificationItem { + id: notificationItem + // let the item bleed into the dialog margins so the close button margins cancel out + y: closable || dismissable || configurable ? -notificationPopup.margins.top : 0 + headingRightPadding: -notificationPopup.margins.right + width: parent.width + hovered: area.containsMouse + maximumLineCount: 8 + bodyCursorShape: notificationPopup.hasDefaultAction ? Qt.PointingHandCursor : 0 + + thumbnailLeftPadding: -notificationPopup.margins.left + thumbnailRightPadding: -notificationPopup.margins.right + thumbnailTopPadding: -notificationPopup.margins.top + thumbnailBottomPadding: -notificationPopup.margins.bottom + + closable: true + onBodyClicked: { + if (area.acceptedButtons & mouse.button) { + area.clicked(null /*mouse*/); + } } + onCloseClicked: notificationPopup.closeClicked() + onDismissClicked: notificationPopup.dismissClicked() + onConfigureClicked: notificationPopup.configureClicked() + onActionInvoked: notificationPopup.actionInvoked(actionName) + onOpenUrl: notificationPopup.openUrl(url) + onFileActionInvoked: notificationPopup.fileActionInvoked() + + onSuspendJobClicked: notificationPopup.suspendJobClicked() + onResumeJobClicked: notificationPopup.resumeJobClicked() + onKillJobClicked: notificationPopup.killJobClicked() } - onCloseClicked: notificationPopup.closeClicked() - onDismissClicked: notificationPopup.dismissClicked() - onConfigureClicked: notificationPopup.configureClicked() - onActionInvoked: notificationPopup.actionInvoked(actionName) - onOpenUrl: notificationPopup.openUrl(url) - onFileActionInvoked: notificationPopup.fileActionInvoked() - - onSuspendJobClicked: notificationPopup.suspendJobClicked() - onResumeJobClicked: notificationPopup.resumeJobClicked() - onKillJobClicked: notificationPopup.killJobClicked() } } } diff --git a/applets/notifications/package/contents/ui/SelectableLabel.qml b/applets/notifications/package/contents/ui/SelectableLabel.qml --- a/applets/notifications/package/contents/ui/SelectableLabel.qml +++ b/applets/notifications/package/contents/ui/SelectableLabel.qml @@ -26,6 +26,7 @@ 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.kirigami 2.11 as Kirigami // NOTE This wrapper item is needed for QQC ScrollView to work // In NotificationItem we just do SelectableLabel {} and then it gets confused @@ -46,6 +47,7 @@ implicitWidth: bodyText.paintedWidth implicitHeight: bodyText.paintedHeight + PlasmaExtras.ScrollArea { id: bodyTextScrollArea @@ -60,7 +62,7 @@ width: bodyTextScrollArea.width // TODO check that this doesn't causes infinite loops when it starts adding and removing the scrollbar //width: bodyTextScrollArea.viewport.width - //enabled: !Settings.isMobile + enabled: !Kirigami.Settings.isMobile color: PlasmaCore.ColorScope.textColor selectedTextColor: theme.viewBackgroundColor @@ -77,7 +79,9 @@ // Work around Qt bug where NativeRendering breaks for non-integer scale factors // https://bugreports.qt.io/browse/QTBUG-67007 renderType: Screen.devicePixelRatio % 1 !== 0 ? Text.QtRendering : Text.NativeRendering - selectByMouse: true + // Selectable only when we are in desktop mode + selectByMouse: !Kirigami.Settings.tabletMode + readOnly: true wrapMode: Text.Wrap textFormat: TextEdit.RichText