diff --git a/shell/contents/lockscreen/BottomIconIndicator.qml b/shell/contents/lockscreen/BottomIconIndicator.qml index 851186b2..227fec7f 100644 --- a/shell/contents/lockscreen/BottomIconIndicator.qml +++ b/shell/contents/lockscreen/BottomIconIndicator.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-FileCopyrightText: 2023-2024 Devin Lin // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick @@ -27,7 +27,6 @@ Loader { Kirigami.Icon { implicitWidth: Kirigami.Units.iconSizes.small implicitHeight: Kirigami.Units.iconSizes.small - opacity: 1 - flickable.openFactor Kirigami.Theme.colorSet: Kirigami.Theme.Complementary source: "arrow-up" @@ -39,7 +38,6 @@ Loader { Kirigami.Icon { source: 'fingerprint-symbolic' - opacity: 1 - flickable.openFactor implicitWidth: Kirigami.Units.iconSizes.medium implicitHeight: Kirigami.Units.iconSizes.medium diff --git a/shell/contents/lockscreen/Clock.qml b/shell/contents/lockscreen/Clock.qml index 678424bb..94066c13 100644 --- a/shell/contents/lockscreen/Clock.qml +++ b/shell/contents/lockscreen/Clock.qml @@ -1,6 +1,6 @@ /* * SPDX-FileCopyrightText: 2019 Nicolas Fella - * SPDX-FileCopyrightText: 2020-2022 Devin Lin + * SPDX-FileCopyrightText: 2020-2024 Devin Lin * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -17,9 +17,9 @@ Item { id: root implicitHeight: clockColumn.implicitHeight implicitWidth: clockColumn.implicitWidth - + property int layoutAlignment - + P5Support.DataSource { id: timeSource engine: "time" @@ -27,19 +27,19 @@ Item { interval: 60000 intervalAlignment: P5Support.Types.AlignToMinute } - + ColumnLayout { id: clockColumn spacing: Kirigami.Units.gridUnit - + anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - + PC3.Label { text: Qt.formatTime(timeSource.data["Local"]["DateTime"], MobileShell.ShellUtil.isSystem24HourFormat ? "h:mm" : "h:mm ap") color: "white" - + Layout.alignment: root.layoutAlignment font.weight: Font.Bold font.pointSize: 36 @@ -49,11 +49,11 @@ Item { blurMax: 16 } } - + PC3.Label { text: Qt.formatDate(timeSource.data["Local"]["DateTime"], "ddd, MMM d") color: "white" - + Layout.alignment: root.layoutAlignment font.weight: Font.Bold font.pointSize: 10 diff --git a/shell/contents/lockscreen/FlickContainer.qml b/shell/contents/lockscreen/FlickContainer.qml index af827b09..3cbf24a4 100644 --- a/shell/contents/lockscreen/FlickContainer.qml +++ b/shell/contents/lockscreen/FlickContainer.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-FileCopyrightText: 2021-2024 Devin Lin // SPDX-License-Identifier: LGPL-2.0-or-later import QtQuick 2.15 @@ -9,59 +9,66 @@ import org.kde.plasma.private.mobileshell as MobileShell MobileShell.SwipeArea { id: root - mode: MobileShell.SwipeArea.VerticalOnly - - property int position: 0 - required property real keypadHeight - + + readonly property real openFactor: position / keypadHeight + property real position: 0 + property bool movingUp: false + property real __oldPosition: position + signal opened() - + + mode: MobileShell.SwipeArea.VerticalOnly + function cancelAnimations() { positionAnim.stop(); } - + function goToOpenPosition() { positionAnim.to = keypadHeight; positionAnim.restart(); } - + function goToClosePosition() { positionAnim.to = 0; positionAnim.restart(); } - + function updateState() { // don't update state if at end if (position <= 0 || position >= keypadHeight) return; - + if (movingUp) { goToOpenPosition(); } else { goToClosePosition(); } } - + NumberAnimation on position { id: positionAnim - duration: Kirigami.Units.veryLongDuration + duration: 800 easing.type: Easing.OutExpo - + onFinished: { if (root.position === keypadHeight) { root.opened(); } } } - - property int oldPosition: position - property bool movingUp: false - + onPositionChanged: { - movingUp = oldPosition <= position; - oldPosition = position; + movingUp = __oldPosition <= position; + __oldPosition = position; + + // Limit position to between 0 and keypadHeight + if (position > keypadHeight) { + position = keypadHeight; + } else if (position < 0) { + position = 0; + } } - + onSwipeStarted: cancelAnimations(); onSwipeEnded: { if (!positionAnim.running) { diff --git a/shell/contents/lockscreen/Keypad.qml b/shell/contents/lockscreen/Keypad.qml index 3988e5fa..66b3d32b 100644 --- a/shell/contents/lockscreen/Keypad.qml +++ b/shell/contents/lockscreen/Keypad.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020-2023 Devin Lin +// SPDX-FileCopyrightText: 2020-2024 Devin Lin // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick @@ -13,182 +13,153 @@ import org.kde.plasma.private.mobileshell as MobileShell import org.kde.kirigami 2.12 as Kirigami -Rectangle { - id: keypadRoot - +Item { + id: root + required property real openProgress required property var lockScreenState - + property alias passwordBar: passwordBar - - // 0 - keypad is not shown, 1 - keypad is shown - property double swipeProgress - - // slightly translucent background, for key contrast - color: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.backgroundColor, {"alpha": 0.9*255}) - - // colour calculations - readonly property color buttonColor: Qt.lighter(Kirigami.Theme.backgroundColor, 1.3) - readonly property color buttonPressedColor: Qt.darker(Kirigami.Theme.backgroundColor, 1.08) - readonly property color buttonTextColor: Kirigami.Theme.textColor - readonly property color dropShadowColor: Qt.darker(Kirigami.Theme.backgroundColor, 1.2) - readonly property color headerBackgroundColor: Qt.lighter(Kirigami.Theme.backgroundColor, 1.3) - - opacity: Math.sin((Math.PI / 2) * swipeProgress + 1.5 * Math.PI) + 1 - - implicitHeight: { - if (passwordBar.isPinMode && !Qt.inputMethod.visible) { - return Kirigami.Units.gridUnit * 17; - } else { - return Math.min(root.height - passwordBar.implicitHeight, // don't make the password bar go off the screen - Kirigami.Units.smallSpacing * 2 + Qt.inputMethod.keyboardRectangle.height + passwordBar.implicitHeight); - } - } - - Behavior on implicitHeight { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - } MobileShell.HapticsEffect { id: haptics } - // pin display and bar - PasswordBar { - id: passwordBar - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - color: keypadRoot.headerBackgroundColor - opacity: (Math.sin(2*((Math.PI / 2) * keypadRoot.swipeProgress + 1.5 * Math.PI)) + 1) - - lockScreenState: keypadRoot.lockScreenState - - keypadOpen: swipeProgress === 1 - previewCharIndex: -2 - - layer.enabled: true - layer.effect: MultiEffect { - blurMax: 16 - shadowEnabled: true - shadowVerticalOffset: 1 - shadowOpacity: 0.3 - shadowColor: keypadRoot.dropShadowColor - } - } - - // actual number keys + // Column layout - most cases ColumnLayout { - visible: opacity > 0 - opacity: passwordBar.isPinMode ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: Kirigami.Units.longDuration - easing.type: Easing.InOutQuad - } - } - - anchors { - left: parent.left - right: parent.right - top: passwordBar.bottom - bottom: parent.bottom - topMargin: Kirigami.Units.gridUnit - bottomMargin: Kirigami.Units.gridUnit - } + id: keypadVerticalContainer + visible: root.height > Kirigami.Units.gridUnit * 25 + + anchors.centerIn: parent spacing: Kirigami.Units.gridUnit - GridLayout { - id: grid - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.leftMargin: Kirigami.Units.gridUnit * 0.5 - Layout.rightMargin: Kirigami.Units.gridUnit * 0.5 - Layout.maximumWidth: Kirigami.Units.gridUnit * 22 - Layout.maximumHeight: Kirigami.Units.gridUnit * 12.5 - opacity: (Math.sin(2*((Math.PI / 2) * keypadRoot.swipeProgress + 1.5 * Math.PI)) + 1) - - columns: 4 - - readonly property real keyRadius: 5 - - // numpad keys - Repeater { - model: ["1", "2", "3", "R", "4", "5", "6", "0", "7", "8", "9", "E"] + LayoutItemProxy { target: header } + LayoutItemProxy { target: keypadGrid } + } - delegate: AbstractButton { - id: button - Layout.fillWidth: true - Layout.fillHeight: true - visible: modelData.length > 0 - opacity: (Math.sin(2*((Math.PI / 2) * keypadRoot.swipeProgress + 1.5 * Math.PI)) + 1) + // Row layout - used when there is restricted height + RowLayout { + id: keypadHorizontalContainer + visible: !keypadVerticalContainer.visible - background: Rectangle { - id: keyRect - radius: grid.keyRadius - color: button.pressed ? keypadRoot.buttonPressedColor : keypadRoot.buttonColor + anchors.centerIn: parent + spacing: Kirigami.Units.gridUnit * 2 + + LayoutItemProxy { target: header } + LayoutItemProxy { target: keypadGrid } + } + + ColumnLayout { + id: header + spacing: Kirigami.Units.gridUnit + + // label ("wrong pin", "enter pin") + Label { + id: descriptionLabel + Layout.alignment: Qt.AlignHCenter + opacity: root.lockScreenState.password.length === 0 ? 1 : 0 + text: root.lockScreenState.pinLabel + font.pointSize: 12 + font.bold: true + color: 'white' + + // Enforce extra margin at top of vertical container + Layout.topMargin: keypadVerticalContainer.visible ? Kirigami.Units.gridUnit * 3 : 0 + + Behavior on opacity { + NumberAnimation { duration: 200 } + } + } + + // pin display and bar + PasswordBar { + id: passwordBar + Layout.preferredWidth: Kirigami.Units.gridUnit * 14 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2.5 + + lockScreenState: root.lockScreenState + isKeypadOpen: root.openProgress >= 0.9 + } + } + + GridLayout { + id: keypadGrid + columnSpacing: Kirigami.Units.gridUnit + rowSpacing: Kirigami.Units.gridUnit + uniformCellHeights: true + uniformCellWidths: true + + readonly property real intendedWidth: Kirigami.Units.gridUnit * 14 + + Layout.preferredWidth: Kirigami.Units.gridUnit * 14 + Layout.preferredHeight: Kirigami.Units.gridUnit * 22 + + readonly property real cellLength: (intendedWidth - columnSpacing * 2) / 3 + + columns: 3 + + // numpad keys + Repeater { + model: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "R", "0", "E"] + + delegate: AbstractButton { + id: button + implicitWidth: keypadGrid.cellLength + implicitHeight: keypadGrid.cellLength + visible: modelData.length > 0 + enabled: root.openProgress >= 0.8 // Only enable after a certain point in animation + + opacity: enabled + Behavior on opacity { + NumberAnimation { duration: 100 * index } + } + + background: Rectangle { + readonly property real restingOpacity: (modelData !== "R" && modelData !== "E") ? 0.2 : 0.0 + radius: width + color: Qt.rgba(255, 255, 255, + button.pressed ? 0.5 : restingOpacity) + } + + onPressedChanged: { + if (pressed) { + haptics.buttonVibrate(); + } + } + + onClicked: { + if (modelData === "R") { + passwordBar.backspace(); + } else if (modelData === "E") { + passwordBar.enter(); + } else { + passwordBar.keyPress(modelData); + } + } + + onPressAndHold: { + if (modelData === "R") { + haptics.buttonVibrate(); + passwordBar.clear(); + } + } + + contentItem: Item { + PlasmaComponents.Label { + visible: modelData !== "R" && modelData !== "E" + text: modelData + anchors.centerIn: parent + font.pointSize: 18 + color: 'white' } - layer.enabled: true - layer.effect: MultiEffect { - blurMax: 16 - shadowEnabled: true - shadowVerticalOffset: 1 - shadowOpacity: 0.3 - shadowColor: keypadRoot.dropShadowColor - } - - onPressedChanged: { - if (pressed) { - haptics.buttonVibrate(); - } - } - - onClicked: { - if (modelData === "R") { - passwordBar.backspace(); - } else if (modelData === "E") { - passwordBar.enter(); - } else { - passwordBar.keyPress(modelData); - } - } - onPressAndHold: { - if (modelData === "R") { - haptics.buttonVibrate(); - passwordBar.clear(); - } - } - - contentItem: Item { - PlasmaComponents.Label { - visible: modelData !== "R" && modelData !== "E" - text: modelData - anchors.centerIn: parent - font.pointSize: 18 - font.weight: Font.Light - color: keypadRoot.buttonTextColor - } - - Kirigami.Icon { - visible: modelData === "R" - anchors.centerIn: parent - width: Kirigami.Units.iconSizes.small - height: Kirigami.Units.iconSizes.small - source: "edit-clear" - } - - Kirigami.Icon { - visible: modelData === "E" - anchors.centerIn: parent - width: Kirigami.Units.iconSizes.small - height: Kirigami.Units.iconSizes.small - source: "go-next" - } + Kirigami.Icon { + visible: modelData === "R" || modelData === "E" + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.small + height: Kirigami.Units.iconSizes.small + source: modelData === "R" ? "edit-clear" : "go-next" + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary } } } diff --git a/shell/contents/lockscreen/LockScreen.qml b/shell/contents/lockscreen/LockScreen.qml index 6322a8e1..30e000b4 100644 --- a/shell/contents/lockscreen/LockScreen.qml +++ b/shell/contents/lockscreen/LockScreen.qml @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2019 Nicolas Fella -// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-FileCopyrightText: 2021-2024 Devin Lin // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick @@ -19,47 +19,56 @@ import org.kde.kirigami 2.12 as Kirigami Item { id: root - property var lockScreenState: LockScreenState {} - property var notifModel: Notifications.WatchedNotificationsModel {} + readonly property var lockScreenState: LockScreenState {} + readonly property var notifModel: Notifications.WatchedNotificationsModel {} - // only show widescreen mode for short height devices (ex. phone landscape) - property bool isWidescreen: root.height < 720 && (root.height < root.width * 0.75) + // Only show widescreen mode for short height devices (ex. phone landscape) + readonly property bool isWidescreen: root.height < 720 && (root.height < root.width * 0.75) property bool notificationsShown: false - readonly property bool drawerOpen: flickable.openFactor >= 1 - property var passwordBar: keypadLoader.item.passwordBar + property var passwordBar: keypad.passwordBar - // listen for keyboard events, and focus on input area - Component.onCompleted: forceActiveFocus(); + Component.onCompleted: { + forceActiveFocus(); + + // Go to closed position when loaded + flickable.position = 0; + flickable.goToClosePosition(); + } + + // Listen for keyboard events, and focus on input area Keys.onPressed: { - passwordBar.isPinMode = false; + root.lockScreenState.isKeyboardMode = true; flickable.goToOpenPosition(); passwordBar.textField.forceActiveFocus(); } - // wallpaper blur + // Wallpaper blur Loader { anchors.fill: parent asynchronous: true sourceComponent: WallpaperBlur { source: wallpaper - shouldBlur: root.notificationsShown || root.drawerOpen // only blur once animation finished for performance + opacity: flickable.openFactor } } Connections { target: root.lockScreenState - // ensure keypad is opened when password is updated (ex. keyboard) + // Ensure keypad is opened when password is updated (ex. keyboard) function onPasswordChanged() { - flickable.goToOpenPosition() + if (root.lockScreenState.password !== "") { + flickable.goToOpenPosition(); + } } } Item { + id: lockscreenContainer anchors.fill: parent - // header bar and action drawer + // Header bar and action drawer Loader { id: headerBarLoader z: 1 // on top of flick area @@ -80,67 +89,35 @@ Item { id: flickable anchors.fill: parent - property real openFactor: position / keypadHeight - - onOpened: { - if (root.lockScreenState.passwordless) { - // try unlocking if flicked to the top, and we have passwordless login - root.lockScreenState.tryPassword(); - } - } - + // Distance to swipe to fully open keypad keypadHeight: Kirigami.Units.gridUnit * 20 - // go to closed position when loaded - Component.onCompleted: { - flickable.position = 0; - flickable.goToClosePosition(); - } - - // update position, and cap it at the keypad height - onPositionChanged: { - if (position > keypadHeight) { - position = keypadHeight; - } else if (position < 0) { - position = 0; + // Clear entered password after closing keypad + onOpenFactorChanged: { + if (flickable.openFactor < 0.1) { + root.passwordBar.clear(); } } - LockScreenNarrowContent { - id: phoneComponent + LockScreenContent { + id: lockScreenContent - visible: !isWidescreen - active: visible - opacity: 1 - flickable.openFactor + isVertical: !root.isWidescreen + opacity: Math.max(0, 1 - flickable.openFactor * 2) + transform: [ + Scale { + origin.x: lockScreenContent.width / 2 + origin.y: lockScreenContent.height / 2 + yScale: 1 - (flickable.openFactor * 2) * 0.1 + xScale: 1 - (flickable.openFactor * 2) * 0.1 + } + ] fullHeight: root.height lockScreenState: root.lockScreenState notificationsModel: root.notifModel onNotificationsShownChanged: root.notificationsShown = notificationsShown - - onPasswordRequested: flickable.goToOpenPosition() - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - - // move while swiping up - transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) } - } - - LockScreenWideScreenContent { - id: tabletComponent - - visible: isWidescreen - active: visible - opacity: 1 - flickable.openFactor - - lockScreenState: root.lockScreenState - notificationsModel: root.notifModel - onNotificationsShownChanged: root.notificationsShown = notificationsShown - onPasswordRequested: flickable.goToOpenPosition() anchors.topMargin: headerBarLoader.statusBarHeight @@ -148,75 +125,36 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - - // move while swiping up - transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) } } // scroll up icon BottomIconIndicator { id: scrollUpIconLoader lockScreenState: root.lockScreenState + opacity: Math.max(0, 1 - flickable.openFactor * 2) anchors.bottom: parent.bottom - anchors.bottomMargin: Kirigami.Units.gridUnit + flickable.position * 0.5 + anchors.bottomMargin: Kirigami.Units.gridUnit + flickable.position * 0.1 anchors.horizontalCenter: parent.horizontalCenter } - // password keypad - Loader { - id: keypadLoader - width: parent.width - asynchronous: true - active: !root.lockScreenState.passwordless // only load keypad if not passwordless + Rectangle { + id: keypadScrim + anchors.fill: parent + visible: opacity > 0 + opacity: flickable.openFactor + color: Qt.rgba(0, 0, 0, 0.5) + } - anchors.bottom: parent.bottom + Keypad { + id: keypad + anchors.fill: parent + openProgress: flickable.openFactor + lockScreenState: root.lockScreenState - sourceComponent: ColumnLayout { - property alias passwordBar: keypad.passwordBar - - transform: Translate { y: flickable.keypadHeight - flickable.position } - spacing: 0 - - // info notification text - Label { - Layout.fillWidth: true - Layout.rightMargin: Kirigami.Units.largeSpacing - Layout.leftMargin: Kirigami.Units.largeSpacing - Layout.bottomMargin: Kirigami.Units.smallSpacing * 2 - font.pointSize: 9 - - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - text: root.lockScreenState.info - opacity: (root.lockScreenState.info.length === 0 || flickable.openFactor < 1) ? 0 : 1 - color: 'white' - - Behavior on opacity { - NumberAnimation { duration: 200 } - } - } - - // scroll down icon - Kirigami.Icon { - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: Kirigami.Units.gridUnit - implicitWidth: Kirigami.Units.iconSizes.small - implicitHeight: Kirigami.Units.iconSizes.small - Kirigami.Theme.colorSet: Kirigami.Theme.Complementary - source: "arrow-down" - opacity: Math.sin((Math.PI / 2) * flickable.openFactor + 1.5 * Math.PI) + 1 - } - - Keypad { - id: keypad - Layout.fillWidth: true - focus: true - - lockScreenState: root.lockScreenState - swipeProgress: flickable.openFactor - } - } + // only show in last 50% of anim + opacity: (flickable.openFactor - 0.5) * 2 + transform: Translate { y: (flickable.keypadHeight - flickable.position) * 0.1 } } } } diff --git a/shell/contents/lockscreen/LockScreenContent.qml b/shell/contents/lockscreen/LockScreenContent.qml new file mode 100644 index 00000000..5c479567 --- /dev/null +++ b/shell/contents/lockscreen/LockScreenContent.qml @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2021-2024 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +import org.kde.kirigami 2.20 as Kirigami +import org.kde.plasma.workspace.keyboardlayout 1.0 +import org.kde.notificationmanager as Notifications +import org.kde.plasma.private.mobileshell as MobileShell + +Item { + id: root + + required property var lockScreenState + required property bool isVertical + + property var notificationsModel: [] + property bool notificationsShown: false + + property real fullHeight + + signal passwordRequested() + + // Vertical layout + ColumnLayout { + id: verticalLayout + visible: root.isVertical + spacing: 0 + + // center clock when no notifications are shown, otherwise move the clock upward + anchors.topMargin: !root.notificationsShown ? Math.round(root.fullHeight / 2 - (verticalLayout.implicitHeight / 2)) : Kirigami.Units.gridUnit * 5 + anchors.bottomMargin: Kirigami.Units.gridUnit + anchors.fill: parent + + // animate + Behavior on anchors.topMargin { + NumberAnimation { + duration: Kirigami.Units.veryLongDuration + easing.type: Easing.InOutExpo + } + } + + LayoutItemProxy { target: clockAndMediaWidget } + LayoutItemProxy { target: notificationComponent } + } + + // Horizontal layout (landscape on smaller devices) + Item { + id: horizontalLayout + anchors.fill: parent + visible: !root.isVertical + + ColumnLayout { + id: leftLayout + width: parent.width / 2 + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + leftMargin: Kirigami.Units.gridUnit * 3 + } + + LayoutItemProxy { target: clockAndMediaWidget } + } + + ColumnLayout { + id: rightLayout + anchors { + top: parent.top + bottom: parent.bottom + left: leftLayout.right + right: parent.right + rightMargin: Kirigami.Units.gridUnit + } + + LayoutItemProxy { target: notificationComponent } + } + } + + // Clock and media widget column + ColumnLayout { + id: clockAndMediaWidget + Layout.fillWidth: true + Layout.fillHeight: root.isVertical + spacing: Kirigami.Units.gridUnit * 2 + + Clock { + layoutAlignment: root.isVertical ? Qt.AlignHCenter : Qt.AlignLeft + Layout.alignment: root.isVertical ? Qt.AlignHCenter : Qt.AlignLeft + Layout.fillWidth: true + Layout.bottomMargin: root.isVertical ? Kirigami.Units.gridUnit * 2 : 0 + } + + MobileShell.MediaControlsWidget { + Layout.alignment: root.isVertical ? Qt.AlignHCenter : Qt.AlignLeft + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * 25 + Layout.leftMargin: root.isVertical ? Kirigami.Units.gridUnit : 0 + Layout.rightMargin: root.isVertical ? Kirigami.Units.gridUnit : 0 + + layer.enabled: true + layer.effect: MultiEffect { + blurMax: 16 + shadowEnabled: true + shadowVerticalOffset: 1 + shadowOpacity: 0.5 + shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1) + } + } + } + + NotificationsComponent { + id: notificationComponent + lockScreenState: root.lockScreenState + notificationsModel: root.notificationsModel + + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.fillHeight: true + Layout.fillWidth: true + Layout.maximumWidth: Kirigami.Units.gridUnit * (25 + 2) // clip margins + + leftMargin: root.isVertical ? 0 : Kirigami.Units.gridUnit + rightMargin: root.isVertical ? 0 : Kirigami.Units.gridUnit + bottomMargin: root.isVertical ? 0 : Kirigami.Units.gridUnit + topMargin: Kirigami.Units.gridUnit + + onPasswordRequested: root.passwordRequested() + onNotificationsShownChanged: root.notificationsShown = notificationsShown + } +} \ No newline at end of file diff --git a/shell/contents/lockscreen/LockScreenNarrowContent.qml b/shell/contents/lockscreen/LockScreenNarrowContent.qml deleted file mode 100644 index 7d248cf7..00000000 --- a/shell/contents/lockscreen/LockScreenNarrowContent.qml +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2023 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -import QtQuick -import QtQuick.Layouts -import QtQuick.Effects - -import org.kde.kirigami 2.20 as Kirigami -import org.kde.plasma.workspace.keyboardlayout 1.0 -import org.kde.notificationmanager as Notifications -import org.kde.plasma.private.mobileshell as MobileShell - -Loader { - id: root - - required property var lockScreenState - - property var notificationsModel: [] - property bool notificationsShown: false - - property real fullHeight - - signal passwordRequested() - - // avoid topMargin animation when item is being loaded - onLoaded: loadTimer.restart(); - Timer { - id: loadTimer - interval: 200 - } - - // move while swiping up - transform: Translate { y: Math.round((1 - root.opacity) * (-root.height / 6)) } - - asynchronous: true - sourceComponent: Item { - ColumnLayout { - id: column - spacing: 0 - - // center clock when no notifications are shown, otherwise move the clock upward - anchors.topMargin: !root.notificationsShown ? Math.round(root.fullHeight / 2 - (column.implicitHeight / 2)) : Kirigami.Units.gridUnit * 5 - anchors.bottomMargin: Kirigami.Units.gridUnit - anchors.fill: parent - - // animate - Behavior on anchors.topMargin { - NumberAnimation { - duration: loadTimer.running ? 0 : Kirigami.Units.veryLongDuration - easing.type: Easing.InOutExpo - } - } - - Clock { - layoutAlignment: Qt.AlignHCenter - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: Kirigami.Units.gridUnit * 2 // keep spacing even if media controls are gone - } - - MobileShell.MediaControlsWidget { - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.maximumWidth: Kirigami.Units.gridUnit * 25 - Layout.leftMargin: Kirigami.Units.gridUnit - Layout.rightMargin: Kirigami.Units.gridUnit - - layer.enabled: true - layer.effect: MultiEffect { - blurMax: 16 - shadowEnabled: true - shadowVerticalOffset: 1 - shadowOpacity: 0.5 - shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1) - } - } - - NotificationsComponent { - id: notificationComponent - lockScreenState: root.lockScreenState - notificationsModel: root.notificationsModel - - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - Layout.fillWidth: true - Layout.maximumWidth: Kirigami.Units.gridUnit * (25 + 2) // clip margins - topMargin: Kirigami.Units.gridUnit - - onPasswordRequested: root.passwordRequested() - onNotificationsShownChanged: root.notificationsShown = notificationsShown - } - } - } -} diff --git a/shell/contents/lockscreen/LockScreenState.qml b/shell/contents/lockscreen/LockScreenState.qml index cec9f317..5ac7de7b 100644 --- a/shell/contents/lockscreen/LockScreenState.qml +++ b/shell/contents/lockscreen/LockScreenState.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-FileCopyrightText: 2022-2024 Devin Lin // SPDX-License-Identifier: GPL-2.0-or-later import QtQml @@ -8,77 +8,97 @@ import org.kde.kscreenlocker 1.0 as ScreenLocker QtObject { id: root - + // current password being typed property string password: "" - + // whether waiting for authentication after trying password property bool waitingForAuth: false - + // the info message given property string info: "" - + // whether the lockscreen was passwordless property bool passwordless: false // TODO true - + // whether the device can log in with fingerprint readonly property bool isFingerprintSupported: authenticator.authenticatorTypes & ScreenLocker.Authenticator.Fingerprint + // whether we are in keyboard mode (hiding the numpad) + property bool isKeyboardMode: false + + property string pinLabel: enterPinLabel + readonly property string enterPinLabel: i18n("Enter PIN") + readonly property string wrongPinLabel: i18n("Wrong PIN") + signal reset() signal unlockSucceeded() signal unlockFailed() - + + Component.onCompleted: authenticator.startAuthenticating(); + function tryPassword() { if (root.password !== '') { // prevent typing lock when password is empty - waitingForAuth = true; + root.waitingForAuth = true; } - authenticator.startAuthenticating(); + authenticator.respond(root.password); } - + function resetPassword() { password = ""; root.reset(); } - + + function resetPinLabel(): void { + pinLabel = enterPinLabel; + } + + property var graceLockTimer: Timer { + interval: 1000 + onTriggered: { + root.waitingForAuth = false; + root.password = ""; + authenticator.startAuthenticating(); + } + } + property var connections: Connections { target: authenticator - + function onSucceeded() { console.log('login succeeded'); root.waitingForAuth = false; root.unlockSucceeded(); Qt.quit(); } - - function onFailed() { + + function onFailed(kind: int): void { + if (kind != 0) { // if this is coming from the noninteractive authenticators + return; + } console.log('login failed'); - root.waitingForAuth = false; - root.password = ""; + graceLockTimer.restart(); + root.pinLabel = root.wrongPinLabel; root.unlockFailed(); } - + function onInfoMessageChanged() { console.log('info: ' + authenticator.infoMessage); root.info += authenticator.infoMessage + " "; } - + // TODO function onErrorMessageChanged() { console.log('error: ' + authenticator.errorMessage); } - + // TODO function onPromptChanged() { console.log('prompt: ' + authenticator.prompt); } - + function onPromptForSecretChanged() { console.log('prompt secret: ' + authenticator.promptForSecret); - if (root.password !== "") { - authenticator.respond(root.password); - authenticator.startAuthenticating(); - } } - } } diff --git a/shell/contents/lockscreen/LockScreenWideScreenContent.qml b/shell/contents/lockscreen/LockScreenWideScreenContent.qml deleted file mode 100644 index df50d083..00000000 --- a/shell/contents/lockscreen/LockScreenWideScreenContent.qml +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Devin Lin -// SPDX-License-Identifier: GPL-2.0-or-later - -import QtQuick -import QtQuick.Layouts -import QtQuick.Effects - -import org.kde.kirigami 2.20 as Kirigami -import org.kde.plasma.workspace.keyboardlayout 1.0 -import org.kde.notificationmanager as Notifications -import org.kde.plasma.private.mobileshell as MobileShell - -Loader { - id: root - - required property var lockScreenState - - property var notificationsModel: [] - property bool notificationsShown: false - - signal passwordRequested() - - asynchronous: true - sourceComponent: Item { - Item { - id: clock - width: parent.width / 2 - anchors { - top: parent.top - bottom: parent.bottom - left: parent.left - leftMargin: Kirigami.Units.gridUnit * 3 - } - - ColumnLayout { - id: tabletLayout - anchors.centerIn: parent - spacing: Kirigami.Units.gridUnit - - Clock { - layoutAlignment: Qt.AlignLeft - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: Kirigami.Units.gridUnit * 20 - } - - MobileShell.MediaControlsWidget { - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.maximumWidth: Kirigami.Units.gridUnit * 25 - - layer.enabled: true - layer.effect: MultiEffect { - blurMax: 16 - shadowEnabled: true - shadowVerticalOffset: 1 - shadowOpacity: 0.5 - shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.1) - } - } - } - } - - // tablet notifications list - ColumnLayout { - id: tabletNotificationsList - anchors { - top: parent.top - bottom: parent.bottom - left: clock.right - right: parent.right - rightMargin: Kirigami.Units.gridUnit - } - - NotificationsComponent { - id: notificationComponent - lockScreenState: root.lockScreenState - notificationsModel: root.notificationsModel - - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.fillWidth: true - Layout.fillHeight: true - Layout.topMargin: Kirigami.Units.gridUnit * 2 - Layout.bottomMargin: Kirigami.Units.gridUnit - Layout.minimumWidth: Kirigami.Units.gridUnit * 15 - Layout.maximumWidth: Kirigami.Units.gridUnit * 25 - - leftMargin: Kirigami.Units.gridUnit - rightMargin: Kirigami.Units.gridUnit - bottomMargin: Kirigami.Units.gridUnit - topMargin: Kirigami.Units.gridUnit - - onPasswordRequested: root.passwordRequested() - onNotificationsShownChanged: root.notificationsShown = notificationsShown - } - } - } -} diff --git a/shell/contents/lockscreen/PasswordBar.qml b/shell/contents/lockscreen/PasswordBar.qml index 33fddae4..b34e8287 100644 --- a/shell/contents/lockscreen/PasswordBar.qml +++ b/shell/contents/lockscreen/PasswordBar.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020-2022 Devin Lin +// SPDX-FileCopyrightText: 2020-2024 Devin Lin // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick @@ -12,42 +12,31 @@ import org.kde.kirigami 2.12 as Kirigami Rectangle { id: root - implicitHeight: Kirigami.Units.gridUnit * 2.5 - required property var lockScreenState - + property alias textField: textField - - // toggle between pin and password mode - property bool isPinMode: true - + + required property bool isKeypadOpen + // for displaying temporary number in pin dot display property int previewCharIndex: -2 - - property string pinLabel: i18n("Enter PIN") - - property bool keypadOpen - - readonly property color headerTextColor: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.75*255}) - readonly property color headerTextInactiveColor: Kirigami.ColorUtils.adjustColor(Kirigami.Theme.textColor, {"alpha": 0.4*255}) - + + readonly property color headerTextColor: Qt.rgba(255, 255, 255, 1) + readonly property color headerTextInactiveColor: Qt.rgba(255, 255, 255, 0.4) + + radius: Kirigami.Units.largeSpacing + color: Qt.rgba(255, 255, 255, 0.3) + // model for shown dots // we need to use a listmodel to avoid all delegates from reloading ListModel { id: dotDisplayModel } - + + // Listen to lockscreen state changes Connections { target: root.lockScreenState - - function onUnlockSucceeded() { - root.pinLabel = i18n("Logging in..."); - } - - function onUnlockFailed() { - root.pinLabel = i18n("Wrong PIN"); - } - + function onPasswordChanged() { while (root.lockScreenState.password.length < dotDisplayModel.count) { dotDisplayModel.remove(dotDisplayModel.count - 1); @@ -57,8 +46,8 @@ Rectangle { } } } - - // keypad functions + + // Keypad functions function backspace() { if (!lockScreenState.waitingForAuth) { root.previewCharIndex = -2; @@ -72,31 +61,28 @@ Rectangle { lockScreenState.resetPassword(); } } - + function enter() { lockScreenState.tryPassword(); - - if (keypadOpen && !isPinMode) { + + if (root.isKeypadOpen && root.lockScreenState.isKeyboardMode) { // make sure keyboard doesn't close openKeyboardTimer.restart(); } } - + function keyPress(data) { if (!lockScreenState.waitingForAuth) { - - if (root.pinLabel !== i18n("Enter PIN")) { - root.pinLabel = i18n("Enter PIN"); - } - + root.lockScreenState.resetPinLabel(); + root.previewCharIndex = lockScreenState.password.length; lockScreenState.password += data - + // trigger turning letter into dot later letterTimer.restart(); } } - + // HACK: we have to open the virtual keyboard after a certain amount of time or else it will close anyway Timer { id: openKeyboardTimer @@ -105,7 +91,7 @@ Rectangle { repeat: false onTriggered: Keyboards.KWinVirtualKeyboard.active = true } - + // trigger turning letter into dot after 500 milliseconds Timer { id: letterTimer @@ -116,24 +102,24 @@ Rectangle { root.previewCharIndex = -2; } } - + // hidden textfield so that the virtual keyboard shows up TextField { id: textField visible: false - focus: keypadOpen && !isPinMode + focus: root.isKeypadOpen && root.lockScreenState.isKeyboardMode z: 1 inputMethodHints: Qt.ImhNoPredictiveText - + onFocusChanged: { if (focus) { Keyboards.KWinVirtualKeyboard.active = true; } } - + property bool externalEdit: false property string prevText: "" - + Connections { target: root.lockScreenState @@ -144,13 +130,13 @@ Rectangle { } } } - + onEditingFinished: { if (textField.focus) { root.enter(); } } - + onTextChanged: { if (!externalEdit) { if (prevText.length > text.length) { // backspace @@ -165,16 +151,16 @@ Rectangle { externalEdit = false; } } - + MouseArea { anchors.fill: parent onClicked: { // clicking on rectangle opens keyboard if not already open - if (!isPinMode) { + if (root.lockScreenState.isKeyboardMode) { Keyboards.KWinVirtualKeyboard.active = true; } } - + // toggle between showing keypad and not ToolButton { anchors.right: parent.right @@ -182,37 +168,24 @@ Rectangle { anchors.bottom: parent.bottom anchors.margins: Kirigami.Units.smallSpacing implicitWidth: height - icon.name: root.isPinMode ? "input-keyboard-virtual-symbolic" : "input-dialpad-symbolic" + icon.name: root.lockScreenState.isKeyboardMode ? "input-dialpad-symbolic" : "input-keyboard-virtual-symbolic" onClicked: { - root.isPinMode = !root.isPinMode; - if (!root.isPinMode) { + root.lockScreenState.isKeyboardMode = !root.lockScreenState.isKeyboardMode; + if (root.lockScreenState.isKeyboardMode) { Keyboards.KWinVirtualKeyboard.active = true; } } } - - // label ("wrong pin", "enter pin") - Label { - opacity: root.lockScreenState.password.length === 0 ? 1 : 0 - anchors.centerIn: parent - text: root.pinLabel - font.pointSize: 12 - color: root.headerTextColor - - Behavior on opacity { - NumberAnimation { duration: 200 } - } - } - + // pin dot display ColumnLayout { anchors.fill: parent - + ListView { id: dotDisplay property int dotWidth: 6 - + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.bottomMargin: Math.round(dotWidth / 2) @@ -222,7 +195,7 @@ Rectangle { orientation: ListView.Horizontal spacing: 8 model: dotDisplayModel - + Behavior on implicitWidth { NumberAnimation { duration: 50 } } @@ -231,7 +204,7 @@ Rectangle { width: dotDisplay.dotWidth height: dotDisplay.dotWidth property bool showChar: index === root.previewCharIndex - + Component.onCompleted: { if (showChar) { charAnimation.to = 1; @@ -242,7 +215,7 @@ Rectangle { dotAnimation.restart(); } } - + onShowCharChanged: { if (!showChar) { charAnimation.to = 0; @@ -252,14 +225,14 @@ Rectangle { dotAnimation.start(); } } - + Rectangle { // dot id: dot scale: 0 anchors.fill: parent radius: width color: lockScreenState.waitingForAuth ? root.headerTextInactiveColor : root.headerTextColor // dim when waiting for auth - + PropertyAnimation { id: dotAnimation target: dot; @@ -267,7 +240,7 @@ Rectangle { duration: 50 } } - + Label { // number/letter id: charLabel scale: 0 @@ -275,7 +248,7 @@ Rectangle { color: lockScreenState.waitingForAuth ? root.headerTextInactiveColor : root.headerTextColor // dim when waiting for auth text: model.char font.pointSize: 12 - + PropertyAnimation { id: charAnimation target: charLabel; diff --git a/shell/contents/lockscreen/WallpaperBlur.qml b/shell/contents/lockscreen/WallpaperBlur.qml index a574311a..8fe76cd2 100644 --- a/shell/contents/lockscreen/WallpaperBlur.qml +++ b/shell/contents/lockscreen/WallpaperBlur.qml @@ -11,14 +11,4 @@ MultiEffect { blurEnabled: true blurMax: 50 blur: 1.0 - - property bool shouldBlur - opacity: shouldBlur ? 1 : 0 - - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.InOutQuad - } - } }