The shell drops its own player: BackgroundMedia is image-only, and the lock loads Owe.LockFeedSurface through a Loader, so a system without the module shows no lock video instead of losing the whole lock screen. The feed pauses per output when the panel blanks or power saver turns on. The lock view keeps its still effect path and darkens the feed for legibility. QtMultimedia and the shell video pause policy are gone, and the base package list requires owe and owe-lockfeed instead.
243 lines
8.8 KiB
QML
243 lines
8.8 KiB
QML
import QtQuick
|
|
import QtQuick.Effects
|
|
import qs.Commons
|
|
import qs.Ui
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property string backgroundPath: ""
|
|
property int backgroundVersion: 0
|
|
property bool fingerprintConfigured: false
|
|
property bool authenticatingPassword: false
|
|
property string failureMessage: ""
|
|
property int failedAttempts: 0
|
|
property bool inputEnabled: true
|
|
property bool loadBackground: true
|
|
// A locked session blanks the displays after a few seconds. Nothing is
|
|
// visible from then until the user wakes it, so a video must not keep
|
|
// decoding through what is usually the longest part of a lock.
|
|
property bool displaysBlank: false
|
|
property bool powerSaverActive: false
|
|
property string passwordText: ""
|
|
property bool syncingPasswordText: false
|
|
|
|
readonly property string placeholderText: "Enter Password"
|
|
readonly property int fieldWidth: 381
|
|
readonly property int fieldHeight: 67
|
|
readonly property int outlineThickness: 3
|
|
readonly property int fieldFontSize: Math.round(Style.font.heading * 1.125)
|
|
readonly property int passwordDotFontSize: Math.round(Style.font.heading * 1.33)
|
|
readonly property int passwordDotLetterSpacing: Math.round(Style.font.heading * 0.19)
|
|
// Space to keep clear on each side of the field for the fingerprint icon
|
|
// (icon width plus a gap) so the centered dots never run under it.
|
|
readonly property real fingerprintReserve: fingerprintConfigured ? Math.round(fingerprintIcon.implicitWidth + 12) : 0
|
|
// Shrink the dots to fit once the password outgrows the field, so every
|
|
// keystroke stays visible — otherwise long passwords clip with no feedback.
|
|
readonly property real passwordDotScale: dotMetrics.advanceWidth > 0
|
|
? Math.min(1, (passwordInput.width - 4) / dotMetrics.advanceWidth)
|
|
: 1
|
|
readonly property bool showPasswordCursor: inputEnabled && !authenticatingPassword && failureMessage.length === 0
|
|
readonly property bool errorState: failureMessage.length > 0
|
|
readonly property var inputBorderSpec: errorState
|
|
? Border.surfaceSpec("lock", "border-error", Color.lock.borderError, root.outlineThickness, "border-alpha")
|
|
: Border.surfaceSpec("lock", "border-active", Color.lock.borderActive, root.outlineThickness, "border-alpha")
|
|
|
|
readonly property bool video: Util.isVideoPath(root.backgroundPath)
|
|
readonly property bool feedActive: root.video && root.loadBackground && !root.displaysBlank && !root.powerSaverActive
|
|
|
|
signal submitPassword(string password)
|
|
signal passwordTextEdited(string password)
|
|
signal clearFailureRequested()
|
|
signal wakeRequested()
|
|
|
|
function forcePasswordFocus() {
|
|
passwordInput.forceActiveFocus()
|
|
}
|
|
|
|
function clearPassword() {
|
|
passwordTextEdited("")
|
|
}
|
|
|
|
function syncPasswordText() {
|
|
if (passwordInput.text === passwordText) return
|
|
syncingPasswordText = true
|
|
passwordInput.text = passwordText
|
|
syncingPasswordText = false
|
|
}
|
|
|
|
onPasswordTextChanged: syncPasswordText()
|
|
onInputEnabledChanged: {
|
|
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
|
}
|
|
Component.onCompleted: {
|
|
syncPasswordText()
|
|
if (inputEnabled) Qt.callLater(forcePasswordFocus)
|
|
}
|
|
|
|
// Measures the masked password at full size; passwordDotScale compares this
|
|
// against the field width to decide how far the dots must shrink to fit.
|
|
TextMetrics {
|
|
id: dotMetrics
|
|
font.family: Style.font.family
|
|
font.pixelSize: root.passwordDotFontSize
|
|
font.letterSpacing: root.passwordDotLetterSpacing
|
|
text: "●".repeat(passwordInput.text.length)
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Color.background
|
|
|
|
BackgroundMedia {
|
|
id: wallpaper
|
|
anchors.fill: parent
|
|
path: root.video ? "" : root.backgroundPath
|
|
version: root.backgroundVersion
|
|
visible: !root.video
|
|
}
|
|
|
|
// OWE decodes the video once and feeds these frames to every lock surface.
|
|
// The feed pauses when this output blanks or on battery power saver.
|
|
Loader {
|
|
id: feedLoader
|
|
anchors.fill: parent
|
|
active: root.video
|
|
source: "LockFeedSurface.qml"
|
|
visible: status === Loader.Ready
|
|
|
|
Binding {
|
|
target: feedLoader.item
|
|
property: "feedEnabled"
|
|
value: root.feedActive
|
|
when: feedLoader.item !== null
|
|
restoreMode: Binding.RestoreNone
|
|
}
|
|
}
|
|
|
|
MultiEffect {
|
|
anchors.fill: wallpaper
|
|
source: root.video ? null : wallpaper
|
|
visible: !root.video
|
|
autoPaddingEnabled: false
|
|
blurEnabled: root.loadBackground && wallpaper.ready
|
|
blur: 1.0
|
|
blurMax: 128
|
|
blurMultiplier: 1.25
|
|
contrast: -0.08
|
|
}
|
|
|
|
// The feed item cannot be sampled by MultiEffect on every renderer.
|
|
// Keep video wallpapers visible and darken them slightly for legibility.
|
|
Rectangle {
|
|
anchors.fill: feedLoader
|
|
visible: root.video
|
|
color: "#22000000"
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
onClicked: { root.wakeRequested(); root.forcePasswordFocus() }
|
|
onPositionChanged: root.wakeRequested()
|
|
}
|
|
|
|
BorderSurface {
|
|
id: inputField
|
|
width: root.fieldWidth
|
|
height: root.fieldHeight
|
|
anchors.centerIn: parent
|
|
color: Color.lock.background
|
|
borderSpec: root.inputBorderSpec
|
|
radius: Style.cornerRadius
|
|
clip: true
|
|
|
|
TextInput {
|
|
id: passwordInput
|
|
anchors.fill: parent
|
|
anchors.topMargin: inputField.borderTop
|
|
// Reserve the fingerprint icon's width on both sides so the centered
|
|
// dots stay symmetric and never slide under the icon as they grow.
|
|
anchors.rightMargin: inputField.borderRight + 18 + root.fingerprintReserve
|
|
anchors.bottomMargin: inputField.borderBottom
|
|
anchors.leftMargin: inputField.borderLeft + 18 + root.fingerprintReserve
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
horizontalAlignment: TextInput.AlignHCenter
|
|
activeFocusOnPress: true
|
|
clip: true
|
|
enabled: root.inputEnabled && !root.authenticatingPassword
|
|
readOnly: root.authenticatingPassword
|
|
echoMode: TextInput.Password
|
|
passwordCharacter: "\u25CF"
|
|
passwordMaskDelay: 0
|
|
color: Color.lock.text
|
|
selectionColor: Color.lock.selection
|
|
selectedTextColor: Color.lock.text
|
|
font.family: Style.font.family
|
|
font.pixelSize: text.length > 0 ? Math.max(1, Math.floor(root.passwordDotFontSize * root.passwordDotScale)) : root.fieldFontSize
|
|
font.letterSpacing: text.length > 0 ? root.passwordDotLetterSpacing * root.passwordDotScale : 0
|
|
cursorVisible: activeFocus && root.showPasswordCursor && text.length > 0
|
|
cursorDelegate: Rectangle {
|
|
width: 2
|
|
color: Color.lock.text
|
|
visible: passwordInput.cursorVisible
|
|
}
|
|
|
|
onTextChanged: {
|
|
if (!root.syncingPasswordText) root.passwordTextEdited(text)
|
|
if (text.length > 0) {
|
|
root.wakeRequested()
|
|
}
|
|
if (text.length > 0 && root.failureMessage.length > 0) root.clearFailureRequested()
|
|
}
|
|
|
|
onAccepted: {
|
|
var submitted = root.passwordText
|
|
root.passwordTextEdited("")
|
|
if (submitted.length > 0) root.submitPassword(submitted)
|
|
}
|
|
|
|
Keys.onPressed: function(event) {
|
|
root.wakeRequested()
|
|
if (event.key === Qt.Key_Escape || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_U)) {
|
|
root.passwordTextEdited("")
|
|
event.accepted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.fill: passwordInput
|
|
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
|
|
visible: passwordInput.text.length === 0
|
|
color: root.authenticatingPassword ? Color.lock.text : (root.failureMessage.length > 0 ? Color.lock.textError : Color.lock.placeholder)
|
|
font.family: Style.font.family
|
|
font.pixelSize: root.fieldFontSize
|
|
font.italic: !root.authenticatingPassword && root.failureMessage.length > 0
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// Fingerprint hint pinned inside the field's right edge when a sensor is
|
|
// enrolled, so the user knows they can touch to unlock instead of typing.
|
|
// Matches hyprlock, which draws its fingerprint icon in the same spot.
|
|
Text {
|
|
id: fingerprintIcon
|
|
objectName: "fingerprintIndicator"
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: inputField.borderRight + 18
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: root.fingerprintConfigured
|
|
text: ""
|
|
color: Color.lock.placeholder
|
|
font.family: Style.font.family
|
|
font.pixelSize: Math.round(root.fieldFontSize * 1.1)
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|