lockscreen: Refactor swipe and use mobileshell notification component

This commit is contained in:
Devin Lin 2022-02-12 00:19:44 -05:00
parent dca8064ca2
commit d9719b8845
12 changed files with 618 additions and 668 deletions

View file

@ -36,6 +36,7 @@ TaskSwitcher 1.0 taskswitcher/TaskSwitcher.qml
KRunnerWidget 1.0 widgets/krunner/KRunnerWidget.qml
MediaControlsWidget 1.0 widgets/mediacontrols/MediaControlsWidget.qml
NotificationsWidget 1.0 widgets/notifications/NotificationsWidget.qml
NotificationsModelType 1.0 widgets/notifications/NotificationsModelType.qml
# /
singleton HomeScreenControls 1.0 HomeScreenControls.qml

View file

@ -20,7 +20,19 @@ import org.kde.kcoreaddons 1.0 as KCoreAddons
Item {
id: notificationItem
required property NotificationManager.Notifications notificationsModel
required property var notificationsModel
required property int notificationsModelType
/**
* Whether the notification is allowed to invoke any action, or if it should instead
* emit the runActionRequested(action) signal, containing the code to run.
*
* This is useful for cases like the lockscreen, where actions should only be run after
* the user logs in.
*/
property bool requestToInvoke: false
property var model
property int modelIndex
@ -29,7 +41,7 @@ Item {
readonly property int notificationType: model.type
readonly property bool inGroup: model.isInGroup
readonly property bool inGroup: model.isInGroup || false
readonly property bool inHistory: true
readonly property string applicationIconSource: model.applicationIconName
@ -88,6 +100,12 @@ Item {
return labels;
}
/**
* This signal is emitted and intended for the parent to make its own decision
* on whether to run the requested notification action.
*/
signal runActionRequested()
signal actionInvoked(string actionName)
signal replied(string text)
signal openUrl(string url)
@ -97,45 +115,124 @@ Item {
signal resumeJobClicked
signal killJobClicked
onActionInvoked: {
if (actionName === "default") {
notificationsModel.invokeDefaultAction(notificationsModel.index(modelIndex, 0));
} else {
notificationsModel.invokeAction(notificationsModel.index(modelIndex, 0), actionName);
}
expire();
}
onOpenUrl: {
Qt.openUrlExternally(url);
expire();
}
onFileActionInvoked: {
if (action.objectName === "movetotrash" || action.objectName === "deletefile") {
close();
} else {
expire();
}
}
onSuspendJobClicked: notificationsModel.suspendJob(notificationsModel.index(modelIndex, 0))
onResumeJobClicked: notificationsModel.resumeJob(notificationsModel.index(modelIndex, 0))
onKillJobClicked: notificationsModel.killJob(notificationsModel.index(modelIndex, 0))
function expire() {
if (model.resident) {
model.expired = true;
} else {
if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
notificationsModel.expire(model.notificationId);
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
notificationsModel.expire(notificationsModel.index(modelIndex, 0));
}
}
}
function close() {
if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
notificationsModel.close(model.notificationId);
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
notificationsModel.close(notificationsModel.index(modelIndex, 0));
}
}
// TODO call
function configure() {
notificationsModel.configure(notificationsModel.index(modelIndex, 0))
}
property var pendingAction: () => {}
function runPendingAction() {
pendingAction();
}
onActionInvoked: {
let action = () => {
if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
if (actionName === "") {
notificationsModel.invokeDefaultAction(model.notificationId);
} else {
notificationsModel.invokeAction(notificationItem.model.notificationId, actionName);
}
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
if (actionName === "default") {
notificationsModel.invokeDefaultAction(notificationsModel.index(modelIndex, 0));
} else {
notificationsModel.invokeAction(notificationsModel.index(modelIndex, 0), actionName);
}
}
expire();
}
if (notificationItem.requestToInvoke) {
pendingAction = action;
runActionRequested();
} else {
action();
}
}
onOpenUrl: {
let action = () => {
Qt.openUrlExternally(url);
expire();
}
if (notificationItem.requestToInvoke) {
pendingAction = action;
runActionRequested();
} else {
action();
}
}
onFileActionInvoked: {
let action = () => {
if (action.objectName === "movetotrash" || action.objectName === "deletefile") {
close();
} else {
expire();
}
}
if (notificationItem.requestToInvoke) {
pendingAction = action;
runActionRequested();
} else {
action();
}
}
onSuspendJobClicked: {
let action = () => notificationsModel.suspendJob(notificationsModel.index(modelIndex, 0));
if (notificationItem.requestToInvoke) {
pendingAction = action;
runActionRequested();
} else {
action();
}
}
onResumeJobClicked: {
let action = () => notificationsModel.resumeJob(notificationsModel.index(modelIndex, 0));
if (notificationItem.requestToInvoke) {
pendingAction = action;
runActionRequested();
} else {
action();
}
}
onKillJobClicked: {
let action = () => notificationsModel.killJob(notificationsModel.index(modelIndex, 0));
if (notificationItem.requestToInvoke) {
pendingAction = action;
runActionRequested();
} else {
action();
}
}
}

