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
@ -87,7 +99,13 @@ 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 {
notificationsModel.expire(notificationsModel.index(modelIndex, 0)); if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
notificationsModel.expire(model.notificationId);
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
notificationsModel.expire(notificationsModel.index(modelIndex, 0));
}
} }
} }
function close() { function close() {
notificationsModel.close(notificationsModel.index(modelIndex, 0)); if (notificationsModelType === NotificationsModelType.WatchedNotificationsModel) {
notificationsModel.close(model.notificationId);
} else if (notificationsModelType === NotificationsModelType.NotificationsModel) {
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 {
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 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))
onClicked: list.setGroupExpanded(model.index, !model.isGroupExpanded)
}
} }
} }
} }

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
@ -65,236 +66,231 @@ PlasmaCore.ColorScope {
} }
} }
} }
Notifications.WatchedNotificationsModel {
id: notifModel
}
// header bar
Loader {
id: headerBar
asynchronous: true
anchors {
top: parent.top
left: parent.left
right: parent.right
}
height: PlasmaCore.Units.gridUnit * 1.25
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
sourceComponent: MobileShell.StatusBar {
id: statusBar
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
backgroundColor: "transparent"
showSecondRow: false
showDropShadow: true
showTime: false
disableSystemTray: true // HACK: prevent SIGABRT
}
}
// phone lockscreen component FlickContainer {
Loader { id: flickable
id: phoneComponent
visible: !isWidescreen
active: visible
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
asynchronous: true
z: passwordFlickable.contentY === 0 ? 5 : 0 // in front of password flickable when closed
anchors {
top: parent.top
bottom: scrollUpIcon.top
left: parent.left
right: parent.right
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 {
id: loadTimer
interval: PlasmaCore.Units.longDuration
}
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
sourceComponent: ColumnLayout {
id: phoneClockComponent
spacing: 0
Clock {
id: phoneClock
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
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
Layout.leftMargin: PlasmaCore.Units.gridUnit
Layout.rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsList {
id: phoneNotificationsList
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
}
}
}
// tablet lockscreen component
Loader {
id: tabletComponent
visible: isWidescreen
active: visible
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
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
anchors.bottom: scrollUpIcon.top
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
sourceComponent: Item {
Item {
id: tabletClockComponent
width: parent.width / 2
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: PlasmaCore.Units.gridUnit * 3
}
ColumnLayout {
id: tabletLayout
anchors.centerIn: parent
spacing: PlasmaCore.Units.gridUnit
Clock {
id: tabletClock
alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 20
}
MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
}
}
}
// tablet notifications list
ColumnLayout {
id: tabletNotificationsList
anchors {
top: parent.top
bottom: parent.bottom
left: tabletClockComponent.right
right: parent.right
rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsList {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillWidth: true
Layout.maximumHeight: parent.height
Layout.minimumHeight: this.notificationListHeight
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 15
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
onCountChanged: root.notificationsShown = count !== 0
}
}
}
}
// scroll up icon
PlasmaCore.IconItem {
id: scrollUpIcon
anchors.bottom: parent.bottom
anchors.bottomMargin: PlasmaCore.Units.gridUnit + passwordFlickable.contentY * 0.5
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
opacity: 1 - (passwordFlickable.contentY / passwordFlickable.columnHeight)
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-up"
}
Flickable {
id: passwordFlickable
anchors.fill: parent anchors.fill: parent
property int columnHeight: PlasmaCore.Units.gridUnit * 20 property real openFactor: position / keypadHeight
property int oldContentY: contentY
height: columnHeight + root.height Component.onCompleted: flickable.goToClosePosition()
contentHeight: columnHeight + root.height
boundsBehavior: Flickable.StopAtBounds
// always snap to end (either hidden or shown) onPositionChanged: {
onMovementEnded: { if (position > keypadHeight) {
if (!atYBeginning && !atYEnd) { position = keypadHeight;
if (contentY > columnHeight - contentY) { } else if (position < 0) {
flick(0, -1000); position = 0;
} else { }
flick(0, 1000); }
keypadHeight: PlasmaCore.Units.gridUnit * 20
Item {
width: flickable.width
height: flickable.height
y: flickable.contentY // effectively anchored to the screen
// header bar
Loader {
id: headerBar
asynchronous: true
anchors {
top: parent.top
left: parent.left
right: parent.right
}
height: PlasmaCore.Units.gridUnit * 1.25
opacity: 1 - flickable.openFactor
sourceComponent: MobileShell.StatusBar {
id: statusBar
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
backgroundColor: "transparent"
showSecondRow: false
showDropShadow: true
showTime: false
disableSystemTray: true // HACK: prevent SIGABRT
} }
} }
}
// phone lockscreen component
// wipe password if it is more than half way down the screen Loader {
onContentYChanged: { id: phoneComponent
if (contentY < columnHeight / 2 && oldContentY >= columnHeight / 2) { visible: !isWidescreen
keypad.reset(); active: visible
opacity: 1 - flickable.openFactor
asynchronous: true
anchors {
top: parent.top
bottom: scrollUpIcon.top
left: parent.left
right: parent.right
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 {
id: loadTimer
interval: PlasmaCore.Units.longDuration
}
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
sourceComponent: ColumnLayout {
id: phoneClockComponent
spacing: 0
Clock {
id: phoneClock
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
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
Layout.leftMargin: PlasmaCore.Units.gridUnit
Layout.rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsComponent {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.maximumWidth: PlasmaCore.Units.gridUnit * (25 + 2) // clip margins
topMargin: PlasmaCore.Units.gridUnit
leftMargin: PlasmaCore.Units.gridUnit
rightMargin: PlasmaCore.Units.gridUnit
}
}
} }
oldContentY = contentY;
}
// keypad area
ColumnLayout {
id: passwordLayout
anchors.bottom: parent.bottom
width: parent.width // tablet lockscreen component
spacing: PlasmaCore.Units.gridUnit Loader {
id: tabletComponent
visible: isWidescreen
active: visible
opacity: 1 - flickable.openFactor
asynchronous: true
anchors.top: headerBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: scrollUpIcon.top
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
sourceComponent: Item {
Item {
id: tabletClockComponent
width: parent.width / 2
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: PlasmaCore.Units.gridUnit * 3
}
ColumnLayout {
id: tabletLayout
anchors.centerIn: parent
spacing: PlasmaCore.Units.gridUnit
Clock {
id: tabletClock
alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 20
}
MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 25
}
}
}
// tablet notifications list
ColumnLayout {
id: tabletNotificationsList
anchors {
top: parent.top
bottom: parent.bottom
left: tabletClockComponent.right
right: parent.right
rightMargin: PlasmaCore.Units.gridUnit
}
NotificationsComponent {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillWidth: true
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
leftMargin: PlasmaCore.Units.gridUnit
rightMargin: PlasmaCore.Units.gridUnit
bottomMargin: PlasmaCore.Units.gridUnit
topMargin: PlasmaCore.Units.gridUnit
}
}
}
}
// scroll down icon // scroll up icon
PlasmaCore.IconItem { PlasmaCore.IconItem {
Layout.alignment: Qt.AlignHCenter id: scrollUpIcon
anchors.bottom: parent.bottom
anchors.bottomMargin: PlasmaCore.Units.gridUnit + flickable.position * 0.5
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 - flickable.openFactor
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-down" source: "arrow-up"
opacity: Math.sin((Math.PI / 2) * (passwordFlickable.contentY / passwordFlickable.columnHeight) + 1.5 * Math.PI) + 1
} }
// password keypad
ColumnLayout {
id: passwordLayout
anchors.bottom: parent.bottom
transform: Translate { y: flickable.keypadHeight - flickable.position }
width: parent.width
spacing: PlasmaCore.Units.gridUnit
// scroll down icon
PlasmaCore.IconItem {
Layout.alignment: Qt.AlignHCenter
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-down"
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
}
} }
} }
} }

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
}
}
}
}