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 KRunnerWidget 1.0 widgets/krunner/KRunnerWidget.qml
MediaControlsWidget 1.0 widgets/mediacontrols/MediaControlsWidget.qml MediaControlsWidget 1.0 widgets/mediacontrols/MediaControlsWidget.qml
NotificationsWidget 1.0 widgets/notifications/NotificationsWidget.qml NotificationsWidget 1.0 widgets/notifications/NotificationsWidget.qml
NotificationsModelType 1.0 widgets/notifications/NotificationsModelType.qml
# / # /
singleton HomeScreenControls 1.0 HomeScreenControls.qml singleton HomeScreenControls 1.0 HomeScreenControls.qml

View file

@ -20,7 +20,19 @@ import org.kde.kcoreaddons 1.0 as KCoreAddons
Item { Item {
id: notificationItem 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 var model
property int modelIndex property int modelIndex
@ -29,7 +41,7 @@ Item {
readonly property int notificationType: model.type 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 bool inHistory: true
readonly property string applicationIconSource: model.applicationIconName readonly property string applicationIconSource: model.applicationIconName
@ -88,6 +100,12 @@ Item {
return labels; 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 actionInvoked(string actionName)
signal replied(string text) signal replied(string text)
signal openUrl(string url) signal openUrl(string url)
@ -97,45 +115,124 @@ Item {
signal resumeJobClicked signal resumeJobClicked
signal killJobClicked 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() { function expire() {
if (model.resident) { if (model.resident) {
model.expired = true; model.expired = true;
} else { } else {
if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
notificationsModel.expire(model.notificationId);
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
notificationsModel.expire(notificationsModel.index(modelIndex, 0)); notificationsModel.expire(notificationsModel.index(modelIndex, 0));
} }
} }
}
function close() { function close() {
if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
notificationsModel.close(model.notificationId);
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
notificationsModel.close(notificationsModel.index(modelIndex, 0)); notificationsModel.close(notificationsModel.index(modelIndex, 0));
} }
}
// TODO call // TODO call
function configure() { function configure() {
notificationsModel.configure(notificationsModel.index(modelIndex, 0)) 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

@ -3,7 +3,7 @@
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de> * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
* *
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/ */
import QtQuick 2.8 import QtQuick 2.8
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1

View file

@ -24,7 +24,7 @@ import "util.js" as Util
// notification properties are in BaseNotificationItem // notification properties are in BaseNotificationItem
BaseNotificationItem { BaseNotificationItem {
id: notificationItem id: notificationItem
implicitHeight: mainCard.implicitHeight implicitHeight: mainCard.implicitHeight + mainCard.anchors.topMargin + notificationHeading.height
// notification heading for groups with one element // notification heading for groups with one element
NotificationGroupHeader { 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 { Item {
id: root id: root
/**
* The notification model for the widget.
*/
property var historyModel: [] 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 {} 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 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() { function clearHistory() {
historyModel.clear(NotificationManager.Notifications.ClearExpired); historyModel.clear(NotificationManager.Notifications.ClearExpired);
} }
/**
* Open the system notification settings.
*/
function openNotificationSettings() { function openNotificationSettings() {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications"); MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_notifications");
} }
@ -52,6 +93,8 @@ Item {
model: historyModel model: historyModel
currentIndex: -1 currentIndex: -1
property var pendingNotificationWithAction
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
@ -72,7 +115,7 @@ Item {
width: parent.width - (PlasmaCore.Units.largeSpacing * 4) width: parent.width - (PlasmaCore.Units.largeSpacing * 4)
text: i18n("Notification service not available") 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 { PlasmaComponents3.Label {
// Checking valid to avoid creating ServerInfo object if everything is alright // Checking valid to avoid creating ServerInfo object if everything is alright
@ -153,23 +196,41 @@ Item {
spacing: PlasmaCore.Units.smallSpacing spacing: PlasmaCore.Units.smallSpacing
NotificationItem { NotificationItem {
id: notificationItem
Layout.fillWidth: true Layout.fillWidth: true
model: delegateLoader.model model: delegateLoader.model
modelIndex: delegateLoader.index modelIndex: delegateLoader.index
notificationsModel: historyModel notificationsModel: root.historyModel
notificationsModelType: root.historyModelType
timeSource: timeDataSource 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" icon.name: model.isGroupExpanded ? "arrow-up" : "arrow-down"
text: model.isGroupExpanded ? i18n("Show Fewer") text: model.isGroupExpanded ? i18n("Show Fewer")
: i18nc("Expand to show n more notifications", : i18nc("Expand to show n more notifications",
"Show %1 More", (model.groupChildrenCount - model.expandedGroupChildrenCount)) "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) 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,9 +1,9 @@
/* /*
SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de> * SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com> * SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
*
SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 1.1 import QtQuick.Controls 1.1
@ -29,20 +29,21 @@ PlasmaCore.ColorScope {
anchors.fill: parent anchors.fill: parent
function isPinDrawerOpen() { function isPinDrawerOpen() {
return passwordFlickable.contentY === passwordFlickable.columnHeight; return flickable.openFactor >= 1;
} }
function askPassword() { function askPassword() {
showPasswordAnim.restart(); flickable.goToOpenPosition();
} }
NumberAnimation {
id: showPasswordAnim Notifications.WatchedNotificationsModel {
target: passwordFlickable id: notifModel
property: "contentY" }
from: 0
to: passwordFlickable.contentHeight - passwordFlickable.height Image {
duration: PlasmaCore.Units.longDuration id: wallpaper
easing.type: Easing.InOutQuad anchors.fill: parent
source: "/home/devin/Pictures/Wallpaper/1920x1080_1620115524334.jpeg"
} }
// blur background once keypad is open // blur background once keypad is open
@ -66,9 +67,28 @@ PlasmaCore.ColorScope {
} }
} }
Notifications.WatchedNotificationsModel { FlickContainer {
id: notifModel 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 // header bar
Loader { Loader {
@ -80,7 +100,7 @@ PlasmaCore.ColorScope {
right: parent.right right: parent.right
} }
height: PlasmaCore.Units.gridUnit * 1.25 height: PlasmaCore.Units.gridUnit * 1.25
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight) opacity: 1 - flickable.openFactor
sourceComponent: MobileShell.StatusBar { sourceComponent: MobileShell.StatusBar {
id: statusBar id: statusBar
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
@ -98,10 +118,9 @@ PlasmaCore.ColorScope {
id: phoneComponent id: phoneComponent
visible: !isWidescreen visible: !isWidescreen
active: visible active: visible
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight) opacity: 1 - flickable.openFactor
asynchronous: true asynchronous: true
z: passwordFlickable.contentY === 0 ? 5 : 0 // in front of password flickable when closed
anchors { anchors {
top: parent.top top: parent.top
bottom: scrollUpIcon.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 topMargin: item && !root.notificationsShown ? Math.round(root.height / 2 - (item.implicitHeight / 2 + PlasmaCore.Units.gridUnit * 2)) : PlasmaCore.Units.gridUnit * 5
bottomMargin: PlasmaCore.Units.gridUnit bottomMargin: PlasmaCore.Units.gridUnit
} }
Behavior on anchors.topMargin { Behavior on anchors.topMargin {
NumberAnimation { NumberAnimation {
duration: loadTimer.running ? 0 : PlasmaCore.Units.longDuration duration: loadTimer.running ? 0 : PlasmaCore.Units.longDuration
easing.type: Easing.InOutQuad easing.type: Easing.InOutQuad
} }
} }
// avoid topMargin animation when item is being loaded // avoid topMargin animation when item is being loaded
onLoaded: loadTimer.restart(); onLoaded: loadTimer.restart();
Timer { Timer {
@ -135,6 +156,7 @@ PlasmaCore.ColorScope {
alignment: Qt.AlignHCenter alignment: Qt.AlignHCenter
Layout.bottomMargin: PlasmaCore.Units.gridUnit * 2 // keep spacing even if media controls are gone Layout.bottomMargin: PlasmaCore.Units.gridUnit * 2 // keep spacing even if media controls are gone
} }
MobileShell.MediaControlsWidget { MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
@ -143,13 +165,13 @@ PlasmaCore.ColorScope {
Layout.rightMargin: PlasmaCore.Units.gridUnit Layout.rightMargin: PlasmaCore.Units.gridUnit
} }
NotificationsList { NotificationsComponent {
id: phoneNotificationsList
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: PlasmaCore.Units.gridUnit Layout.maximumWidth: PlasmaCore.Units.gridUnit * (25 + 2) // clip margins
z: passwordFlickable.contentY === 0 ? 5 : 0 // prevent mousearea from interfering with pin drawer topMargin: PlasmaCore.Units.gridUnit
onCountChanged: root.notificationsShown = count !== 0 leftMargin: PlasmaCore.Units.gridUnit
rightMargin: PlasmaCore.Units.gridUnit
} }
} }
} }
@ -159,10 +181,9 @@ PlasmaCore.ColorScope {
id: tabletComponent id: tabletComponent
visible: isWidescreen visible: isWidescreen
active: visible active: visible
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight) opacity: 1 - flickable.openFactor
asynchronous: true asynchronous: true
z: passwordFlickable.contentY === 0 ? 5 : 0 // in front of password flickable when closed
anchors.top: headerBar.bottom anchors.top: headerBar.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -212,14 +233,18 @@ PlasmaCore.ColorScope {
rightMargin: PlasmaCore.Units.gridUnit rightMargin: PlasmaCore.Units.gridUnit
} }
NotificationsList { NotificationsComponent {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumHeight: parent.height Layout.fillHeight: true
Layout.minimumHeight: this.notificationListHeight Layout.topMargin: PlasmaCore.Units.gridUnit * 2
Layout.bottomMargin: PlasmaCore.Units.gridUnit
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 15 Layout.minimumWidth: PlasmaCore.Units.gridUnit * 15
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25 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 { PlasmaCore.IconItem {
id: scrollUpIcon id: scrollUpIcon
anchors.bottom: parent.bottom 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 anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight) opacity: 1 - flickable.openFactor
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-up" source: "arrow-up"
} }
Flickable { // password keypad
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
ColumnLayout { ColumnLayout {
id: passwordLayout id: passwordLayout
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
transform: Translate { y: flickable.keypadHeight - flickable.position }
width: parent.width width: parent.width
spacing: PlasmaCore.Units.gridUnit spacing: PlasmaCore.Units.gridUnit
@ -285,13 +280,13 @@ PlasmaCore.ColorScope {
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-down" 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 { Keypad {
id: keypad id: keypad
focus: true focus: true
swipeProgress: passwordFlickable.contentY / passwordFlickable.columnHeight swipeProgress: flickable.openFactor
Layout.fillWidth: true Layout.fillWidth: true
onPasswordChanged: { onPasswordChanged: {
passwordFlickable.contentY = passwordFlickable.contentHeight - passwordFlickable.height passwordFlickable.contentY = passwordFlickable.contentHeight - passwordFlickable.height
@ -299,6 +294,7 @@ PlasmaCore.ColorScope {
} }
} }
} }
}
LockOsd { LockOsd {
anchors.top: parent.top 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
}
}
}
}