diff --git a/components/mobileshell/qml/qmldir b/components/mobileshell/qml/qmldir index d6fbd659..2be22e28 100644 --- a/components/mobileshell/qml/qmldir +++ b/components/mobileshell/qml/qmldir @@ -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 diff --git a/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml b/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml index 47060141..f36a1fa3 100644 --- a/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml +++ b/components/mobileshell/qml/widgets/notifications/BaseNotificationItem.qml @@ -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(); + } + } } diff --git a/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml b/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml index d4b5184a..75622d95 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationGroupHeader.qml @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik * * 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 diff --git a/components/mobileshell/qml/widgets/notifications/NotificationItem.qml b/components/mobileshell/qml/widgets/notifications/NotificationItem.qml index 0d7c2589..7879d3ac 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationItem.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationItem.qml @@ -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 { diff --git a/components/mobileshell/qml/widgets/notifications/NotificationsModelType.qml b/components/mobileshell/qml/widgets/notifications/NotificationsModelType.qml new file mode 100644 index 00000000..2867de77 --- /dev/null +++ b/components/mobileshell/qml/widgets/notifications/NotificationsModelType.qml @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2022 Devin Lin + * + * 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 + } +} diff --git a/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml b/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml index bd2707e7..e58b0913 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml @@ -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) + } } } } diff --git a/look-and-feel/contents/lockscreen/DraggableDelegate.qml b/look-and-feel/contents/lockscreen/DraggableDelegate.qml deleted file mode 100644 index d93833b3..00000000 --- a/look-and-feel/contents/lockscreen/DraggableDelegate.qml +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2019 Marco Martin - * - * 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 - } -} diff --git a/look-and-feel/contents/lockscreen/FlickContainer.qml b/look-and-feel/contents/lockscreen/FlickContainer.qml new file mode 100644 index 00000000..5f978ed1 --- /dev/null +++ b/look-and-feel/contents/lockscreen/FlickContainer.qml @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * 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; + } +} + + diff --git a/look-and-feel/contents/lockscreen/LockScreen.qml b/look-and-feel/contents/lockscreen/LockScreen.qml index 82173291..3f98c47a 100644 --- a/look-and-feel/contents/lockscreen/LockScreen.qml +++ b/look-and-feel/contents/lockscreen/LockScreen.qml @@ -1,9 +1,9 @@ /* -SPDX-FileCopyrightText: 2019 Nicolas Fella -SPDX-FileCopyrightText: 2021 Devin Lin - -SPDX-License-Identifier: GPL-2.0-or-later -*/ + * SPDX-FileCopyrightText: 2019 Nicolas Fella + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * 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 + } } } } diff --git a/look-and-feel/contents/lockscreen/NotificationsComponent.qml b/look-and-feel/contents/lockscreen/NotificationsComponent.qml new file mode 100644 index 00000000..358ca2a8 --- /dev/null +++ b/look-and-feel/contents/lockscreen/NotificationsComponent.qml @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2022 Devin Lin + * + * 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(); + } + } + } +} diff --git a/look-and-feel/contents/lockscreen/NotificationsList.qml b/look-and-feel/contents/lockscreen/NotificationsList.qml deleted file mode 100644 index f93d7c1d..00000000 --- a/look-and-feel/contents/lockscreen/NotificationsList.qml +++ /dev/null @@ -1,143 +0,0 @@ -/* -SPDX-FileCopyrightText: 2020 Devin Lin - -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 } - } - } -} diff --git a/look-and-feel/contents/lockscreen/SimpleNotification.qml b/look-and-feel/contents/lockscreen/SimpleNotification.qml deleted file mode 100644 index f547e955..00000000 --- a/look-and-feel/contents/lockscreen/SimpleNotification.qml +++ /dev/null @@ -1,212 +0,0 @@ -/* -SPDX-FileCopyrightText: 2020 Devin Lin - -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 - } - } - } -}