View file

@ -24,7 +24,7 @@ import "util.js" as Util
// notification properties are in BaseNotificationItem
BaseNotificationItem {
id: notificationItem
implicitHeight: mainCard.implicitHeight
implicitHeight: mainCard.implicitHeight + mainCard.anchors.topMargin + notificationHeading.height
// notification heading for groups with one element
NotificationGroupHeader {

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.15
QtObject {
enum ModelType {
NotificationsModel, // used on the shell
WatchedNotificationsModel // used on the lockscreen
}
}

View file

@ -26,15 +26,56 @@ import org.kde.notificationmanager 1.0 as NotificationManager
Item {
id: root
/**
* The notification model for the widget.
*/
property var historyModel: []
/**
* The type of notification model used for the widget.
*/
property int historyModelType: NotificationsModelType.NotificationsModel
/**
* The notification model settings for the widget.
*/
property var notificationSettings: NotificationManager.Settings {}
/**
* Whether invoking notification actions requires authentiation of some sort.
*
* If set to true, any attempted invoking will trigger the unlockRequested() signal.
* Any consumers can then call the runPendingAction() function if authenticated to proceed
* executing the notification action.
*/
property bool actionsRequireUnlock: false
/**
* Whether the widget has notifications.
*/
readonly property bool hasNotifications: list.count > 0
enum ModelType {
NotificationsModel, // used in the logged-in shell
WatchedNotificationsModel // used on the lockscreen
}
signal unlockRequested()
function runPendingAction() {
list.pendingNotificationWithAction.runPendingAction();
}
/**
* Clears the history of the notification model.
*/
function clearHistory() {
historyModel.clear(NotificationManager.Notifications.ClearExpired);
}
/**
* Open the system notification settings.
*/
function openNotificationSettings() {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications");
}
@ -52,6 +93,8 @@ Item {
model: historyModel
currentIndex: -1
property var pendingNotificationWithAction
boundsBehavior: Flickable.StopAtBounds
spacing: Kirigami.Units.largeSpacing
@ -72,7 +115,7 @@ Item {
width: parent.width - (PlasmaCore.Units.largeSpacing * 4)
text: i18n("Notification service not available")
visible: list.count === 0 && !NotificationManager.Server.valid
visible: list.count === 0 && !NotificationManager.Server.valid && historyModelType === NotificationsModelType.NotificationsModel
PlasmaComponents3.Label {
// Checking valid to avoid creating ServerInfo object if everything is alright
@ -153,23 +196,41 @@ Item {
spacing: PlasmaCore.Units.smallSpacing
NotificationItem {
id: notificationItem
Layout.fillWidth: true
model: delegateLoader.model
modelIndex: delegateLoader.index
notificationsModel: historyModel
notificationsModel: root.historyModel
notificationsModelType: root.historyModelType
timeSource: timeDataSource
requestToInvoke: root.actionsRequireUnlock
onRunActionRequested: {
list.pendingNotificationWithAction = notificationItem;
root.unlockRequested();
}
}
PlasmaComponents3.ToolButton {
Loader {
height: visible ? implicitHeight : 0
visible: active
active: {
// if we have the WatchedNotificationsModel, we don't have notification grouping support
if (typeof model.groupChildrenCount === 'undefined')
return false;
return (model.groupChildrenCount > model.expandedGroupChildrenCount || model.isGroupExpanded)
&& delegateLoader.ListView.nextSection !== delegateLoader.ListView.section
}
sourceComponent: 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)
&& delegateLoader.ListView.nextSection !== delegateLoader.ListView.section
onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded)
height: visible ? implicitHeight : 0
}
}
}
}

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.10
import org.kde.kirigami 2.11 as Kirigami
MouseArea {
id: delegate
property Item contentItem
property bool draggable: false
signal dismissRequested
anchors.fill: contentItem
implicitWidth: contentItem ? contentItem.implicitWidth : 0
implicitHeight: contentItem ? contentItem.implicitHeight : 0
opacity: 1 - Math.min(1, 1.5 * Math.abs(x) / width)
drag {
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: PlasmaCore.Units.longDuration
}
}

View file

@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import org.kde.plasma.core 2.1 as PlasmaCore
Flickable {
id: root
property int position: 0
required property real keypadHeight
function cancelAnimations() {
positionAnim.stop();
}
function goToOpenPosition() {
positionAnim.to = keypadHeight;
positionAnim.restart();
}
function goToClosePosition() {
positionAnim.to = 0;
positionAnim.restart();
}
function updateState() {
if (movingUp) {
goToOpenPosition();
} else {
goToClosePosition();
}
}
NumberAnimation on position {
id: positionAnim
duration: PlasmaCore.Units.longDuration * 2
easing.type: Easing.OutCubic
}
// we use flickable solely for capturing flicks, not positioning elements
contentWidth: width
contentHeight: height + 99999
contentX: 0
contentY: startContentY
readonly property real startContentY: contentHeight / 2
property bool positionChangedDueToFlickable: false
property int oldPosition: position
property bool movingUp: false
onPositionChanged: {
movingUp = oldPosition <= position;
oldPosition = position;
// ensure that flickable is not moving when other sources are changing position
if (!positionChangedDueToFlickable) {
cancelMovement();
}
positionChangedDueToFlickable = true;
}
// update position from flickable movement
property real oldContentY
onContentYChanged: {
positionChangedDueToFlickable = true;
position += (contentY - oldContentY);
oldContentY = contentY;
}
onMovementStarted: cancelAnimations();
onMovementEnded: {
if (!positionAnim.running) {
updateState();
}
resetPosition();
}
onFlickEnded: resetPosition();
onDraggingChanged: {
if (!dragging) {
cancelMovement();
resetPosition();
if (!positionAnim.running) {
root.updateState();
}
} else {
cancelAnimations();
}
}
function cancelMovement() {
root.cancelFlick();
// HACK: cancelFlick() doesn't seem to cancel flicks...
root.flick(-horizontalVelocity, -verticalVelocity);
}
function resetPosition() {
positionChangedDueToFlickable = true;
oldContentY = startContentY;
contentY = startContentY;
}
}

View file

@ -1,8 +1,8 @@
/*
SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12
@ -29,20 +29,21 @@ PlasmaCore.ColorScope {
anchors.fill: parent
function isPinDrawerOpen() {
return passwordFlickable.contentY === passwordFlickable.columnHeight;
return flickable.openFactor >= 1;
}
function askPassword() {
showPasswordAnim.restart();
flickable.goToOpenPosition();
}
NumberAnimation {
id: showPasswordAnim
target: passwordFlickable
property: "contentY"
from: 0
to: passwordFlickable.contentHeight - passwordFlickable.height
duration: PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
Notifications.WatchedNotificationsModel {
id: notifModel
}
Image {
id: wallpaper
anchors.fill: parent
source: "/home/devin/Pictures/Wallpaper/1920x1080_1620115524334.jpeg"
}
// blur background once keypad is open
@ -66,9 +67,28 @@ PlasmaCore.ColorScope {
}
}
Notifications.WatchedNotificationsModel {
id: notifModel
FlickContainer {
id: flickable
anchors.fill: parent
property real openFactor: position / keypadHeight
Component.onCompleted: flickable.goToClosePosition()
onPositionChanged: {
if (position > keypadHeight) {
position = keypadHeight;
} else if (position < 0) {
position = 0;
}
}
keypadHeight: PlasmaCore.Units.gridUnit * 20
Item {
width: flickable.width
height: flickable.height
y: flickable.contentY // effectively anchored to the screen
// header bar
Loader {
@ -80,7 +100,7 @@ PlasmaCore.ColorScope {
right: parent.right
}
height: PlasmaCore.Units.gridUnit * 1.25
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
opacity: 1 - flickable.openFactor
sourceComponent: MobileShell.StatusBar {
id: statusBar
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
@ -98,10 +118,9 @@ PlasmaCore.ColorScope {
id: phoneComponent
visible: !isWidescreen
active: visible
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
opacity: 1 - flickable.openFactor
asynchronous: true
z: passwordFlickable.contentY === 0 ? 5 : 0 // in front of password flickable when closed
anchors {
top: parent.top
bottom: scrollUpIcon.top
@ -110,12 +129,14 @@ PlasmaCore.ColorScope {
topMargin: item && !root.notificationsShown ? Math.round(root.height / 2 - (item.implicitHeight / 2 + PlasmaCore.Units.gridUnit * 2)) : PlasmaCore.Units.gridUnit * 5
bottomMargin: PlasmaCore.Units.gridUnit
}
Behavior on anchors.topMargin {
NumberAnimation {
duration: loadTimer.running ? 0 : PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad
}
}
// avoid topMargin animation when item is being loaded
onLoaded: loadTimer.restart();
Timer {
@ -135,6 +156,7 @@ PlasmaCore.ColorScope {
alignment: Qt.AlignHCenter
Layout.bottomMargin: PlasmaCore.Units.gridUnit * 2 // keep spacing even if media controls are gone
}
MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
@ -143,13 +165,13 @@ PlasmaCore.ColorScope {
Layout.rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsList {
id: phoneNotificationsList
NotificationsComponent {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: PlasmaCore.Units.gridUnit
z: passwordFlickable.contentY === 0 ? 5 : 0 // prevent mousearea from interfering with pin drawer
onCountChanged: root.notificationsShown = count !== 0
Layout.maximumWidth: PlasmaCore.Units.gridUnit * (25 + 2) // clip margins
topMargin: PlasmaCore.Units.gridUnit
leftMargin: PlasmaCore.Units.gridUnit
rightMargin: PlasmaCore.Units.gridUnit
}
}
}
@ -159,10 +181,9 @@ PlasmaCore.ColorScope {
id: tabletComponent
visible: isWidescreen
active: visible
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
opacity: 1 - flickable.openFactor
asynchronous: true
z: passwordFlickable.contentY === 0 ? 5 : 0 // in front of password flickable when closed
anchors.top: headerBar.bottom
anchors.left: parent.left
anchors.right: parent.right
@ -212,14 +233,18 @@ PlasmaCore.ColorScope {
rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsList {
NotificationsComponent {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillWidth: true
Layout.maximumHeight: parent.height
Layout.minimumHeight: this.notificationListHeight
Layout.fillHeight: true
Layout.topMargin: PlasmaCore.Units.gridUnit * 2
Layout.bottomMargin: PlasmaCore.Units.gridUnit
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 15
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
onCountChanged: root.notificationsShown = count !== 0
leftMargin: PlasmaCore.Units.gridUnit
rightMargin: PlasmaCore.Units.gridUnit
bottomMargin: PlasmaCore.Units.gridUnit
topMargin: PlasmaCore.Units.gridUnit
}
}
}
@ -229,51 +254,21 @@ PlasmaCore.ColorScope {
PlasmaCore.IconItem {
id: scrollUpIcon
anchors.bottom: parent.bottom
anchors.bottomMargin: PlasmaCore.Units.gridUnit + passwordFlickable.contentY * 0.5
anchors.bottomMargin: PlasmaCore.Units.gridUnit + flickable.position * 0.5
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
opacity: 1 - flickable.openFactor
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-up"
}
Flickable {
id: passwordFlickable
anchors.fill: parent
property int columnHeight: PlasmaCore.Units.gridUnit * 20
property int oldContentY: contentY
height: columnHeight + root.height
contentHeight: columnHeight + root.height
boundsBehavior: Flickable.StopAtBounds
// always snap to end (either hidden or shown)
onMovementEnded: {
if (!atYBeginning && !atYEnd) {
if (contentY > columnHeight - contentY) {
flick(0, -1000);
} else {
flick(0, 1000);
}
}
}
// wipe password if it is more than half way down the screen
onContentYChanged: {
if (contentY < columnHeight / 2 && oldContentY >= columnHeight / 2) {
keypad.reset();
}
oldContentY = contentY;
}
// keypad area
// password keypad
ColumnLayout {
id: passwordLayout
anchors.bottom: parent.bottom
transform: Translate { y: flickable.keypadHeight - flickable.position }
width: parent.width
spacing: PlasmaCore.Units.gridUnit
@ -285,13 +280,13 @@ PlasmaCore.ColorScope {
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-down"
opacity: Math.sin((Math.PI / 2) * (passwordFlickable.contentY / passwordFlickable.columnHeight) + 1.5 * Math.PI) + 1
opacity: Math.sin((Math.PI / 2) * flickable.openFactor + 1.5 * Math.PI) + 1
}
Keypad {
id: keypad
focus: true
swipeProgress: passwordFlickable.contentY / passwordFlickable.columnHeight
swipeProgress: flickable.openFactor
Layout.fillWidth: true
onPasswordChanged: {
passwordFlickable.contentY = passwordFlickable.contentHeight - passwordFlickable.height
@ -299,6 +294,7 @@ PlasmaCore.ColorScope {
}
}
}
}
LockOsd {
anchors.top: parent.top

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2022 Devin Lin <espidev@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtGraphicalEffects 1.12
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.notificationmanager 1.1 as Notifications
import org.kde.plasma.private.mobileshell 1.0 as MobileShell
Rectangle {
id: rect
color: "transparent"
clip: true
property real leftMargin: 0
property real rightMargin: 0
property real topMargin: 0
property real bottomMargin: 0
PlasmaCore.ColorScope {
anchors.fill: parent
anchors.topMargin: rect.topMargin
anchors.bottomMargin: rect.bottomMargin
anchors.leftMargin: rect.leftMargin
anchors.rightMargin: rect.rightMargin
colorGroup: PlasmaCore.Theme.NormalColorGroup
Connections {
target: authenticator
function onSucceeded() {
if (phoneNotificationsList.requestNotificationAction) {
phoneNotificationsList.runPendingAction();
phoneNotificationsList.requestNotificationAction = false;
}
}
function onFailed() {
phoneNotificationsList.requestNotificationAction = false;
}
}
MobileShell.NotificationsWidget {
id: phoneNotificationsList
anchors.fill: parent
historyModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
actionsRequireUnlock: true
historyModel: notifModel
property bool requestNotificationAction: false
onHasNotificationsChanged: root.notificationsShown = hasNotifications
onUnlockRequested: {
requestNotificationAction = true;
root.askPassword();
}
}
}
}

View file

@ -1,143 +0,0 @@
/*
SPDX-FileCopyrightText: 2020 Devin Lin <espidev@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtGraphicalEffects 1.12
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.notificationmanager 1.1 as Notifications
import org.kde.kirigami 2.13 as Kirigami
Item {
id: notificationsRoot
property alias notificationListHeight: notificationListView.contentHeight
property int count: notificationListView.count
clip: true
property var pendingAction: {"notificationId": 0, "actionName": ""}
Rectangle {
z: 1
anchors {
top: parent.top
left: parent.left
right: parent.right
}
visible: !notificationListView.atYBeginning
height: PlasmaCore.Units.gridUnit
gradient: Gradient {
GradientStop {
position: 1.0
color: "transparent"
}
GradientStop {
position: 0.0
color: Qt.rgba(0, 0, 0, 0.3)
}
}
Rectangle {
anchors {
left: parent.left
right: parent.right
}
height: 1
color: Qt.rgba(1, 1, 1, 0.5)
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
}
}
Rectangle {
z: 1
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
visible: !notificationListView.atYEnd
height: PlasmaCore.Units.gridUnit
gradient: Gradient {
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 0, 0.3)
}
GradientStop {
position: 0.0
color: "transparent"
}
}
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 1
color: Qt.rgba(1, 1, 1, 0.5)
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
}
}
Connections {
target: authenticator
function onSucceeded() {
if (notificationsRoot.pendingAction.notificationId !== 0) {
if (notificationsRoot.pendingAction.actionName.length == 0) {
notifModel.invokeDefaultAction(pendingAction.notificationId);
} else {
notifModel.invokeAction(pendingAction.notificationId, pendingAction.actionName);
}
notificationsRoot.pendingAction = {"notificationId": 0, "actionName":""};
}
}
function onFailed() {
notificationsRoot.pendingAction = {"notificationId": 0, "actionName":""};
}
}
Component {
id: notificationComponent
ColumnLayout {
width: notificationListView.width
spacing: PlasmaCore.Units.smallSpacing
// insert application heading here once application grouping is implemented
SimpleNotification {
notification: model
}
}
}
ListView {
id: notificationListView
model: notifModel
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
}
width: Math.min(PlasmaCore.Units.gridUnit * 25, parent.width - PlasmaCore.Units.gridUnit * 2)
height: Math.min(contentHeight, parent.height) // don't take up the entire screen for notification list view
interactive: contentHeight > parent.height // only allow scrolling on notifications list if it is long enough
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
spacing: PlasmaCore.Units.gridUnit
delegate: Kirigami.DelegateRecycler {
sourceComponent: notificationComponent
}
add: Transition {
NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: Kirigami.Units.shortDuration }
NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: Kirigami.Units.shortDuration }
}
displaced: Transition {
NumberAnimation { properties: "x,y"; duration: Kirigami.Units.shortDuration; easing.type: Easing.InOutQuad }
}
}
}

View file

@ -1,212 +0,0 @@
/*
SPDX-FileCopyrightText: 2020 Devin Lin <espidev@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick 2.12
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtGraphicalEffects 1.12
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.notificationmanager 1.1 as Notifications
import org.kde.kquickcontrolsaddons 2.0 as KQCAddons
// meant to be temporary, until the notifications components in plasma-workspace are available to used
// https://invent.kde.org/plasma/plasma-workspace/-/blob/master/applets/notifications/package/contents/ui/NotificationItem.qml
Item {
id: notificationItem
property var notification
anchors.left: parent.left
anchors.right: parent.right
height: notifLayout.height + PlasmaCore.Units.gridUnit
opacity: 1 - Math.min(1, 1.5 * Math.abs(rect.x) / width) // opacity during dismiss swipe
RectangularGlow {
anchors.topMargin: 1
anchors.leftMargin: 1
anchors.fill: rect
cornerRadius: rect.radius * 2
glowRadius: 2
spread: 0.2
color: "#616161"
}
// notification
Rectangle {
id: rect
radius: 5
color: "white"
height: parent.height
width: parent.width
border.color: "#bdbdbd"
border.width: 1
ColumnLayout {
id: notifLayout
anchors {
left: parent.left
leftMargin: PlasmaCore.Units.gridUnit * 0.5
right: parent.right
rightMargin: PlasmaCore.Units.gridUnit * 0.5
verticalCenter: parent.verticalCenter
}
RowLayout {
Layout.fillWidth: true
spacing: PlasmaCore.Units.smallSpacing / 2
// notif body
ColumnLayout {
id: textLayout
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
spacing: PlasmaCore.Units.gridUnit / 2
Label {
text: notification.summary
color: "#212121"
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
font.pointSize: 11
}
Label {
text: notification.body
color: "#616161"
Layout.fillWidth: true
wrapMode: Text.WordWrap
elide: Text.ElideRight
font.pointSize: 10
}
}
// notification icon
Item {
id: iconContainer
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredWidth: PlasmaCore.Units.iconSizes.large
Layout.preferredHeight: PlasmaCore.Units.iconSizes.large
Layout.topMargin: PlasmaCore.Units.smallSpacing
Layout.bottomMargin: PlasmaCore.Units.smallSpacing
visible: iconItem.active || imageItem.active
PlasmaCore.IconItem {
id: iconItem
// don't show two identical icons
readonly property bool active: valid && source != notification.applicationIconSource
anchors.fill: parent
usesPlasmaTheme: false
smooth: true
source: {
let icon = notification.icon;
if (typeof icon !== "string") return "";
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 notification.icon === "object" ? notification.icon : undefined
}
}
}
Flow {
id: actionsflow
Layout.fillWidth: true
spacing: PlasmaCore.Units.smallSpacing
layoutDirection: Qt.RightToLeft
Repeater {
id: actionRepeater
model: {
var buttons = [];
var actionNames = (notificationItem.notification.actionNames || []);
var actionLabels = (notificationItem.notification.actionLabels || []);
// HACK We want the actions to be right-aligned but Flow also reverses
for (var i = actionNames.length - 1; i >= 0; --i) {
buttons.push({
actionName: actionNames[i],
label: actionLabels[i]
});
}
return buttons;
}
PlasmaComponents3.ToolButton {
flat: false
// why does it spit "cannot assign undefined to string" when a notification becomes expired?
text: modelData.label || ""
onClicked: {
if (notificationItem.notification.category === "x-kde.incoming-call") {
notifModel.invokeAction(notificationItem.notification.notificationId, modelData.actionName);
} else {
notificationsRoot.pendingAction = {"notificationId": notificationItem.notification.notificationId,
"actionName":modelData.actionName};
root.askPassword();
}
}
}
}
}
}
// swipe gesture for dismissing notification (left/right)
MouseArea {
id: dismissSwipe
anchors.fill: parent
drag.axis: Drag.XAxis
drag.target: rect
onPressed: {
let pos = mapToItem(actionsflow, mouse.x, mouse.y);
if (actionsflow.childAt(pos.x, pos.y)) {
mouse.accepted = false;
}
}
onReleased: {
if (Math.abs(rect.x) > width / 2) { // dismiss notification when finished swipe
notifModel.close(notificationItem.notification.notificationId);
} else {
slideAnim.restart();
}
if (notificationItem.notification.hasDefaultAction && Math.abs(rect.x) < PlasmaCore.Units.gridUnit) {
if (notificationItem.notification.category === "x-kde.incoming-call") {
notifModel.invokeDefaultAction(notificationItem.notification.notificationId);
} else {
notificationsRoot.pendingAction = {"notificationId": notificationItem.notification.notificationId,
"actionName": ""};
root.askPassword();
}
}
}
NumberAnimation {
id: slideAnim
target: rect
property: "x"
to: 0
duration: PlasmaCore.Units.longDuration
}
}
}
}