From 810c8caf74e414a170c5552908fee18ddfd22572 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Fri, 4 Sep 2026 14:58:34 +0000 Subject: [PATCH] Refine Plasma lock screen authentication retries --- README.md | 4 +- plasma-always-show-unlock/LockScreenUi.patch | 96 ++++--- .../plasma-always-show-unlock.spec | 6 +- .../tests/LockScreenUi.qml | 249 ++++++++++++++++++ .../tests/test-plasma-always-show-unlock | 15 +- 5 files changed, 325 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 42ca102..6e2f30e 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,9 @@ display is turned off or the system resumes from sleep. The package applies a narrow patch to Plasma 6.7's lock-screen QML and reapplies it after `plasma-desktop` updates. An unknown future layout is left unchanged instead of being modified speculatively. Removing the package restores Plasma's -original behavior. +original behavior. It retries lock-screen authentication after resume and the +automatic-lock grace period, but does not modify `kscreenlocker` authentication +code or `fprintd`. ### Plasma task-group shortcuts diff --git a/plasma-always-show-unlock/LockScreenUi.patch b/plasma-always-show-unlock/LockScreenUi.patch index 42c844b..187e589 100644 --- a/plasma-always-show-unlock/LockScreenUi.patch +++ b/plasma-always-show-unlock/LockScreenUi.patch @@ -1,6 +1,6 @@ --- a/contents/lockscreen/LockScreenUi.qml +++ b/contents/lockscreen/LockScreenUi.qml -@@ -83,6 +83,15 @@ +@@ -83,20 +83,32 @@ } } @@ -16,7 +16,24 @@ SessionManagement { id: sessionManagement } -@@ -108,7 +117,6 @@ + + KeyboardIndicator.KeyState { + id: capsLockState + key: Qt.Key_CapsLock + } + + Connections { + target: sessionManagement + function onAboutToSuspend() { + root.clearPassword(); + } ++ function onResumingFromSuspend() { ++ resumeAuthenticationTimer.restart(); ++ } + } + + RejectPasswordAnimation { +@@ -108,7 +120,6 @@ id: lockScreenRoot property bool uiVisible: false @@ -24,8 +41,12 @@ property bool blockUI: containsMouse && (mainStack.depth > 1 || mainBlock.mainPasswordBox.text.length > 0 || inputPanel.keyboardActive) x: parent.x -@@ -119,10 +127,7 @@ - cursorShape: uiVisible ? Qt.ArrowCursor : Qt.BlankCursor +@@ -118,13 +129,10 @@ + width: parent.width + height: parent.height + hoverEnabled: true +- cursorShape: uiVisible ? Qt.ArrowCursor : Qt.BlankCursor ++ cursorShape: Qt.ArrowCursor drag.filterChildren: true onPressed: uiVisible = true; - onPositionChanged: { @@ -36,49 +57,42 @@ onUiVisibleChanged: { if (uiVisible) { Window.window.requestActivate(); -@@ -135,6 +140,13 @@ +@@ -135,6 +143,18 @@ } authenticator.startAuthenticating(); } + Timer { -+ // Keep the authentication backend active while the prompt is visible. -+ interval: 1000 -+ running: parent.uiVisible -+ repeat: true ++ id: resumeAuthenticationTimer ++ interval: 1500 ++ onTriggered: authenticator.startAuthenticating() ++ } ++ Timer { ++ id: postGraceAuthenticationTimer ++ // Retry once after Plasma's default five-second lock grace period. ++ interval: 6000 ++ running: root.viewVisible + onTriggered: authenticator.startAuthenticating() + } onBlockUIChanged: { if (blockUI) { fadeoutTimer.running = false; -@@ -143,19 +155,12 @@ - fadeoutTimer.restart(); - } - } -- onExited: { -- uiVisible = false; -- } - Keys.onEscapePressed: { -- // If the escape key is pressed, kscreenlocker will turn off the screen. -- // We do not want to show the password prompt in this case. -- if (uiVisible) { -- uiVisible = false; -- if (inputPanel.keyboardActive) { -- inputPanel.showHide(); -- } -- root.clearPassword(); -+ // KScreenLocker will turn off the screen; leave the prompt ready for wake-up. -+ if (inputPanel.keyboardActive) { -+ inputPanel.showHide(); - } -+ root.clearPassword(); - } - Keys.onPressed: event => { - uiVisible = true; -@@ -167,7 +172,6 @@ - onTriggered: { - if (!lockScreenRoot.blockUI) { - mainBlock.mainPasswordBox.showPassword = false; -- lockScreenRoot.uiVisible = false; - } - } - } +@@ -194,6 +214,6 @@ + WallpaperFader { + anchors.fill: parent +- state: lockScreenRoot.uiVisible ? "on" : "off" ++ state: "on" + source: wallpaper + mainStack: mainStack + footer: footer +@@ -211,7 +231,7 @@ + samples: 15 + spread: 0.2 + color : Qt.rgba(0, 0, 0, 0.7) +- opacity: lockScreenRoot.uiVisible ? 0 : 1 ++ opacity: 0 + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.veryLongDuration * 2 +@@ -265 +285 @@ +- lockScreenUiVisible: lockScreenRoot.uiVisible ++ lockScreenUiVisible: true diff --git a/plasma-always-show-unlock/plasma-always-show-unlock.spec b/plasma-always-show-unlock/plasma-always-show-unlock.spec index e8e27f2..88ccaa2 100644 --- a/plasma-always-show-unlock/plasma-always-show-unlock.spec +++ b/plasma-always-show-unlock/plasma-always-show-unlock.spec @@ -1,6 +1,6 @@ Name: plasma-always-show-unlock Version: 0.1.0 -Release: 1%{?dist} +Release: 8%{?dist} Summary: Always show the Plasma lock-screen unlock prompt License: MIT AND GPL-2.0-or-later @@ -56,5 +56,9 @@ fi %{_datadir}/plasma-always-show-unlock/LockScreenUi.patch %changelog +* Thu Sep 03 2026 fedora-tools contributors - 0.1.0-8 +- Keep the prompt visible without continuously restarting authentication +- Retry authentication after resume and the automatic-lock grace period + * Wed Sep 02 2026 fedora-tools contributors - 0.1.0-1 - Initial package diff --git a/plasma-always-show-unlock/tests/LockScreenUi.qml b/plasma-always-show-unlock/tests/LockScreenUi.qml index 3bab403..4640fa1 100644 --- a/plasma-always-show-unlock/tests/LockScreenUi.qml +++ b/plasma-always-show-unlock/tests/LockScreenUi.qml @@ -176,3 +176,252 @@ Item { interval: 3000 onTriggered: root.notification = "" } + Timer { + id: graceLockTimer + interval: 3000 + onTriggered: { + root.clearPassword(); + authenticator.startAuthenticating(); + } + } + + PropertyAnimation { + id: launchAnimation + target: lockScreenRoot + property: "opacity" + from: 0 + to: 1 + duration: Kirigami.Units.veryLongDuration * 2 + } + + Component.onCompleted: launchAnimation.start(); + + WallpaperFader { + anchors.fill: parent + state: lockScreenRoot.uiVisible ? "on" : "off" + source: wallpaper + mainStack: mainStack + footer: footer + clock: clock + alwaysShowClock: config.alwaysShowClock && !config.hideClockWhenIdle + } + + DropShadow { + id: clockShadow + anchors.fill: clock + source: clock + visible: !lockScreenUi.softwareRendering && config.alwaysShowClock + radius: 7 + verticalOffset: 0.8 + samples: 15 + spread: 0.2 + color : Qt.rgba(0, 0, 0, 0.7) + opacity: lockScreenRoot.uiVisible ? 0 : 1 + Behavior on opacity { + OpacityAnimator { + duration: Kirigami.Units.veryLongDuration * 2 + easing.type: Easing.InOutQuad + } + } + } + + Clock { + id: clock + property Item shadow: clockShadow + visible: y > 0 && config.alwaysShowClock + anchors.horizontalCenter: parent.horizontalCenter + y: (mainBlock.userList.y + mainStack.y)/2 - height/2 + Layout.alignment: Qt.AlignBaseline + } + + ListModel { + id: users + + Component.onCompleted: { + users.append({ + name: kscreenlocker_userName, + realName: kscreenlocker_userName, + icon: kscreenlocker_userImage !== "" + ? "file://" + kscreenlocker_userImage.split("/").map(encodeURIComponent).join("/") + : "", + }) + } + } + + StackView { + id: mainStack + anchors { + left: parent.left + right: parent.right + } + height: lockScreenRoot.height + Kirigami.Units.gridUnit * 3 + focus: true //StackView is an implicit focus scope, so we need to give this focus so the item inside will have it + + // this isn't implicit, otherwise items still get processed for the scenegraph + visible: opacity > 0 + + initialItem: MainBlock { + id: mainBlock + lockScreenUiVisible: lockScreenRoot.uiVisible + + showUserList: userList.y + mainStack.y > 0 + + enabled: !graceLockTimer.running + + StackView.onStatusChanged: { + // prepare for presenting again to the user + if (StackView.status === StackView.Activating) { + mainPasswordBox.clear(); + mainPasswordBox.focus = true; + root.notification = ""; + } + } + userListModel: users + + + notificationMessage: { + const parts = []; + if (capsLockState.locked) { + parts.push(i18ndc("plasma_shell_org.kde.plasma.desktop", "@info:status", "Caps Lock is on")); + } + if (root.notification) { + parts.push(root.notification); + } + return parts.join(" • "); + } + + onPasswordResult: password => { + authenticator.respond(password) + } + + actionItems: [ + ActionButton { + text: i18ndc("plasma_shell_org.kde.plasma.desktop", "@action:button", "Slee&p") + icon.name: "system-suspend" + onClicked: sessionManagement.suspend() + visible: sessionManagement.canSuspend + }, + ActionButton { + text: i18ndc("plasma_shell_org.kde.plasma.desktop", "@action:button", "&Hibernate") + icon.name: "system-suspend-hibernate" + onClicked: sessionManagement.hibernate() + visible: sessionManagement.canHibernate + }, + ActionButton { + text: i18ndc("plasma_shell_org.kde.plasma.desktop", "@action:button", "Switch &User") + icon.name: "system-switch-user" + onClicked: { + sessionManagement.switchUser(); + } + visible: sessionManagement.canSwitchUser + } + ] + + Loader { + Layout.topMargin: Kirigami.Units.smallSpacing // some distance to the password field + Layout.fillWidth: true + Layout.preferredHeight: item ? item.implicitHeight : 0 + active: config.showMediaControls + source: "MediaControls.qml" + } + } + } + + VirtualKeyboardLoader { + id: inputPanel + + z: 1 + + screenRoot: lockScreenRoot + mainStack: mainStack + mainBlock: mainBlock + passwordField: mainBlock.mainPasswordBox + } + + Loader { + z: 2 + active: root.viewVisible + source: "LockOsd.qml" + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: Kirigami.Units.gridUnit + } + } + + // Note: Containment masks stretch clickable area of their buttons to + // the screen edges, essentially making them adhere to Fitts's law. + // Due to virtual keyboard button having an icon, buttons may have + // different heights, so fillHeight is required. + // + // Note for contributors: Keep this in sync with SDDM Main.qml footer. + RowLayout { + id: footer + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + margins: Kirigami.Units.smallSpacing + } + spacing: Kirigami.Units.smallSpacing + + PlasmaComponents3.ToolButton { + id: virtualKeyboardButton + + focusPolicy: Qt.TabFocus + text: i18ndc("plasma_shell_org.kde.plasma.desktop", "Button to show/hide virtual keyboard", "Virtual Keyboard") + icon.name: inputPanel.keyboardActive ? "input-keyboard-virtual-on" : "input-keyboard-virtual-off" + onClicked: { + // Otherwise the password field loses focus and virtual keyboard + // keystrokes get eaten + mainBlock.mainPasswordBox.forceActiveFocus(); + inputPanel.showHide() + } + + visible: inputPanel.status === Loader.Ready + + Layout.fillHeight: true + containmentMask: Item { + parent: virtualKeyboardButton + anchors.fill: parent + anchors.leftMargin: -footer.anchors.margins + anchors.bottomMargin: -footer.anchors.margins + } + } + + PlasmaComponents3.ToolButton { + id: keyboardButton + + focusPolicy: Qt.TabFocus + Accessible.description: i18ndc("plasma_shell_org.kde.plasma.desktop", "Button to change keyboard layout", "Switch layout") + icon.name: "input-keyboard" + + PW.KeyboardLayoutSwitcher { + id: keyboardLayoutSwitcher + + anchors.fill: parent + acceptedButtons: Qt.NoButton + } + + text: keyboardLayoutSwitcher.layoutNames.longName + onClicked: keyboardLayoutSwitcher.keyboardLayout.switchToNextLayout() + + visible: keyboardLayoutSwitcher.hasMultipleKeyboardLayouts + + Layout.fillHeight: true + containmentMask: Item { + parent: keyboardButton + anchors.fill: parent + anchors.leftMargin: virtualKeyboardButton.visible ? 0 : -footer.anchors.margins + anchors.bottomMargin: -footer.anchors.margins + } + } + + Item { + Layout.fillWidth: true + } + + Battery {} + } + } +} diff --git a/plasma-always-show-unlock/tests/test-plasma-always-show-unlock b/plasma-always-show-unlock/tests/test-plasma-always-show-unlock index 21258b5..d2f162f 100755 --- a/plasma-always-show-unlock/tests/test-plasma-always-show-unlock +++ b/plasma-always-show-unlock/tests/test-plasma-always-show-unlock @@ -35,9 +35,20 @@ fi run_tool apply >/dev/null run_tool status >/dev/null grep -q 'function onViewVisibleChanged()' "$target" -grep -q 'running: parent.uiVisible' "$target" +grep -q 'id: resumeAuthenticationTimer' "$target" +grep -q 'id: postGraceAuthenticationTimer' "$target" +grep -q 'interval: 1500' "$target" +grep -q 'interval: 6000' "$target" +grep -q 'running: root.viewVisible' "$target" grep -q 'onTriggered: authenticator.startAuthenticating()' "$target" -if grep -q 'seenPositionChange\|lockScreenRoot.uiVisible = false' "$target"; then +grep -q 'function onResumingFromSuspend()' "$target" +[[ $(grep -c 'resumeAuthenticationTimer.restart()' "$target") -eq 1 ]] +grep -q 'cursorShape: Qt.ArrowCursor' "$target" +grep -q 'state: "on"' "$target" +grep -q 'opacity: 0' "$target" +grep -q 'lockScreenUiVisible: true' "$target" +grep -q 'lockScreenRoot.uiVisible = false' "$target" +if grep -q 'seenPositionChange\|running: parent.uiVisible\|repeat: true' "$target"; then printf 'The patched fixture still contains prompt-hiding logic.\n' >&2 exit 1 fi