diff --git a/applets/notifications/package/contents/ui/FullRepresentation.qml b/applets/notifications/package/contents/ui/FullRepresentation.qml index 0b13b6e2d..22867985a 100644 --- a/applets/notifications/package/contents/ui/FullRepresentation.qml +++ b/applets/notifications/package/contents/ui/FullRepresentation.qml @@ -20,12 +20,14 @@ import org.kde.notificationmanager 1.0 as NotificationManager import "global" -PlasmaComponents3.Page { +PlasmaExtras.Representation { // TODO these should be configurable in the future readonly property int dndMorningHour: 6 readonly property int dndEveningHour: 20 Layout.fillHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical + collapseMarginsHint: true + // HACK forward focus to the list onActiveFocusChanged: { if (activeFocus) { @@ -222,373 +224,363 @@ PlasmaComponents3.Page { } } - ColumnLayout{ - // FIXME fix popup size when resizing panel smaller (so it collapses) - //Layout.preferredWidth: PlasmaCore.Units.gridUnit * 18 - //Layout.preferredHeight: PlasmaCore.Units.gridUnit * 24 - //Layout.minimumWidth: PlasmaCore.Units.gridUnit * 10 - //Layout.minimumHeight: PlasmaCore.Units.gridUnit * 15 + PlasmaComponents3.ScrollView { + id: scrollView anchors.fill: parent - - spacing: PlasmaCore.Units.smallSpacing - - // actual notifications - PlasmaComponents3.ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: PlasmaCore.Units.gridUnit * 18 - Layout.preferredHeight: PlasmaCore.Units.gridUnit * 24 - Layout.leftMargin: PlasmaCore.Units.smallSpacing - background: null - - // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 - PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff - - ListView { - id: list - model: historyModel - currentIndex: -1 - - Keys.onDeletePressed: { - var idx = historyModel.index(currentIndex, 0); - if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) { - historyModel.close(idx); - // TODO would be nice to stay inside the current group when deleting an item - } + background: null + leftPadding: PlasmaCore.Units.smallSpacing * 2 + rightPadding: PlasmaCore.Units.smallSpacing * 2 + + + // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 + PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff + + contentItem: ListView { + id: list + model: historyModel + currentIndex: -1 + topMargin: PlasmaCore.Units.smallSpacing * 2 + bottomMargin: PlasmaCore.Units.smallSpacing * 2 + spacing: PlasmaCore.Units.smallSpacing + + Keys.onDeletePressed: { + var idx = historyModel.index(currentIndex, 0); + if (historyModel.data(idx, NotificationManager.Notifications.ClosableRole)) { + historyModel.close(idx); + // TODO would be nice to stay inside the current group when deleting an item } - Keys.onEnterPressed: Keys.onReturnPressed(event) - Keys.onReturnPressed: { - // Trigger default action, if any - var idx = historyModel.index(currentIndex, 0); - if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) { - historyModel.invokeDefaultAction(idx); - return; - } - - // Trigger thumbnail URL if there's one - var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole); - if (urls && urls.length === 1) { - Qt.openUrlExternally(urls[0]); - return; - } - - // TODO for finished jobs trigger "Open" or "Open Containing Folder" action - } - Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled) - Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled) - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Home: - currentIndex = 0; - break; - case Qt.Key_End: - currentIndex = count - 1; - break; - } + } + Keys.onEnterPressed: Keys.onReturnPressed(event) + Keys.onReturnPressed: { + // Trigger default action, if any + var idx = historyModel.index(currentIndex, 0); + if (historyModel.data(idx, NotificationManager.Notifications.HasDefaultActionRole)) { + historyModel.invokeDefaultAction(idx); + return; } - function isRowExpanded(row) { - var idx = historyModel.index(row, 0); - return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); + // Trigger thumbnail URL if there's one + var urls = historyModel.data(idx, NotificationManager.Notifications.UrlsRole); + if (urls && urls.length === 1) { + Qt.openUrlExternally(urls[0]); + return; } - function setGroupExpanded(row, expanded) { - var rowIdx = historyModel.index(row, 0); - var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); - var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); + // TODO for finished jobs trigger "Open" or "Open Containing Folder" action + } + Keys.onLeftPressed: setGroupExpanded(currentIndex, LayoutMirroring.enabled) + Keys.onRightPressed: setGroupExpanded(currentIndex, !LayoutMirroring.enabled) + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Home: + currentIndex = 0; + break; + case Qt.Key_End: + currentIndex = count - 1; + break; + } + } - historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); + function isRowExpanded(row) { + var idx = historyModel.index(row, 0); + return historyModel.data(idx, NotificationManager.Notifications.IsGroupExpandedRole); + } - // If the current item went away when the group collapsed, scroll to the group heading - if (!persistentRowIdx || !persistentRowIdx.valid) { - if (persistentGroupIdx && persistentGroupIdx.valid) { - list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); - // When closed via keyboard, also set a sane current index - if (list.currentIndex > -1) { - list.currentIndex = persistentGroupIdx.row; - } - } - } - } + function setGroupExpanded(row, expanded) { + var rowIdx = historyModel.index(row, 0); + var persistentRowIdx = historyModel.makePersistentModelIndex(rowIdx); + var persistentGroupIdx = historyModel.makePersistentModelIndex(historyModel.groupIndex(rowIdx)); - highlightMoveDuration: 0 - highlightResizeDuration: 0 - // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus - highlight: PlasmaCore.FrameSvgItem { - imagePath: "widgets/listitem" - prefix: "pressed" - } + historyModel.setData(rowIdx, expanded, NotificationManager.Notifications.IsGroupExpandedRole); - add: Transition { - SequentialAnimation { - PropertyAction { property: "opacity"; value: 0 } - PauseAnimation { duration: PlasmaCore.Units.longDuration } - ParallelAnimation { - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: PlasmaCore.Units.longDuration } - NumberAnimation { property: "height"; from: 0; duration: PlasmaCore.Units.longDuration } + // If the current item went away when the group collapsed, scroll to the group heading + if (!persistentRowIdx || !persistentRowIdx.valid) { + if (persistentGroupIdx && persistentGroupIdx.valid) { + list.positionViewAtIndex(persistentGroupIdx.row, ListView.Contain); + // When closed via keyboard, also set a sane current index + if (list.currentIndex > -1) { + list.currentIndex = persistentGroupIdx.row; } } } - addDisplaced: Transition { - NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } - } + } - remove: Transition { - id: removeTransition + highlightMoveDuration: 0 + highlightResizeDuration: 0 + // Not using PlasmaComponents.Highlight as this is only for indicating keyboard focus + highlight: PlasmaCore.FrameSvgItem { + imagePath: "widgets/listitem" + prefix: "pressed" + } + + add: Transition { + SequentialAnimation { + PropertyAction { property: "opacity"; value: 0 } + PauseAnimation { duration: PlasmaCore.Units.longDuration } ParallelAnimation { - NumberAnimation { property: "opacity"; to: 0; duration: PlasmaCore.Units.longDuration } - NumberAnimation { - id: removeXAnimation - property: "x" - to: list.width - duration: PlasmaCore.Units.longDuration - } + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: PlasmaCore.Units.longDuration } + NumberAnimation { property: "height"; from: 0; duration: PlasmaCore.Units.longDuration } } } - removeDisplaced: Transition { - SequentialAnimation { - PauseAnimation { duration: PlasmaCore.Units.longDuration } - NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + } + addDisplaced: Transition { + NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } + } + + remove: Transition { + id: removeTransition + ParallelAnimation { + NumberAnimation { property: "opacity"; to: 0; duration: PlasmaCore.Units.longDuration } + NumberAnimation { + id: removeXAnimation + property: "x" + to: list.width - (scrollView.PlasmaComponents3.ScrollBar.vertical.visible ? PlasmaCore.Units.smallSpacing * 4 : 0) + duration: PlasmaCore.Units.longDuration } } - - // This is so the delegates can detect the change in "isInGroup" and show a separator - section { - property: "isInGroup" - criteria: ViewSection.FullString + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: PlasmaCore.Units.longDuration } + NumberAnimation { properties: "y"; duration: PlasmaCore.Units.longDuration } } + } - delegate: DraggableDelegate { - id: delegate - width: list.width - contentItem: delegateLoader + // This is so the delegates can detect the change in "isInGroup" and show a separator + section { + property: "isInGroup" + criteria: ViewSection.FullString + } - draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType + delegate: DraggableDelegate { + id: delegate + width: ListView.view.width - (scrollView.PlasmaComponents3.ScrollBar.vertical.visible ? PlasmaCore.Units.smallSpacing * 4 : 0) + contentItem: delegateLoader - onDismissRequested: { - // Setting the animation target explicitly before removing the notification: - // Using ViewTransition.item.x to get the x position in the animation - // causes random crash in attached property access (cf. Bug 414066) - if (x < 0) { - removeXAnimation.to = -list.width; - } + draggable: !model.isGroup && model.type != NotificationManager.Notifications.JobType - historyModel.close(historyModel.index(index, 0)); + onDismissRequested: { + // Setting the animation target explicitly before removing the notification: + // Using ViewTransition.item.x to get the x position in the animation + // causes random crash in attached property access (cf. Bug 414066) + if (x < 0) { + removeXAnimation.to = -delegate.width; } - Loader { - id: delegateLoader - width: list.width - sourceComponent: model.isGroup ? groupDelegate : notificationDelegate + historyModel.close(historyModel.index(index, 0)); + } - Component { - id: groupDelegate - NotificationHeader { - applicationName: model.applicationName - applicationIconSource: model.applicationIconName - originName: model.originName || "" + Loader { + id: delegateLoader + width: delegate.width + sourceComponent: model.isGroup ? groupDelegate : notificationDelegate - // don't show timestamp for group + Component { + id: groupDelegate + NotificationHeader { + applicationName: model.applicationName + applicationIconSource: model.applicationIconName + originName: model.originName || "" - configurable: model.configurable - closable: model.closable - closeButtonTooltip: i18n("Close Group") + // don't show timestamp for group - onCloseClicked: historyModel.close(historyModel.index(index, 0)) - onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) - } + configurable: model.configurable + closable: model.closable + closeButtonTooltip: i18n("Close Group") + + onCloseClicked: historyModel.close(historyModel.index(index, 0)) + onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) } + } - Component { - id: notificationDelegate - ColumnLayout { - spacing: PlasmaCore.Units.smallSpacing - - RowLayout { - Item { - id: groupLineContainer - Layout.fillHeight: true - Layout.topMargin: PlasmaCore.Units.smallSpacing - width: PlasmaCore.Units.iconSizes.small - visible: model.isInGroup - - PlasmaCore.SvgItem { - elementId: "vertical-line" - svg: lineSvg - anchors.horizontalCenter: parent.horizontalCenter - // Want a thicker than default bar - width: Math.min(groupLineContainer.width, naturalSize.width * PlasmaCore.Units.devicePixelRatio * 3) - height: parent.height - } + Component { + id: notificationDelegate + ColumnLayout { + spacing: PlasmaCore.Units.smallSpacing + + RowLayout { + Item { + id: groupLineContainer + Layout.fillHeight: true + Layout.topMargin: PlasmaCore.Units.smallSpacing + width: PlasmaCore.Units.iconSizes.small + visible: model.isInGroup + + PlasmaCore.SvgItem { + elementId: "vertical-line" + svg: lineSvg + anchors.horizontalCenter: parent.horizontalCenter + // Want a thicker than default bar + width: Math.min(groupLineContainer.width, naturalSize.width * PlasmaCore.Units.devicePixelRatio * 3) + height: parent.height } + } + + NotificationItem { + Layout.fillWidth: true - NotificationItem { - Layout.fillWidth: true - - notificationType: model.type - - inGroup: model.isInGroup - inHistory: true - listViewParent: list - - 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; + notificationType: model.type + + inGroup: model.isInGroup + inHistory: true + listViewParent: list + + 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 } - actionLabels: { - var labels = (model.actionLabels || []); - if (addDefaultAction) { - labels.unshift(model.defaultActionLabel); - } - return labels; + return actions; + } + actionLabels: { + var labels = (model.actionLabels || []); + if (addDefaultAction) { + labels.unshift(model.defaultActionLabel); } + return labels; + } - onCloseClicked: close() - - onDismissClicked: { - model.dismissed = false; - root.closePlasmoid(); - } - onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) + onCloseClicked: close() - onActionInvoked: { - if (actionName === "default") { - historyModel.invokeDefaultAction(historyModel.index(index, 0)); - } else { - historyModel.invokeAction(historyModel.index(index, 0), actionName); - } + onDismissClicked: { + model.dismissed = false; + root.closePlasmoid(); + } + onConfigureClicked: historyModel.configure(historyModel.index(index, 0)) - expire(); + onActionInvoked: { + if (actionName === "default") { + historyModel.invokeDefaultAction(historyModel.index(index, 0)); + } else { + historyModel.invokeAction(historyModel.index(index, 0), actionName); } - onOpenUrl: { - Qt.openUrlExternally(url); + + expire(); + } + onOpenUrl: { + Qt.openUrlExternally(url); + expire(); + } + onFileActionInvoked: { + if (action.objectName === "movetotrash" || action.objectName === "deletefile") { + close(); + } else { expire(); } - onFileActionInvoked: { - if (action.objectName === "movetotrash" || action.objectName === "deletefile") { - close(); - } else { - expire(); - } - } + } - 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)) - function expire() { - if (model.resident) { - model.expired = true; - } else { - historyModel.expire(historyModel.index(index, 0)); - } + function expire() { + if (model.resident) { + model.expired = true; + } else { + historyModel.expire(historyModel.index(index, 0)); } + } - function close() { - historyModel.close(historyModel.index(index, 0)); - } + function close() { + historyModel.close(historyModel.index(index, 0)); } } + } - PlasmaComponents3.ToolButton { - icon.name: 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) - && delegate.ListView.nextSection !== delegate.ListView.section - onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) - } + PlasmaComponents3.ToolButton { + icon.name: 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) + && delegate.ListView.nextSection !== delegate.ListView.section + onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded) + } - PlasmaCore.SvgItem { - Layout.fillWidth: true - Layout.bottomMargin: PlasmaCore.Units.smallSpacing - elementId: "horizontal-line" - svg: lineSvg + PlasmaCore.SvgItem { + Layout.fillWidth: true + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + elementId: "horizontal-line" + svg: lineSvg - // property is only atached to the delegate itself (the Loader in our case) - visible: (!model.isInGroup || delegate.ListView.nextSection !== delegate.ListView.section) - && delegate.ListView.nextSection !== "" // don't show after last item - } + // property is only atached to the delegate itself (the Loader in our case) + visible: (!model.isInGroup || delegate.ListView.nextSection !== delegate.ListView.section) + && delegate.ListView.nextSection !== "" // don't show after last item } } } } + } + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) - PlasmaExtras.PlaceholderMessage { - anchors.centerIn: parent - width: parent.width - (PlasmaCore.Units.largeSpacing * 4) + text: i18n("No unread notifications") + visible: list.count === 0 && NotificationManager.Server.valid + } - text: i18n("No unread notifications") - visible: list.count === 0 && NotificationManager.Server.valid - } + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (PlasmaCore.Units.largeSpacing * 4) - PlasmaExtras.PlaceholderMessage { - anchors.centerIn: parent - width: parent.width - (PlasmaCore.Units.largeSpacing * 4) - - text: i18n("Notification service not available") - visible: list.count === 0 && !NotificationManager.Server.valid - - // TODO: port to using the subtitle property once it exists - PlasmaComponents3.Label { - // Checking valid to avoid creating ServerInfo object if everything is alright - readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner - : null - - // PlasmaExtras.PlaceholderMessage is internally a ColumnLayout, - // so we can use Layout.whatever properties here - Layout.fillWidth: true - wrapMode: Text.WordWrap - text: currentOwner ? i18nc("Vendor and product name", - "Notifications are currently provided by '%1 %2'", - currentOwner.vendor, - currentOwner.name) - : "" - visible: currentOwner && currentOwner.vendor && currentOwner.name - } + text: i18n("Notification service not available") + visible: list.count === 0 && !NotificationManager.Server.valid + + // TODO: port to using the subtitle property once it exists + PlasmaComponents3.Label { + // Checking valid to avoid creating ServerInfo object if everything is alright + readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner + : null + + // PlasmaExtras.PlaceholderMessage is internally a ColumnLayout, + // so we can use Layout.whatever properties here + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: currentOwner ? i18nc("Vendor and product name", + "Notifications are currently provided by '%1 %2'", + currentOwner.vendor, + currentOwner.name) + : "" + visible: currentOwner && currentOwner.vendor && currentOwner.name } } }