Files
2026-04-08 19:12:07 -04:00

1217 lines
43 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.Hardware
import qs.Services.Keyboard
import qs.Services.Location
import qs.Services.Media
import qs.Widgets
import qs.Widgets.AudioSpectrum
Item {
id: root
anchors.fill: parent
required property var lockControl
required property var batteryIndicator
required property var keyboardLayout
required property TextInput passwordInput
// Whether to enable lock screen animations (smooth cursor blink).
// Defaults to false to reduce GPU usage. Set Settings.data.general.lockScreenAnimations = true to restore.
readonly property bool animationsEnabled: Settings.data.general.lockScreenAnimations || false
Component.onCompleted: {
if (Settings.data.general.autoStartAuth) {
doUnlock();
}
}
function doUnlock() {
if (lockControl) {
lockControl.tryUnlock();
}
}
// Timer properties
readonly property int timerDuration: Settings.data.general.lockScreenCountdownDuration
property string pendingAction: ""
property bool timerActive: false
property int timeRemaining: 0
readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
// Timer management functions
function startTimer(action) {
// Check if global countdown is disabled
if (!Settings.data.general.enableLockScreenCountdown) {
executeAction(action);
return;
}
if (timerActive && pendingAction === action) {
// Second click - execute immediately
executeAction(action);
return;
}
pendingAction = action;
timeRemaining = timerDuration;
timerActive = true;
countdownTimer.start();
}
function cancelTimer() {
timerActive = false;
pendingAction = "";
timeRemaining = 0;
countdownTimer.stop();
}
function executeAction(action) {
// Stop timer but don't reset other properties yet
countdownTimer.stop();
// Execute the action
switch (action) {
case "logout":
CompositorService.logout();
break;
case "suspend":
CompositorService.suspend();
break;
case "hibernate":
CompositorService.hibernate();
break;
case "reboot":
CompositorService.reboot();
break;
case "userspaceReboot":
CompositorService.userspaceReboot();
break;
case "shutdown":
CompositorService.shutdown();
break;
}
// Reset timer state
cancelTimer();
}
// Countdown timer
Timer {
id: countdownTimer
interval: 100
repeat: true
onTriggered: {
timeRemaining -= interval;
if (timeRemaining <= 0) {
executeAction(pendingAction);
}
}
}
// Compact status indicators container (compact mode only)
Rectangle {
width: {
var hasBattery = batteryIndicator.isReady;
var hasKeyboard = keyboardLayout.currentLayout !== "Unknown";
var hasCaps = LockKeysService.capsLockOn;
var hasCapsSlot = hasBattery || hasKeyboard || hasCaps;
var visibleCount = 0;
if (hasBattery)
visibleCount++;
if (hasKeyboard)
visibleCount++;
if (hasCapsSlot)
visibleCount++;
if (visibleCount >= 3) {
return 280;
} else if (visibleCount === 2) {
return 200;
} else if (visibleCount === 1) {
return 120;
} else {
return 0;
}
}
height: 40
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 96 + (Settings.data.general.compactLockScreen ? 116 : 220)
topLeftRadius: Style.radiusL
topRightRadius: Style.radiusL
color: Color.mSurface
visible: Settings.data.general.compactLockScreen && (batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn)
RowLayout {
id: compactStatusRow
anchors.centerIn: parent
spacing: Style.marginL
// Battery indicator
RowLayout {
spacing: Style.marginS
visible: batteryIndicator.isReady
NIcon {
icon: batteryIndicator.icon
pointSize: Style.fontSizeM
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
}
}
// Keyboard layout indicator
RowLayout {
spacing: 6
visible: keyboardLayout.currentLayout !== "Unknown"
NIcon {
icon: "keyboard"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
elide: Text.ElideRight
}
}
// Caps Lock indicator
RowLayout {
spacing: 6
visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn
NIcon {
icon: "lock"
pointSize: Style.fontSizeM
color: LockKeysService.capsLockOn ? Color.mPrimary : Qt.alpha(Color.mOnSurfaceVariant, 0.5)
}
NText {
text: I18n.tr("bar.lock-keys.show-caps-lock-label")
color: LockKeysService.capsLockOn ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.65)
pointSize: Style.fontSizeM
elide: Text.ElideRight
}
}
}
}
// Bottom container with weather, password input and controls
Rectangle {
id: bottomContainer
// Support for removing the session/power buttons at the bottom.
readonly property int deltaY: Settings.data.general.showSessionButtonsOnLockScreen ? 0 : (Settings.data.general.compactLockScreen ? 36 : 48) + 14
height: {
let calcHeight = Settings.data.general.compactLockScreen ? 120 : 220;
if (!Settings.data.general.showSessionButtonsOnLockScreen) {
calcHeight -= bottomContainer.deltaY;
}
return calcHeight;
}
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 100 + bottomContainer.deltaY
radius: Style.radiusL
color: Color.mSurface
width: Settings.data.general.showHibernateOnLockScreen ? 860 : 810
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
spacing: Style.marginL
// Top info row
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 65
spacing: Style.marginXL
visible: !Settings.data.general.compactLockScreen
// Media widget with visualizer
Item {
Layout.preferredWidth: Style.marginM
visible: MediaService.currentPlayer && MediaService.canPlay
}
Rectangle {
Layout.preferredWidth: 220
// Expand to take remaining space when weather is hidden
Layout.fillWidth: !(Settings.data.location.weatherEnabled && LocationService.data.weather !== null)
Layout.preferredHeight: 50
radius: Style.radiusL
color: "transparent"
clip: true
visible: MediaService.currentPlayer && MediaService.canPlay
Loader {
anchors.fill: parent
anchors.margins: 4
active: Settings.data.audio.visualizerType === "linear"
z: 0
sourceComponent: NLinearSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.4
mirrored: Settings.data.audio.spectrumMirrored
}
}
Loader {
anchors.fill: parent
anchors.margins: 4
active: Settings.data.audio.visualizerType === "mirrored"
z: 0
sourceComponent: NMirroredSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.4
mirrored: Settings.data.audio.spectrumMirrored
}
}
Loader {
anchors.fill: parent
anchors.margins: 4
active: Settings.data.audio.visualizerType === "wave"
z: 0
sourceComponent: NWaveSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.4
mirrored: Settings.data.audio.spectrumMirrored
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: Style.marginM
z: 1
Rectangle {
Layout.preferredWidth: 34
Layout.preferredHeight: 34
radius: Math.min(Style.radiusL, width / 2)
color: "transparent"
clip: true
NImageRounded {
anchors.fill: parent
anchors.margins: 2
radius: Math.min(Style.radiusL, width / 2)
imagePath: MediaService.trackArtUrl
fallbackIcon: "disc"
fallbackIconSize: Style.fontSizeM
borderColor: Color.mOutline
borderWidth: Style.borderS
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
NText {
text: MediaService.trackTitle || "No media"
pointSize: Style.fontSizeM
color: Color.mOnSurface
Layout.fillWidth: true
elide: Text.ElideRight
}
NText {
text: MediaService.trackArtist || ""
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
elide: Text.ElideRight
}
}
// Media controls (when enabled)
RowLayout {
spacing: Style.marginXS
visible: Settings.data.general.enableLockScreenMediaControls
Layout.alignment: Qt.AlignHCenter
Rectangle {
width: 28
height: 28
radius: Math.min(Style.radiusL, width / 2)
color: prevButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1)
visible: MediaService.canGoPrevious
NIcon {
anchors.centerIn: parent
icon: "media-prev"
pointSize: Style.fontSizeM
color: prevButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
id: prevButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
Rectangle {
width: 32
height: 32
radius: Math.min(Style.radiusL, width / 2)
color: playPauseButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.15)
visible: MediaService.canPlay || MediaService.canPause
NIcon {
anchors.centerIn: parent
icon: MediaService.isPlaying ? "media-pause" : "media-play"
pointSize: Style.fontSizeL
color: playPauseButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
id: playPauseButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
Rectangle {
width: 28
height: 28
radius: Math.min(Style.radiusL, width / 2)
color: nextButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1)
visible: MediaService.canGoNext
NIcon {
anchors.centerIn: parent
icon: "media-next"
pointSize: Style.fontSizeM
color: nextButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
id: nextButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: MediaService.canGoNext ? MediaService.next() : {}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
}
}
}
Rectangle {
Layout.preferredWidth: 1
Layout.fillHeight: true
Layout.rightMargin: 4
color: Qt.alpha(Color.mOutline, 0.3)
visible: MediaService.currentPlayer && MediaService.canPlay
}
Item {
Layout.preferredWidth: Style.marginM
visible: !(MediaService.currentPlayer && MediaService.canPlay)
}
// Current weather
RowLayout {
visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
Layout.preferredWidth: 180
spacing: Style.marginM
Item {
Layout.preferredWidth: lockMainWeatherIconSide
Layout.preferredHeight: lockMainWeatherIconSide
Layout.alignment: Qt.AlignVCenter
readonly property int lockMainWeatherIconSide: Math.round(Style.fontSizeXXXL * Style.uiScaleRatio * 1.6)
NIcon {
visible: !LocationService.taliaWeatherMascotActive || !weatherReady
anchors.centerIn: parent
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "weather-cloud-off"
pointSize: Style.fontSizeXXXL
color: Color.mPrimary
}
Loader {
active: LocationService.taliaWeatherMascotActive && weatherReady
anchors.fill: parent
asynchronous: true
sourceComponent: Component {
Image {
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
asynchronous: true
source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.current_weather.weathercode))
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL
NText {
text: {
var temp = LocationService.data.weather.current_weather.temperature;
var suffix = "C";
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp);
suffix = "F";
}
temp = Math.round(temp);
return temp + "°" + suffix;
}
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: {
var wind = LocationService.data.weather.current_weather.windspeed;
var unit = "km/h";
if (Settings.data.location.useFahrenheit) {
wind = wind * 0.621371; // Convert km/h to mph
unit = "mph";
}
wind = Math.round(wind);
return wind + " " + unit;
}
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
text: Settings.data.location.name.split(",")[0]
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
visible: !Settings.data.location.hideWeatherCityName
}
NText {
text: (LocationService.data.weather.current && LocationService.data.weather.current.relativehumidity_2m) ? LocationService.data.weather.current.relativehumidity_2m + "% humidity" : ""
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
}
}
}
// Forecast
RowLayout {
visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null
Layout.preferredWidth: 260
Layout.rightMargin: 8
spacing: Style.marginXS
Repeater {
model: MediaService.currentPlayer && MediaService.canPlay ? 2 : 4
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS + 1
NText {
text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
return I18n.locale.toString(weatherDate, "ddd");
}
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
Item {
Layout.preferredWidth: lockForecastWeatherIconSide
Layout.preferredHeight: lockForecastWeatherIconSide
Layout.alignment: Qt.AlignHCenter
readonly property int lockForecastWeatherIconSide: Math.round(Style.fontSizeXL * Style.uiScaleRatio * 1.6)
NIcon {
visible: !LocationService.taliaWeatherMascotActive
anchors.centerIn: parent
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
pointSize: Style.fontSizeXL
color: Color.mOnSurfaceVariant
}
Loader {
active: LocationService.taliaWeatherMascotActive
anchors.fill: parent
asynchronous: true
sourceComponent: Component {
Image {
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
asynchronous: true
source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.daily.weathercode[index]))
}
}
}
}
NText {
text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index];
var min = LocationService.data.weather.daily.temperature_2m_min[index];
if (Settings.data.location.useFahrenheit) {
max = LocationService.celsiusToFahrenheit(max);
min = LocationService.celsiusToFahrenheit(min);
}
max = Math.round(max);
min = Math.round(min);
return max + "°/" + min + "°";
}
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
}
}
}
Item {
Layout.fillWidth: batteryIndicator.isReady
}
// Battery and Keyboard Layout (full mode only)
ColumnLayout {
Layout.alignment: (batteryIndicator.isReady) ? (Qt.AlignRight | Qt.AlignVCenter) : Qt.AlignVCenter
spacing: Style.marginM
visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn
// Battery
RowLayout {
spacing: Style.marginXS
visible: batteryIndicator.isReady
NIcon {
icon: batteryIndicator.icon
pointSize: Style.fontSizeM
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
}
}
// Keyboard Layout
RowLayout {
spacing: Style.marginXS
visible: keyboardLayout.currentLayout !== "Unknown"
NIcon {
icon: "keyboard"
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
}
NText {
text: keyboardLayout.currentLayout
color: Color.mOnSurfaceVariant
pointSize: Style.fontSizeM
elide: Text.ElideRight
}
}
// Caps Lock
RowLayout {
spacing: Style.marginXS
visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn
NIcon {
icon: "lock"
pointSize: Style.fontSizeM
color: LockKeysService.capsLockOn ? Color.mPrimary : Qt.alpha(Color.mOnSurfaceVariant, 0.5)
}
NText {
text: I18n.tr("bar.lock-keys.show-caps-lock-label")
color: LockKeysService.capsLockOn ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.65)
pointSize: Style.fontSizeM
elide: Text.ElideRight
}
}
}
Item {
Layout.preferredWidth: Style.marginM
}
}
// Password input
RowLayout {
Layout.fillWidth: true
spacing: 0
Item {
Layout.preferredWidth: Style.marginM
}
Rectangle {
id: passwordInputContainer
Layout.fillWidth: true
Layout.preferredHeight: 48
radius: Style.iRadiusL
color: Color.mSurface
border.color: passwordInput.activeFocus ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3)
border.width: passwordInput.activeFocus ? 2 : 1
property bool passwordVisible: false
// Ctrl + A to highlight the portion
Shortcut {
sequence: StandardKey.SelectAll
enabled: passwordInput.activeFocus
onActivated: passwordInput.selectAll()
}
// Esc to clear selection
Shortcut {
sequences: [StandardKey.Cancel]
enabled: passwordInput.activeFocus && passwordInput.selectionStart !== passwordInput.selectionEnd
onActivated: passwordInput.deselect()
}
Row {
anchors.left: parent.left
anchors.leftMargin: 18
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginL
NIcon {
icon: "login-2"
pointSize: Style.fontSizeL
color: passwordInput.activeFocus ? Color.mPrimary : Color.mOnSurfaceVariant
anchors.verticalCenter: parent.verticalCenter
}
Row {
spacing: 0
Rectangle {
width: 2
height: 20
color: Color.mPrimary
visible: passwordInput.activeFocus && passwordInput.text.length === 0
anchors.verticalCenter: parent.verticalCenter
// Smooth fade animation (when animations enabled)
SequentialAnimation on opacity {
loops: Animation.Infinite
running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length === 0
NumberAnimation {
to: 0
duration: 530
}
NumberAnimation {
to: 1
duration: 530
}
}
// Simple toggle (when animations disabled) — no per-frame repaints
Timer {
interval: 530
running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length === 0
repeat: true
onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1
}
}
// Host for dots / plain text and the caret (caret x follows passwordInput.cursorPosition)
Item {
id: passwordVisualHost
height: 20
width: passwordInputContainer.passwordVisible ? Math.min(visiblePasswordPlainText.implicitWidth, 550) : Math.min(passwordDisplayContent.width, 550)
anchors.verticalCenter: parent.verticalCenter
readonly property real caretVisualX: {
const len = passwordInput.text.length;
if (len <= 0)
return 0;
if (passwordInputContainer.passwordVisible) {
const adv = passwordCaretFontMetrics.advanceWidth(passwordInput.text.substring(0, passwordInput.cursorPosition));
return Math.max(0, Math.min(adv, width));
}
const w = passwordDisplayContent.width;
if (w <= 0)
return 0;
return Math.max(0, Math.min((passwordInput.cursorPosition / len) * w, width));
}
// Password dots display with selection support
Item {
width: Math.min(passwordDisplayContent.width, 550)
height: 20
visible: passwordInput.text.length > 0 && !passwordInputContainer.passwordVisible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
clip: true
// Proportional selection highlight behind the dots
Rectangle {
id: selectionHighlight
visible: passwordInput.selectionStart !== passwordInput.selectionEnd && passwordInput.text.length > 0
color: Qt.alpha(Color.mPrimary, 0.8)
height: parent.height + Style.marginS
anchors.verticalCenter: parent.verticalCenter
x: (passwordInput.selectionStart / passwordInput.text.length) * passwordDisplayContent.width
width: ((passwordInput.selectionEnd - passwordInput.selectionStart) / passwordInput.text.length) * passwordDisplayContent.width
}
Row {
id: passwordDisplayContent
spacing: Style.marginXXXS
anchors.verticalCenter: parent.verticalCenter
Repeater {
id: iconRepeater
model: ScriptModel {
values: Array(passwordInput.text.length)
}
property list<string> passwordChars: ["circle-filled", "pentagon-filled", "michelin-star-filled", "square-rounded-filled", "guitar-pick-filled", "blob-filled", "triangle-filled"]
NIcon {
id: icon
required property int index
// This will be called with index = -1 when the TextInput is deleted
// So we make sur index is positive to avoid warning on array accesses
property bool drawCustomChar: index >= 0 && Settings.data.general.passwordChars
// Flip color when this dot falls inside the active selection range
property bool isSelected: index >= 0 && passwordInput.selectionStart !== passwordInput.selectionEnd && index >= passwordInput.selectionStart && index < passwordInput.selectionEnd
icon: drawCustomChar ? iconRepeater.passwordChars[index % iconRepeater.passwordChars.length] : "circle-filled"
pointSize: Style.fontSizeL
color: isSelected ? Color.mOnPrimary : Color.mPrimary
opacity: 1.0
scale: animationsEnabled ? 0.5 : 1
ParallelAnimation {
id: iconAnim
NumberAnimation {
target: icon
properties: "scale"
to: 1
duration: Style.animationFast
easing.type: Easing.BezierSpline
easing.bezierCurve: Easing.OutInBounce
}
}
Component.onCompleted: {
if (animationsEnabled) {
iconAnim.start();
}
}
}
}
}
// Mouse area for click-to-position and drag-to-select
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
property int dragStartPos: 0
property bool pendingSelectAll: false
// Resets double-click state if user pauses too long between clicks
Timer {
id: doubleClickResetTimer
interval: 600
onTriggered: parent.pendingSelectAll = false
}
function charIndexFromX(mouseX) {
if (passwordInput.text.length === 0)
return 0;
var charWidth = passwordDisplayContent.width / passwordInput.text.length;
// floor so clicking anywhere on a dot selects that dot, not the next
return Math.max(0, Math.min(passwordInput.text.length - 1, Math.floor(mouseX / charWidth)));
}
onPressed: function (mouse) {
doubleClickResetTimer.stop();
passwordInput.forceActiveFocus();
dragStartPos = charIndexFromX(mouse.x);
passwordInput.cursorPosition = dragStartPos;
}
onPositionChanged: function (mouse) {
pendingSelectAll = false;
var curPos = charIndexFromX(mouse.x);
if (curPos <= dragStartPos) {
passwordInput.select(curPos, dragStartPos + 1);
} else {
passwordInput.select(dragStartPos, curPos + 1);
}
}
onDoubleClicked: function (mouse) {
passwordInput.forceActiveFocus();
if (pendingSelectAll) {
passwordInput.selectAll();
pendingSelectAll = false;
} else {
var pos = charIndexFromX(mouse.x);
passwordInput.select(pos, Math.min(pos + 1, passwordInput.text.length));
pendingSelectAll = true;
doubleClickResetTimer.restart();
}
}
}
}
NText {
id: visiblePasswordPlainText
text: passwordInput.text
color: Color.mPrimary
pointSize: Style.fontSizeM
visible: passwordInput.text.length > 0 && passwordInputContainer.passwordVisible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: Math.min(implicitWidth, 550)
}
FontMetrics {
id: passwordCaretFontMetrics
font: visiblePasswordPlainText.font
}
Rectangle {
width: 2
height: 20
x: passwordVisualHost.caretVisualX
color: Color.mPrimary
// Hide the cursor when text is selected
visible: passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
anchors.verticalCenter: parent.verticalCenter
// Smooth fade animation (when animations enabled)
SequentialAnimation on opacity {
loops: Animation.Infinite
running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
NumberAnimation {
to: 0
duration: 530
}
NumberAnimation {
to: 1
duration: 530
}
}
// Simple toggle (when animations disabled) — no per-frame repaints
Timer {
interval: 530
running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd
repeat: true
onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1
}
}
}
}
}
// Eye button to toggle password visibility
Rectangle {
anchors.right: submitButton.left
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
width: 36
height: 36
radius: Math.min(Style.iRadiusL, width / 2)
color: eyeButtonArea.containsMouse ? Color.mPrimary : "transparent"
visible: passwordInput.text.length > 0
enabled: !lockContext || !lockContext.unlockInProgress
NIcon {
anchors.centerIn: parent
icon: parent.parent.passwordVisible ? "eye-off" : "eye"
pointSize: Style.fontSizeM
color: eyeButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
id: eyeButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.parent.passwordVisible = !parent.parent.passwordVisible
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
// Submit button
Rectangle {
id: submitButton
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
width: 36
height: 36
radius: Math.min(Style.iRadiusL, width / 2)
color: submitButtonArea.containsMouse ? Color.mPrimary : "transparent"
border.color: Color.mPrimary
border.width: Style.borderS
enabled: !lockContext || !lockContext.unlockInProgress
NIcon {
anchors.centerIn: parent
icon: "arrow-forward"
pointSize: Style.fontSizeM
color: submitButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
MouseArea {
id: submitButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.doUnlock()
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.OutCubic
}
}
}
Item {
Layout.preferredWidth: Style.marginM
}
}
// Session control buttons
RowLayout {
id: sessionButtonRow
Layout.fillWidth: true
Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
visible: Settings.data.general.showSessionButtonsOnLockScreen
readonly property int buttonCount: Settings.data.general.showHibernateOnLockScreen ? 5 : 4
readonly property real availableWidth: bottomContainer.width - 48
readonly property real buttonWidth: (availableWidth - (buttonCount - 1) * spacing) / buttonCount
readonly property real buttonHeight: sessionButtonRow.height
Item {
Layout.preferredWidth: sessionButtonRow.buttonWidth
Layout.preferredHeight: sessionButtonRow.buttonHeight
NButton {
anchors.fill: parent
icon: "logout"
text: I18n.tr("common.logout")
outlined: true
backgroundColor: Color.mOnSurfaceVariant
textColor: Color.mOnPrimary
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
horizontalAlignment: Qt.AlignHCenter
buttonRadius: Style.radiusL
onClicked: startTimer("logout")
}
}
Item {
Layout.preferredWidth: sessionButtonRow.buttonWidth
Layout.preferredHeight: sessionButtonRow.buttonHeight
NButton {
anchors.fill: parent
icon: "suspend"
text: I18n.tr("common.suspend")
outlined: true
backgroundColor: Color.mOnSurfaceVariant
textColor: Color.mOnPrimary
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
horizontalAlignment: Qt.AlignHCenter
buttonRadius: Style.radiusL
onClicked: startTimer("suspend")
}
}
Item {
Layout.preferredWidth: sessionButtonRow.buttonWidth
Layout.preferredHeight: sessionButtonRow.buttonHeight
visible: Settings.data.general.showHibernateOnLockScreen
NButton {
anchors.fill: parent
icon: "hibernate"
text: I18n.tr("common.hibernate")
outlined: true
backgroundColor: Color.mOnSurfaceVariant
textColor: Color.mOnPrimary
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
horizontalAlignment: Qt.AlignHCenter
buttonRadius: Style.radiusL
onClicked: startTimer("hibernate")
}
}
Item {
Layout.preferredWidth: sessionButtonRow.buttonWidth
Layout.preferredHeight: sessionButtonRow.buttonHeight
NButton {
anchors.fill: parent
icon: "reboot"
text: I18n.tr("common.reboot")
outlined: true
backgroundColor: Color.mOnSurfaceVariant
textColor: Color.mOnPrimary
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
horizontalAlignment: Qt.AlignHCenter
buttonRadius: Style.radiusL
onClicked: startTimer("reboot")
}
}
Item {
Layout.preferredWidth: sessionButtonRow.buttonWidth
Layout.preferredHeight: sessionButtonRow.buttonHeight
NButton {
anchors.fill: parent
icon: "shutdown"
text: I18n.tr("common.shutdown")
outlined: true
backgroundColor: Color.mError
textColor: Color.mOnError
fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM
iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL
horizontalAlignment: Qt.AlignHCenter
buttonRadius: Style.radiusL
onClicked: startTimer("shutdown")
}
}
}
}
}
}