From 14d4d05acc235ab7033316d16530783c90e95faa Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Fri, 5 Sep 2025 23:31:35 +0200 Subject: ... --- accounts/gkleen@sif/shell/quickshell/Bar.qml | 6 +- accounts/gkleen@sif/shell/quickshell/Clock.qml | 309 +++++++++++---------- .../gkleen@sif/shell/quickshell/LockSurface.qml | 223 +++++++++++++++ .../gkleen@sif/shell/quickshell/Lockscreen.qml | 222 ++------------- .../gkleen@sif/shell/quickshell/displaymanager.qml | 115 ++++++++ 5 files changed, 522 insertions(+), 353 deletions(-) create mode 100644 accounts/gkleen@sif/shell/quickshell/LockSurface.qml create mode 100644 accounts/gkleen@sif/shell/quickshell/displaymanager.qml (limited to 'accounts/gkleen@sif/shell/quickshell') diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml index aab1607f..38225d74 100644 --- a/accounts/gkleen@sif/shell/quickshell/Bar.qml +++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml @@ -7,8 +7,6 @@ PanelWindow { required property var screen - property var calendarMouseArea: clock.calendarMouseArea - anchors { top: true left: true @@ -80,8 +78,6 @@ PanelWindow { width: 4 } - Clock { - id: clock - } + Clock {} } } \ No newline at end of file diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml index d0c9178b..382af168 100644 --- a/accounts/gkleen@sif/shell/quickshell/Clock.qml +++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml @@ -22,18 +22,6 @@ Item { hoverEnabled: true enabled: clockItem.calendarPopup - property real angleRem: 0 - property real sensitivity: 1 / 120 - - function scrollYear(event) { - angleRem += event.angleDelta.y; - const d = Math.round(angleRem * sensitivity); - yearCalendar.year += d; - angleRem -= d / sensitivity; - } - - onWheel: event => scrollYear(event) - Item { anchors.fill: parent @@ -56,204 +44,233 @@ Item { } } - PopupWindow { - id: tooltip + Loader { + id: tooltipLoader - property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse + active: false - anchor { - item: clockMouseArea - edges: Edges.Bottom | Edges.Left + Connections { + target: clockMouseArea + function onContainsMouseChanged() { + if (clockMouseArea.containsMouse) + tooltipLoader.active = true; + } } - visible: false - onNextVisibleChanged: hangTimer.restart() + sourceComponent: PopupWindow { + id: tooltip - Timer { - id: hangTimer - interval: 100 - onTriggered: tooltip.visible = tooltip.nextVisible - } + property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse - implicitWidth: clockTooltipContent.width - implicitHeight: clockTooltipContent.height - color: "black" + anchor { + item: clockMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false - onVisibleChanged: { - yearCalendar.year = chrono.date.getFullYear(); - clockMouseArea.angleRem = 0; - } + onNextVisibleChanged: hangTimer.restart() - WrapperMouseArea { - id: tooltipMouseArea + Timer { + id: hangTimer + interval: 100 + onTriggered: tooltip.visible = tooltip.nextVisible + } - hoverEnabled: true - enabled: true + implicitWidth: clockTooltipContent.width + implicitHeight: clockTooltipContent.height + color: "black" - onWheel: event => clockMouseArea.scrollYear(event) + onVisibleChanged: { + yearCalendar.year = chrono.date.getFullYear(); + yearCalendar.angleRem = 0; + } - anchors.fill: parent + WrapperMouseArea { + id: tooltipMouseArea - WrapperItem { - id: clockTooltipContent + hoverEnabled: true + enabled: true - margin: 8 - leftMargin: 0 + onWheel: event => yearCalendar.scrollYear(event) - ColumnLayout { - Text { - id: yearLabel + anchors.fill: parent - horizontalAlignment: Text.AlignHCenter + WrapperItem { + id: clockTooltipContent - font.pointSize: 14 - font.family: "Fira Sans" - font.features: { "tnum": 1 } - color: "white" + margin: 8 + leftMargin: 0 - text: yearCalendar.year + ColumnLayout { + Text { + id: yearLabel - Layout.fillWidth: true - Layout.bottomMargin: 8 - } + horizontalAlignment: Text.AlignHCenter - GridLayout { - property int year: chrono.date.getFullYear() + font.pointSize: 14 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + color: "white" - id: yearCalendar + text: yearCalendar.year - columns: 3 - columnSpacing: 16 - rowSpacing: 16 + Layout.fillWidth: true + Layout.bottomMargin: 8 + } - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: false + GridLayout { + property int year: chrono.date.getFullYear() - Repeater { - model: 12 + id: yearCalendar - GridLayout { - columns: 2 + columns: 3 + columnSpacing: 16 + rowSpacing: 16 - required property int index - property int month: index + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: false - id: monthCalendar + property real angleRem: 0 + property real sensitivity: 1 / 120 - Layout.alignment: Qt.AlignTop | Qt.AlignRight - Layout.fillWidth: false + function scrollYear(event) { + angleRem += event.angleDelta.y; + const d = Math.round(angleRem * sensitivity); + yearCalendar.year += d; + angleRem -= d / sensitivity; + } - Text { - Layout.column: 1 - Layout.fillWidth: true + Connections { + target: clockMouseArea + function onWheel(event) { yearCalendar.scrollYear(event); } + } - horizontalAlignment: Text.AlignHCenter + Repeater { + model: 12 - font.pointSize: 10 - font.family: "Fira Sans" + GridLayout { + columns: 2 - text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM") + required property int index + property int month: index - color: "#ffead3" - } + id: monthCalendar - DayOfWeekRow { - locale: grid.locale + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.fillWidth: false - Layout.row: 1 - Layout.column: 1 - Layout.fillWidth: true + Text { + Layout.column: 1 + Layout.fillWidth: true - delegate: WrapperItem { - required property string shortName + horizontalAlignment: Text.AlignHCenter - width: dowLabel.contentWidth + 6 + font.pointSize: 10 + font.family: "Fira Sans" - Text { - id: dowLabel + text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM") - anchors.fill: parent + color: "#ffead3" + } - font.pointSize: 10 - font.family: "Fira Sans" + DayOfWeekRow { + locale: grid.locale - text: parent.shortName - color: "#ffcc66" + Layout.row: 1 + Layout.column: 1 + Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } - } + delegate: WrapperItem { + required property string shortName - WeekNumberColumn { - month: grid.month - year: grid.year - locale: grid.locale + width: dowLabel.contentWidth + 6 - Layout.fillHeight: true + Text { + id: dowLabel - delegate: Text { - required property int weekNumber + anchors.fill: parent - opacity: { - const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); - const dayOfWeek = simple.getDay(); - const isoWeekStart = simple; + font.pointSize: 10 + font.family: "Fira Sans" - isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); - if (dayOfWeek > 4) { - isoWeekStart.setDate(isoWeekStart.getDate() + 7); - } + text: parent.shortName + color: "#ffcc66" - for (let i = 0; i < 7; i++) { - const dayInWeek = new Date(isoWeekStart); - dayInWeek.setDate(dayInWeek.getDate() + i); - if (dayInWeek.getMonth() == monthCalendar.month) - return 1; + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } - - return 0; } + } - font.pointSize: 10 - font.family: "Fira Sans" - font.features: { "tnum": 1 } - - text: weekNumber - color: "#99ffdd" + WeekNumberColumn { + month: grid.month + year: grid.year + locale: grid.locale - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - } + Layout.fillHeight: true - MonthGrid { - id: grid + delegate: Text { + required property int weekNumber - year: yearCalendar.year - month: monthCalendar.month - locale: Qt.locale("en_DK") + opacity: { + const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7); + const dayOfWeek = simple.getDay(); + const isoWeekStart = simple; - Layout.fillWidth: true - Layout.fillHeight: true + isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1); + if (dayOfWeek > 4) { + isoWeekStart.setDate(isoWeekStart.getDate() + 7); + } - delegate: Text { - required property var model + for (let i = 0; i < 7; i++) { + const dayInWeek = new Date(isoWeekStart); + dayInWeek.setDate(dayInWeek.getDate() + i); + if (dayInWeek.getMonth() == monthCalendar.month) + return 1; + } - opacity: model.month === monthCalendar.month ? 1 : 0 + return 0; + } font.pointSize: 10 font.family: "Fira Sans" font.features: { "tnum": 1 } - property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day - - text: model.day - color: today ? "#ff6699" : "white" + text: weekNumber + color: "#99ffdd" horizontalAlignment: Text.AlignRight verticalAlignment: Text.AlignVCenter + } + } + + MonthGrid { + id: grid + + year: yearCalendar.year + month: monthCalendar.month + locale: Qt.locale("en_DK") + + Layout.fillWidth: true + Layout.fillHeight: true + + delegate: Text { + required property var model + + opacity: model.month === monthCalendar.month ? 1 : 0 + + font.pointSize: 10 + font.family: "Fira Sans" + font.features: { "tnum": 1 } + + property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day + + text: model.day + color: today ? "#ff6699" : "white" + + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } } } } diff --git a/accounts/gkleen@sif/shell/quickshell/LockSurface.qml b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml new file mode 100644 index 00000000..18698725 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml @@ -0,0 +1,223 @@ +import Quickshell.Widgets +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Fusion +import qs.Services +import QtQml + +Item { + id: lockSurface + + property var screen + property list messages: [] + property bool responseRequired: false + property bool responseVisible: false + property string currentText: "" + property bool authRunning: false + + signal response(string responseText) + + anchors.fill: parent + + Item { + id: background + + anchors.fill: parent + + property Img current: one + property string source: selector.selected + + WallpaperSelector { + id: selector + seed: lockSurface.screen?.name || "" + } + + onSourceChanged: { + if (!source) + current = null; + else if (current === one) + two.update() + else + one.update() + } + + Img { id: one } + Img { id: two } + + component Img: Item { + id: img + + property string source + + function update() { + source = background.source || "" + } + + anchors.fill: parent + + Image { + id: imageSource + + source: img.source + sourceSize: Qt.size(parent.width, parent.height) + fillMode: Image.PreserveAspectCrop + smooth: true + visible: false + asynchronous: true + cache: false + + onStatusChanged: { + if (status === Image.Ready) { + background.current = img + } + } + } + + MultiEffect { + id: imageEffect + + source: imageSource + anchors.fill: parent + blurEnabled: true + blur: 1 + blurMax: 64 + blurMultiplier: 2 + + opacity: 0 + + states: State { + name: "visible" + when: background.current === img + + PropertyChanges { + imageEffect.opacity: 1 + } + StateChangeScript { + name: "unloadOther" + script: { + if (img === one) + two.source = "" + if (img === two) + one.source = "" + } + } + } + + transitions: Transition { + SequentialAnimation { + NumberAnimation { + target: imageEffect + properties: "opacity" + duration: 5000 + easing.type: Easing.OutCubic + } + ScriptAction { + scriptName: "unloadOther" + } + } + } + } + } + } + + Item { + anchors { + top: lockSurface.top + left: lockSurface.left + right: lockSurface.right + } + + implicitWidth: lockSurface.width + implicitHeight: 21 + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.75) + } + + Clock { + anchors.centerIn: parent + calendarPopup: false + } + } + + WrapperRectangle { + id: unlockUi + + Keys.onPressed: event => { + if (!lockSurface.authRunning) { + event.accepted = true; + lockSurface.authRunning = true; + } + } + focus: !passwordBox.visible + + visible: lockSurface.authRunning + + color: Qt.rgba(0, 0, 0, 0.75) + margin: 8 + + anchors.centerIn: parent + + ColumnLayout { + spacing: 4 + + BusyIndicator { + visible: running + running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired + } + + Repeater { + model: lockSurface.messages + + Text { + required property var modelData + + font.pointSize: 10 + font.family: "Fira Sans" + color: modelData.error ? "#f28a21" : "#ffffff" + + text: String(modelData.text).trim() + + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + } + + TextField { + id: passwordBox + + visible: lockSurface.responseRequired + echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + onTextChanged: lockSurface.currentText = passwordBox.text + onAccepted: { + passwordBox.readOnly = true; + lockSurface.response(lockSurface.currentText); + } + + Connections { + target: lockSurface + function onCurrentTextChanged() { + passwordBox.text = lockSurface.currentText + } + } + Connections { + target: lockSurface + function onResponseRequiredChanged() { + if (lockSurface.responseRequired) + passwordBox.readOnly = false; + passwordBox.focus = true; + passwordBox.selectAll(); + } + } + + Layout.topMargin: 4 + Layout.fillWidth: true + } + } + } +} diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml index 7cb1cc67..cc82a275 100644 --- a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml +++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml @@ -2,14 +2,7 @@ import Quickshell import Quickshell.Wayland import Quickshell.Io import Quickshell.Services.Pam -import Quickshell.Widgets -import QtQuick.Effects -import QtQuick.Layouts -import QtQuick -import QtQuick.Controls -import QtQuick.Controls.Fusion -import qs.Services -// import QtQml.Models +import QtQml Scope { id: lockscreen @@ -53,205 +46,30 @@ Scope { WlSessionLockSurface { id: lockSurface - color: "#000000" + color: "black" - Item { - id: background + LockSurface { + id: surfaceContent - anchors.fill: parent - - property Img current: one - property string source: selector.selected - - WallpaperSelector { - id: selector - seed: lockSurface.screen?.name || "" - } - - onSourceChanged: { - if (!source) - current = null; - else if (current === one) - two.update() - else - one.update() - } - - Img { id: one } - Img { id: two } - - component Img: Item { - id: img - - property string source - - function update() { - source = background.source || "" - } - - anchors.fill: parent - - Image { - id: imageSource - - source: img.source - sourceSize: Qt.size(parent.width, parent.height) - fillMode: Image.PreserveAspectCrop - smooth: true - visible: false - asynchronous: true - cache: false - - onStatusChanged: { - if (status === Image.Ready) { - background.current = img - } - } - } - - MultiEffect { - id: imageEffect - - source: imageSource - anchors.fill: parent - blurEnabled: true - blur: 1 - blurMax: 64 - blurMultiplier: 2 - - opacity: 0 - - states: State { - name: "visible" - when: background.current === img - - PropertyChanges { - imageEffect.opacity: 1 - } - StateChangeScript { - name: "unloadOther" - script: { - if (img === one) - two.source = "" - if (img === two) - one.source = "" - } - } - } - - transitions: Transition { - SequentialAnimation { - NumberAnimation { - target: imageEffect - properties: "opacity" - duration: 5000 - easing.type: Easing.OutCubic - } - ScriptAction { - scriptName: "unloadOther" - } - } - } - } - } - } - - Item { - anchors { - top: lockSurface.top - left: lockSurface.left - right: lockSurface.right - } - - implicitWidth: lockSurface.width - implicitHeight: 21 - - Rectangle { - anchors.fill: parent - color: Qt.rgba(0, 0, 0, 0.75) + onResponse: responseText => pam.respond(responseText) + onAuthRunningChanged: { + if (authRunning) + pam.start(); } - - Clock { - anchors.centerIn: parent - calendarPopup: false + Connections { + target: pam + function onMessagesChanged() { surfaceContent.messages = pam.messages; } + function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; } + function onActiveChanged() { surfaceContent.authRunning = pam.active; } } - } - - WrapperRectangle { - id: unlockUi - - Keys.onPressed: event => { - if (!pam.active) { - event.accepted = true; - pam.start(); - } + onCurrentTextChanged: lockscreen.currentText = currentText + Connections { + target: lockscreen + function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; } } - focus: !passwordBox.visible - - visible: pam.active - - color: Qt.rgba(0, 0, 0, 0.75) - margin: 8 - - anchors.centerIn: parent - - ColumnLayout { - spacing: 4 - - BusyIndicator { - visible: running - running: !Array.from(pam.messages).length && !pam.responseRequired - } - - Repeater { - model: pam.messages - - Text { - required property var modelData - - font.pointSize: 10 - font.family: "Fira Sans" - color: modelData.error ? "#f28a21" : "#ffffff" - - text: modelData.text - - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - } - } - - TextField { - id: passwordBox - - visible: pam.responseRequired - echoMode: pam.responseVisible ? TextInput.Normal : TextInput.Password - inputMethodHints: Qt.ImhSensitiveData - - onTextChanged: lockscreen.currentText = passwordBox.text - onAccepted: { - passwordBox.readOnly = true; - pam.respond(lockscreen.currentText); - } - - Connections { - target: lockscreen - function onCurrentTextChanged() { - passwordBox.text = lockscreen.currentText - } - } - Connections { - target: pam - function onResponseRequiredChanged() { - if (pam.responseRequired) - passwordBox.readOnly = false; - passwordBox.focus = true; - passwordBox.selectAll(); - } - } - - Layout.topMargin: 4 - Layout.fillWidth: true - } + Connections { + target: lockSurface + function onScreenChanged() { surfaceContent.screen = lockSurface.screen; } } } } diff --git a/accounts/gkleen@sif/shell/quickshell/displaymanager.qml b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml new file mode 100644 index 00000000..b452c03d --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml @@ -0,0 +1,115 @@ +//@ pragma UseQApplication + +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import Quickshell.Services.Greetd +import QtQml + + +ShellRoot { + id: displaymanager + + settings.watchFiles: false + + property string currentText: "" + property string username: @username@ + property list command: @niri_session@ + property list messages: [] + property bool responseRequired: false + property bool responseVisible: false + + signal startAuth() + + onStartAuth: { + if (Greetd.state !== GreetdState.Inactive) + Greetd.cancelSession(); + displaymanager.messages = []; + Greetd.createSession(displaymanager.username); + } + + Connections { + target: Greetd + function onStateChanged() { + console.log("greetd state: ", GreetdState.toString(Greetd.state)); + if (Greetd.state === GreetdState.ReadyToLaunch) + Greetd.launch(displaymanager.command); + } + function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) { + displaymanager.responseVisible = echoResponse; + displaymanager.responseRequired = responseRequired; + displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]); + } + function onAuthFailure(message: string) { + displaymanager.responseRequired = false; + displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]); + } + } + + Component.onCompleted: { + if (Greetd.state !== GreetdState.Inactive) + Greetd.cancelSession(); + } + + Variants { + model: Quickshell.screens + + delegate: Scope { + id: screenScope + + required property var modelData + + PanelWindow { + color: "black" + + screen: screenScope.modelData + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + LockSurface { + id: surfaceContent + + screen: screenScope.modelData + + onCurrentTextChanged: displaymanager.currentText = currentText + Connections { + target: displaymanager + function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; } + function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); } + function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; } + function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; } + } + + onResponse: responseText => Greetd.respond(responseText); + Connections { + target: Greetd + function onStateChanged() { + if (Greetd.state === GreetdState.Authenticating) { + surfaceContent.authRunning = true; + } else { + surfaceContent.authRunning = false; + } + } + } + + onAuthRunningChanged: { + if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating) + displaymanager.startAuth(); + } + Component.onCompleted: { + surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating + surfaceContent.messages = Array.from(displaymanager.messages); + surfaceContent.responseVisible = displaymanager.responseVisible; + surfaceContent.responseRequired = displaymanager.responseRequired; + surfaceContent.currentText = displaymanager.currentText; + } + } + } + } + } +} -- cgit v1.2.3