quickshell: touch PIN lock screen for the phone
The OSK is layershell and can't rise above a session lock, so the lock surface carries its own keypad. Lock state persists to states.json so a qs crash while locked comes back locked; with lock.launchOnStartup the lock screen is the greeter (greetd autologins straight into Hyprland). Keypad gated behind lock.touchKeypad so desktop deploys keep the stock surface.
This commit is contained in:
parent
620c6778ce
commit
72f5169458
6 changed files with 691 additions and 0 deletions
|
|
@ -36,6 +36,10 @@ modules/ii/dock/DockButton.qml ii/modules/ii/dock/DockButton.qml
|
|||
modules/ii/dock/DockSeparator.qml ii/modules/ii/dock/DockSeparator.qml
|
||||
modules/ii/dock/DockStack.qml ii/modules/ii/dock/DockStack.qml
|
||||
modules/ii/overview/Overview.qml ii/modules/ii/overview/Overview.qml
|
||||
modules/common/Persistent.qml ii/modules/common/Persistent.qml
|
||||
modules/common/panels/lock/LockScreen.qml ii/modules/common/panels/lock/LockScreen.qml
|
||||
modules/ii/lock/Lock.qml ii/modules/ii/lock/Lock.qml
|
||||
modules/ii/lock/TouchLockSurface.qml ii/modules/ii/lock/TouchLockSurface.qml
|
||||
pill/shell.qml pill/shell.qml
|
||||
"
|
||||
|
||||
|
|
|
|||
|
|
@ -385,6 +385,10 @@ Singleton {
|
|||
property JsonObject lock: JsonObject {
|
||||
property bool useHyprlock: false
|
||||
property bool launchOnStartup: false
|
||||
// Souveraine: PIN keypad surface for touch panels (phone).
|
||||
// The OSK can't rise above a session lock, so the lock
|
||||
// surface must carry its own input.
|
||||
property bool touchKeypad: false
|
||||
property JsonObject blur: JsonObject {
|
||||
property bool enable: true
|
||||
property real radius: 100
|
||||
|
|
|
|||
174
surfaces/quickshell/modules/common/Persistent.qml
Normal file
174
surfaces/quickshell/modules/common/Persistent.qml
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// Souveraine patch to ii's stock Persistent.qml.
|
||||
//
|
||||
// Adds states.lock.locked: written by LockScreen.qml whenever the session
|
||||
// lock engages/releases, so a quickshell crash or restart while locked
|
||||
// comes back locked instead of silently dropping the lock. Everything else
|
||||
// is unchanged stock ii — diff against upstream before re-applying if ii
|
||||
// updates.
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property alias states: persistentStatesJsonAdapter
|
||||
property string fileDir: Directories.state
|
||||
property string fileName: "states.json"
|
||||
property string filePath: `${root.fileDir}/${root.fileName}`
|
||||
|
||||
property bool ready: false
|
||||
property string previousHyprlandInstanceSignature: ""
|
||||
property bool isNewHyprlandInstance: previousHyprlandInstanceSignature !== states.hyprlandInstanceSignature
|
||||
|
||||
onReadyChanged: {
|
||||
root.previousHyprlandInstanceSignature = root.states.hyprlandInstanceSignature
|
||||
root.states.hyprlandInstanceSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") || ""
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fileReloadTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
persistentStatesFileView.reload()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fileWriteTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
persistentStatesFileView.writeAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: persistentStatesFileView
|
||||
path: root.filePath
|
||||
|
||||
watchChanges: true
|
||||
onFileChanged: fileReloadTimer.restart()
|
||||
onAdapterUpdated: fileWriteTimer.restart()
|
||||
onLoaded: root.ready = true
|
||||
onLoadFailed: error => {
|
||||
console.log("Failed to load persistent states file:", error);
|
||||
if (error == FileViewError.FileNotFound) {
|
||||
fileWriteTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
adapter: JsonAdapter {
|
||||
id: persistentStatesJsonAdapter
|
||||
|
||||
property string hyprlandInstanceSignature: ""
|
||||
|
||||
property JsonObject ai: JsonObject {
|
||||
property string model: "gemini-2.5-flash"
|
||||
property real temperature: 0.5
|
||||
}
|
||||
|
||||
property JsonObject cheatsheet: JsonObject {
|
||||
property int tabIndex: 0
|
||||
}
|
||||
|
||||
property JsonObject sidebar: JsonObject {
|
||||
property JsonObject bottomGroup: JsonObject {
|
||||
property bool collapsed: false
|
||||
property int tab: 0
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject booru: JsonObject {
|
||||
property bool allowNsfw: false
|
||||
property string provider: "yandere"
|
||||
}
|
||||
|
||||
property JsonObject idle: JsonObject {
|
||||
property bool inhibit: false
|
||||
}
|
||||
|
||||
property JsonObject lock: JsonObject {
|
||||
property bool locked: false
|
||||
}
|
||||
|
||||
property JsonObject overlay: JsonObject {
|
||||
property list<string> open: ["crosshair", "recorder", "volumeMixer", "resources"]
|
||||
property JsonObject crosshair: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: true
|
||||
property real x: 827
|
||||
property real y: 441
|
||||
property real width: 250
|
||||
property real height: 100
|
||||
}
|
||||
property JsonObject floatingImage: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 1650
|
||||
property real y: 390
|
||||
property real width: 0
|
||||
property real height: 0
|
||||
}
|
||||
property JsonObject fpsLimiter: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 1570
|
||||
property real y: 615
|
||||
property real width: 280
|
||||
property real height: 80
|
||||
}
|
||||
property JsonObject recorder: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 80
|
||||
property real y: 80
|
||||
property real width: 350
|
||||
property real height: 130
|
||||
}
|
||||
property JsonObject resources: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: true
|
||||
property real x: 1500
|
||||
property real y: 770
|
||||
property real width: 350
|
||||
property real height: 200
|
||||
property int tabIndex: 0
|
||||
}
|
||||
property JsonObject volumeMixer: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 80
|
||||
property real y: 280
|
||||
property real width: 350
|
||||
property real height: 600
|
||||
property int tabIndex: 0
|
||||
}
|
||||
property JsonObject notes: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: true
|
||||
property real x: 1400
|
||||
property real y: 42
|
||||
property real width: 460
|
||||
property real height: 330
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject timer: JsonObject {
|
||||
property JsonObject pomodoro: JsonObject {
|
||||
property bool running: false
|
||||
property int start: 0
|
||||
property bool isBreak: false
|
||||
property int cycle: 0
|
||||
}
|
||||
property JsonObject stopwatch: JsonObject {
|
||||
property bool running: false
|
||||
property int start: 0
|
||||
property list<var> laps: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
surfaces/quickshell/modules/common/panels/lock/LockScreen.qml
Normal file
164
surfaces/quickshell/modules/common/panels/lock/LockScreen.qml
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// Souveraine patch to ii's stock LockScreen.qml.
|
||||
//
|
||||
// Two changes, both for phone duty where the lock is the only gate on the
|
||||
// device:
|
||||
// 1. The lock state is mirrored into Persistent.states.lock.locked, so a
|
||||
// quickshell crash/restart while locked comes back locked instead of
|
||||
// dropping the session lock on the floor.
|
||||
// 2. initIfReady() also locks when the persisted flag says we died locked,
|
||||
// not only on a fresh Hyprland instance.
|
||||
// Everything else is unchanged stock ii.
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property Component lockSurface
|
||||
property alias context: lockContext
|
||||
property Component sessionLockSurface: WlSessionLockSurface {
|
||||
id: sessionLockSurface
|
||||
color: "transparent"
|
||||
Loader {
|
||||
active: GlobalStates.screenLocked
|
||||
anchors.fill: parent
|
||||
opacity: active ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
sourceComponent: root.lockSurface
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: unlockKeyringProc
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
KeyringStorage.fetchKeyringData();
|
||||
}
|
||||
}
|
||||
function unlockKeyring() {
|
||||
unlockKeyringProc.exec({
|
||||
environment: ({
|
||||
"UNLOCK_PASSWORD": lockContext.currentText
|
||||
}),
|
||||
command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")]
|
||||
})
|
||||
}
|
||||
|
||||
// This stores all the information shared between the lock surfaces on each screen.
|
||||
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
|
||||
LockContext {
|
||||
id: lockContext
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
Persistent.states.lock.locked = GlobalStates.screenLocked;
|
||||
if (GlobalStates.screenLocked) {
|
||||
lockContext.reset();
|
||||
lockContext.tryFingerUnlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnlocked: (targetAction) => {
|
||||
// Perform the target action if it's not just unlocking
|
||||
if (targetAction == LockContext.ActionEnum.Poweroff) {
|
||||
Session.poweroff();
|
||||
return;
|
||||
} else if (targetAction == LockContext.ActionEnum.Reboot) {
|
||||
Session.reboot();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the keyring if configured to do so
|
||||
if (Config.options.lock.security.unlockKeyring) root.unlockKeyring(); // Async
|
||||
|
||||
// Unlock the screen before exiting, or the compositor will display a
|
||||
// fallback lock you can't interact with.
|
||||
GlobalStates.screenLocked = false;
|
||||
|
||||
// Reset
|
||||
lockContext.reset();
|
||||
|
||||
// Post-unlock actions
|
||||
if (lockContext.alsoInhibitIdle) {
|
||||
lockContext.alsoInhibitIdle = false;
|
||||
Idle.toggleInhibit(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
locked: GlobalStates.screenLocked
|
||||
surface: root.sessionLockSurface
|
||||
}
|
||||
|
||||
function lock() {
|
||||
if (Config.options.lock.useHyprlock) {
|
||||
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
|
||||
return;
|
||||
}
|
||||
GlobalStates.screenLocked = true;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lock"
|
||||
|
||||
function activate(): void {
|
||||
root.lock();
|
||||
}
|
||||
function focus(): void {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lock"
|
||||
description: "Locks the screen"
|
||||
|
||||
onPressed: {
|
||||
root.lock()
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lockFocus"
|
||||
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
|
||||
+ "decides to keyboard-unfocus the lock screen"
|
||||
|
||||
onPressed: {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function initIfReady() {
|
||||
if (!Config.ready || !Persistent.ready) return;
|
||||
if (Config.options.lock.launchOnStartup && (Persistent.isNewHyprlandInstance || Persistent.states.lock.locked)) {
|
||||
root.lock();
|
||||
} else {
|
||||
KeyringStorage.fetchKeyringData();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() {
|
||||
root.initIfReady();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Persistent
|
||||
function onReadyChanged() {
|
||||
root.initIfReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
surfaces/quickshell/modules/ii/lock/Lock.qml
Normal file
87
surfaces/quickshell/modules/ii/lock/Lock.qml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Souveraine patch to ii's stock Lock.qml.
|
||||
//
|
||||
// Selects the lock surface by config: lock.touchKeypad picks the
|
||||
// TouchLockSurface (PIN pad for the phone's touch panel) instead of the
|
||||
// stock keyboard-driven LockSurface. Everything else is unchanged stock ii.
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.panels.lock
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
LockScreen {
|
||||
id: root
|
||||
|
||||
// Monitor name -> workspace id to restore on unlock (set when locking)
|
||||
property var savedWorkspaces: ({})
|
||||
|
||||
Timer {
|
||||
id: restoreTimer
|
||||
interval: 150
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
var batch = ""
|
||||
for (var j = 0; j < Quickshell.screens.length; ++j) {
|
||||
var monName = Quickshell.screens[j].name
|
||||
var wsId = root.savedWorkspaces[monName]
|
||||
if (wsId !== undefined) {
|
||||
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${monName}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${wsId}})';`
|
||||
}
|
||||
}
|
||||
if (batch.length > 0) {
|
||||
Quickshell.execDetached(["bash", "-c", batch])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lockSurface: Config.options.lock.touchKeypad ? touchSurfaceComponent : desktopSurfaceComponent
|
||||
|
||||
property Component desktopSurfaceComponent: LockSurface {
|
||||
context: root.context
|
||||
}
|
||||
property Component touchSurfaceComponent: TouchLockSurface {
|
||||
context: root.context
|
||||
}
|
||||
|
||||
// Single batch for lock and unlock so we don't race multiple hyprctl calls
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
if (GlobalStates.screenLocked) {
|
||||
// Lock: save workspace per monitor and move all to temp workspace in one batch
|
||||
var next = {}
|
||||
var batch = "keyword animation workspaces,1,7,menu_decel,slidevert; "
|
||||
for (var i = 0; i < Quickshell.screens.length; ++i) {
|
||||
var mon = Quickshell.screens[i].name
|
||||
var mData = HyprlandData.monitors.find(m => m.name === mon)
|
||||
if (mData?.activeWorkspace == undefined) {
|
||||
return;
|
||||
}
|
||||
var ws = (mData?.activeWorkspace?.id ?? 1)
|
||||
next[mon] = ws
|
||||
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${mon}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${2147483647 - ws}})';`
|
||||
}
|
||||
root.savedWorkspaces = next
|
||||
Quickshell.execDetached(["bash", "-c", batch])
|
||||
} else {
|
||||
restoreTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push everything down (visual only; workspace switch is in Connections above)
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: Scope {
|
||||
required property ShellScreen modelData
|
||||
property bool shouldPush: GlobalStates.screenLocked
|
||||
property string targetMonitorName: modelData.name
|
||||
property int verticalMovementDistance: modelData.height
|
||||
property int horizontalSqueeze: modelData.width * 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
258
surfaces/quickshell/modules/ii/lock/TouchLockSurface.qml
Normal file
258
surfaces/quickshell/modules/ii/lock/TouchLockSurface.qml
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
// Souveraine addition: touch-first lock surface for the phone.
|
||||
//
|
||||
// A PIN keypad instead of ii's keyboard-driven LockSurface — on the phone
|
||||
// the OSK is a layershell surface and can never appear above the session
|
||||
// lock, so the lock surface must carry its own input. Auth goes through the
|
||||
// same LockContext/PAM machinery as the desktop surface; hardware keyboards
|
||||
// still work via the Keys handlers. The big clock comes from Background.qml
|
||||
// (Overlay layer while locked), so none is drawn here.
|
||||
//
|
||||
// Sized for 540x1080 logical (1080x2160 @ scale 2).
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.panels.lock
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property LockContext context
|
||||
readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower
|
||||
|
||||
readonly property int keySize: 96
|
||||
readonly property int keySpacing: 18
|
||||
|
||||
function forceFieldFocus() {
|
||||
root.forceActiveFocus();
|
||||
}
|
||||
Connections {
|
||||
target: root.context
|
||||
function onShouldReFocus() {
|
||||
forceFieldFocus();
|
||||
}
|
||||
}
|
||||
Component.onCompleted: forceFieldFocus()
|
||||
onPressed: forceFieldFocus()
|
||||
|
||||
function pressDigit(d) {
|
||||
root.context.resetClearTimer();
|
||||
root.context.currentText += d;
|
||||
}
|
||||
|
||||
// Hardware keyboard entry (USB-C keyboard); mirrors the desktop surface
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
root.context.resetClearTimer();
|
||||
if (event.key === Qt.Key_Backspace) {
|
||||
root.context.currentText = (event.modifiers & Qt.ControlModifier)
|
||||
? "" : root.context.currentText.slice(0, -1);
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (root.context.currentText.length > 0) root.context.tryUnlock();
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
root.context.currentText = "";
|
||||
} else if (event.text.length === 1 && event.text >= " ") {
|
||||
root.context.currentText += event.text;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: padColumn
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 48
|
||||
}
|
||||
spacing: 20
|
||||
|
||||
// Entered-PIN dots, with the empty-state hint behind them.
|
||||
// In a ColumnLayout so ErrorShakeAnimation's Layout.leftMargin works.
|
||||
Item {
|
||||
id: dotsArea
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: Math.max(dotsRow.width, hintText.width, 1)
|
||||
implicitHeight: 36
|
||||
opacity: root.context.unlockInProgress ? 0.5 : 1
|
||||
|
||||
Row {
|
||||
id: dotsRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 13
|
||||
Repeater {
|
||||
model: Math.min(root.context.currentText.length, 14)
|
||||
Rectangle {
|
||||
width: 13
|
||||
height: 13
|
||||
radius: 6.5
|
||||
color: GlobalStates.screenUnlockFailed
|
||||
? Appearance.colors.colError : Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
id: hintText
|
||||
anchors.centerIn: parent
|
||||
visible: root.context.currentText.length === 0
|
||||
text: GlobalStates.screenUnlockFailed
|
||||
? Translation.tr("Incorrect PIN") : Translation.tr("Enter PIN")
|
||||
color: GlobalStates.screenUnlockFailed
|
||||
? Appearance.colors.colError : Appearance.colors.colSubtext
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
}
|
||||
|
||||
ErrorShakeAnimation {
|
||||
id: wrongPinShakeAnim
|
||||
target: dotsArea
|
||||
}
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenUnlockFailedChanged() {
|
||||
if (GlobalStates.screenUnlockFailed) wrongPinShakeAnim.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
columns: 3
|
||||
columnSpacing: root.keySpacing
|
||||
rowSpacing: root.keySpacing
|
||||
|
||||
Repeater {
|
||||
model: ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
KeypadButton {
|
||||
id: digitKey
|
||||
required property string modelData
|
||||
onClicked: root.pressDigit(digitKey.modelData)
|
||||
contentItem: StyledText {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: digitKey.modelData
|
||||
font.pixelSize: 30
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeypadButton {
|
||||
enabled: root.context.currentText.length > 0 && !root.context.unlockInProgress
|
||||
colBackground: "transparent"
|
||||
onClicked: {
|
||||
root.context.resetClearTimer();
|
||||
root.context.currentText = root.context.currentText.slice(0, -1);
|
||||
}
|
||||
onPressAndHold: root.context.currentText = ""
|
||||
contentItem: KeypadIcon {
|
||||
text: "backspace"
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
|
||||
KeypadButton {
|
||||
onClicked: root.pressDigit("0")
|
||||
contentItem: StyledText {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: "0"
|
||||
font.pixelSize: 30
|
||||
color: Appearance.colors.colOnLayer1
|
||||
}
|
||||
}
|
||||
|
||||
KeypadButton {
|
||||
id: confirmKey
|
||||
enabled: root.context.currentText.length > 0 && !root.context.unlockInProgress
|
||||
toggled: true
|
||||
onClicked: root.context.tryUnlock()
|
||||
contentItem: KeypadIcon {
|
||||
text: "arrow_right_alt"
|
||||
color: confirmKey.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility row: power / battery / reboot, kept small and away from digits
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 6
|
||||
spacing: 36
|
||||
|
||||
UtilityButton {
|
||||
iconName: "power_settings_new"
|
||||
targetAction: LockContext.ActionEnum.Poweroff
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 5
|
||||
visible: Battery.available
|
||||
MaterialSymbol {
|
||||
fill: 1
|
||||
text: Battery.isCharging ? "bolt" : "battery_android_full"
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
color: (Battery.isLow && !Battery.isCharging)
|
||||
? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
StyledText {
|
||||
text: Math.round(Battery.percentage * 100) + "%"
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
UtilityButton {
|
||||
iconName: "restart_alt"
|
||||
targetAction: LockContext.ActionEnum.Reboot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component KeypadButton: RippleButton {
|
||||
implicitWidth: root.keySize
|
||||
implicitHeight: root.keySize
|
||||
buttonRadius: root.keySize / 2
|
||||
enabled: !root.context.unlockInProgress
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer1, 0.4)
|
||||
}
|
||||
|
||||
component KeypadIcon: MaterialSymbol {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 30
|
||||
}
|
||||
|
||||
// Same semantics as the desktop surface's password-guarded power buttons:
|
||||
// with requirePasswordToPower the action arms and the PIN confirms it,
|
||||
// otherwise it fires immediately.
|
||||
component UtilityButton: RippleButton {
|
||||
id: utilBtn
|
||||
required property string iconName
|
||||
required property var targetAction
|
||||
|
||||
implicitWidth: 56
|
||||
implicitHeight: 56
|
||||
buttonRadius: 28
|
||||
toggled: root.context.targetAction === utilBtn.targetAction
|
||||
colBackground: "transparent"
|
||||
|
||||
onClicked: {
|
||||
if (!root.requirePasswordToPower) {
|
||||
root.context.unlocked(utilBtn.targetAction);
|
||||
return;
|
||||
}
|
||||
if (root.context.targetAction === utilBtn.targetAction) {
|
||||
root.context.resetTargetAction();
|
||||
} else {
|
||||
root.context.targetAction = utilBtn.targetAction;
|
||||
root.context.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: KeypadIcon {
|
||||
text: utilBtn.iconName
|
||||
color: utilBtn.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue