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
@ -87,7 +99,13 @@ 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 {
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() {
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
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

@ -3,7 +3,7 @@
* 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
*/
*/
import QtQuick 2.8
import QtQuick.Layouts 1.1

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 {
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)
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))
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: 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
import QtQuick.Controls 1.1
@ -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
@ -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
Loader {
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
FlickContainer {
id: flickable
anchors.fill: parent
property int columnHeight: PlasmaCore.Units.gridUnit * 20
property int oldContentY: contentY
property real openFactor: position / keypadHeight
height: columnHeight + root.height
contentHeight: columnHeight + root.height
boundsBehavior: Flickable.StopAtBounds
Component.onCompleted: flickable.goToClosePosition()
// always snap to end (either hidden or shown)
onMovementEnded: {
if (!atYBeginning && !atYEnd) {
if (contentY > columnHeight - contentY) {
flick(0, -1000);
} else {
flick(0, 1000);
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 {
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
}
}
}
// wipe password if it is more than half way down the screen
onContentYChanged: {
if (contentY < columnHeight / 2 && oldContentY >= columnHeight / 2) {
keypad.reset();
// phone lockscreen component
Loader {
id: phoneComponent
visible: !isWidescreen
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
spacing: PlasmaCore.Units.gridUnit
// tablet lockscreen component
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 {
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
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
opacity: 1 - flickable.openFactor
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-down"
opacity: Math.sin((Math.PI / 2) * (passwordFlickable.contentY / passwordFlickable.columnHeight) + 1.5 * Math.PI) + 1
source: "arrow-up"
}
// 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 {
id: keypad
focus: true
swipeProgress: passwordFlickable.contentY / passwordFlickable.columnHeight
Layout.fillWidth: true
onPasswordChanged: {
passwordFlickable.contentY = passwordFlickable.contentHeight - passwordFlickable.height
Keypad {
id: keypad
focus: true
swipeProgress: flickable.openFactor
Layout.fillWidth: true
onPasswordChanged: {
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
}
}
}
}